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::FormulaEdit { .. } => "FORMULA",
|
||||||
AppMode::FormulaPanel => "FORMULAS",
|
AppMode::FormulaPanel => "FORMULAS",
|
||||||
AppMode::CategoryPanel => "CATEGORIES",
|
AppMode::CategoryPanel => "CATEGORIES",
|
||||||
|
AppMode::CategoryAdd { .. } => "NEW CATEGORY",
|
||||||
AppMode::ItemAdd { .. } => "ADD ITEMS",
|
AppMode::ItemAdd { .. } => "ADD ITEMS",
|
||||||
AppMode::ViewPanel => "VIEWS",
|
AppMode::ViewPanel => "VIEWS",
|
||||||
AppMode::TileSelect { .. } => "TILES",
|
AppMode::TileSelect { .. } => "TILES",
|
||||||
|
|||||||
@ -18,6 +18,8 @@ pub enum AppMode {
|
|||||||
FormulaEdit { buffer: String },
|
FormulaEdit { buffer: String },
|
||||||
FormulaPanel,
|
FormulaPanel,
|
||||||
CategoryPanel,
|
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.
|
/// Quick-add items to `category`: Enter adds and stays open, Esc closes.
|
||||||
ItemAdd { category: String, buffer: String },
|
ItemAdd { category: String, buffer: String },
|
||||||
ViewPanel,
|
ViewPanel,
|
||||||
@ -89,6 +91,7 @@ impl App {
|
|||||||
AppMode::FormulaEdit { .. } => { self.handle_formula_edit_key(key)?; }
|
AppMode::FormulaEdit { .. } => { self.handle_formula_edit_key(key)?; }
|
||||||
AppMode::FormulaPanel => { self.handle_formula_panel_key(key)?; }
|
AppMode::FormulaPanel => { self.handle_formula_panel_key(key)?; }
|
||||||
AppMode::CategoryPanel => { self.handle_category_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::ItemAdd { .. } => { self.handle_item_add_key(key)?; }
|
||||||
AppMode::ViewPanel => { self.handle_view_panel_key(key)?; }
|
AppMode::ViewPanel => { self.handle_view_panel_key(key)?; }
|
||||||
AppMode::TileSelect { .. } => { self.handle_tile_select_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 ──────────────────────────────────────────────
|
// ── Tile movement ──────────────────────────────────────────────
|
||||||
// T = enter tile select mode (single key, no Ctrl needed)
|
// T = enter tile select mode (single key, no Ctrl needed)
|
||||||
(KeyCode::Char('T'), _) => {
|
(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') => {
|
KeyCode::Char('a') | KeyCode::Char('o') => {
|
||||||
if let Some(cat_name) = cat_names.get(self.cat_panel_cursor) {
|
if let Some(cat_name) = cat_names.get(self.cat_panel_cursor) {
|
||||||
self.mode = AppMode::ItemAdd {
|
self.mode = AppMode::ItemAdd {
|
||||||
@ -625,7 +638,7 @@ impl App {
|
|||||||
buffer: String::new(),
|
buffer: String::new(),
|
||||||
};
|
};
|
||||||
} else {
|
} 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(())
|
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<()> {
|
fn handle_item_add_key(&mut self, key: KeyEvent) -> Result<()> {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
@ -1013,8 +1065,9 @@ impl App {
|
|||||||
AppMode::Editing { .. } => "Enter:commit Esc:cancel",
|
AppMode::Editing { .. } => "Enter:commit Esc:cancel",
|
||||||
AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back",
|
AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back",
|
||||||
AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression",
|
AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression",
|
||||||
AppMode::CategoryPanel => "jk:nav Space:cycle-axis a:add-items Esc:back",
|
AppMode::CategoryPanel => "jk:nav Space:cycle-axis n:new-cat a:add-items Esc:back",
|
||||||
AppMode::ItemAdd { category, .. } => "Enter:add & continue Tab:same Esc:done",
|
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::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::TileSelect { .. } => "hl:select Enter:cycle r/c/p:set-axis Esc:back",
|
||||||
AppMode::CommandMode { .. } => ":q quit :w save :import :add-cat :formula :help",
|
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> {
|
impl<'a> Widget for CategoryPanel<'a> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let is_item_add = matches!(self.mode, AppMode::ItemAdd { .. });
|
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) ")
|
(Color::Green, " Categories — Adding items (Enter:add Esc:done) ")
|
||||||
} else if is_active {
|
} else if is_active {
|
||||||
(Color::Cyan, " Categories a:add-items Space:cycle-axis ")
|
(Color::Cyan, " Categories n:new a:add-items Space:axis ")
|
||||||
} else {
|
} else {
|
||||||
(Color::DarkGray, " Categories ")
|
(Color::DarkGray, " Categories ")
|
||||||
};
|
};
|
||||||
@ -108,19 +111,26 @@ impl<'a> Widget for CategoryPanel<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline prompt at the bottom when in ItemAdd mode
|
// Inline prompt at the bottom for CategoryAdd or ItemAdd
|
||||||
if let AppMode::ItemAdd { category, buffer } = self.mode {
|
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 sep_y = inner.y + list_height;
|
||||||
let prompt_y = sep_y + 1;
|
let prompt_y = sep_y + 1;
|
||||||
if sep_y < inner.y + inner.height {
|
if sep_y < inner.y + inner.height {
|
||||||
let sep = "─".repeat(inner.width as usize);
|
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 {
|
if prompt_y < inner.y + inner.height {
|
||||||
let prompt = format!(" + {buffer}▌");
|
buf.set_string(inner.x, prompt_y, &prompt_text,
|
||||||
buf.set_string(inner.x, prompt_y, &prompt,
|
Style::default().fg(prompt_color).add_modifier(Modifier::BOLD));
|
||||||
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,8 @@ impl Widget for HelpWidget {
|
|||||||
("", "", norm),
|
("", "", norm),
|
||||||
("Panels", "", head),
|
("Panels", "", head),
|
||||||
(" F", "Toggle Formula panel (n:new d:del)", key),
|
(" 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),
|
(" V", "Toggle View panel (n:new d:del Enter:switch)", key),
|
||||||
(" Tab", "Focus next open panel", key),
|
(" Tab", "Focus next open panel", key),
|
||||||
("", "", norm),
|
("", "", 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