Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00499fc2bf | |||
| 178983bcbf | |||
| e09ddf71a7 | |||
| f8f8f537c3 |
@@ -8,7 +8,3 @@ symbols.json
|
||||
profile.json
|
||||
profile.json.gz
|
||||
bench/*.txt
|
||||
|
||||
# Added by git-smart-commit
|
||||
*.patch
|
||||
*.improv
|
||||
|
||||
+56
-123
@@ -123,7 +123,12 @@ impl CmdRegistry {
|
||||
|
||||
/// Register a command with both a text parser and an interactive constructor.
|
||||
/// The name is derived from a prototype command instance.
|
||||
pub fn register(&mut self, prototype: &dyn Cmd, parse: ParseFn, interactive: InteractiveFn) {
|
||||
pub fn register(
|
||||
&mut self,
|
||||
prototype: &dyn Cmd,
|
||||
parse: ParseFn,
|
||||
interactive: InteractiveFn,
|
||||
) {
|
||||
self.entries.push(CmdEntry {
|
||||
name: prototype.name(),
|
||||
parse: Box::new(parse),
|
||||
@@ -312,14 +317,7 @@ impl Cmd for MoveSelection {
|
||||
let col_max = self.cursor.col_count.saturating_sub(1);
|
||||
let nr = (self.cursor.row as i32 + self.dr).clamp(0, row_max as i32) as usize;
|
||||
let nc = (self.cursor.col as i32 + self.dc).clamp(0, col_max as i32) as usize;
|
||||
viewport_effects(
|
||||
nr,
|
||||
nc,
|
||||
self.cursor.row_offset,
|
||||
self.cursor.col_offset,
|
||||
self.cursor.visible_rows,
|
||||
self.cursor.visible_cols,
|
||||
)
|
||||
viewport_effects(nr, nc, self.cursor.row_offset, self.cursor.col_offset, self.cursor.visible_rows, self.cursor.visible_cols)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,27 +336,16 @@ impl Cmd for JumpToEdge {
|
||||
}
|
||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let (nr, nc) = if self.is_row {
|
||||
let r = if self.end {
|
||||
self.cursor.row_count.saturating_sub(1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let r = if self.end { self.cursor.row_count.saturating_sub(1) } else { 0 };
|
||||
(r, self.cursor.col)
|
||||
} else {
|
||||
let c = if self.end {
|
||||
self.cursor.col_count.saturating_sub(1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let c = if self.end { self.cursor.col_count.saturating_sub(1) } else { 0 };
|
||||
(self.cursor.row, c)
|
||||
};
|
||||
viewport_effects(
|
||||
nr,
|
||||
nc,
|
||||
self.cursor.row_offset,
|
||||
self.cursor.col_offset,
|
||||
self.cursor.visible_rows,
|
||||
self.cursor.visible_cols,
|
||||
nr, nc,
|
||||
self.cursor.row_offset, self.cursor.col_offset,
|
||||
self.cursor.visible_rows, self.cursor.visible_cols,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -724,7 +711,10 @@ impl Cmd for AddRecordRow {
|
||||
let coords: Vec<(String, String)> = page_cats
|
||||
.iter()
|
||||
.map(|cat| {
|
||||
let sel = view.page_selection(cat).unwrap_or("").to_string();
|
||||
let sel = view
|
||||
.page_selection(cat)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
(cat.clone(), sel)
|
||||
})
|
||||
.filter(|(_, v)| !v.is_empty())
|
||||
@@ -782,14 +772,7 @@ impl Cmd for EnterAdvance {
|
||||
} else {
|
||||
(r, c) // already at bottom-right; stay
|
||||
};
|
||||
viewport_effects(
|
||||
nr,
|
||||
nc,
|
||||
self.cursor.row_offset,
|
||||
self.cursor.col_offset,
|
||||
self.cursor.visible_rows,
|
||||
self.cursor.visible_cols,
|
||||
)
|
||||
viewport_effects(nr, nc, self.cursor.row_offset, self.cursor.col_offset, self.cursor.visible_rows, self.cursor.visible_cols)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,9 +1424,9 @@ impl Cmd for TogglePruneEmpty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between records mode and pivot mode using the view stack.
|
||||
/// Entering records mode creates a `_Records` view and switches to it.
|
||||
/// Leaving records mode navigates back to the previous view.
|
||||
/// Toggle between records mode (_Index on Row, _Dim on Column) and
|
||||
/// pivot mode (auto-assigned axes). In records mode every cell is shown
|
||||
/// as a flat row; in pivot mode the view is a cross-tab.
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleRecordsMode;
|
||||
impl Cmd for ToggleRecordsMode {
|
||||
@@ -1933,10 +1916,7 @@ fn commit_cell_value(key: &CellKey, value: &str, effects: &mut Vec<Box<dyn Effec
|
||||
effects.push(Box::new(effect::SetCell(key.clone(), CellValue::Number(n))));
|
||||
effects.push(effect::mark_dirty());
|
||||
} else {
|
||||
effects.push(Box::new(effect::SetCell(
|
||||
key.clone(),
|
||||
CellValue::Text(value.to_string()),
|
||||
)));
|
||||
effects.push(Box::new(effect::SetCell(key.clone(), CellValue::Text(value.to_string()))));
|
||||
effects.push(effect::mark_dirty());
|
||||
}
|
||||
}
|
||||
@@ -2440,9 +2420,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
r.register_pure(&AddItemInGroupCmd(vec![]), AddItemInGroupCmd::parse);
|
||||
r.register_pure(&SetCellCmd(vec![]), SetCellCmd::parse);
|
||||
r.register(
|
||||
&ClearCellCommand {
|
||||
key: CellKey::new(vec![]),
|
||||
},
|
||||
&ClearCellCommand { key: CellKey::new(vec![]) },
|
||||
|args| {
|
||||
if args.is_empty() {
|
||||
return Err("clear-cell requires at least one Cat/Item coordinate".into());
|
||||
@@ -2473,11 +2451,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|
||||
// ── Navigation ───────────────────────────────────────────────────────
|
||||
r.register(
|
||||
&MoveSelection {
|
||||
dr: 0,
|
||||
dc: 0,
|
||||
cursor: CursorState::default(),
|
||||
},
|
||||
&MoveSelection { dr: 0, dc: 0, cursor: CursorState::default() },
|
||||
|args| {
|
||||
require_args("move-selection", args, 2)?;
|
||||
let dr = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
@@ -2512,40 +2486,18 @@ pub fn default_registry() -> CmdRegistry {
|
||||
macro_rules! reg_jump {
|
||||
($r:expr, $is_row:expr, $end:expr, $name:expr) => {
|
||||
$r.register(
|
||||
&JumpToEdge {
|
||||
cursor: CursorState::default(),
|
||||
is_row: $is_row,
|
||||
end: $end,
|
||||
cmd_name: $name,
|
||||
},
|
||||
|_| {
|
||||
Ok(Box::new(JumpToEdge {
|
||||
cursor: CursorState::default(),
|
||||
is_row: $is_row,
|
||||
end: $end,
|
||||
cmd_name: $name,
|
||||
}))
|
||||
},
|
||||
|_, ctx| {
|
||||
Ok(Box::new(JumpToEdge {
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
is_row: $is_row,
|
||||
end: $end,
|
||||
cmd_name: $name,
|
||||
}))
|
||||
},
|
||||
&JumpToEdge { cursor: CursorState::default(), is_row: $is_row, end: $end, cmd_name: $name },
|
||||
|_| Ok(Box::new(JumpToEdge { cursor: CursorState::default(), is_row: $is_row, end: $end, cmd_name: $name })),
|
||||
|_, ctx| Ok(Box::new(JumpToEdge { cursor: CursorState::from_ctx(ctx), is_row: $is_row, end: $end, cmd_name: $name })),
|
||||
);
|
||||
};
|
||||
}
|
||||
reg_jump!(r, true, false, "jump-first-row");
|
||||
reg_jump!(r, true, true, "jump-last-row");
|
||||
reg_jump!(r, true, false, "jump-first-row");
|
||||
reg_jump!(r, true, true, "jump-last-row");
|
||||
reg_jump!(r, false, false, "jump-first-col");
|
||||
reg_jump!(r, false, true, "jump-last-col");
|
||||
reg_jump!(r, false, true, "jump-last-col");
|
||||
r.register(
|
||||
&ScrollRows {
|
||||
delta: 0,
|
||||
cursor: CursorState::default(),
|
||||
},
|
||||
&ScrollRows { delta: 0, cursor: CursorState::default() },
|
||||
|args| {
|
||||
require_args("scroll-rows", args, 1)?;
|
||||
let n = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
@@ -2573,10 +2525,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&PageScroll {
|
||||
direction: 0,
|
||||
cursor: CursorState::default(),
|
||||
},
|
||||
&PageScroll { direction: 0, cursor: CursorState::default() },
|
||||
|args| {
|
||||
require_args("page-scroll", args, 1)?;
|
||||
let dir = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
@@ -2595,9 +2544,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&EnterAdvance {
|
||||
cursor: CursorState::default(),
|
||||
},
|
||||
&EnterAdvance { cursor: CursorState::default() },
|
||||
|_| {
|
||||
Ok(Box::new(EnterAdvance {
|
||||
cursor: CursorState {
|
||||
@@ -2621,9 +2568,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|
||||
// ── Cell operations ──────────────────────────────────────────────────
|
||||
r.register(
|
||||
&YankCell {
|
||||
key: CellKey::new(vec![]),
|
||||
},
|
||||
&YankCell { key: CellKey::new(vec![]) },
|
||||
|args| {
|
||||
if args.is_empty() {
|
||||
return Err("yank requires at least one Cat/Item coordinate".into());
|
||||
@@ -2667,16 +2612,20 @@ pub fn default_registry() -> CmdRegistry {
|
||||
r.register_nullary(|| Box::new(SaveCmd));
|
||||
r.register_nullary(|| Box::new(EnterSearchMode));
|
||||
r.register(
|
||||
&EnterEditMode {
|
||||
initial_value: String::new(),
|
||||
},
|
||||
&EnterEditMode { initial_value: String::new() },
|
||||
|args| {
|
||||
let val = args.first().cloned().unwrap_or_default();
|
||||
Ok(Box::new(EnterEditMode { initial_value: val }))
|
||||
},
|
||||
|_args, ctx| {
|
||||
let current = ctx
|
||||
.cell_key
|
||||
.as_ref()
|
||||
.and_then(|k| ctx.model.get_cell(k).cloned())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default();
|
||||
Ok(Box::new(EnterEditMode {
|
||||
initial_value: ctx.display_value.clone(),
|
||||
initial_value: current,
|
||||
}))
|
||||
},
|
||||
);
|
||||
@@ -2744,11 +2693,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|
||||
// ── Panel operations ─────────────────────────────────────────────────
|
||||
r.register(
|
||||
&TogglePanelAndFocus {
|
||||
panel: Panel::Formula,
|
||||
open: true,
|
||||
focused: true,
|
||||
},
|
||||
&TogglePanelAndFocus { panel: Panel::Formula, open: true, focused: true },
|
||||
|args| {
|
||||
// Parse: toggle-panel-and-focus <panel> [open] [focused]
|
||||
require_args("toggle-panel-and-focus", args, 1)?;
|
||||
@@ -2771,14 +2716,8 @@ pub fn default_registry() -> CmdRegistry {
|
||||
Panel::View => ctx.view_panel_open,
|
||||
};
|
||||
let currently_focused = match panel {
|
||||
Panel::Formula => matches!(
|
||||
ctx.mode,
|
||||
AppMode::FormulaPanel | AppMode::FormulaEdit { .. }
|
||||
),
|
||||
Panel::Category => matches!(
|
||||
ctx.mode,
|
||||
AppMode::CategoryPanel | AppMode::CategoryAdd { .. } | AppMode::ItemAdd { .. }
|
||||
),
|
||||
Panel::Formula => matches!(ctx.mode, AppMode::FormulaPanel | AppMode::FormulaEdit { .. }),
|
||||
Panel::Category => matches!(ctx.mode, AppMode::CategoryPanel | AppMode::CategoryAdd { .. } | AppMode::ItemAdd { .. }),
|
||||
Panel::View => matches!(ctx.mode, AppMode::ViewPanel),
|
||||
};
|
||||
let (open, focused) = if currently_open && currently_focused {
|
||||
@@ -2794,10 +2733,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&TogglePanelVisibility {
|
||||
panel: Panel::Formula,
|
||||
currently_open: false,
|
||||
},
|
||||
&TogglePanelVisibility { panel: Panel::Formula, currently_open: false },
|
||||
|args| {
|
||||
require_args("toggle-panel-visibility", args, 1)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
@@ -2821,11 +2757,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&CyclePanelFocus {
|
||||
formula_open: false,
|
||||
category_open: false,
|
||||
view_open: false,
|
||||
},
|
||||
&CyclePanelFocus { formula_open: false, category_open: false, view_open: false },
|
||||
|_| {
|
||||
Ok(Box::new(CyclePanelFocus {
|
||||
formula_open: false,
|
||||
@@ -2842,12 +2774,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
},
|
||||
);
|
||||
r.register(
|
||||
&MovePanelCursor {
|
||||
panel: Panel::Formula,
|
||||
delta: 0,
|
||||
current: 0,
|
||||
max: 0,
|
||||
},
|
||||
&MovePanelCursor { panel: Panel::Formula, delta: 0, current: 0, max: 0 },
|
||||
|args| {
|
||||
require_args("move-panel-cursor", args, 2)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
@@ -2876,7 +2803,9 @@ pub fn default_registry() -> CmdRegistry {
|
||||
}))
|
||||
},
|
||||
);
|
||||
r.register_nullary(|| Box::new(DeleteFormulaAtCursor));
|
||||
r.register_nullary(|| {
|
||||
Box::new(DeleteFormulaAtCursor)
|
||||
});
|
||||
r.register_nullary(|| Box::new(AddRecordRow));
|
||||
r.register_nullary(|| Box::new(OpenRecordRow));
|
||||
r.register_nullary(|| Box::new(TogglePruneEmpty));
|
||||
@@ -2904,8 +2833,12 @@ pub fn default_registry() -> CmdRegistry {
|
||||
});
|
||||
|
||||
// ── Grid operations ──────────────────────────────────────────────────
|
||||
r.register_nullary(|| Box::new(ToggleGroupUnderCursor));
|
||||
r.register_nullary(|| Box::new(ToggleColGroupUnderCursor));
|
||||
r.register_nullary(|| {
|
||||
Box::new(ToggleGroupUnderCursor)
|
||||
});
|
||||
r.register_nullary(|| {
|
||||
Box::new(ToggleColGroupUnderCursor)
|
||||
});
|
||||
r.register_nullary(|| Box::new(HideSelectedRowItem));
|
||||
|
||||
// ── Text buffer ──────────────────────────────────────────────────────
|
||||
|
||||
+8
-37
@@ -143,7 +143,8 @@ impl Keymap {
|
||||
.or_else(|| {
|
||||
// Retry Char keys without modifiers (shift is implicit in the char)
|
||||
if matches!(key, KeyCode::Char(_)) && mods != KeyModifiers::NONE {
|
||||
self.bindings.get(&KeyPattern::Key(key, KeyModifiers::NONE))
|
||||
self.bindings
|
||||
.get(&KeyPattern::Key(key, KeyModifiers::NONE))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -425,24 +426,9 @@ impl KeymapSet {
|
||||
fp.bind(KeyCode::Char('o'), none, "enter-formula-edit");
|
||||
fp.bind(KeyCode::Char('d'), none, "delete-formula-at-cursor");
|
||||
fp.bind(KeyCode::Delete, none, "delete-formula-at-cursor");
|
||||
fp.bind_args(
|
||||
KeyCode::Char('F'),
|
||||
none,
|
||||
"toggle-panel-and-focus",
|
||||
vec!["formula".into()],
|
||||
);
|
||||
fp.bind_args(
|
||||
KeyCode::Char('C'),
|
||||
none,
|
||||
"toggle-panel-and-focus",
|
||||
vec!["category".into()],
|
||||
);
|
||||
fp.bind_args(
|
||||
KeyCode::Char('V'),
|
||||
none,
|
||||
"toggle-panel-and-focus",
|
||||
vec!["view".into()],
|
||||
);
|
||||
fp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]);
|
||||
fp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]);
|
||||
fp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]);
|
||||
set.insert(ModeKey::FormulaPanel, Arc::new(fp));
|
||||
|
||||
// ── Category panel ───────────────────────────────────────────────
|
||||
@@ -523,24 +509,9 @@ impl KeymapSet {
|
||||
vp.bind(KeyCode::Char('o'), none, "create-and-switch-view");
|
||||
vp.bind(KeyCode::Char('d'), none, "delete-view-at-cursor");
|
||||
vp.bind(KeyCode::Delete, none, "delete-view-at-cursor");
|
||||
vp.bind_args(
|
||||
KeyCode::Char('V'),
|
||||
none,
|
||||
"toggle-panel-and-focus",
|
||||
vec!["view".into()],
|
||||
);
|
||||
vp.bind_args(
|
||||
KeyCode::Char('C'),
|
||||
none,
|
||||
"toggle-panel-and-focus",
|
||||
vec!["category".into()],
|
||||
);
|
||||
vp.bind_args(
|
||||
KeyCode::Char('F'),
|
||||
none,
|
||||
"toggle-panel-and-focus",
|
||||
vec!["formula".into()],
|
||||
);
|
||||
vp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]);
|
||||
vp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]);
|
||||
vp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]);
|
||||
set.insert(ModeKey::ViewPanel, Arc::new(vp));
|
||||
|
||||
// ── Tile select ──────────────────────────────────────────────────
|
||||
|
||||
+11
-3
@@ -257,7 +257,6 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
|
||||
f.render_widget(
|
||||
GridWidget::new(
|
||||
&app.model,
|
||||
&app.layout,
|
||||
&app.mode,
|
||||
&app.search_query,
|
||||
&app.buffers,
|
||||
@@ -283,7 +282,11 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||
Some((format!("edit: {buf}▌"), Color::Green))
|
||||
}
|
||||
AppMode::FormulaEdit { .. } => {
|
||||
let buf = app.buffers.get("formula").map(|s| s.as_str()).unwrap_or("");
|
||||
let buf = app
|
||||
.buffers
|
||||
.get("formula")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
Some((format!("formula: {buf}▌"), Color::Cyan))
|
||||
}
|
||||
AppMode::CategoryAdd { .. } => {
|
||||
@@ -299,7 +302,11 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
|
||||
Some((format!("add item to {category}: {buf}▌"), Color::Green))
|
||||
}
|
||||
AppMode::ExportPrompt { .. } => {
|
||||
let buf = app.buffers.get("export").map(|s| s.as_str()).unwrap_or("");
|
||||
let buf = app
|
||||
.buffers
|
||||
.get("export")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
Some((format!("export path: {buf}▌"), Color::Yellow))
|
||||
}
|
||||
_ => None,
|
||||
@@ -342,6 +349,7 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) {
|
||||
f.render_widget(Paragraph::new(line).style(mode_style(&app.mode)), area);
|
||||
}
|
||||
|
||||
|
||||
fn draw_welcome(f: &mut Frame, area: Rect) {
|
||||
let popup = centered_popup(area, 58, 20);
|
||||
let inner = draw_popup_frame(f, popup, " Welcome to improvise ", Color::Blue);
|
||||
|
||||
+4
-1
@@ -93,6 +93,7 @@ impl std::fmt::Display for CellValue {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct InternedKey(pub Vec<(Symbol, Symbol)>);
|
||||
|
||||
|
||||
/// Serialized as a list of (key, value) pairs so CellKey doesn't need
|
||||
/// to implement the `Serialize`-as-string requirement for JSON object keys.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -179,7 +180,9 @@ impl DataStore {
|
||||
|
||||
/// Iterate over all cells, yielding (CellKey, &CellValue) pairs.
|
||||
pub fn iter_cells(&self) -> impl Iterator<Item = (CellKey, &CellValue)> {
|
||||
self.cells.iter().map(|(k, v)| (self.to_cell_key(k), v))
|
||||
self.cells
|
||||
.iter()
|
||||
.map(|(k, v)| (self.to_cell_key(k), v))
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &CellKey) {
|
||||
|
||||
+2
-1
@@ -132,7 +132,8 @@ impl Model {
|
||||
self.data.remove(&k);
|
||||
}
|
||||
// Remove formulas targeting this category
|
||||
self.formulas.retain(|f| f.target_category != name);
|
||||
self.formulas
|
||||
.retain(|f| f.target_category != name);
|
||||
}
|
||||
|
||||
/// Remove an item from a category and all cells that reference it.
|
||||
|
||||
+16
-34
@@ -128,7 +128,9 @@ impl<'a> GridWidget<'a> {
|
||||
v
|
||||
};
|
||||
let col_x_at = |ci: usize| -> u16 {
|
||||
area.x + row_header_width + col_x[ci].saturating_sub(col_x[col_offset])
|
||||
area.x
|
||||
+ row_header_width
|
||||
+ col_x[ci].saturating_sub(col_x[col_offset])
|
||||
};
|
||||
let col_w_at = |ci: usize| -> u16 { *col_widths.get(ci).unwrap_or(&MIN_COL_WIDTH) };
|
||||
|
||||
@@ -179,11 +181,7 @@ impl<'a> GridWidget<'a> {
|
||||
buf.set_string(
|
||||
x,
|
||||
y,
|
||||
format!(
|
||||
"{:<width$}",
|
||||
truncate(&label, cw.saturating_sub(1)),
|
||||
width = cw
|
||||
),
|
||||
format!("{:<width$}", truncate(&label, cw.saturating_sub(1)), width = cw),
|
||||
group_style,
|
||||
);
|
||||
}
|
||||
@@ -235,11 +233,7 @@ impl<'a> GridWidget<'a> {
|
||||
buf.set_string(
|
||||
x,
|
||||
y,
|
||||
format!(
|
||||
"{:>width$}",
|
||||
truncate(&label, cw.saturating_sub(1)),
|
||||
width = cw
|
||||
),
|
||||
format!("{:>width$}", truncate(&label, cw.saturating_sub(1)), width = cw),
|
||||
styled,
|
||||
);
|
||||
}
|
||||
@@ -363,9 +357,7 @@ impl<'a> GridWidget<'a> {
|
||||
ds.pending_edits
|
||||
.get(&(ri, col_name))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
layout.display_text(self.model, ri, ci, fmt_comma, fmt_decimals)
|
||||
})
|
||||
.unwrap_or_else(|| layout.display_text(self.model, ri, ci, fmt_comma, fmt_decimals))
|
||||
} else {
|
||||
layout.display_text(self.model, ri, ci, fmt_comma, fmt_decimals)
|
||||
};
|
||||
@@ -502,9 +494,9 @@ impl<'a> Widget for GridWidget<'a> {
|
||||
block.render(area, buf);
|
||||
|
||||
// Page axis bar
|
||||
if !self.layout.page_coords.is_empty() && inner.height > 0 {
|
||||
let page_info: Vec<String> = self
|
||||
.layout
|
||||
let layout = GridLayout::new(self.model, self.model.active_view());
|
||||
if !layout.page_coords.is_empty() && inner.height > 0 {
|
||||
let page_info: Vec<String> = layout
|
||||
.page_coords
|
||||
.iter()
|
||||
.map(|(cat, sel)| format!("{cat} = {sel}"))
|
||||
@@ -533,12 +525,7 @@ impl<'a> Widget for GridWidget<'a> {
|
||||
/// Header widths use the widest *individual* level label (not the joined
|
||||
/// multi-level string), matching how the grid renderer draws each level on
|
||||
/// its own row with repeat-suppression.
|
||||
pub fn compute_col_widths(
|
||||
model: &Model,
|
||||
layout: &GridLayout,
|
||||
fmt_comma: bool,
|
||||
fmt_decimals: u8,
|
||||
) -> Vec<u16> {
|
||||
pub fn compute_col_widths(model: &Model, layout: &GridLayout, fmt_comma: bool, fmt_decimals: u8) -> Vec<u16> {
|
||||
let n = layout.col_count();
|
||||
let mut widths = vec![0u16; n];
|
||||
// Measure individual header level labels
|
||||
@@ -620,16 +607,9 @@ pub fn compute_row_header_width(layout: &GridLayout) -> u16 {
|
||||
}
|
||||
|
||||
/// Count how many columns fit starting from `col_offset` given the available width.
|
||||
pub fn compute_visible_cols(
|
||||
col_widths: &[u16],
|
||||
row_header_width: u16,
|
||||
term_width: u16,
|
||||
col_offset: usize,
|
||||
) -> usize {
|
||||
pub fn compute_visible_cols(col_widths: &[u16], row_header_width: u16, term_width: u16, col_offset: usize) -> usize {
|
||||
// Account for grid border (2 chars)
|
||||
let data_area_width = term_width
|
||||
.saturating_sub(2)
|
||||
.saturating_sub(row_header_width);
|
||||
let data_area_width = term_width.saturating_sub(2).saturating_sub(row_header_width);
|
||||
let mut acc = 0u16;
|
||||
let mut count = 0usize;
|
||||
for ci in col_offset..col_widths.len() {
|
||||
@@ -677,7 +657,6 @@ mod tests {
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::Model;
|
||||
use crate::ui::app::AppMode;
|
||||
use crate::view::GridLayout;
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -733,7 +712,10 @@ mod tests {
|
||||
// Fill every cell so nothing is pruned as empty.
|
||||
for t in ["Food", "Clothing"] {
|
||||
for mo in ["Jan", "Feb"] {
|
||||
m.set_cell(coord(&[("Type", t), ("Month", mo)]), CellValue::Number(1.0));
|
||||
m.set_cell(
|
||||
coord(&[("Type", t), ("Month", mo)]),
|
||||
CellValue::Number(1.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
m
|
||||
|
||||
+5
-22
@@ -32,26 +32,10 @@ impl<'a> TileBar<'a> {
|
||||
tile_cat_idx,
|
||||
}
|
||||
}
|
||||
fn axis_display(axis: Axis) -> (&'static str, Color) {
|
||||
match axis {
|
||||
Axis::Row => ("|", Color::Green),
|
||||
Axis::Column => ("-", Color::Blue),
|
||||
Axis::Page => ("=", Color::Magenta),
|
||||
Axis::None => (".", Color::DarkGray),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for TileBar<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Clear the line to avoid stale characters from previous renders
|
||||
buf.set_string(
|
||||
area.x,
|
||||
area.y,
|
||||
" ".repeat(area.width as usize),
|
||||
Style::default(),
|
||||
);
|
||||
|
||||
let view = self.model.active_view();
|
||||
|
||||
let selected_cat_idx = if matches!(self.mode, AppMode::TileSelect) {
|
||||
@@ -66,7 +50,7 @@ impl<'a> Widget for TileBar<'a> {
|
||||
|
||||
let cat_names: Vec<&str> = self.model.category_names();
|
||||
for (i, cat_name) in cat_names.iter().enumerate() {
|
||||
let (axis_symbol, axis_color) = TileBar::axis_display(view.axis_of(cat_name));
|
||||
let (axis_symbol, axis_color) = axis_display(view.axis_of(cat_name));
|
||||
let label = format!(" [{cat_name} {axis_symbol}] ");
|
||||
let is_selected = selected_cat_idx == Some(i);
|
||||
|
||||
@@ -79,23 +63,22 @@ impl<'a> Widget for TileBar<'a> {
|
||||
Style::default().fg(axis_color)
|
||||
};
|
||||
|
||||
let label_w = label.width() as u16;
|
||||
if x + label_w > area.x + area.width {
|
||||
if x + label.len() as u16 > area.x + area.width {
|
||||
break;
|
||||
}
|
||||
buf.set_string(x, area.y, &label, style);
|
||||
x += label_w;
|
||||
x += label.len() as u16;
|
||||
}
|
||||
|
||||
// Hint
|
||||
if matches!(self.mode, AppMode::TileSelect) {
|
||||
let hint = " [Enter] cycle axis [r/c/p] set axis [←→] select [Esc] cancel";
|
||||
if x + hint.width() as u16 <= area.x + area.width {
|
||||
if x + hint.len() as u16 <= area.x + area.width {
|
||||
buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray));
|
||||
}
|
||||
} else {
|
||||
let hint = " Ctrl+↑↓←→ to move tiles";
|
||||
if x + hint.width() as u16 <= area.x + area.width {
|
||||
if x + hint.len() as u16 <= area.x + area.width {
|
||||
buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -157,7 +157,7 @@ impl GridLayout {
|
||||
.collect()
|
||||
};
|
||||
// Sort for deterministic ordering
|
||||
records.sort_by(|a, b| a.0 .0.cmp(&b.0 .0));
|
||||
records.sort_by(|a, b| a.0.0.cmp(&b.0.0));
|
||||
|
||||
// Synthesize row items: one per record, labeled with its index
|
||||
let row_items: Vec<AxisEntry> = (0..records.len())
|
||||
@@ -200,7 +200,7 @@ impl GridLayout {
|
||||
// col_item is a category name
|
||||
let found = record
|
||||
.0
|
||||
.0
|
||||
.0
|
||||
.iter()
|
||||
.find(|(c, _)| c == &col_item)
|
||||
.map(|(_, v)| v.clone());
|
||||
@@ -610,7 +610,10 @@ mod tests {
|
||||
m.category_mut("Col").unwrap().add_item("Y");
|
||||
// Only X has data; Y is entirely empty
|
||||
m.set_cell(
|
||||
CellKey::new(vec![("Row".into(), "A".into()), ("Col".into(), "X".into())]),
|
||||
CellKey::new(vec![
|
||||
("Row".into(), "A".into()),
|
||||
("Col".into(), "X".into()),
|
||||
]),
|
||||
CellValue::Number(1.0),
|
||||
);
|
||||
|
||||
@@ -640,9 +643,7 @@ mod tests {
|
||||
v.set_axis("_Dim", Axis::Column);
|
||||
let layout = GridLayout::new(&m, m.active_view());
|
||||
assert!(layout.is_records_mode());
|
||||
let cols: Vec<String> = (0..layout.col_count())
|
||||
.map(|i| layout.col_label(i))
|
||||
.collect();
|
||||
let cols: Vec<String> = (0..layout.col_count()).map(|i| layout.col_label(i)).collect();
|
||||
// All columns return synthetic keys
|
||||
let value_col = cols.iter().position(|c| c == "Value").unwrap();
|
||||
let key = layout.cell_key(0, value_col).unwrap();
|
||||
@@ -662,9 +663,7 @@ mod tests {
|
||||
v.set_axis("_Index", Axis::Row);
|
||||
v.set_axis("_Dim", Axis::Column);
|
||||
let layout = GridLayout::new(&m, m.active_view());
|
||||
let cols: Vec<String> = (0..layout.col_count())
|
||||
.map(|i| layout.col_label(i))
|
||||
.collect();
|
||||
let cols: Vec<String> = (0..layout.col_count()).map(|i| layout.col_label(i)).collect();
|
||||
|
||||
// Value column resolves to the cell value
|
||||
let value_col = cols.iter().position(|c| c == "Value").unwrap();
|
||||
|
||||
Reference in New Issue
Block a user