Feat: UI/UX fixes + removing clones from places
This commit is contained in:
@@ -1,144 +0,0 @@
|
||||
use crate::page::Page;
|
||||
|
||||
pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut bindings = vec![
|
||||
("F1–F7", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine/Script"),
|
||||
("Ctrl+←→↑↓", "Navigate", "Switch between adjacent views"),
|
||||
];
|
||||
if !plugin_mode {
|
||||
bindings.push(("q", "Quit", "Quit application"));
|
||||
}
|
||||
bindings.extend([
|
||||
("s", "Save", "Save project"),
|
||||
("l", "Load", "Load project"),
|
||||
("?", "Keybindings", "Show this help"),
|
||||
("F12", "Restart", "Full restart from step 0"),
|
||||
]);
|
||||
|
||||
// Page-specific bindings
|
||||
match page {
|
||||
Page::Main => {
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||
}
|
||||
bindings.push(("Alt+↑↓", "Pattern", "Previous/next pattern"));
|
||||
bindings.push(("Alt+←→", "Bank", "Previous/next bank"));
|
||||
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
|
||||
bindings.push(("Shift+←→↑↓", "Select", "Extend selection"));
|
||||
bindings.push(("Esc", "Clear", "Clear selection"));
|
||||
bindings.push(("Enter", "Edit", "Open step editor"));
|
||||
bindings.push(("t", "Toggle", "Toggle selected steps"));
|
||||
bindings.push(("p", "Prelude", "Edit prelude script"));
|
||||
bindings.push(("Tab", "Samples", "Toggle sample browser"));
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy selected steps"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste steps"));
|
||||
bindings.push(("Ctrl+B", "Link", "Paste as linked steps"));
|
||||
bindings.push(("Ctrl+D", "Duplicate", "Duplicate selection"));
|
||||
bindings.push(("Ctrl+H", "Harden", "Convert links to copies"));
|
||||
bindings.push(("Del", "Delete", "Delete step(s)"));
|
||||
bindings.push(("< >", "Length", "Decrease/increase pattern length"));
|
||||
bindings.push(("[ ]", "Speed", "Decrease/increase pattern speed"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("+ -", "Tempo", "Increase/decrease tempo"));
|
||||
bindings.push(("T", "Set tempo", "Open tempo input"));
|
||||
}
|
||||
bindings.push(("L", "Set length", "Open length input"));
|
||||
bindings.push(("S", "Set speed", "Open speed input"));
|
||||
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
||||
bindings.push(("r", "Rename", "Rename current step"));
|
||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||
bindings.push((":", "Jump", "Jump to step number"));
|
||||
bindings.push(("e", "Euclidean", "Distribute linked steps using Euclidean rhythm"));
|
||||
bindings.push(("m", "Mute", "Arm mute for current pattern"));
|
||||
bindings.push(("x", "Solo", "Arm solo for current pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("d", "Eval prelude", "Re-evaluate prelude without editing"));
|
||||
bindings.push(("g", "Share", "Export pattern to clipboard"));
|
||||
bindings.push(("G", "Import", "Import pattern from clipboard"));
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
bindings.push(("Shift+↑↓", "Select", "Extend selection"));
|
||||
bindings.push(("Alt+↑↓", "Shift", "Move patterns up/down"));
|
||||
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
||||
}
|
||||
bindings.push(("Esc", "Back", "Clear armed or go back"));
|
||||
bindings.push(("c", "Launch", "Launch armed changes"));
|
||||
bindings.push(("p", "Arm play", "Arm pattern play toggle"));
|
||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||
bindings.push(("d", "Describe", "Add description to pattern"));
|
||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
||||
bindings.push(("m", "Mute", "Arm mute for pattern"));
|
||||
bindings.push(("x", "Solo", "Arm solo for pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("g", "Share", "Export bank or pattern to clipboard"));
|
||||
bindings.push(("G", "Import", "Import bank or pattern from clipboard"));
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy bank/pattern"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||
bindings.push(("Ctrl+Z", "Undo", "Undo last action"));
|
||||
bindings.push(("Ctrl+Shift+Z", "Redo", "Redo last action"));
|
||||
}
|
||||
Page::Engine => {
|
||||
bindings.push(("Tab", "Section", "Next section"));
|
||||
bindings.push(("Shift+Tab", "Section", "Previous section"));
|
||||
bindings.push(("←→", "Switch", "Switch device type or adjust setting"));
|
||||
bindings.push(("↑↓", "Navigate", "Navigate list items"));
|
||||
bindings.push(("PgUp/Dn", "Page", "Page through device list"));
|
||||
bindings.push(("Enter", "Select", "Select device"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("R", "Restart", "Restart audio engine"));
|
||||
}
|
||||
bindings.push(("A", "Add path", "Add sample path"));
|
||||
bindings.push(("D", "Refresh/Del", "Refresh devices or delete path"));
|
||||
bindings.push(("h", "Hush", "Stop all sounds gracefully"));
|
||||
bindings.push(("p", "Panic", "Stop all sounds immediately"));
|
||||
bindings.push(("r", "Reset", "Reset peak voice counter"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("t", "Test", "Play test tone"));
|
||||
}
|
||||
}
|
||||
Page::Options => {
|
||||
bindings.push(("Tab", "Next", "Move to next option"));
|
||||
bindings.push(("Shift+Tab", "Previous", "Move to previous option"));
|
||||
bindings.push(("↑↓", "Navigate", "Navigate options"));
|
||||
bindings.push(("←→", "Toggle", "Toggle or adjust option"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||
}
|
||||
}
|
||||
Page::Help => {
|
||||
bindings.push(("↑↓ j/k", "Scroll", "Scroll content"));
|
||||
bindings.push(("Tab", "Topic", "Next topic"));
|
||||
bindings.push(("Shift+Tab", "Topic", "Previous topic"));
|
||||
bindings.push(("PgUp/Dn", "Page", "Page scroll"));
|
||||
bindings.push(("n", "Next code", "Jump to next code block"));
|
||||
bindings.push(("p", "Prev code", "Jump to previous code block"));
|
||||
bindings.push(("Enter", "Run code", "Execute focused code block"));
|
||||
bindings.push(("/", "Search", "Activate search"));
|
||||
bindings.push(("Esc", "Clear", "Clear search / deselect block"));
|
||||
}
|
||||
Page::Dict => {
|
||||
bindings.push(("Tab", "Focus", "Toggle category/words focus"));
|
||||
bindings.push(("↑↓ j/k", "Navigate", "Navigate items"));
|
||||
bindings.push(("PgUp/Dn", "Page", "Page scroll"));
|
||||
bindings.push(("/", "Search", "Activate search"));
|
||||
bindings.push(("Ctrl+F", "Search", "Activate search"));
|
||||
bindings.push(("Esc", "Clear", "Clear search"));
|
||||
}
|
||||
Page::Script => {
|
||||
bindings.push(("Enter", "Focus", "Focus editor for typing"));
|
||||
bindings.push(("Esc", "Unfocus", "Unfocus editor to use page keybindings"));
|
||||
bindings.push(("Ctrl+E", "Evaluate", "Compile and check for errors (focused)"));
|
||||
bindings.push(("S", "Set Speed", "Set script speed via text input (unfocused)"));
|
||||
bindings.push(("L", "Set Length", "Set script length via text input (unfocused)"));
|
||||
bindings.push(("Ctrl+S", "Stack", "Toggle stack preview (focused)"));
|
||||
}
|
||||
}
|
||||
|
||||
bindings
|
||||
}
|
||||
@@ -2,7 +2,6 @@ pub mod dict_view;
|
||||
pub mod engine_view;
|
||||
pub mod help_view;
|
||||
pub mod highlight;
|
||||
pub mod keybindings;
|
||||
pub mod main_view;
|
||||
pub mod options_view;
|
||||
pub mod patterns_view;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
@@ -359,8 +359,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
|
||||
let cursor = app.patterns_nav.pattern_cursor;
|
||||
let available = inner.height as usize;
|
||||
// Cursor row takes 2 lines (main + detail); account for 1 extra
|
||||
let max_visible = available.saturating_sub(1).max(1);
|
||||
let max_visible = available.max(1);
|
||||
|
||||
let scroll_offset = if MAX_PATTERNS <= max_visible {
|
||||
0
|
||||
@@ -375,8 +374,6 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
let mut y = inner.y;
|
||||
for visible_idx in 0..visible_count {
|
||||
let idx = scroll_offset + visible_idx;
|
||||
let is_expanded = idx == cursor;
|
||||
let row_h = if is_expanded { 2u16 } else { 1u16 };
|
||||
if y >= inner.y + inner.height {
|
||||
break;
|
||||
}
|
||||
@@ -385,7 +382,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
x: inner.x,
|
||||
y,
|
||||
width: inner.width,
|
||||
height: row_h.min(inner.y + inner.height - y),
|
||||
height: 1u16.min(inner.y + inner.height - y),
|
||||
};
|
||||
|
||||
let is_cursor = is_focused && idx == app.patterns_nav.pattern_cursor;
|
||||
@@ -471,21 +468,9 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
let base_style = Style::new().bg(bg).fg(fg);
|
||||
let bold_style = base_style.add_modifier(Modifier::BOLD);
|
||||
|
||||
let content_area = if is_expanded {
|
||||
let border_color = if is_focused { theme.selection.cursor } else { theme.ui.unfocused };
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::QuadrantOutside)
|
||||
.border_style(Style::new().fg(border_color).bg(bg))
|
||||
.style(Style::new().bg(bg));
|
||||
let content = block.inner(row_area);
|
||||
frame.render_widget(block, row_area);
|
||||
content
|
||||
} else {
|
||||
let bg_block = Block::default().style(Style::new().bg(bg));
|
||||
frame.render_widget(bg_block, row_area);
|
||||
row_area
|
||||
};
|
||||
let bg_block = Block::default().style(Style::new().bg(bg));
|
||||
frame.render_widget(bg_block, row_area);
|
||||
let content_area = row_area;
|
||||
|
||||
let text_area = Rect {
|
||||
x: content_area.x,
|
||||
@@ -521,16 +506,38 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
String::new()
|
||||
};
|
||||
let props_indicator = if has_staged_props { "~" } else { "" };
|
||||
let right_info = if content_count > 0 {
|
||||
format!("{props_indicator}{content_count}/{length}{speed_str}")
|
||||
let quant_sync = if is_selected {
|
||||
format!(
|
||||
"{}:{} ",
|
||||
pattern.quantization.short_label(),
|
||||
pattern.sync_mode.short_label()
|
||||
)
|
||||
} else {
|
||||
format!("{props_indicator} {length}{speed_str}")
|
||||
String::new()
|
||||
};
|
||||
let right_info = if content_count > 0 {
|
||||
format!("{quant_sync}{props_indicator}{content_count}/{length}{speed_str}")
|
||||
} else {
|
||||
format!("{quant_sync}{props_indicator} {length}{speed_str}")
|
||||
};
|
||||
let left_width: usize = spans.iter().map(|s| s.content.chars().count()).sum();
|
||||
let right_width = right_info.chars().count();
|
||||
let padding = (text_area.width as usize).saturating_sub(left_width + right_width + 1);
|
||||
let gap = (text_area.width as usize).saturating_sub(left_width + right_width + 1);
|
||||
|
||||
spans.push(Span::styled(" ".repeat(padding), dim_style));
|
||||
if let Some(desc) = pattern.description.as_deref().filter(|d| !d.is_empty() && gap > 4) {
|
||||
let budget = gap - 2;
|
||||
let char_count = desc.chars().count();
|
||||
if char_count <= budget {
|
||||
spans.push(Span::styled(format!(" {desc}"), dim_style));
|
||||
spans.push(Span::styled(" ".repeat(gap - char_count - 1), dim_style));
|
||||
} else {
|
||||
let truncated: String = desc.chars().take(budget - 1).collect();
|
||||
spans.push(Span::styled(format!(" {truncated}\u{2026}"), dim_style));
|
||||
spans.push(Span::styled(" ", dim_style));
|
||||
}
|
||||
} else {
|
||||
spans.push(Span::styled(" ".repeat(gap), dim_style));
|
||||
}
|
||||
spans.push(Span::styled(right_info, dim_style));
|
||||
|
||||
let spans = if is_playing && !is_cursor && !is_in_range {
|
||||
@@ -543,52 +550,6 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
|
||||
frame.render_widget(Paragraph::new(Line::from(spans)), text_area);
|
||||
|
||||
if is_expanded && content_area.height >= 2 {
|
||||
let detail_area = Rect {
|
||||
x: content_area.x,
|
||||
y: content_area.y + 1,
|
||||
width: content_area.width,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let right_label = format!(
|
||||
"{} · {}",
|
||||
pattern.quantization.label(),
|
||||
pattern.sync_mode.label()
|
||||
);
|
||||
let w = detail_area.width as usize;
|
||||
let label = if let Some(desc) = &pattern.description {
|
||||
let right_len = right_label.chars().count();
|
||||
let max_desc = w.saturating_sub(right_len + 1);
|
||||
let truncated: String = desc.chars().take(max_desc).collect();
|
||||
let pad = w.saturating_sub(truncated.chars().count() + right_len);
|
||||
format!("{truncated}{}{right_label}", " ".repeat(pad))
|
||||
} else {
|
||||
format!("{right_label:>w$}")
|
||||
};
|
||||
let padded_label = label;
|
||||
|
||||
let filled_width = if is_playing {
|
||||
let ratio = snapshot.get_smooth_progress(bank, idx, length, speed.multiplier()).unwrap_or(0.0);
|
||||
(ratio * detail_area.width as f64).min(detail_area.width as f64) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let dim_fg = theme.ui.text_muted;
|
||||
let progress_bg = theme.list.playing_bg;
|
||||
let byte_offset = padded_label
|
||||
.char_indices()
|
||||
.nth(filled_width)
|
||||
.map_or(padded_label.len(), |(i, _)| i);
|
||||
let (left, right) = padded_label.split_at(byte_offset);
|
||||
let detail_spans = vec![
|
||||
Span::styled(left.to_string(), Style::new().bg(progress_bg).fg(dim_fg)),
|
||||
Span::styled(right.to_string(), Style::new().bg(theme.ui.bg).fg(dim_fg)),
|
||||
];
|
||||
frame.render_widget(Paragraph::new(Line::from(detail_spans)), detail_area);
|
||||
}
|
||||
|
||||
y += row_area.height;
|
||||
}
|
||||
|
||||
|
||||
@@ -707,15 +707,6 @@ fn render_modal(
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::JumpToStep(input) => {
|
||||
let pattern_len = app.current_edit_pattern().length;
|
||||
let title = format!("Jump to Step (1-{})", pattern_len);
|
||||
TextInputModal::new(&title, input)
|
||||
.hint("Enter step number")
|
||||
.width(30)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::SetTempo(input) => TextInputModal::new("Set Tempo (20-300 BPM)", input)
|
||||
.hint("Enter BPM")
|
||||
.width(30)
|
||||
@@ -883,6 +874,9 @@ fn render_modal(
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::CommandPalette { input, cursor, scroll } => {
|
||||
render_command_palette(frame, app, input, *cursor, *scroll, term)
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => render_modal_keybindings(frame, app, *scroll, term),
|
||||
Modal::EuclideanDistribution {
|
||||
source_step,
|
||||
@@ -1086,6 +1080,247 @@ fn render_modal_editor(
|
||||
inner
|
||||
}
|
||||
|
||||
fn render_command_palette(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
query: &str,
|
||||
cursor: usize,
|
||||
scroll: usize,
|
||||
term: Rect,
|
||||
) -> Rect {
|
||||
use crate::model::palette::{palette_entries, CommandEntry};
|
||||
|
||||
let theme = theme::get();
|
||||
let entries = palette_entries(query, app.plugin_mode, app);
|
||||
|
||||
// On Main page, numeric input prepends a synthetic "Jump to Step" entry
|
||||
let jump_step: Option<usize> = if app.page == Page::Main
|
||||
&& !query.is_empty()
|
||||
&& query.chars().all(|c| c.is_ascii_digit())
|
||||
{
|
||||
query.parse().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build display rows: each is either a separator header or a command entry
|
||||
struct DisplayRow<'a> {
|
||||
entry: Option<&'a CommandEntry>,
|
||||
separator: Option<&'static str>,
|
||||
is_jump: bool,
|
||||
jump_label: String,
|
||||
}
|
||||
|
||||
let mut rows: Vec<DisplayRow> = Vec::new();
|
||||
|
||||
if let Some(n) = jump_step {
|
||||
rows.push(DisplayRow {
|
||||
entry: None,
|
||||
separator: None,
|
||||
is_jump: true,
|
||||
jump_label: format!("Jump to Step {n}"),
|
||||
});
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
// Grouped by category with separators
|
||||
let mut last_category = "";
|
||||
for e in &entries {
|
||||
if e.category != last_category {
|
||||
rows.push(DisplayRow {
|
||||
entry: None,
|
||||
separator: Some(e.category),
|
||||
is_jump: false,
|
||||
jump_label: String::new(),
|
||||
});
|
||||
last_category = e.category;
|
||||
}
|
||||
rows.push(DisplayRow {
|
||||
entry: Some(e),
|
||||
separator: None,
|
||||
is_jump: false,
|
||||
jump_label: String::new(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for e in &entries {
|
||||
rows.push(DisplayRow {
|
||||
entry: Some(e),
|
||||
separator: None,
|
||||
is_jump: false,
|
||||
jump_label: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Count selectable items (non-separator)
|
||||
let selectable_count = rows.iter().filter(|r| r.separator.is_none()).count();
|
||||
let cursor = cursor.min(selectable_count.saturating_sub(1));
|
||||
|
||||
let width: u16 = 55;
|
||||
let max_height = (term.height as usize * 60 / 100).max(8);
|
||||
let content_height = rows.len() + 4; // input + gap + hint + border padding
|
||||
let height = content_height.min(max_height) as u16;
|
||||
|
||||
let inner = ModalFrame::new(": Command Palette")
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let mut y = inner.y;
|
||||
let content_width = inner.width;
|
||||
|
||||
// Input line
|
||||
let input_line = Line::from(vec![
|
||||
Span::styled("> ", Style::default().fg(theme.modal.confirm)),
|
||||
Span::styled(query, Style::default().fg(theme.ui.text_primary)),
|
||||
Span::styled("\u{2588}", Style::default().fg(theme.modal.confirm)),
|
||||
]);
|
||||
frame.render_widget(
|
||||
Paragraph::new(input_line),
|
||||
Rect::new(inner.x, y, content_width, 1),
|
||||
);
|
||||
y += 1;
|
||||
|
||||
// Visible area for entries
|
||||
let visible_height = inner.height.saturating_sub(2) as usize; // minus input line and hint line
|
||||
|
||||
// Auto-scroll
|
||||
let scroll = {
|
||||
let mut s = scroll;
|
||||
// Map cursor (selectable index) to row index for scrolling
|
||||
let mut selectable_idx = 0;
|
||||
let mut cursor_row = 0;
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
if row.separator.is_some() {
|
||||
continue;
|
||||
}
|
||||
if selectable_idx == cursor {
|
||||
cursor_row = i;
|
||||
break;
|
||||
}
|
||||
selectable_idx += 1;
|
||||
}
|
||||
if cursor_row >= s + visible_height {
|
||||
s = cursor_row + 1 - visible_height;
|
||||
}
|
||||
if cursor_row < s {
|
||||
s = cursor_row;
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
// Render visible rows
|
||||
let mut selectable_idx = rows.iter().take(scroll).filter(|r| r.separator.is_none()).count();
|
||||
for row in rows.iter().skip(scroll).take(visible_height) {
|
||||
if y >= inner.y + inner.height - 1 {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(cat) = row.separator {
|
||||
// Category header
|
||||
let pad = content_width.saturating_sub(cat.len() as u16 + 4) / 2;
|
||||
let sep_left = "\u{2500}".repeat(pad as usize);
|
||||
let sep_right =
|
||||
"\u{2500}".repeat(content_width.saturating_sub(pad + cat.len() as u16 + 4) as usize);
|
||||
let line = Line::from(vec![
|
||||
Span::styled(
|
||||
format!("{sep_left} "),
|
||||
Style::default().fg(theme.ui.text_muted),
|
||||
),
|
||||
Span::styled(cat, Style::default().fg(theme.ui.text_dim)),
|
||||
Span::styled(
|
||||
format!(" {sep_right}"),
|
||||
Style::default().fg(theme.ui.text_muted),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(line), Rect::new(inner.x, y, content_width, 1));
|
||||
y += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_selected = selectable_idx == cursor;
|
||||
let (bg, fg) = if is_selected {
|
||||
(theme.selection.cursor_bg, theme.selection.cursor_fg)
|
||||
} else if selectable_idx.is_multiple_of(2) {
|
||||
(theme.table.row_even, theme.ui.text_primary)
|
||||
} else {
|
||||
(theme.table.row_odd, theme.ui.text_primary)
|
||||
};
|
||||
|
||||
let (name, keybinding) = if row.is_jump {
|
||||
(row.jump_label.as_str(), "")
|
||||
} else if let Some(e) = row.entry {
|
||||
(e.name, e.keybinding)
|
||||
} else {
|
||||
selectable_idx += 1;
|
||||
y += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let key_len = keybinding.len() as u16;
|
||||
let name_width = content_width.saturating_sub(key_len + 2);
|
||||
let truncated_name: String = name.chars().take(name_width as usize).collect();
|
||||
let padding = name_width.saturating_sub(truncated_name.len() as u16);
|
||||
|
||||
let key_fg = if is_selected {
|
||||
theme.selection.cursor_fg
|
||||
} else {
|
||||
theme.ui.text_dim
|
||||
};
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::styled(format!(" {truncated_name}"), Style::default().bg(bg).fg(fg)),
|
||||
Span::styled(
|
||||
" ".repeat(padding as usize),
|
||||
Style::default().bg(bg),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{keybinding} "),
|
||||
Style::default().bg(bg).fg(key_fg),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(line), Rect::new(inner.x, y, content_width, 1));
|
||||
|
||||
selectable_idx += 1;
|
||||
y += 1;
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if selectable_count == 0 {
|
||||
let msg = "No matching commands";
|
||||
let empty_y = inner.y + inner.height / 2;
|
||||
if empty_y < inner.y + inner.height - 1 {
|
||||
frame.render_widget(
|
||||
Paragraph::new(msg)
|
||||
.style(Style::default().fg(theme.ui.text_muted))
|
||||
.alignment(Alignment::Center),
|
||||
Rect::new(inner.x, empty_y, content_width, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hint bar
|
||||
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
||||
let hints = if jump_step.is_some() && cursor == 0 {
|
||||
hint_line(&[
|
||||
("\u{2191}\u{2193}", "navigate"),
|
||||
("Enter", "jump to step"),
|
||||
("Esc", "close"),
|
||||
])
|
||||
} else {
|
||||
hint_line(&[
|
||||
("\u{2191}\u{2193}", "navigate"),
|
||||
("Enter", "run"),
|
||||
("Esc", "close"),
|
||||
])
|
||||
};
|
||||
frame.render_widget(Paragraph::new(hints), hint_area);
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: Rect) -> Rect {
|
||||
let theme = theme::get();
|
||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||
@@ -1098,7 +1333,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
|
||||
.border_color(theme.modal.editor)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page, app.plugin_mode);
|
||||
let bindings = crate::model::palette::bindings_for(app.page, app.plugin_mode);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
|
||||
Reference in New Issue
Block a user