From 6f5fa762a4c37691bb79178202fb50123bd27a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 24 Jan 2026 02:16:18 +0100 Subject: [PATCH] Flash --- crates/ratatui/src/editor.rs | 4 ++++ src/app.rs | 26 +++++++++++++------------ src/commands.rs | 3 +++ src/main.rs | 32 ++++++++++++++++++++----------- src/state/ui.rs | 11 ++++++++++- src/views/render.rs | 37 +++++++++++++++++++++++++++++++----- 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index 5f4b416..2a628ca 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -68,6 +68,10 @@ impl Editor { self.completion.candidates = candidates; } + pub fn insert_str(&mut self, s: &str) { + self.text.insert_str(s); + } + pub fn content(&self) -> String { self.text.lines().join("\n") } diff --git a/src/app.rs b/src/app.rs index b95d480..dc82615 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use crossbeam_channel::Sender; +use ratatui::style::Color; use crate::commands::AppCommand; use crate::engine::{ @@ -319,7 +320,7 @@ impl App { Some(cmds.join("\n")) }; } - self.ui.flash("Script compiled", 150); + self.ui.flash("Script compiled", 150, Color::White); } Err(e) => { if let Some(step) = self @@ -330,7 +331,7 @@ impl App { { step.command = None; } - self.ui.set_status(format!("Script error: {e}")); + self.ui.flash(&format!("Script error: {e}"), 300, Color::Red); } } } @@ -539,7 +540,7 @@ impl App { { self.load_step_to_editor(); } - self.ui.flash("Step deleted", 150); + self.ui.flash("Step deleted", 150, Color::Green); } pub fn reset_pattern(&mut self, bank: usize, pattern: usize) { @@ -548,7 +549,7 @@ impl App { if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } - self.ui.flash("Pattern reset", 150); + self.ui.flash("Pattern reset", 150, Color::Green); } pub fn reset_bank(&mut self, bank: usize) { @@ -559,13 +560,13 @@ impl App { if self.editor_ctx.bank == bank { self.load_step_to_editor(); } - self.ui.flash("Bank reset", 150); + self.ui.flash("Bank reset", 150, Color::Green); } pub fn copy_pattern(&mut self, bank: usize, pattern: usize) { let pat = self.project_state.project.banks[bank].patterns[pattern].clone(); self.copied_pattern = Some(pat); - self.ui.flash("Pattern copied", 150); + self.ui.flash("Pattern copied", 150, Color::Green); } pub fn paste_pattern(&mut self, bank: usize, pattern: usize) { @@ -581,14 +582,14 @@ impl App { if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } - self.ui.flash("Pattern pasted", 150); + self.ui.flash("Pattern pasted", 150, Color::Green); } } pub fn copy_bank(&mut self, bank: usize) { let b = self.project_state.project.banks[bank].clone(); self.copied_bank = Some(b); - self.ui.flash("Bank copied", 150); + self.ui.flash("Bank copied", 150, Color::Green); } pub fn paste_bank(&mut self, bank: usize) { @@ -606,7 +607,7 @@ impl App { if self.editor_ctx.bank == bank { self.load_step_to_editor(); } - self.ui.flash("Bank pasted", 150); + self.ui.flash("Bank pasted", 150, Color::Green); } } @@ -675,7 +676,7 @@ impl App { self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.ui - .flash(&format!("Linked to step {:02}", copied.step + 1), 150); + .flash(&format!("Linked to step {:02}", copied.step + 1), 150, Color::Green); } pub fn harden_step(&mut self) { @@ -708,7 +709,7 @@ impl App { } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); - self.ui.flash("Step hardened", 150); + self.ui.flash("Step hardened", 150, Color::Green); } pub fn open_pattern_modal(&mut self, field: PatternField) { @@ -841,7 +842,8 @@ impl App { AppCommand::Flash { message, duration_ms, - } => self.ui.flash(&message, duration_ms), + color, + } => self.ui.flash(&message, duration_ms, color), AppCommand::OpenModal(modal) => { if matches!(modal, Modal::Editor) { // If current step is a shallow copy, navigate to source step diff --git a/src/commands.rs b/src/commands.rs index 521a313..f18df50 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use ratatui::style::Color; + use crate::engine::PatternChange; use crate::model::PatternSpeed; use crate::state::{Modal, PatternField}; @@ -100,6 +102,7 @@ pub enum AppCommand { Flash { message: String, duration_ms: u64, + color: Color, }, OpenModal(Modal), CloseModal, diff --git a/src/main.rs b/src/main.rs index 5f1d1e6..651a694 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use std::time::Duration; use clap::Parser; -use crossterm::event::{self, Event}; +use crossterm::event::{self, Event, EnableBracketedPaste, DisableBracketedPaste}; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; @@ -137,6 +137,7 @@ fn main() -> io::Result<()> { app.mark_all_patterns_dirty(); enable_raw_mode()?; + io::stdout().execute(EnableBracketedPaste)?; io::stdout().execute(EnterAlternateScreen)?; let backend = CrosstermBackend::new(io::stdout()); let mut terminal = Terminal::new(backend)?; @@ -203,24 +204,33 @@ fn main() -> io::Result<()> { terminal.draw(|frame| views::render(frame, &mut app, &link, &seq_snapshot))?; if event::poll(Duration::from_millis(app.audio.config.refresh_rate.millis()))? { - if let Event::Key(key) = event::read()? { - let mut ctx = InputContext { - app: &mut app, - link: &link, - snapshot: &seq_snapshot, - playing: &playing, - audio_tx: &sequencer.audio_tx, - }; + match event::read()? { + Event::Key(key) => { + let mut ctx = InputContext { + app: &mut app, + link: &link, + snapshot: &seq_snapshot, + playing: &playing, + audio_tx: &sequencer.audio_tx, + }; - if let InputResult::Quit = handle_key(&mut ctx, key) { - break; + if let InputResult::Quit = handle_key(&mut ctx, key) { + break; + } } + Event::Paste(text) => { + if matches!(app.ui.modal, state::Modal::Editor) { + app.editor_ctx.editor.insert_str(&text); + } + } + _ => {} } } } disable_raw_mode()?; + io::stdout().execute(DisableBracketedPaste)?; io::stdout().execute(LeaveAlternateScreen)?; sequencer.shutdown(); diff --git a/src/state/ui.rs b/src/state/ui.rs index 439cf56..0e1cb5e 100644 --- a/src/state/ui.rs +++ b/src/state/ui.rs @@ -1,5 +1,7 @@ use std::time::{Duration, Instant}; +use ratatui::style::Color; + use crate::state::Modal; pub struct Sparkle { @@ -13,6 +15,7 @@ pub struct UiState { pub sparkles: Vec, pub status_message: Option, pub flash_until: Option, + pub flash_color: Color, pub modal: Modal, pub doc_topic: usize, pub doc_scroll: usize, @@ -28,6 +31,7 @@ impl Default for UiState { sparkles: Vec::new(), status_message: None, flash_until: None, + flash_color: Color::Green, modal: Modal::None, doc_topic: 0, doc_scroll: 0, @@ -40,9 +44,14 @@ impl Default for UiState { } impl UiState { - pub fn flash(&mut self, msg: &str, duration_ms: u64) { + pub fn flash(&mut self, msg: &str, duration_ms: u64, color: Color) { self.status_message = Some(msg.to_string()); self.flash_until = Some(Instant::now() + Duration::from_millis(duration_ms)); + self.flash_color = color; + } + + pub fn flash_color(&self) -> Option { + if self.is_flashing() { Some(self.flash_color) } else { None } } pub fn set_status(&mut self, msg: String) { diff --git a/src/views/render.rs b/src/views/render.rs index c35c0c2..e36da34 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -498,10 +498,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let height = (term.height * 60 / 100).max(10); let step_num = app.editor_ctx.step + 1; - let border_color = if app.ui.is_flashing() { - Color::Green - } else { - Color::Rgb(100, 160, 180) + let flash_color = app.ui.flash_color(); + let border_color = match flash_color { + Some(c) => c, + None => Color::Rgb(100, 160, 180), }; let inner = ModalFrame::new(&format!("Step {step_num:02} Script")) @@ -537,7 +537,34 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term highlight::highlight_line_with_runtime(line, &exec, &sel) }; - app.editor_ctx.editor.render(frame, inner, &highlighter); + 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); + + if let Some(c) = flash_color { + let bg = match c { + Color::Red => Color::Rgb(60, 10, 10), + Color::White => Color::Rgb(30, 30, 40), + _ => Color::Rgb(10, 30, 10), + }; + 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); + + let dim = Style::default().fg(Color::DarkGray); + let key = Style::default().fg(Color::Yellow); + let hint = Line::from(vec![ + Span::styled("Esc", key), Span::styled(" save ", dim), + Span::styled("C-e", key), Span::styled(" eval ", dim), + Span::styled("C-u", key), Span::styled("/", dim), + Span::styled("C-r", key), Span::styled(" undo/redo ", dim), + Span::styled("C-j", key), Span::styled("/", dim), + Span::styled("C-k", key), Span::styled(" del-bol/eol ", dim), + Span::styled("C-x", key), Span::styled("/", dim), + Span::styled("C-c", key), Span::styled("/", dim), + Span::styled("C-y", key), Span::styled(" cut/copy/paste ", dim), + ]); + frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area); } } }