From a9ce70d29294d36aa82c6137beeb7a7df62acbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 27 Jan 2026 15:23:04 +0100 Subject: [PATCH] ok --- docs/welcome.md | 55 ++++++++++++++ src/app.rs | 35 +++++++-- src/commands.rs | 5 ++ src/input.rs | 53 ++++++++------ src/state/ui.rs | 22 +++++- src/views/help_view.rs | 159 ++++++++++++++++++++++++++++++++++------- src/views/render.rs | 159 ++++++++++++++++++++++++++--------------- 7 files changed, 378 insertions(+), 110 deletions(-) create mode 100644 docs/welcome.md diff --git a/docs/welcome.md b/docs/welcome.md new file mode 100644 index 0000000..da319b7 --- /dev/null +++ b/docs/welcome.md @@ -0,0 +1,55 @@ +# Welcome to Cagire + +Cagire is a terminal-based step sequencer for live music. Each step in a pattern contains a Forth script that controls sound synthesis and effects. + +## Pages + +Navigate between pages with **Ctrl+Left/Right**: + +- **Sequencer**: Edit patterns, write Forth scripts, manage playback slots +- **Patterns**: Browse and select across 16 banks x 16 patterns +- **Engine**: Audio device selection, sample loading, CPU and voice monitoring +- **Options**: Application settings +- **Dict**: Forth word reference, organized by category +- **Help**: Documentation (you are here) + +## Quick Start + +1. Press **Space** to start the transport +2. Use **arrow keys** to navigate steps +3. Press **Tab** to focus the editor +4. Write a Forth script (e.g. `/sound/sine`) +5. Press **Ctrl+E** to compile +6. Press **Tab** to return to the sequencer +7. Press **Enter** to activate the step +8. Press **g** to queue the pattern to a slot + +## Example Script + +A step that plays a filtered saw with reverb: + +``` +"saw" s c4 note 0.5 dur 0.3 decay + 0.6 gain 2000 lpf 0.4 lpq + 0.3 verb 2.0 verbdecay . +``` + +A rhythmic pattern using subdivisions and probability: + +``` +"kick" s 0.8 gain . +div + "hat" s < 0.3 0.5 0.7 > gain . . +~ +{ "snare" s 0.6 gain . } often +``` + +## Concepts + +**Banks and Patterns**: 16 banks of 16 patterns each. Each pattern has up to 32 steps with configurable length and speed. + +**Slots**: 8 concurrent playback slots. Queue patterns into slots with **g**. Slot changes are quantized to bar boundaries. + +**Ableton Link**: Cagire syncs tempo and phase with other Link-enabled applications on your network. + +**Forth**: The scripting language used in each step. Open the **Dict** page to browse all available words. diff --git a/src/app.rs b/src/app.rs index b08063f..3fecd96 100644 --- a/src/app.rs +++ b/src/app.rs @@ -939,18 +939,45 @@ impl App { // Help navigation AppCommand::HelpNextTopic => { self.ui.help_topic = (self.ui.help_topic + 1) % help_view::topic_count(); - self.ui.help_scroll = 0; } AppCommand::HelpPrevTopic => { let count = help_view::topic_count(); self.ui.help_topic = (self.ui.help_topic + count - 1) % count; - self.ui.help_scroll = 0; } AppCommand::HelpScrollDown(n) => { - self.ui.help_scroll = self.ui.help_scroll.saturating_add(n); + let s = self.ui.help_scroll_mut(); + *s = s.saturating_add(n); } AppCommand::HelpScrollUp(n) => { - self.ui.help_scroll = self.ui.help_scroll.saturating_sub(n); + let s = self.ui.help_scroll_mut(); + *s = s.saturating_sub(n); + } + AppCommand::HelpActivateSearch => { + self.ui.help_search_active = true; + } + AppCommand::HelpClearSearch => { + self.ui.help_search_query.clear(); + self.ui.help_search_active = false; + } + AppCommand::HelpSearchInput(c) => { + self.ui.help_search_query.push(c); + if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) { + self.ui.help_topic = topic; + self.ui.help_scrolls[topic] = line; + } + } + AppCommand::HelpSearchBackspace => { + self.ui.help_search_query.pop(); + if self.ui.help_search_query.is_empty() { + return; + } + if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) { + self.ui.help_topic = topic; + self.ui.help_scrolls[topic] = line; + } + } + AppCommand::HelpSearchConfirm => { + self.ui.help_search_active = false; } // Dictionary navigation diff --git a/src/commands.rs b/src/commands.rs index 6364e05..e8eefce 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -130,6 +130,11 @@ pub enum AppCommand { HelpPrevTopic, HelpScrollDown(usize), HelpScrollUp(usize), + HelpActivateSearch, + HelpClearSearch, + HelpSearchInput(char), + HelpSearchBackspace, + HelpSearchConfirm, // Dictionary navigation DictToggleFocus, diff --git a/src/input.rs b/src/input.rs index 110ac53..483a62e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -361,7 +361,11 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } } else { let dir = state.current_dir(); - if dir.is_dir() { Some(dir) } else { None } + if dir.is_dir() { + Some(dir) + } else { + None + } }; if let Some(path) = sample_path { let index = doux::loader::scan_samples_dir(&path); @@ -573,8 +577,8 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { - use cagire_ratatui::TreeLineKind; use crate::engine::AudioCommand; + use cagire_ratatui::TreeLineKind; let state = match &mut ctx.app.panel.side { Some(SidePanel::SampleBrowser(s)) => s, @@ -625,8 +629,7 @@ fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { TreeLineKind::File => { let folder = &entry.folder; let idx = entry.index; - let cmd = - format!("/sound/{folder}/n/{idx}/gain/0.5/dur/1"); + let cmd = format!("/sound/{folder}/n/{idx}/gain/0.5/dur/1"); let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate(cmd)); } _ => state.toggle_expand(), @@ -979,10 +982,9 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0, KeyCode::Char('t') => { - let _ = ctx - .audio_tx - .load() - .send(AudioCommand::Evaluate("/sound/sine/dur/0.5/decay/0.2".into())); + let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate( + "/sound/sine/dur/0.5/decay/0.2".into(), + )); } _ => {} } @@ -1035,7 +1037,22 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { + if ctx.app.ui.help_search_active { + match key.code { + KeyCode::Esc => ctx.dispatch(AppCommand::HelpClearSearch), + KeyCode::Enter => ctx.dispatch(AppCommand::HelpSearchConfirm), + KeyCode::Backspace => ctx.dispatch(AppCommand::HelpSearchBackspace), + KeyCode::Char(c) => ctx.dispatch(AppCommand::HelpSearchInput(c)), + _ => {} + } + return InputResult::Continue; + } + match key.code { + KeyCode::Char('/') => ctx.dispatch(AppCommand::HelpActivateSearch), + KeyCode::Esc if !ctx.app.ui.help_search_query.is_empty() => { + ctx.dispatch(AppCommand::HelpClearSearch); + } KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::HelpScrollDown(1)), KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::HelpScrollUp(1)), KeyCode::Tab => ctx.dispatch(AppCommand::HelpNextTopic), @@ -1077,18 +1094,14 @@ fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { ctx.dispatch(AppCommand::DictClearSearch); } KeyCode::Tab => ctx.dispatch(AppCommand::DictToggleFocus), - KeyCode::Char('j') | KeyCode::Down => { - match ctx.app.ui.dict_focus { - DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory), - DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)), - } - } - KeyCode::Char('k') | KeyCode::Up => { - match ctx.app.ui.dict_focus { - DictFocus::Categories => ctx.dispatch(AppCommand::DictPrevCategory), - DictFocus::Words => ctx.dispatch(AppCommand::DictScrollUp(1)), - } - } + KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.dict_focus { + DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory), + DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)), + }, + KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.dict_focus { + DictFocus::Categories => ctx.dispatch(AppCommand::DictPrevCategory), + DictFocus::Words => ctx.dispatch(AppCommand::DictScrollUp(1)), + }, KeyCode::PageDown => ctx.dispatch(AppCommand::DictScrollDown(10)), KeyCode::PageUp => ctx.dispatch(AppCommand::DictScrollUp(10)), KeyCode::Char('q') => { diff --git a/src/state/ui.rs b/src/state/ui.rs index 641ef55..54049b5 100644 --- a/src/state/ui.rs +++ b/src/state/ui.rs @@ -26,7 +26,9 @@ pub struct UiState { pub flash_kind: FlashKind, pub modal: Modal, pub help_topic: usize, - pub help_scroll: usize, + pub help_scrolls: Vec, + pub help_search_active: bool, + pub help_search_query: String, pub dict_focus: DictFocus, pub dict_category: usize, pub dict_scroll: usize, @@ -47,7 +49,9 @@ impl Default for UiState { flash_kind: FlashKind::Success, modal: Modal::None, help_topic: 0, - help_scroll: 0, + help_scrolls: vec![0; crate::views::help_view::topic_count()], + help_search_active: false, + help_search_query: String::new(), dict_focus: DictFocus::default(), dict_category: 0, dict_scroll: 0, @@ -62,6 +66,14 @@ impl Default for UiState { } impl UiState { + pub fn help_scroll(&self) -> usize { + self.help_scrolls[self.help_topic] + } + + pub fn help_scroll_mut(&mut self) -> &mut usize { + &mut self.help_scrolls[self.help_topic] + } + pub fn flash(&mut self, msg: &str, duration_ms: u64, kind: FlashKind) { self.status_message = Some(msg.to_string()); self.flash_until = Some(Instant::now() + Duration::from_millis(duration_ms)); @@ -69,7 +81,11 @@ impl UiState { } pub fn flash_kind(&self) -> Option { - if self.is_flashing() { Some(self.flash_kind) } else { None } + if self.is_flashing() { + Some(self.flash_kind) + } else { + None + } } pub fn set_status(&mut self, msg: String) { diff --git a/src/views/help_view.rs b/src/views/help_view.rs index 3a4826b..e33b162 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -2,39 +2,43 @@ use minimad::{Composite, CompositeStyle, Compound, Line}; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line as RLine, Span}; -use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap}; use ratatui::Frame; use crate::app::App; +use crate::views::highlight; -const STATIC_DOCS: &[(&str, &str)] = &[ +// To add a new help topic: drop a .md file in docs/ and add one line here. +const DOCS: &[(&str, &str)] = &[ + ("Welcome", include_str!("../../docs/welcome.md")), ("Keybindings", include_str!("../../docs/keybindings.md")), ("Sequencer", include_str!("../../docs/sequencer.md")), ]; -const TOPICS: &[&str] = &["Keybindings", "Sequencer"]; +pub fn topic_count() -> usize { + DOCS.len() +} pub fn render(frame: &mut Frame, app: &App, area: Rect) { let [topics_area, content_area] = Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(area); render_topics(frame, app, topics_area); - - let topic = TOPICS[app.ui.help_topic]; - render_markdown_content(frame, app, content_area, topic); + render_content(frame, app, content_area); } fn render_topics(frame: &mut Frame, app: &App, area: Rect) { - let items: Vec = TOPICS + let items: Vec = DOCS .iter() .enumerate() - .map(|(i, name)| { - let style = if i == app.ui.help_topic { + .map(|(i, (name, _))| { + let selected = i == app.ui.help_topic; + let style = if selected { Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD) } else { Style::new().fg(Color::White) }; - let prefix = if i == app.ui.help_topic { "> " } else { " " }; + let prefix = if selected { "> " } else { " " }; ListItem::new(format!("{prefix}{name}")).style(style) }) .collect(); @@ -43,39 +47,144 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(list, area); } -fn render_markdown_content(frame: &mut Frame, app: &App, area: Rect, topic: &str) { - let md = STATIC_DOCS - .iter() - .find(|(name, _)| *name == topic) - .map(|(_, content)| *content) - .unwrap_or(""); +fn render_content(frame: &mut Frame, app: &App, area: Rect) { + let (name, md) = DOCS[app.ui.help_topic]; + let query = &app.ui.help_search_query; + let has_query = !query.is_empty(); + let query_lower = query.to_lowercase(); + let lines = parse_markdown(md); - let visible_height = area.height.saturating_sub(2) as usize; - let total_lines = lines.len(); - let max_scroll = total_lines.saturating_sub(visible_height); - let scroll = app.ui.help_scroll.min(max_scroll); + let has_search_bar = app.ui.help_search_active || has_query; + let search_bar_height: u16 = u16::from(has_search_bar); + let visible_height = area.height.saturating_sub(6 + search_bar_height) as usize; + let max_scroll = lines.len().saturating_sub(visible_height); + let scroll = app.ui.help_scroll().min(max_scroll); let visible: Vec = lines .into_iter() .skip(scroll) .take(visible_height) + .map(|line| { + if has_query { + highlight_line(line, &query_lower) + } else { + line + } + }) .collect(); - let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(topic)); - frame.render_widget(para, area); + let content_area = if has_search_bar { + let [content, search] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area); + render_search_bar(frame, app, search); + content + } else { + area + }; + + let para = Paragraph::new(visible) + .block( + Block::default() + .borders(Borders::ALL) + .title(name) + .padding(Padding::new(2, 2, 2, 2)), + ) + .wrap(Wrap { trim: false }); + frame.render_widget(para, content_area); +} + +fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { + let style = if app.ui.help_search_active { + Style::new().fg(Color::Yellow) + } else { + Style::new().fg(Color::DarkGray) + }; + let cursor = if app.ui.help_search_active { "█" } else { "" }; + let text = format!(" /{}{cursor}", app.ui.help_search_query); + frame.render_widget(Paragraph::new(text).style(style), area); +} + +fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> { + let mut result: Vec> = Vec::new(); + for span in line.spans { + let lower = span.content.to_lowercase(); + if !lower.contains(query) { + result.push(span); + continue; + } + let content = span.content.to_string(); + let base_style = span.style; + let hl_style = base_style.bg(Color::Yellow).fg(Color::Black); + let mut start = 0; + let lower_bytes = lower.as_bytes(); + let query_bytes = query.as_bytes(); + while let Some(pos) = find_bytes(&lower_bytes[start..], query_bytes) { + let abs = start + pos; + if abs > start { + result.push(Span::styled(content[start..abs].to_string(), base_style)); + } + result.push(Span::styled( + content[abs..abs + query.len()].to_string(), + hl_style, + )); + start = abs + query.len(); + } + if start < content.len() { + result.push(Span::styled(content[start..].to_string(), base_style)); + } + } + RLine::from(result) +} + +fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { + haystack.windows(needle.len()).position(|w| w == needle) +} + +/// Find first line matching query across all topics. Returns (topic_index, line_index). +pub fn find_match(query: &str) -> Option<(usize, usize)> { + let query = query.to_lowercase(); + for (topic_idx, (_, content)) in DOCS.iter().enumerate() { + for (line_idx, line) in content.lines().enumerate() { + if line.to_lowercase().contains(&query) { + return Some((topic_idx, line_idx)); + } + } + } + None +} + +fn code_border_style() -> Style { + Style::new().fg(Color::Rgb(60, 60, 70)) } fn parse_markdown(md: &str) -> Vec> { let text = minimad::Text::from(md); let mut lines = Vec::new(); + let mut code_line_nr: usize = 0; + for line in text.lines { match line { + Line::Normal(composite) if composite.style == CompositeStyle::Code => { + code_line_nr += 1; + let raw: String = composite.compounds.iter().map(|c| &*c.src).collect(); + let mut spans = vec![ + Span::styled(format!(" {code_line_nr:>2} "), code_border_style()), + Span::styled("│ ", code_border_style()), + ]; + spans.extend( + highlight::highlight_line(&raw) + .into_iter() + .map(|(style, text)| Span::styled(text, style)), + ); + lines.push(RLine::from(spans)); + } Line::Normal(composite) => { + code_line_nr = 0; lines.push(composite_to_line(composite)); } - Line::TableRow(_) | Line::HorizontalRule | Line::CodeFence(_) | Line::TableRule(_) => { + _ => { lines.push(RLine::from("")); } } @@ -133,7 +242,3 @@ fn compound_to_span(compound: Compound, base: Style) -> Span<'static> { Span::styled(compound.src.to_string(), style) } - -pub fn topic_count() -> usize { - TOPICS.len() -} diff --git a/src/views/render.rs b/src/views/render.rs index 47844ec..c18f3de 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -12,20 +12,31 @@ use crate::model::SourceSpan; use crate::page::Page; use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel}; use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}; -use crate::widgets::{ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal}; +use crate::widgets::{ + ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal, +}; -use super::{dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view}; +use super::{ + dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view, +}; -fn adjust_spans_for_line(spans: &[SourceSpan], line_start: usize, line_len: usize) -> Vec { - spans.iter().filter_map(|s| { - if s.end <= line_start || s.start >= line_start + line_len { - return None; - } - Some(SourceSpan { - start: s.start.max(line_start) - line_start, - end: (s.end.min(line_start + line_len)) - line_start, +fn adjust_spans_for_line( + spans: &[SourceSpan], + line_start: usize, + line_len: usize, +) -> Vec { + spans + .iter() + .filter_map(|s| { + if s.end <= line_start || s.start >= line_start + line_len { + return None; + } + Some(SourceSpan { + start: s.start.max(line_start) - line_start, + end: (s.end.min(line_start + line_len)) - line_start, + }) }) - }).collect() + .collect() } pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) { @@ -60,19 +71,15 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() { if body_area.width >= 120 { let panel_width = body_area.width * 35 / 100; - let [main, side] = Layout::horizontal([ - Constraint::Fill(1), - Constraint::Length(panel_width), - ]) - .areas(body_area); + let [main, side] = + Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)]) + .areas(body_area); (main, Some(side)) } else { let panel_height = body_area.height * 40 / 100; - let [main, side] = Layout::vertical([ - Constraint::Fill(1), - Constraint::Length(panel_height), - ]) - .areas(body_area); + let [main, side] = + Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)]) + .areas(body_area); (main, Some(side)) } } else { @@ -106,7 +113,11 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc .iter() .map(|p| { let (col, row) = p.grid_pos(); - NavTile { col, row, name: p.name() } + NavTile { + col, + row, + name: p.name(), + } }) .collect(); let selected = app.page.grid_pos(); @@ -170,9 +181,13 @@ fn render_header( // Fill indicator let fill = app.live_keys.fill(); let fill_style = if fill { - Style::new().bg(Color::Rgb(30, 30, 35)).fg(Color::Rgb(100, 220, 100)) + Style::new() + .bg(Color::Rgb(30, 30, 35)) + .fg(Color::Rgb(100, 220, 100)) } else { - Style::new().bg(Color::Rgb(30, 30, 35)).fg(Color::Rgb(60, 60, 70)) + Style::new() + .bg(Color::Rgb(30, 30, 35)) + .fg(Color::Rgb(60, 60, 70)) }; frame.render_widget( Paragraph::new(if fill { "F" } else { "·" }) @@ -303,21 +318,14 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { ("Enter", "Select"), ("A", "Add path"), ], - Page::Options => vec![ - ("Tab", "Next"), - ("←→", "Toggle"), - ("Space", "Play"), - ], + Page::Options => vec![("Tab", "Next"), ("←→", "Toggle"), ("Space", "Play")], Page::Help => vec![ ("↑↓", "Scroll"), ("Tab", "Topic"), ("PgUp/Dn", "Page"), - ], - Page::Dict => vec![ - ("Tab", "Focus"), - ("↑↓", "Navigate"), ("/", "Search"), ], + Page::Dict => vec![("Tab", "Focus"), ("↑↓", "Navigate"), ("/", "Search")], }; let page_width = page_indicator.chars().count(); @@ -505,8 +513,16 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term .lines() .map(|line_str| { let tokens = if let Some(t) = trace { - let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len()); - let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len()); + let exec = adjust_spans_for_line( + &t.executed_spans, + line_start, + line_str.len(), + ); + let sel = adjust_spans_for_line( + &t.selected_spans, + line_start, + line_str.len(), + ); highlight_line_with_runtime(line_str, &exec, &sel) } else { highlight_line(line_str) @@ -544,7 +560,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term .render_centered(frame, term); let trace = if app.ui.runtime_highlight && app.playback.playing { - let source = app.current_edit_pattern().resolve_source(app.editor_ctx.step); + let source = app + .current_edit_pattern() + .resolve_source(app.editor_ctx.step); snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source) } else { None @@ -575,11 +593,22 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let (search_area, editor_area, hint_area) = if show_search { let search_area = Rect::new(inner.x, inner.y, inner.width, 1); - let editor_area = Rect::new(inner.x, inner.y + 1, inner.width, inner.height.saturating_sub(2)); - let hint_area = Rect::new(inner.x, inner.y + 1 + editor_area.height, inner.width, 1); + let editor_area = Rect::new( + inner.x, + inner.y + 1, + inner.width, + inner.height.saturating_sub(2), + ); + let hint_area = + Rect::new(inner.x, inner.y + 1 + editor_area.height, inner.width, 1); (Some(search_area), editor_area, hint_area) } else { - let editor_area = Rect::new(inner.x, inner.y, inner.width, inner.height.saturating_sub(1)); + let editor_area = Rect::new( + inner.x, + inner.y, + inner.width, + inner.height.saturating_sub(1), + ); let hint_area = Rect::new(inner.x, inner.y + editor_area.height, inner.width, 1); (None, editor_area, hint_area) }; @@ -590,7 +619,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term } else { Style::default().fg(Color::DarkGray) }; - let cursor = if app.editor_ctx.editor.search_active() { "_" } else { "" }; + let cursor = if app.editor_ctx.editor.search_active() { + "_" + } else { + "" + }; let text = format!("/{}{}", app.editor_ctx.editor.search_query(), cursor); frame.render_widget(Paragraph::new(Line::from(Span::styled(text, style))), sa); } @@ -604,24 +637,35 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let flash_block = Block::default().style(Style::default().bg(bg)); frame.render_widget(flash_block, editor_area); } - app.editor_ctx.editor.render(frame, editor_area, &highlighter); + app.editor_ctx + .editor + .render(frame, editor_area, &highlighter); let dim = Style::default().fg(Color::DarkGray); let key = Style::default().fg(Color::Yellow); let hint = if app.editor_ctx.editor.search_active() { Line::from(vec![ - Span::styled("Enter", key), Span::styled(" confirm ", dim), - Span::styled("Esc", key), Span::styled(" cancel", dim), + Span::styled("Enter", key), + Span::styled(" confirm ", dim), + Span::styled("Esc", key), + Span::styled(" cancel", dim), ]) } else { Line::from(vec![ - Span::styled("Esc", key), Span::styled(" save ", dim), - Span::styled("C-e", key), Span::styled(" eval ", dim), - Span::styled("C-f", key), Span::styled(" find ", dim), - Span::styled("C-n", key), Span::styled("/", dim), - Span::styled("C-p", key), Span::styled(" next/prev ", dim), - Span::styled("C-u", key), Span::styled("/", dim), - Span::styled("C-r", key), Span::styled(" undo/redo", dim), + Span::styled("Esc", key), + Span::styled(" save ", dim), + Span::styled("C-e", key), + Span::styled(" eval ", dim), + Span::styled("C-f", key), + Span::styled(" find ", dim), + Span::styled("C-n", key), + Span::styled("/", dim), + Span::styled("C-p", key), + Span::styled(" next/prev ", dim), + Span::styled("C-u", key), + Span::styled("/", dim), + Span::styled("C-r", key), + Span::styled(" undo/redo", dim), ]) }; frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area); @@ -654,7 +698,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let fields = [ ("Name", name.as_str(), *field == PatternPropsField::Name), - ("Length", length.as_str(), *field == PatternPropsField::Length), + ( + "Length", + length.as_str(), + *field == PatternPropsField::Length, + ), ("Speed", speed.label(), *field == PatternPropsField::Speed), ( "Quantization", @@ -676,7 +724,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let (label_style, value_style) = if *selected { ( - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), Style::default().fg(Color::White).bg(Color::DarkGray), ) } else { @@ -693,10 +743,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term Paragraph::new(format!("{label}:")).style(label_style), label_area, ); - frame.render_widget( - Paragraph::new(*value).style(value_style), - value_area, - ); + frame.render_widget(Paragraph::new(*value).style(value_style), value_area); } let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);