diff --git a/src/main.rs b/src/main.rs index 45c4aaa..f0ef42d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -286,6 +286,7 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) { AppMode::FormulaEdit { .. } => "FORMULA", AppMode::FormulaPanel => "FORMULAS", AppMode::CategoryPanel => "CATEGORIES", + AppMode::CategoryAdd { .. } => "NEW CATEGORY", AppMode::ItemAdd { .. } => "ADD ITEMS", AppMode::ViewPanel => "VIEWS", AppMode::TileSelect { .. } => "TILES", diff --git a/src/ui/app.rs b/src/ui/app.rs index f0d72c5..0536410 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -18,6 +18,8 @@ pub enum AppMode { FormulaEdit { buffer: String }, FormulaPanel, CategoryPanel, + /// Quick-add a new category: Enter adds and stays open, Esc closes. + CategoryAdd { buffer: String }, /// Quick-add items to `category`: Enter adds and stays open, Esc closes. ItemAdd { category: String, buffer: String }, ViewPanel, @@ -89,6 +91,7 @@ impl App { AppMode::FormulaEdit { .. } => { self.handle_formula_edit_key(key)?; } AppMode::FormulaPanel => { self.handle_formula_panel_key(key)?; } AppMode::CategoryPanel => { self.handle_category_panel_key(key)?; } + AppMode::CategoryAdd { .. } => { self.handle_category_add_key(key)?; } AppMode::ItemAdd { .. } => { self.handle_item_add_key(key)?; } AppMode::ViewPanel => { self.handle_view_panel_key(key)?; } AppMode::TileSelect { .. } => { self.handle_tile_select_key(key)?; } @@ -266,6 +269,12 @@ impl App { } } + // N = quick-add a new category (opens Category panel in add mode) + (KeyCode::Char('N'), _) => { + self.category_panel_open = true; + self.mode = AppMode::CategoryAdd { buffer: String::new() }; + } + // ── Tile movement ────────────────────────────────────────────── // T = enter tile select mode (single key, no Ctrl needed) (KeyCode::Char('T'), _) => { @@ -617,7 +626,11 @@ impl App { } } } - // a / o — open quick-add mode for the selected category + // n — add a new category + KeyCode::Char('n') => { + self.mode = AppMode::CategoryAdd { buffer: String::new() }; + } + // a / o — open quick-add items mode for the selected category KeyCode::Char('a') | KeyCode::Char('o') => { if let Some(cat_name) = cat_names.get(self.cat_panel_cursor) { self.mode = AppMode::ItemAdd { @@ -625,7 +638,7 @@ impl App { buffer: String::new(), }; } else { - self.status_msg = "No category selected. Add a category first with :add-cat .".to_string(); + self.status_msg = "No category selected. Press n to add a category first.".to_string(); } } _ => {} @@ -633,6 +646,45 @@ impl App { Ok(()) } + fn handle_category_add_key(&mut self, key: KeyEvent) -> Result<()> { + match key.code { + KeyCode::Esc => { + self.mode = AppMode::CategoryPanel; + self.status_msg = String::new(); + } + KeyCode::Enter | KeyCode::Tab => { + let buf = if let AppMode::CategoryAdd { buffer } = &self.mode { + buffer.trim().to_string() + } else { return Ok(()); }; + + if !buf.is_empty() { + let result = command::dispatch(&mut self.model, &Command::AddCategory { name: buf.clone() }); + if result.ok { + // Move cursor to the new category + self.cat_panel_cursor = self.model.categories.len().saturating_sub(1); + let count = self.model.categories.len(); + self.status_msg = format!("Added category \"{buf}\" ({count} total). Enter to add more, Esc to finish."); + self.dirty = true; + } else { + self.status_msg = result.message.unwrap_or_default(); + } + } + // Stay in CategoryAdd for the next entry + if let AppMode::CategoryAdd { ref mut buffer } = self.mode { + buffer.clear(); + } + } + KeyCode::Char(c) => { + if let AppMode::CategoryAdd { ref mut buffer } = self.mode { buffer.push(c); } + } + KeyCode::Backspace => { + if let AppMode::CategoryAdd { ref mut buffer } = self.mode { buffer.pop(); } + } + _ => {} + } + Ok(()) + } + fn handle_item_add_key(&mut self, key: KeyEvent) -> Result<()> { match key.code { KeyCode::Esc => { @@ -1013,8 +1065,9 @@ impl App { AppMode::Editing { .. } => "Enter:commit Esc:cancel", AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back", AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression", - AppMode::CategoryPanel => "jk:nav Space:cycle-axis a:add-items Esc:back", - AppMode::ItemAdd { category, .. } => "Enter:add & continue Tab:same Esc:done", + AppMode::CategoryPanel => "jk:nav Space:cycle-axis n:new-cat a:add-items Esc:back", + AppMode::CategoryAdd { .. } => "Enter:add & continue Tab:same Esc:done — type a category name", + AppMode::ItemAdd { .. } => "Enter:add & continue Tab:same Esc:done — type an item name", AppMode::ViewPanel => "jk:nav Enter:switch n:new d:delete Esc:back", AppMode::TileSelect { .. } => "hl:select Enter:cycle r/c/p:set-axis Esc:back", AppMode::CommandMode { .. } => ":q quit :w save :import :add-cat :formula :help", diff --git a/src/ui/category_panel.rs b/src/ui/category_panel.rs index 8b58b03..107c373 100644 --- a/src/ui/category_panel.rs +++ b/src/ui/category_panel.rs @@ -24,12 +24,15 @@ impl<'a> CategoryPanel<'a> { impl<'a> Widget for CategoryPanel<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let is_item_add = matches!(self.mode, AppMode::ItemAdd { .. }); - let is_active = matches!(self.mode, AppMode::CategoryPanel) || is_item_add; + let is_cat_add = matches!(self.mode, AppMode::CategoryAdd { .. }); + let is_active = matches!(self.mode, AppMode::CategoryPanel) || is_item_add || is_cat_add; - let (border_color, title) = if is_item_add { + let (border_color, title) = if is_cat_add { + (Color::Yellow, " Categories — New category (Enter:add Esc:done) ") + } else if is_item_add { (Color::Green, " Categories — Adding items (Enter:add Esc:done) ") } else if is_active { - (Color::Cyan, " Categories a:add-items Space:cycle-axis ") + (Color::Cyan, " Categories n:new a:add-items Space:axis ") } else { (Color::DarkGray, " Categories ") }; @@ -108,19 +111,26 @@ impl<'a> Widget for CategoryPanel<'a> { } } - // Inline prompt at the bottom when in ItemAdd mode - if let AppMode::ItemAdd { category, buffer } = self.mode { - let sep_y = inner.y + list_height; - let prompt_y = sep_y + 1; - if sep_y < inner.y + inner.height { - let sep = "─".repeat(inner.width as usize); - buf.set_string(inner.x, sep_y, &sep, Style::default().fg(Color::Green)); + // Inline prompt at the bottom for CategoryAdd or ItemAdd + let (prompt_color, prompt_text) = match self.mode { + AppMode::CategoryAdd { buffer } => { + (Color::Yellow, format!(" + category: {buffer}▌")) } - if prompt_y < inner.y + inner.height { - let prompt = format!(" + {buffer}▌"); - buf.set_string(inner.x, prompt_y, &prompt, - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)); + AppMode::ItemAdd { buffer, .. } => { + (Color::Green, format!(" + item: {buffer}▌")) } + _ => return, + }; + + let sep_y = inner.y + list_height; + let prompt_y = sep_y + 1; + if sep_y < inner.y + inner.height { + let sep = "─".repeat(inner.width as usize); + buf.set_string(inner.x, sep_y, &sep, Style::default().fg(prompt_color)); + } + if prompt_y < inner.y + inner.height { + buf.set_string(inner.x, prompt_y, &prompt_text, + Style::default().fg(prompt_color).add_modifier(Modifier::BOLD)); } } } diff --git a/src/ui/help.rs b/src/ui/help.rs index 82d94fb..8625f98 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -51,7 +51,8 @@ impl Widget for HelpWidget { ("", "", norm), ("Panels", "", head), (" F", "Toggle Formula panel (n:new d:del)", key), - (" C", "Toggle Category panel (Space:cycle-axis)", key), + (" C", "Toggle Category panel (n:new-cat a:add-items)", key), + (" N", "New category quick-add (from anywhere)", key), (" V", "Toggle View panel (n:new d:del Enter:switch)", key), (" Tab", "Focus next open panel", key), ("", "", norm), diff --git a/whatever.improv b/whatever.improv new file mode 100644 index 0000000..4250f06 --- /dev/null +++ b/whatever.improv @@ -0,0 +1,227 @@ +{ + "name": "New Model", + "categories": { + "Type": { + "id": 0, + "name": "Type", + "items": { + "Food": { + "id": 0, + "name": "Food", + "group": null + }, + "Clothing": { + "id": 1, + "name": "Clothing", + "group": null + }, + "Gas": { + "id": 2, + "name": "Gas", + "group": null + }, + "Medical": { + "id": 3, + "name": "Medical", + "group": null + } + }, + "groups": [], + "next_item_id": 4 + }, + "Month": { + "id": 1, + "name": "Month", + "items": { + "Janury": { + "id": 0, + "name": "Janury", + "group": null + }, + "February": { + "id": 1, + "name": "February", + "group": null + }, + "March": { + "id": 2, + "name": "March", + "group": null + } + }, + "groups": [], + "next_item_id": 3 + }, + "Recipient": { + "id": 2, + "name": "Recipient", + "items": { + "Bob": { + "id": 0, + "name": "Bob", + "group": null + }, + "Joe": { + "id": 1, + "name": "Joe", + "group": null + }, + "Mary": { + "id": 2, + "name": "Mary", + "group": null + }, + "Jane": { + "id": 3, + "name": "Jane", + "group": null + }, + "Will": { + "id": 4, + "name": "Will", + "group": null + } + }, + "groups": [], + "next_item_id": 5 + }, + "Payer": { + "id": 3, + "name": "Payer", + "items": { + "Bernadette": { + "id": 0, + "name": "Bernadette", + "group": null + }, + "Ed": { + "id": 1, + "name": "Ed", + "group": null + } + }, + "groups": [], + "next_item_id": 2 + } + }, + "data": [ + [ + [ + [ + "Month", + "Janury" + ], + [ + "Payer", + "Bernadette" + ], + [ + "Recipient", + "Bob" + ], + [ + "Type", + "Clothing" + ] + ], + { + "Number": 3.0 + } + ], + [ + [ + [ + "Month", + "Janury" + ], + [ + "Payer", + "Bernadette" + ], + [ + "Recipient", + "Bob" + ], + [ + "Type", + "Food" + ] + ], + { + "Number": 12.0 + } + ], + [ + [ + [ + "Month", + "Janury" + ], + [ + "Payer", + "Bernadette" + ], + [ + "Recipient", + "Bob" + ], + [ + "Type", + "Gas" + ] + ], + { + "Number": 4.0 + } + ], + [ + [ + [ + "Month", + "Janury" + ], + [ + "Payer", + "Bernadette" + ], + [ + "Recipient", + "Bob" + ], + [ + "Type", + "Medical" + ] + ], + { + "Number": 5.0 + } + ] + ], + "formulas": [], + "views": { + "Default": { + "name": "Default", + "category_axes": { + "Type": "Row", + "Month": "Column", + "Recipient": "Page", + "Payer": "Page" + }, + "page_selections": { + "Recipient": "Bob" + }, + "hidden_items": {}, + "collapsed_groups": {}, + "number_format": ",.0", + "row_offset": 0, + "col_offset": 0, + "selected": [ + 1, + 1 + ] + } + }, + "active_view": "Default", + "next_category_id": 4 +}