//! item-screen key-input handler (split out of tui/input.rs). use super::{ App, AppEvent, ConfirmAction, DataPayload, KeyCode, KeyEvent, MnwApiClient, Screen, load_item_detail, load_license_keys, mpsc, search_tags, }; pub(crate) async fn handle_item_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { use crate::tui::item::ItemEditField; // Handle tag search mode if app.tag_searching { match key.code { KeyCode::Esc => { app.tag_searching = false; app.edit_buffer.clear(); app.tag_search_results.clear(); app.item_status = None; } KeyCode::Enter => { // Add the first search result as a tag if let (Some(tag), Some(detail)) = (app.tag_search_results.first(), &app.item_detail) { let tag_id = tag.id.clone(); let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); let tag_name = tag.name.clone(); tokio::spawn(async move { match api.add_item_tag(&user_id, &item_id, &tag_id).await { Ok(()) => { // Reload tags let tags = api .list_item_tags(&user_id, &item_id) .await .unwrap_or_default(); let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemTags { tags })) .await; let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: format!("Added tag: {tag_name}"), // Reuse error channel for status })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); app.tag_searching = false; app.edit_buffer.clear(); app.tag_search_results.clear(); app.item_status = None; } } KeyCode::Backspace => { app.edit_buffer.pop(); if app.edit_buffer.len() >= 2 { let api = api.clone(); let query = app.edit_buffer.clone(); let tx = tx.clone(); tokio::spawn(async move { search_tags(&api, &query, &tx).await; }); } else { app.tag_search_results.clear(); } } KeyCode::Char(c) => { app.edit_buffer.push(c); if app.edit_buffer.len() >= 2 { let api = api.clone(); let query = app.edit_buffer.clone(); let tx = tx.clone(); tokio::spawn(async move { search_tags(&api, &query, &tx).await; }); } let results_preview: String = app .tag_search_results .iter() .take(3) .map(|t| t.name.as_str()) .collect::>() .join(", "); app.item_status = Some(format!( "Tag: {}_ {}", app.edit_buffer, if results_preview.is_empty() { String::new() } else { format!("[{results_preview}]") }, )); } _ => {} } return; } // Cancel pending confirmation on any key other than the confirmation key if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('d' | 'D')) { app.confirm_action = None; app.item_status = None; } // Handle editing mode if let Some(field) = app.item_editing { match key.code { KeyCode::Esc => { app.item_editing = None; app.edit_buffer.clear(); app.item_status = None; } KeyCode::Enter => { if let Some(ref detail) = app.item_detail { let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); let buffer = app.edit_buffer.clone(); match field { ItemEditField::Title => { if buffer.is_empty() { app.item_editing = None; app.edit_buffer.clear(); } else { let title = buffer; tokio::spawn(async move { match api .update_item( &user_id, &item_id, Some(&title), None, None, None, ) .await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemUpdated { detail: d }, )) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemActionError { error: e.to_string(), }, )) .await; } } }); } } ItemEditField::Description => { let desc = if buffer.is_empty() { None } else { Some(buffer.as_str().to_string()) }; tokio::spawn(async move { match api .update_item( &user_id, &item_id, None, desc.as_deref(), None, None, ) .await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d, })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemActionError { error: e.to_string(), }, )) .await; } } }); } ItemEditField::Price => { let cents = crate::tui::parse_price(&buffer); tokio::spawn(async move { match api .update_item(&user_id, &item_id, None, None, Some(cents), None) .await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d, })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemActionError { error: e.to_string(), }, )) .await; } } }); } } } if app.item_editing.is_some() { // Only clear if not already cleared by the empty-buffer path app.item_editing = None; app.edit_buffer.clear(); } return; } KeyCode::Backspace => { app.edit_buffer.pop(); } KeyCode::Char(c) => { app.edit_buffer.push(c); } _ => {} } return; } // Normal mode match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc | KeyCode::Char('q' | 'Q') => { // Go back to project view if let Screen::Item(project_idx, _) = screen { let pidx = *project_idx; *screen = Screen::Project(pidx); app.item_detail = None; app.item_versions.clear(); app.item_status = None; app.item_editing = None; app.selected_index = 0; } } KeyCode::Char('e' | 'E') => { // Start editing — cycle through Title → Description → Price app.item_editing = Some(ItemEditField::Title); app.edit_buffer.clear(); app.item_status = Some("Editing title (Enter to save, Esc to cancel)".to_string()); } KeyCode::Tab => { // Cycle edit fields when in edit mode (already started with [e]) if let Some(field) = app.item_editing { let next = match field { ItemEditField::Title => ItemEditField::Description, ItemEditField::Description => ItemEditField::Price, ItemEditField::Price => ItemEditField::Title, }; app.item_editing = Some(next); app.edit_buffer.clear(); let label = match next { ItemEditField::Title => "title", ItemEditField::Description => "description", ItemEditField::Price => "price", }; app.item_status = Some(format!("Editing {label} (Enter to save, Esc to cancel)")); } } KeyCode::Char('p' | 'P') => { if let Some(ref detail) = app.item_detail && !detail.is_public { let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); app.item_status = Some("Publishing...".to_string()); tokio::spawn(async move { match api.publish_item(&user_id, &item_id).await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); } } KeyCode::Char('u' | 'U') => { if let Some(ref detail) = app.item_detail && detail.is_public { let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); app.item_status = Some("Unpublishing...".to_string()); tokio::spawn(async move { match api.unpublish_item(&user_id, &item_id).await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); } } KeyCode::Char('d' | 'D') => { if let Some(ref detail) = app.item_detail { if matches!(app.confirm_action, Some(ConfirmAction::DeleteItem)) { // Confirmed — execute delete app.confirm_action = None; let item_id = detail.id.clone(); let item_title = detail.title.clone(); let user_id = app.user.user_id.clone(); tracing::info!( user_id = %user_id, item_id = %item_id, item_title = %item_title, "delete item confirmed" ); let api = api.clone(); let tx = tx.clone(); app.item_status = Some("Deleting...".to_string()); tokio::spawn(async move { match api.delete_item(&user_id, &item_id).await { Ok(()) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemDeleted)) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); } else { // First press — ask for confirmation app.confirm_action = Some(ConfirmAction::DeleteItem); app.item_status = Some(format!( "Delete '{}'? Press d again to confirm", detail.title )); } } } KeyCode::Char('t' | 'T') if app.item_detail.is_some() => { app.tag_searching = true; app.edit_buffer.clear(); app.tag_search_results.clear(); app.item_status = Some("Tag: type to search, Enter to add first result, Esc to cancel".to_string()); } KeyCode::Char('l' | 'L') => { // Open license keys screen if let Screen::Item(project_idx, item_id) = &*screen { let pidx = *project_idx; let iid = item_id.clone(); let item_title = app .item_detail .as_ref() .map(|d| d.title.clone()) .unwrap_or_default(); *screen = Screen::Keys(pidx, iid.clone()); app.license_keys.clear(); app.keys_item_title = Some(item_title); app.keys_status = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_license_keys(&api, &user_id, &iid, &tx).await; }); } } KeyCode::Char('r' | 'R') => { if let Screen::Item(_, item_id) = &*screen { app.loading = true; let item_id = item_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_item_detail(&api, &user_id, &item_id, &tx).await; }); } } _ => {} } }