From 4d4d6a347034014fbdacd50f914f1ad794360866 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jun 2025 23:17:49 +0100 Subject: [PATCH] Began work on panes and buffers --- .gitignore | 2 + src/action.rs | 111 ++++++++++------ src/main.rs | 35 ++--- src/state.rs | 76 +++++------ src/terminal.rs | 331 ++++++++++++++++++++++++++++++----------------- src/theme.rs | 7 + src/ui/input.rs | 29 +++-- src/ui/mod.rs | 41 +++--- src/ui/panes.rs | 111 +++++++++++++++- src/ui/prompt.rs | 82 +++++++----- src/ui/root.rs | 71 ++++++---- 11 files changed, 590 insertions(+), 306 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d022cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +zte.log diff --git a/src/action.rs b/src/action.rs index d554de4..b6a047b 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,8 +1,5 @@ -use crate::{ - state::State, - terminal::TerminalEvent, -}; -use crossterm::event::{KeyEvent, KeyCode, KeyEventKind, KeyModifiers}; +use crate::{state::State, terminal::TerminalEvent}; +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; #[derive(Clone, Debug)] pub enum Dir { @@ -14,14 +11,15 @@ pub enum Dir { #[derive(Clone, Debug)] pub enum Action { - Char(char), // Insert a character - Backspace, // Backspace a character - Move(Dir), // Move the cursor - Cancel, // Cancels the current context - Go, // Search, accept, or select the current option - Quit, // Quit the application - OpenPrompt, // Open the command prompt - Show(String), // Display some arbitrary text to the user + Char(char), // Insert a character + Backspace, // Backspace a character + Move(Dir), // Move the cursor + PaneMove(Dir), // Move panes + Cancel, // Cancels the current context + Go, // Search, accept, or select the current option + Quit, // Quit the application + OpenPrompt, // Open the command prompt + Show(String), // Display some arbitrary text to the user } pub enum Event { @@ -35,7 +33,7 @@ impl Event { pub fn from_raw(e: TerminalEvent) -> Self { Self::Raw(RawEvent(e)) } - + /// Turn the event into an action (if possible). /// /// The translation function allows elements to translate raw events into their own context-specific actions. @@ -51,22 +49,44 @@ pub struct RawEvent(TerminalEvent); impl RawEvent { pub fn to_char(&self) -> Option { - match &self.0 { + match self.0 { TerminalEvent::Key(KeyEvent { code, - modifiers: KeyModifiers::NONE, + modifiers, kind: KeyEventKind::Press | KeyEventKind::Repeat, .. }) => match code { - KeyCode::Char(c) => Some(*c), - KeyCode::Backspace => Some('\x08'), - KeyCode::Enter => Some('\n'), + KeyCode::Char(c) + if matches!(modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT) => + { + Some(c) + } + KeyCode::Backspace if modifiers == KeyModifiers::NONE => Some('\x08'), + KeyCode::Enter if modifiers == KeyModifiers::NONE => Some('\n'), _ => None, }, _ => None, } } - + + pub fn to_pane_move(&self) -> Option { + match &self.0 { + TerminalEvent::Key(KeyEvent { + code, + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press | KeyEventKind::Repeat, + .. + }) => match code { + KeyCode::Char('a') => Some(Dir::Left), + KeyCode::Char('d') => Some(Dir::Right), + KeyCode::Char('w') => Some(Dir::Up), + KeyCode::Char('s') => Some(Dir::Down), + _ => None, + }, + _ => None, + } + } + pub fn to_move(&self) -> Option { match &self.0 { TerminalEvent::Key(KeyEvent { @@ -84,31 +104,40 @@ impl RawEvent { _ => None, } } - + pub fn is_go(&self) -> bool { - matches!(&self.0, TerminalEvent::Key(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - .. - })) + matches!( + &self.0, + TerminalEvent::Key(KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + .. + }) + ) } - + pub fn is_prompt(&self) -> bool { - matches!(&self.0, TerminalEvent::Key(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::ALT, - kind: KeyEventKind::Press, - .. - })) + matches!( + &self.0, + TerminalEvent::Key(KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + .. + }) + ) } - + pub fn is_cancel(&self) -> bool { - matches!(&self.0, TerminalEvent::Key(KeyEvent { - code: KeyCode::Esc, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - .. - })) + matches!( + &self.0, + TerminalEvent::Key(KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + .. + }) + ) } } diff --git a/src/main.rs b/src/main.rs index 27c4d5c..249f58e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,17 @@ mod action; -mod ui; -mod terminal; mod state; +mod terminal; mod theme; +mod ui; use crate::{ - terminal::{Terminal, TerminalEvent, Color}, - action::{Event, Action, Dir}, + action::{Action, Dir, Event}, state::State, + terminal::{Color, Terminal, TerminalEvent}, ui::{Element as _, Visual as _}, }; use clap::Parser; -use std::{ - path::PathBuf, - time::Duration, - io, -}; +use std::{io, path::PathBuf, time::Duration}; #[derive(Parser, Debug)] struct Args { @@ -31,28 +27,33 @@ pub enum Error { fn main() -> Result<(), Error> { let args = Args::parse(); println!("{args:?}"); - + let mut state = State::try_from(args)?; - let mut ui = ui::Root::new(&state); - + let open_buffers = state.buffers.keys().collect::>(); + let mut ui = ui::Root::new(&mut state, &open_buffers); + Terminal::with(move |term| { loop { // Render the state to the screen term.update(|fb| ui.render(&state, fb)); - + // Wait for a while term.wait_at_least(Duration::from_millis(250)); state.tick(); - + while let Some(ev) = term.get_event() { // Resize events are special and need handling by the terminal - if let TerminalEvent::Resize(cols, rows) = ev { term.set_size([cols, rows]); } - + if let TerminalEvent::Resize(cols, rows) = ev { + term.set_size([cols, rows]); + } + // Have the UI handle events if ui .handle(Event::from_raw(ev)) .map_or(false, |r| r.should_end()) - { return Ok(()); } + { + return Ok(()); + } } } }) diff --git a/src/state.rs b/src/state.rs index e3c07c4..17e2848 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,49 +1,43 @@ use crate::{ theme, - ui::{self, Resp, Element as _}, - Action, Event, Args, Error, Color, + ui::{self, Element as _, Resp}, + Action, Args, Color, Error, Event, }; -use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind}; -use slotmap::{HopSlotMap, new_key_type}; -use std::collections::HashMap; +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use slotmap::{new_key_type, HopSlotMap}; +use std::{io, path::PathBuf}; new_key_type! { - // Per-activity - pub struct ViewId; - pub struct ActivityId; + pub struct BufferId; + pub struct CursorId; } -pub struct Cursor { - base: (usize, usize), - pos: (usize, usize), +#[derive(Default)] +pub struct Cursor {} + +pub struct Buffer { + pub path: PathBuf, + pub chars: Vec, + pub cursors: HopSlotMap, } -pub struct FileView { - line: usize, - cursor: Cursor, - // For searches - // view_cursor: Option, -} - -pub struct File { - views: HopSlotMap, -} - -pub struct ConsoleView { - line: usize, -} - -pub struct Console { - views: HopSlotMap, -} - -pub enum Activity { - File(File), - Console(Console), +impl Buffer { + pub fn new(path: PathBuf) -> Result { + let chars = match std::fs::read_to_string(&path) { + Ok(s) => s.chars().collect(), + Err(err) if err.kind() == io::ErrorKind::NotFound => Vec::new(), + Err(err) => return Err(err.into()), + }; + Ok(Self { + path, + chars, + cursors: HopSlotMap::default(), + }) + } } pub struct State { - pub activities: HopSlotMap, + pub buffers: HopSlotMap, pub tick: u64, pub theme: theme::Theme, } @@ -51,15 +45,21 @@ pub struct State { impl TryFrom for State { type Error = Error; fn try_from(args: Args) -> Result { - Ok(Self { - activities: HopSlotMap::default(), + let mut this = Self { + buffers: HopSlotMap::default(), tick: 0, theme: theme::Theme::default(), - }) + }; + + for path in args.paths { + this.buffers.insert(Buffer::new(path)?); + } + + Ok(this) } } -impl State { +impl State { pub fn tick(&mut self) { self.tick += 1; } diff --git a/src/terminal.rs b/src/terminal.rs index ed3d640..c4329a5 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,25 +1,17 @@ use crate::{theme, Error}; pub use crossterm::{ - cursor::SetCursorStyle as CursorStyle, - event::Event as TerminalEvent, - style::Color, + cursor::SetCursorStyle as CursorStyle, event::Event as TerminalEvent, style::Color, }; +use crossterm::{ + cursor, event, style, terminal, ExecutableCommand, QueueableCommand, SynchronizedUpdate, +}; use std::{ + borrow::Borrow, io::{self, StdoutLock, Write as _}, panic, time::Duration, - borrow::Borrow, -}; -use crossterm::{ - event, - style, - cursor, - terminal, - ExecutableCommand, - QueueableCommand, - SynchronizedUpdate, }; #[derive(Copy, Clone, PartialEq)] @@ -45,23 +37,32 @@ pub struct Rect<'a> { origin: [u16; 2], size: [u16; 2], fb: &'a mut Framebuffer, + has_focus: bool, } impl<'a> Rect<'a> { fn get_mut(&mut self, pos: [usize; 2]) -> Option<&mut Cell> { if pos[0] < self.size()[0] && pos[1] < self.size()[1] { - let offs = [self.origin[0] as usize + pos[0], self.origin[1] as usize + pos[1]]; + let offs = [ + self.origin[0] as usize + pos[0], + self.origin[1] as usize + pos[1], + ]; Some(&mut self.fb.cells[offs[1] * self.fb.size[0] as usize + offs[0]]) } else { None } } - - pub fn with(&mut self, f: impl FnOnce(&mut Rect) -> R) -> R { f(self) } - + + pub fn with(&mut self, f: impl FnOnce(&mut Rect) -> R) -> R { + f(self) + } + pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect { Rect { - origin: [self.origin[0] + origin[0] as u16, self.origin[1] + origin[1] as u16], + origin: [ + self.origin[0] + origin[0] as u16, + self.origin[1] + origin[1] as u16, + ], size: [ size[0].min((self.size[0] as usize).saturating_sub(origin[0])) as u16, size[1].min((self.size[1] as usize).saturating_sub(origin[1])) as u16, @@ -69,63 +70,140 @@ impl<'a> Rect<'a> { fg: self.fg, bg: self.bg, fb: self.fb, + has_focus: self.has_focus, } } - + pub fn with_border(&mut self, theme: &theme::BorderTheme) -> Rect { let edge = self.size().map(|e| e.saturating_sub(1)); for col in 0..edge[0] { - self.get_mut([col, 0]).map(|c| c.c = theme.top); - self.get_mut([col, edge[1]]).map(|c| c.c = theme.bottom); + self.get_mut([col, 0]).map(|c| { + c.c = theme.top; + c.fg = theme.fg; + }); + self.get_mut([col, edge[1]]).map(|c| { + c.c = theme.bottom; + c.fg = theme.fg; + }); } for row in 0..edge[1] { - self.get_mut([0, row]).map(|c| c.c = theme.left); - self.get_mut([edge[0], row]).map(|c| c.c = theme.right); + self.get_mut([0, row]).map(|c| { + c.c = theme.left; + c.fg = theme.fg; + }); + self.get_mut([edge[0], row]).map(|c| { + c.c = theme.right; + c.fg = theme.fg; + }); } - self.get_mut([0, 0]).map(|c| c.c = theme.top_left); - self.get_mut([edge[0], 0]).map(|c| c.c = theme.top_right); - self.get_mut([0, edge[1]]).map(|c| c.c = theme.bottom_left); - self.get_mut([edge[0], edge[1]]).map(|c| c.c = theme.bottom_right); + self.get_mut([0, 0]).map(|c| { + c.c = theme.top_left; + c.fg = theme.fg; + }); + self.get_mut([edge[0], 0]).map(|c| { + c.c = theme.top_right; + c.fg = theme.fg; + }); + self.get_mut([0, edge[1]]).map(|c| { + c.c = theme.bottom_left; + c.fg = theme.fg; + }); + self.get_mut([edge[0], edge[1]]).map(|c| { + c.c = theme.bottom_right; + c.fg = theme.fg; + }); self.rect([1, 1], self.size().map(|e| e.saturating_sub(2))) } - + pub fn with_fg(&mut self, fg: Color) -> Rect { - Rect { fg, bg: self.bg, origin: self.origin, size: self.size, fb: self.fb } - } - - pub fn with_bg(&mut self, bg: Color) -> Rect { - Rect { fg: self.fg, bg, origin: self.origin, size: self.size, fb: self.fb } + Rect { + fg, + bg: self.bg, + origin: self.origin, + size: self.size, + fb: self.fb, + has_focus: self.has_focus, + } + } + + pub fn with_bg(&mut self, bg: Color) -> Rect { + Rect { + fg: self.fg, + bg, + origin: self.origin, + size: self.size, + fb: self.fb, + has_focus: self.has_focus, + } + } + + pub fn with_focus(&mut self, focus: bool) -> Rect { + Rect { + fg: self.fg, + bg: self.bg, + origin: self.origin, + size: self.size, + fb: self.fb, + has_focus: self.has_focus && focus, + } + } + + pub fn has_focus(&self) -> bool { + self.has_focus + } + + pub fn size(&self) -> [usize; 2] { + self.size.map(|e| e as usize) } - pub fn size(&self) -> [usize; 2] { self.size.map(|e| e as usize) } - pub fn fill(&mut self, c: char) -> Rect { for row in 0..self.size()[1] { for col in 0..self.size()[0] { - let cell = Cell { c, fg: self.fg, bg: self.bg }; - if let Some(c) = self.get_mut([col, row]) { *c = cell; } + let cell = Cell { + c, + fg: self.fg, + bg: self.bg, + }; + if let Some(c) = self.get_mut([col, row]) { + *c = cell; + } } } self.rect([0, 0], self.size()) } - - pub fn text>(&mut self, origin: [usize; 2], text: impl IntoIterator) -> Rect { + + pub fn text>( + &mut self, + origin: [usize; 2], + text: impl IntoIterator, + ) -> Rect { for (idx, c) in text.into_iter().enumerate() { if origin[0] + idx >= self.size()[0] { break; } else { - let cell = Cell { c: *c.borrow(), fg: self.fg, bg: self.bg }; - if let Some(c) = self.get_mut([origin[0] + idx, origin[1]]) { *c = cell; } + let cell = Cell { + c: *c.borrow(), + fg: self.fg, + bg: self.bg, + }; + if let Some(c) = self.get_mut([origin[0] + idx, origin[1]]) { + *c = cell; + } } } self.rect([0, 0], self.size()) } - + pub fn set_cursor(&mut self, cursor: [usize; 2], style: CursorStyle) -> Rect { - self.fb.cursor = Some(( - [self.origin[0] + cursor[0] as u16, self.origin[1] + cursor[1] as u16], - style, - )); + if self.has_focus { + self.fb.cursor = Some(( + [ + self.origin[0] + cursor[0] as u16, + self.origin[1] + cursor[1] as u16, + ], + style, + )); + } self.rect([0, 0], self.size()) } } @@ -145,6 +223,7 @@ impl Framebuffer { origin: [0, 0], size: self.size, fb: self, + has_focus: true, } } } @@ -160,112 +239,128 @@ impl<'a> Terminal<'a> { let _ = terminal::enable_raw_mode(); let _ = stdout.execute(terminal::EnterAlternateScreen); } - + fn leave(mut stdout: impl io::Write) { let _ = terminal::disable_raw_mode(); let _ = stdout.execute(terminal::LeaveAlternateScreen); let _ = stdout.execute(cursor::Show); } - - pub fn with(f: impl FnOnce(&mut Self) -> Result + panic::UnwindSafe) -> Result { + + pub fn with( + f: impl FnOnce(&mut Self) -> Result + panic::UnwindSafe, + ) -> Result { let size = terminal::window_size()?; - + Self::enter(io::stdout().lock()); - + let mut this = Self { stdout: io::stdout().lock(), size: [size.columns, size.rows], fb: [Framebuffer::default(), Framebuffer::default()], }; - + let hook = panic::take_hook(); - panic::set_hook(Box::new(move |panic| { Self::leave(io::stdout().lock()); hook(panic); })); + panic::set_hook(Box::new(move |panic| { + Self::leave(io::stdout().lock()); + hook(panic); + })); let res = f(&mut this); - + Self::leave(io::stdout().lock()); - + res } - + pub fn set_size(&mut self, size: [u16; 2]) { self.size = size; } - + pub fn update(&mut self, render: impl FnOnce(&mut Rect)) { // Reset framebuffer if self.fb[0].size != self.size { self.fb[0].size = self.size; - self.fb[0].cells.resize(self.size[0] as usize * self.size[1] as usize, Cell::default()); + self.fb[0].cells.resize( + self.size[0] as usize * self.size[1] as usize, + Cell::default(), + ); } self.fb[0].cursor = None; - + render(&mut self.fb[0].rect()); - - self.stdout.sync_update(|stdout| { - let mut cursor_pos = [0, 0]; - let mut fg = Color::Reset; - let mut bg = Color::Reset; - stdout - .queue(cursor::MoveTo(cursor_pos[0], cursor_pos[1])).unwrap() - .queue(style::SetForegroundColor(fg)).unwrap() - .queue(style::SetBackgroundColor(bg)).unwrap() - .queue(cursor::Hide).unwrap(); - - // Write out changes - for row in 0..self.size[1] { - for col in 0..self.size[0] { - let pos = row as usize * self.size[0] as usize + col as usize; - let cell = self.fb[0].cells[pos]; - - let changed = self.fb[0].size != self.fb[1].size - || cell != self.fb[1].cells[pos]; - - if changed { - if cursor_pos != [col, row] { - // Minimise the work done to move the cursor around - if cursor_pos[1] == row { - stdout.queue(cursor::MoveToColumn(col)).unwrap(); - } else if cursor_pos[0] == col { - stdout.queue(cursor::MoveToRow(row)).unwrap(); - } else { - stdout.queue(cursor::MoveTo(col, row)).unwrap(); + + self.stdout + .sync_update(|stdout| { + let mut cursor_pos = [0, 0]; + let mut fg = Color::Reset; + let mut bg = Color::Reset; + stdout + .queue(cursor::MoveTo(cursor_pos[0], cursor_pos[1])) + .unwrap() + .queue(style::SetForegroundColor(fg)) + .unwrap() + .queue(style::SetBackgroundColor(bg)) + .unwrap() + .queue(cursor::Hide) + .unwrap(); + + // Write out changes + for row in 0..self.size[1] { + for col in 0..self.size[0] { + let pos = row as usize * self.size[0] as usize + col as usize; + let cell = self.fb[0].cells[pos]; + + let changed = + self.fb[0].size != self.fb[1].size || cell != self.fb[1].cells[pos]; + + if changed { + if cursor_pos != [col, row] { + // Minimise the work done to move the cursor around + if cursor_pos[1] == row { + stdout.queue(cursor::MoveToColumn(col)).unwrap(); + } else if cursor_pos[0] == col { + stdout.queue(cursor::MoveToRow(row)).unwrap(); + } else { + stdout.queue(cursor::MoveTo(col, row)).unwrap(); + } + cursor_pos = [col, row]; } - cursor_pos = [col, row]; + if fg != cell.fg { + fg = cell.fg; + stdout.queue(style::SetForegroundColor(fg)).unwrap(); + } + if bg != cell.bg { + bg = cell.bg; + stdout.queue(style::SetBackgroundColor(bg)).unwrap(); + } + + stdout.queue(style::Print(self.fb[0].cells[pos].c)).unwrap(); + + // Move cursor + cursor_pos[0] += 1; } - if fg != cell.fg { - fg = cell.fg; - stdout.queue(style::SetForegroundColor(fg)).unwrap(); - } - if bg != cell.bg { - bg = cell.bg; - stdout.queue(style::SetBackgroundColor(bg)).unwrap(); - } - - stdout.queue(style::Print(self.fb[0].cells[pos].c)).unwrap(); - - // Move cursor - cursor_pos[0] += 1; - if cursor_pos[0] >= self.size[0] { cursor_pos = [0, cursor_pos[1] + 1]; } } } - } - - if let Some(([col, row], style)) = self.fb[0].cursor { - stdout - .queue(cursor::MoveTo(col, row)).unwrap() - .queue(style).unwrap() - .queue(cursor::Show).unwrap(); - } else { - stdout.queue(cursor::Hide).unwrap(); - } - }).unwrap(); - + + if let Some(([col, row], style)) = self.fb[0].cursor { + stdout + .queue(cursor::MoveTo(col, row)) + .unwrap() + .queue(style) + .unwrap() + .queue(cursor::Show) + .unwrap(); + } else { + stdout.queue(cursor::Hide).unwrap(); + } + }) + .unwrap(); + self.stdout.flush().unwrap(); - + // Switch front and back buffers self.fb.swap(0, 1); } - + // Get the next pending event, if one is available. pub fn get_event(&mut self) -> Option { if event::poll(Duration::ZERO).ok()? { @@ -274,7 +369,7 @@ impl<'a> Terminal<'a> { None } } - + // Wait for the given duration or until an event arrives. pub fn wait_at_least(&mut self, dur: Duration) { event::poll(dur).unwrap(); diff --git a/src/theme.rs b/src/theme.rs index 8ab077b..5512b8f 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -9,6 +9,7 @@ pub struct BorderTheme { pub top_right: char, pub bottom_left: char, pub bottom_right: char, + pub fg: Color, } impl Default for BorderTheme { @@ -22,6 +23,7 @@ impl Default for BorderTheme { top_right: '╮', bottom_left: '╰', bottom_right: '╯', + fg: Color::DarkGrey, } } } @@ -30,6 +32,7 @@ pub struct Theme { pub ui_bg: Color, pub status_bg: Color, pub border: BorderTheme, + pub focus_border: BorderTheme, } impl Default for Theme { @@ -38,6 +41,10 @@ impl Default for Theme { ui_bg: Color::AnsiValue(235), status_bg: Color::AnsiValue(23), border: BorderTheme::default(), + focus_border: BorderTheme { + fg: Color::White, + ..BorderTheme::default() + }, } } } diff --git a/src/ui/input.rs b/src/ui/input.rs index ab6903d..30908b5 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -16,43 +16,48 @@ impl Input { impl Element for Input { fn handle(&mut self, event: Event) -> Result { - match event.to_action(|e| e.to_char().map(Action::Char) - .or_else(|| e.to_move().map(Action::Move))) { + match event.to_action(|e| { + e.to_char() + .map(Action::Char) + .or_else(|| e.to_move().map(Action::Move)) + }) { Some(Action::Char('\x08')) => { self.cursor = self.cursor.saturating_sub(1); if self.text.len() > self.cursor { self.text.remove(self.cursor); } Ok(Resp::handled(None)) - }, + } Some(Action::Char(c)) => { self.text.insert(self.cursor, c); self.cursor += 1; Ok(Resp::handled(None)) - }, + } Some(Action::Move(Dir::Left)) => { self.cursor = self.cursor.saturating_sub(1); Ok(Resp::handled(None)) - }, + } Some(Action::Move(Dir::Right)) => { self.cursor = (self.cursor + 1).min(self.text.len()); Ok(Resp::handled(None)) - }, + } _ => Err(event), } } } -impl Visual for Input { +impl Visual for Input { fn render(&self, state: &State, frame: &mut Rect) { frame.with(|frame| { frame.fill(' '); frame.text([0, 0], self.preamble.chars()); - - frame.rect([self.preamble.chars().count(), 0], frame.size()).with(|frame| { - frame.text([0, 0], &self.text); - frame.set_cursor([self.cursor, 0], CursorStyle::BlinkingBar); - }); + + frame + .rect([self.preamble.chars().count(), 0], frame.size()) + .with(|frame| { + frame.text([0, 0], &self.text); + frame.set_cursor([self.cursor, 0], CursorStyle::BlinkingBar); + }); }); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5904008..a0a3d4c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,20 +1,20 @@ +mod input; +mod panes; mod prompt; mod root; -mod panes; mod status; -mod input; pub use self::{ - prompt::{Prompt, Confirm, Show}, - panes::Panes, - status::Status, - root::Root, input::Input, + panes::Panes, + prompt::{Confirm, Prompt, Show}, + root::Root, + status::Status, }; use crate::{ - terminal::{Rect, Color}, - State, Action, Event, Dir, + terminal::{Color, Rect}, + Action, Dir, Event, State, }; pub enum CannotEnd {} @@ -32,8 +32,10 @@ impl Resp { action: action.into(), } } - - pub fn should_end(&self) -> bool { self.should_end.is_some() } + + pub fn should_end(&self) -> bool { + self.should_end.is_some() + } } impl Resp { @@ -43,7 +45,7 @@ impl Resp { action: action.into(), } } - + pub fn into_can_end(self) -> Resp { Resp { should_end: None, @@ -68,18 +70,17 @@ pub struct Label(String); impl std::ops::Deref for Label { type Target = String; - fn deref(&self) -> &Self::Target { &self.0 } + fn deref(&self) -> &Self::Target { + &self.0 + } } impl Visual for Label { fn render(&self, state: &State, frame: &mut Rect) { - frame - .with_bg(state.theme.ui_bg) - .fill(' ') - .with(|frame| { - for (idx, line) in self.lines().enumerate() { - frame.text([0, idx], line.chars()); - } - }); + frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| { + for (idx, line) in self.lines().enumerate() { + frame.text([0, idx], line.chars()); + } + }); } } diff --git a/src/ui/panes.rs b/src/ui/panes.rs index abd1b79..9459070 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1,12 +1,115 @@ use super::*; +use crate::state::{BufferId, Cursor, CursorId}; -pub struct Panes; +#[derive(Clone)] +pub struct Doc { + buffer: BufferId, + cursor: CursorId, +} -impl Element for Panes { +impl Doc { + pub fn new(state: &mut State, buffer: BufferId) -> Self { + Self { + buffer, + cursor: state.buffers[buffer].cursors.insert(Cursor::default()), + } + } +} + +impl Element for Doc { fn handle(&mut self, event: Event) -> Result { - match event.to_action(|e| None) { - //Some(_) => todo!(), + match event.to_action(|e| { + e.to_char() + .map(Action::Char) + .or_else(|| e.to_move().map(Action::Move)) + .or_else(|| e.to_pane_move().map(Action::PaneMove)) + }) { _ => Err(event), } } } + +impl Visual for Doc { + fn render(&self, state: &State, frame: &mut Rect) { + if let Some(buffer) = state.buffers.get(self.buffer) { + for (i, line) in buffer.chars.split(|c| *c == '\n').enumerate() { + frame.text([0, i], line); + } + } else { + frame.text([0, 0], "[Error: no buffer]".chars()); + } + } +} + +#[derive(Clone)] +pub enum Pane { + Doc(Doc), +} + +pub struct Panes { + selected: usize, + panes: Vec, +} + +impl Panes { + pub fn new(state: &mut State, buffers: &[BufferId]) -> Self { + Self { + selected: 0, + panes: buffers + .iter() + .map(|b| Pane::Doc(Doc::new(state, *b))) + .collect(), + } + } +} + +impl Element for Panes { + fn handle(&mut self, event: Event) -> Result { + match event.to_action(|e| e.to_pane_move().map(Action::PaneMove)) { + Some(Action::PaneMove(Dir::Left)) => { + self.selected = (self.selected + self.panes.len() - 1) % self.panes.len(); + Ok(Resp::handled(None)) + } + Some(Action::PaneMove(Dir::Right)) => { + self.selected = (self.selected + 1) % self.panes.len(); + Ok(Resp::handled(None)) + } + // Pass anything else through to the active pane + err => { + if let Some(pane) = self.panes.get_mut(self.selected) { + // Pass to pane + match pane { + Pane::Doc(doc) => doc.handle(event), + } + } else { + // No active pane, don't handle + Err(event) + } + } + } + } +} + +impl Visual for Panes { + fn render(&self, state: &State, frame: &mut Rect) { + for (i, pane) in self.panes.iter().enumerate() { + let boundary = |i| frame.size()[0] * i / self.panes.len(); + + let (x0, x1) = (boundary(i), boundary(i + 1)); + + let is_selected = self.selected == i; + let border_theme = if frame.has_focus() && is_selected { + &state.theme.focus_border + } else { + &state.theme.border + }; + frame + .rect([x0, 0], [x1 - x0, frame.size()[1]]) + .with_border(border_theme) + .with_focus(is_selected) + .with(|frame| match pane { + Pane::Doc(doc) => doc.render(state, frame), + }); + } + } +} diff --git a/src/ui/prompt.rs b/src/ui/prompt.rs index 995ee71..7d5b8ca 100644 --- a/src/ui/prompt.rs +++ b/src/ui/prompt.rs @@ -27,8 +27,14 @@ impl Prompt { pub fn get_action(&self) -> Option { match self.input.get_text().as_str() { "quit" => Some(Action::Quit), - "version" => Some(Action::Show(format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")))), - "help" => Some(Action::Show(format!("Temporary help info:\n- quit\n- version\n- help"))), + "version" => Some(Action::Show(format!( + "{} {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ))), + "help" => Some(Action::Show(format!( + "Temporary help info:\n- quit\n- version\n- help" + ))), _ => None, } } @@ -36,30 +42,36 @@ impl Prompt { impl Element for Prompt { fn handle(&mut self, event: Event) -> Result, Event> { - match event.to_action(|e| if e.is_cancel() { - Some(Action::Cancel) - } else if e.is_go() { - Some(Action::Go) - } else if e.is_prompt() { - Some(Action::OpenPrompt) - } else { - None + match event.to_action(|e| { + if e.is_cancel() { + Some(Action::Cancel) + } else if e.is_go() { + Some(Action::Go) + } else if e.is_prompt() { + Some(Action::OpenPrompt) + } else { + None + } }) { Some(Action::Cancel /*| Action::Prompt*/) => Ok(Resp::end(None)), - Some(Action::Go) => if let Some(action) = self.get_action() { - Ok(Resp::end(action)) - } else { - Ok(Resp::end(Action::Show(format!("unknown command `{}`", self.input.get_text())))) - }, + Some(Action::Go) => { + if let Some(action) = self.get_action() { + Ok(Resp::end(action)) + } else { + Ok(Resp::end(Action::Show(format!( + "unknown command `{}`", + self.input.get_text() + )))) + } + } _ => self.input.handle(event).map(Resp::into_can_end), } } } impl Visual for Prompt { - fn render(&self, state: &State, frame: &mut Rect) { - frame - .with(|f| self.input.render(state, f)); + fn render(&self, state: &State, frame: &mut Rect) { + frame.with(|f| self.input.render(state, f)); } } @@ -69,10 +81,12 @@ pub struct Show { impl Element for Show { fn handle(&mut self, event: Event) -> Result, Event> { - match event.to_action(|e| if e.is_cancel() { - Some(Action::Cancel) - } else { - None + match event.to_action(|e| { + if e.is_cancel() { + Some(Action::Cancel) + } else { + None + } }) { Some(Action::Cancel) => Ok(Resp::end(None)), _ => Err(event), @@ -85,7 +99,10 @@ impl Visual for Show { let lines = self.label.lines().count(); self.label.render( state, - &mut frame.rect([0, frame.size()[1].saturating_sub(3 + lines)], [frame.size()[0], lines]), + &mut frame.rect( + [0, frame.size()[1].saturating_sub(3 + lines)], + [frame.size()[0], lines], + ), ); } } @@ -97,12 +114,14 @@ pub struct Confirm { impl Element for Confirm { fn handle(&mut self, event: Event) -> Result, Event> { - match event.to_action(|e| if e.is_cancel() || e.to_char() == Some('n') { - Some(Action::Cancel) - } else if e.to_char() == Some('y') { - Some(Action::Go) - } else { - None + match event.to_action(|e| { + if e.is_cancel() || e.to_char() == Some('n') { + Some(Action::Cancel) + } else if e.to_char() == Some('y') { + Some(Action::Go) + } else { + None + } }) { Some(Action::Go) => Ok(Resp::end(Some(self.action.clone()))), Some(Action::Cancel) => Ok(Resp::end(None)), @@ -116,7 +135,10 @@ impl Visual for Confirm { let lines = self.label.lines().count(); self.label.render( state, - &mut frame.rect([0, frame.size()[1].saturating_sub(3 + lines)], [frame.size()[0], lines]), + &mut frame.rect( + [0, frame.size()[1].saturating_sub(3 + lines)], + [frame.size()[0], lines], + ), ); } } diff --git a/src/ui/root.rs b/src/ui/root.rs index bba8db5..6ddf0e3 100644 --- a/src/ui/root.rs +++ b/src/ui/root.rs @@ -1,4 +1,5 @@ use super::*; +use crate::state::BufferId; pub struct Root { panes: Panes, @@ -13,9 +14,9 @@ pub enum Task { } impl Root { - pub fn new(state: &State) -> Self { + pub fn new(state: &mut State, buffers: &[BufferId]) -> Self { Self { - panes: Panes, + panes: Panes::new(state, buffers), status: Status, tasks: Vec::new(), } @@ -29,24 +30,28 @@ impl Element for Root { let action = loop { task_idx = match task_idx.checked_sub(1) { Some(task_idx) => task_idx, - None => break match self.panes.handle(event) { - Ok(resp) => resp.action, - Err(event) => event.to_action(|e| if e.is_prompt() { - Some(Action::OpenPrompt) - } else if e.is_cancel() { - Some(Action::Cancel) - } else { - None - }), - }, + None => { + break match self.panes.handle(event) { + Ok(resp) => resp.action, + Err(event) => event.to_action(|e| { + if e.is_prompt() { + Some(Action::OpenPrompt) + } else if e.is_cancel() { + Some(Action::Cancel) + } else { + None + } + }), + } + } }; - + let res = match &mut self.tasks[task_idx] { Task::Prompt(p) => p.handle(event), Task::Show(s) => s.handle(event), Task::Confirm(c) => c.handle(event), }; - + match res { Ok(resp) => { // If the task has requested that it should end, kill it and all of its children @@ -58,16 +63,19 @@ impl Element for Root { Err(e) => event = e, } }; - - // Handle 'top-level' actions + + // Handle 'top-level' actions if let Some(action) = action { match action { Action::OpenPrompt => { self.tasks.clear(); // Prompt overrides all self.tasks.push(Task::Prompt(Prompt { - input: Input { preamble: "> ", ..Input::default() }, + input: Input { + preamble: "> ", + ..Input::default() + }, })); - }, + } Action::Cancel => self.tasks.push(Task::Confirm(Confirm { label: Label("Are you sure you wish to quit? (y/n)".to_string()), action: Action::Quit, @@ -77,7 +85,7 @@ impl Element for Root { action => todo!("Unhandled action {action:?}"), } } - + // Root element swallows all other events Ok(Resp::handled(None)) } @@ -85,25 +93,36 @@ impl Element for Root { impl Visual for Root { fn render(&self, state: &State, frame: &mut Rect) { - frame - .fill(' '); - + frame.fill(' '); + + let task_has_focus = self.tasks.last().is_some(); + // Display status bar frame .rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3]) - .fill(' ') - .with_border(&state.theme.border) + .with_border(if task_has_focus { + &state.theme.focus_border + } else { + &state.theme.border + }) .with(|frame| { if let Some(Task::Prompt(p)) = self.tasks.last() { p.render(state, frame); } }); - + + frame + .rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)]) + .with_focus(!task_has_focus) + .with(|frame| { + self.panes.render(state, frame); + }); + if let Some(task) = self.tasks.last() { match task { Task::Show(s) => s.render(state, frame), Task::Confirm(c) => c.render(state, frame), - _ => {}, + _ => {} } } }