Add quick-add mode for categories
N (from anywhere) or n (in Category panel) opens an inline prompt to add categories one after another without typing :add-cat each time. - Yellow border + prompt distinguishes it from item-add (green) - Enter / Tab adds the category and clears the buffer, staying open - Esc returns to the category list - Cursor automatically moves to the newly added category Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -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",
|
||||
|
||||
@ -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 <name>.".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",
|
||||
|
||||
@ -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 {
|
||||
// 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}▌"))
|
||||
}
|
||||
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(Color::Green));
|
||||
buf.set_string(inner.x, sep_y, &sep, Style::default().fg(prompt_color));
|
||||
}
|
||||
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));
|
||||
}
|
||||
buf.set_string(inner.x, prompt_y, &prompt_text,
|
||||
Style::default().fg(prompt_color).add_modifier(Modifier::BOLD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
227
whatever.improv
Normal file
227
whatever.improv
Normal file
@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user