Help modal
This commit is contained in:
37
src/input.rs
37
src/input.rs
@@ -572,6 +572,25 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page).len();
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('?') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
*scroll = scroll.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
*scroll = (*scroll + 1).min(bindings_count.saturating_sub(1));
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
*scroll = scroll.saturating_sub(10);
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
*scroll = (*scroll + 10).min(bindings_count.saturating_sub(1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::None => unreachable!(),
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -845,6 +864,9 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
||||
}));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -952,6 +974,9 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -1106,6 +1131,9 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
"/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
));
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -1151,6 +1179,9 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -1184,6 +1215,9 @@ fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
@@ -1229,6 +1263,9 @@ fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
|
||||
@@ -57,4 +57,7 @@ pub enum Modal {
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
},
|
||||
KeybindingsHelp {
|
||||
scroll: usize,
|
||||
},
|
||||
}
|
||||
|
||||
91
src/views/keybindings.rs
Normal file
91
src/views/keybindings.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::page::Page;
|
||||
|
||||
pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut bindings = Vec::new();
|
||||
|
||||
// Global bindings
|
||||
bindings.push(("Ctrl+←→↑↓", "Navigate", "Switch between views"));
|
||||
bindings.push(("q", "Quit", "Quit application"));
|
||||
bindings.push(("?", "Keybindings", "Show this help"));
|
||||
|
||||
// Page-specific bindings
|
||||
match page {
|
||||
Page::Main => {
|
||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||
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", "Preview", "Preview step 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"));
|
||||
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(("s", "Save", "Save project"));
|
||||
bindings.push(("l", "Load", "Load project"));
|
||||
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
||||
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
||||
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy bank/pattern"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||
}
|
||||
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"));
|
||||
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"));
|
||||
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"));
|
||||
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(("/", "Search", "Activate search"));
|
||||
bindings.push(("Esc", "Clear", "Clear search"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
bindings
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Instant;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
@@ -304,11 +304,9 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
("^V", "Paste"),
|
||||
("^B", "Link"),
|
||||
("^D", "Dup"),
|
||||
("^H", "Harden"),
|
||||
("Del", "Delete"),
|
||||
("<>", "Len"),
|
||||
("[]", "Spd"),
|
||||
("+-", "Tempo"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Patterns => vec![
|
||||
("←→↑↓", "Navigate"),
|
||||
@@ -317,6 +315,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
("Esc", "Back"),
|
||||
("r", "Rename"),
|
||||
("Del", "Reset"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Engine => vec![
|
||||
("Tab", "Section"),
|
||||
@@ -324,15 +323,27 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
("↑↓", "Navigate"),
|
||||
("Enter", "Select"),
|
||||
("A", "Add path"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Options => vec![
|
||||
("Tab", "Next"),
|
||||
("←→", "Toggle"),
|
||||
("Space", "Play"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Options => vec![("Tab", "Next"), ("←→", "Toggle"), ("Space", "Play")],
|
||||
Page::Help => vec![
|
||||
("↑↓", "Scroll"),
|
||||
("Tab", "Topic"),
|
||||
("PgUp/Dn", "Page"),
|
||||
("/", "Search"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Dict => vec![
|
||||
("Tab", "Focus"),
|
||||
("↑↓", "Navigate"),
|
||||
("/", "Search"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
Page::Dict => vec![("Tab", "Focus"), ("↑↓", "Navigate"), ("/", "Search")],
|
||||
};
|
||||
|
||||
let page_width = page_indicator.chars().count();
|
||||
@@ -772,5 +783,73 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint), hint_area);
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||
let height = (term.height * 80 / 100).max(15);
|
||||
|
||||
let title = format!("Keybindings — {}", app.page.name());
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(Color::Rgb(100, 160, 180))
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(*scroll)
|
||||
.take(visible_rows)
|
||||
.map(|(i, (key, name, desc))| {
|
||||
let bg = if i % 2 == 0 {
|
||||
Color::Rgb(25, 25, 30)
|
||||
} else {
|
||||
Color::Rgb(35, 35, 42)
|
||||
};
|
||||
Row::new(vec![
|
||||
Cell::from(*key).style(Style::default().fg(Color::Yellow)),
|
||||
Cell::from(*name).style(Style::default().fg(Color::Cyan)),
|
||||
Cell::from(*desc).style(Style::default().fg(Color::White)),
|
||||
])
|
||||
.style(Style::default().bg(bg))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(12),
|
||||
Constraint::Fill(1),
|
||||
],
|
||||
)
|
||||
.column_spacing(2);
|
||||
|
||||
let table_area = Rect {
|
||||
x: inner.x,
|
||||
y: inner.y,
|
||||
width: inner.width,
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
frame.render_widget(table, table_area);
|
||||
|
||||
let hint_area = Rect {
|
||||
x: inner.x,
|
||||
y: inner.y + inner.height.saturating_sub(1),
|
||||
width: inner.width,
|
||||
height: 1,
|
||||
};
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("↑↓", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" scroll ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled("PgUp/Dn", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" page ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled("Esc/?", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" close", Style::default().fg(Color::DarkGray)),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user