feat(ui): simplify AppMode minibuffer handling and panel rendering

Refactor AppMode to use MinibufferConfig for all text-entry modes. Update
command implementations to use new mode constructors. Introduce
PanelContent trait and replace panel structs with content types. Adjust
rendering to use Panel::new and minibuffer configuration. Update imports
and add MinibufferConfig struct. No functional changes; all behavior
preserved.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
This commit is contained in:
Edward Langley
2026-04-07 02:04:46 -07:00
parent de047ddf1a
commit 4b11b6e321
9 changed files with 376 additions and 320 deletions

View File

@ -2,12 +2,12 @@ use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, Borders, Widget},
};
use crate::model::Model;
use crate::ui::app::AppMode;
use crate::ui::cat_tree::{build_cat_tree, CatTreeEntry};
use crate::ui::panel::PanelContent;
use crate::view::Axis;
fn axis_display(axis: Axis) -> (&'static str, Color) {
@ -19,112 +19,89 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
}
}
pub struct CategoryPanel<'a> {
pub model: &'a Model,
pub mode: &'a AppMode,
pub cursor: usize,
pub expanded: &'a std::collections::HashSet<String>,
pub struct CategoryContent<'a> {
model: &'a Model,
tree: Vec<CatTreeEntry>,
}
impl<'a> CategoryPanel<'a> {
pub fn new(
model: &'a Model,
mode: &'a AppMode,
cursor: usize,
expanded: &'a std::collections::HashSet<String>,
) -> Self {
Self {
model,
mode,
cursor,
expanded,
}
impl<'a> CategoryContent<'a> {
pub fn new(model: &'a Model, expanded: &'a std::collections::HashSet<String>) -> Self {
let tree = build_cat_tree(model, expanded);
Self { model, tree }
}
}
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_cat_add = matches!(self.mode, AppMode::CategoryAdd { .. });
let is_active = matches!(self.mode, AppMode::CategoryPanel) || is_item_add || is_cat_add;
impl PanelContent for CategoryContent<'_> {
fn is_active(&self, mode: &AppMode) -> bool {
matches!(
mode,
AppMode::CategoryPanel | AppMode::ItemAdd { .. } | AppMode::CategoryAdd { .. }
)
}
let (border_color, title) = if is_active {
(Color::Cyan, " Categories ")
fn active_color(&self) -> Color {
Color::Cyan
}
fn title(&self) -> &str {
" Categories "
}
fn item_count(&self) -> usize {
self.tree.len()
}
fn empty_message(&self) -> &str {
"(no categories — use :add-cat <name>)"
}
fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer) {
let y = inner.y + index as u16;
let view = self.model.active_view();
let base_style = if is_selected {
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
(Color::DarkGray, " Categories ")
Style::default()
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.title(title);
let inner = block.inner(area);
block.render(area, buf);
let view = self.model.active_view();
let tree = build_cat_tree(self.model, self.expanded);
if tree.is_empty() {
buf.set_string(
inner.x,
inner.y,
"(no categories — use :add-cat <name>)",
Style::default().fg(Color::DarkGray),
);
return;
if is_selected {
let fill = " ".repeat(inner.width as usize);
buf.set_string(inner.x, y, &fill, base_style);
}
for (i, entry) in tree.iter().enumerate() {
if i as u16 >= inner.height {
break;
}
let y = inner.y + i as u16;
let is_selected = i == self.cursor && is_active;
match &self.tree[index] {
CatTreeEntry::Category {
name,
item_count,
expanded,
} => {
let indicator = if *expanded { "" } else { "" };
let (axis_str, axis_color) = axis_display(view.axis_of(name));
let name_part = format!("{indicator} {name} ({item_count})");
let axis_part = format!(" [{axis_str}]");
let base_style = if is_selected {
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
if is_selected {
let fill = " ".repeat(inner.width as usize);
buf.set_string(inner.x, y, &fill, base_style);
}
match entry {
CatTreeEntry::Category {
name,
item_count,
expanded,
} => {
let indicator = if *expanded { "" } else { "" };
let (axis_str, axis_color) = axis_display(view.axis_of(name));
let name_part = format!("{indicator} {name} ({item_count})");
let axis_part = format!(" [{axis_str}]");
buf.set_string(inner.x, y, &name_part, base_style);
if name_part.len() + axis_part.len() < inner.width as usize {
buf.set_string(
inner.x + name_part.len() as u16,
y,
&axis_part,
if is_selected {
base_style
} else {
Style::default().fg(axis_color)
},
);
}
}
CatTreeEntry::Item { item_name, .. } => {
let label = format!(" · {item_name}");
buf.set_string(inner.x, y, &label, base_style);
buf.set_string(inner.x, y, &name_part, base_style);
if name_part.len() + axis_part.len() < inner.width as usize {
buf.set_string(
inner.x + name_part.len() as u16,
y,
&axis_part,
if is_selected {
base_style
} else {
Style::default().fg(axis_color)
},
);
}
}
CatTreeEntry::Item { item_name, .. } => {
let label = format!(" · {item_name}");
buf.set_string(inner.x, y, &label, base_style);
}
}
}
}