Began work on panes and buffers

This commit is contained in:
Joshua Barretto 2025-06-05 23:17:49 +01:00
parent a58caed9e9
commit 4d4d6a3470
11 changed files with 590 additions and 306 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target/
zte.log

View file

@ -1,8 +1,5 @@
use crate::{ use crate::{state::State, terminal::TerminalEvent};
state::State, use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
terminal::TerminalEvent,
};
use crossterm::event::{KeyEvent, KeyCode, KeyEventKind, KeyModifiers};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Dir { pub enum Dir {
@ -14,14 +11,15 @@ pub enum Dir {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Action { pub enum Action {
Char(char), // Insert a character Char(char), // Insert a character
Backspace, // Backspace a character Backspace, // Backspace a character
Move(Dir), // Move the cursor Move(Dir), // Move the cursor
Cancel, // Cancels the current context PaneMove(Dir), // Move panes
Go, // Search, accept, or select the current option Cancel, // Cancels the current context
Quit, // Quit the application Go, // Search, accept, or select the current option
OpenPrompt, // Open the command prompt Quit, // Quit the application
Show(String), // Display some arbitrary text to the user OpenPrompt, // Open the command prompt
Show(String), // Display some arbitrary text to the user
} }
pub enum Event { pub enum Event {
@ -35,7 +33,7 @@ impl Event {
pub fn from_raw(e: TerminalEvent) -> Self { pub fn from_raw(e: TerminalEvent) -> Self {
Self::Raw(RawEvent(e)) Self::Raw(RawEvent(e))
} }
/// Turn the event into an action (if possible). /// Turn the event into an action (if possible).
/// ///
/// The translation function allows elements to translate raw events into their own context-specific actions. /// 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 { impl RawEvent {
pub fn to_char(&self) -> Option<char> { pub fn to_char(&self) -> Option<char> {
match &self.0 { match self.0 {
TerminalEvent::Key(KeyEvent { TerminalEvent::Key(KeyEvent {
code, code,
modifiers: KeyModifiers::NONE, modifiers,
kind: KeyEventKind::Press | KeyEventKind::Repeat, kind: KeyEventKind::Press | KeyEventKind::Repeat,
.. ..
}) => match code { }) => match code {
KeyCode::Char(c) => Some(*c), KeyCode::Char(c)
KeyCode::Backspace => Some('\x08'), if matches!(modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT) =>
KeyCode::Enter => Some('\n'), {
Some(c)
}
KeyCode::Backspace if modifiers == KeyModifiers::NONE => Some('\x08'),
KeyCode::Enter if modifiers == KeyModifiers::NONE => Some('\n'),
_ => None, _ => None,
}, },
_ => None, _ => None,
} }
} }
pub fn to_pane_move(&self) -> Option<Dir> {
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<Dir> { pub fn to_move(&self) -> Option<Dir> {
match &self.0 { match &self.0 {
TerminalEvent::Key(KeyEvent { TerminalEvent::Key(KeyEvent {
@ -84,31 +104,40 @@ impl RawEvent {
_ => None, _ => None,
} }
} }
pub fn is_go(&self) -> bool { pub fn is_go(&self) -> bool {
matches!(&self.0, TerminalEvent::Key(KeyEvent { matches!(
code: KeyCode::Enter, &self.0,
modifiers: KeyModifiers::NONE, TerminalEvent::Key(KeyEvent {
kind: KeyEventKind::Press, code: KeyCode::Enter,
.. modifiers: KeyModifiers::NONE,
})) kind: KeyEventKind::Press,
..
})
)
} }
pub fn is_prompt(&self) -> bool { pub fn is_prompt(&self) -> bool {
matches!(&self.0, TerminalEvent::Key(KeyEvent { matches!(
code: KeyCode::Enter, &self.0,
modifiers: KeyModifiers::ALT, TerminalEvent::Key(KeyEvent {
kind: KeyEventKind::Press, code: KeyCode::Enter,
.. modifiers: KeyModifiers::ALT,
})) kind: KeyEventKind::Press,
..
})
)
} }
pub fn is_cancel(&self) -> bool { pub fn is_cancel(&self) -> bool {
matches!(&self.0, TerminalEvent::Key(KeyEvent { matches!(
code: KeyCode::Esc, &self.0,
modifiers: KeyModifiers::NONE, TerminalEvent::Key(KeyEvent {
kind: KeyEventKind::Press, code: KeyCode::Esc,
.. modifiers: KeyModifiers::NONE,
})) kind: KeyEventKind::Press,
..
})
)
} }
} }

View file

@ -1,21 +1,17 @@
mod action; mod action;
mod ui;
mod terminal;
mod state; mod state;
mod terminal;
mod theme; mod theme;
mod ui;
use crate::{ use crate::{
terminal::{Terminal, TerminalEvent, Color}, action::{Action, Dir, Event},
action::{Event, Action, Dir},
state::State, state::State,
terminal::{Color, Terminal, TerminalEvent},
ui::{Element as _, Visual as _}, ui::{Element as _, Visual as _},
}; };
use clap::Parser; use clap::Parser;
use std::{ use std::{io, path::PathBuf, time::Duration};
path::PathBuf,
time::Duration,
io,
};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Args { struct Args {
@ -31,28 +27,33 @@ pub enum Error {
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
let args = Args::parse(); let args = Args::parse();
println!("{args:?}"); println!("{args:?}");
let mut state = State::try_from(args)?; let mut state = State::try_from(args)?;
let mut ui = ui::Root::new(&state); let open_buffers = state.buffers.keys().collect::<Vec<_>>();
let mut ui = ui::Root::new(&mut state, &open_buffers);
Terminal::with(move |term| { Terminal::with(move |term| {
loop { loop {
// Render the state to the screen // Render the state to the screen
term.update(|fb| ui.render(&state, fb)); term.update(|fb| ui.render(&state, fb));
// Wait for a while // Wait for a while
term.wait_at_least(Duration::from_millis(250)); term.wait_at_least(Duration::from_millis(250));
state.tick(); state.tick();
while let Some(ev) = term.get_event() { while let Some(ev) = term.get_event() {
// Resize events are special and need handling by the terminal // 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 // Have the UI handle events
if ui if ui
.handle(Event::from_raw(ev)) .handle(Event::from_raw(ev))
.map_or(false, |r| r.should_end()) .map_or(false, |r| r.should_end())
{ return Ok(()); } {
return Ok(());
}
} }
} }
}) })

View file

@ -1,49 +1,43 @@
use crate::{ use crate::{
theme, theme,
ui::{self, Resp, Element as _}, ui::{self, Element as _, Resp},
Action, Event, Args, Error, Color, Action, Args, Color, Error, Event,
}; };
use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use slotmap::{HopSlotMap, new_key_type}; use slotmap::{new_key_type, HopSlotMap};
use std::collections::HashMap; use std::{io, path::PathBuf};
new_key_type! { new_key_type! {
// Per-activity pub struct BufferId;
pub struct ViewId; pub struct CursorId;
pub struct ActivityId;
} }
pub struct Cursor { #[derive(Default)]
base: (usize, usize), pub struct Cursor {}
pos: (usize, usize),
pub struct Buffer {
pub path: PathBuf,
pub chars: Vec<char>,
pub cursors: HopSlotMap<CursorId, Cursor>,
} }
pub struct FileView { impl Buffer {
line: usize, pub fn new(path: PathBuf) -> Result<Self, Error> {
cursor: Cursor, let chars = match std::fs::read_to_string(&path) {
// For searches Ok(s) => s.chars().collect(),
// view_cursor: Option<Cursor>, Err(err) if err.kind() == io::ErrorKind::NotFound => Vec::new(),
} Err(err) => return Err(err.into()),
};
pub struct File { Ok(Self {
views: HopSlotMap<ViewId, FileView>, path,
} chars,
cursors: HopSlotMap::default(),
pub struct ConsoleView { })
line: usize, }
}
pub struct Console {
views: HopSlotMap<ViewId, ConsoleView>,
}
pub enum Activity {
File(File),
Console(Console),
} }
pub struct State { pub struct State {
pub activities: HopSlotMap<ActivityId, Activity>, pub buffers: HopSlotMap<BufferId, Buffer>,
pub tick: u64, pub tick: u64,
pub theme: theme::Theme, pub theme: theme::Theme,
} }
@ -51,15 +45,21 @@ pub struct State {
impl TryFrom<Args> for State { impl TryFrom<Args> for State {
type Error = Error; type Error = Error;
fn try_from(args: Args) -> Result<Self, Self::Error> { fn try_from(args: Args) -> Result<Self, Self::Error> {
Ok(Self { let mut this = Self {
activities: HopSlotMap::default(), buffers: HopSlotMap::default(),
tick: 0, tick: 0,
theme: theme::Theme::default(), 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) { pub fn tick(&mut self) {
self.tick += 1; self.tick += 1;
} }

View file

@ -1,25 +1,17 @@
use crate::{theme, Error}; use crate::{theme, Error};
pub use crossterm::{ pub use crossterm::{
cursor::SetCursorStyle as CursorStyle, cursor::SetCursorStyle as CursorStyle, event::Event as TerminalEvent, style::Color,
event::Event as TerminalEvent,
style::Color,
}; };
use crossterm::{
cursor, event, style, terminal, ExecutableCommand, QueueableCommand, SynchronizedUpdate,
};
use std::{ use std::{
borrow::Borrow,
io::{self, StdoutLock, Write as _}, io::{self, StdoutLock, Write as _},
panic, panic,
time::Duration, time::Duration,
borrow::Borrow,
};
use crossterm::{
event,
style,
cursor,
terminal,
ExecutableCommand,
QueueableCommand,
SynchronizedUpdate,
}; };
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
@ -45,23 +37,32 @@ pub struct Rect<'a> {
origin: [u16; 2], origin: [u16; 2],
size: [u16; 2], size: [u16; 2],
fb: &'a mut Framebuffer, fb: &'a mut Framebuffer,
has_focus: bool,
} }
impl<'a> Rect<'a> { impl<'a> Rect<'a> {
fn get_mut(&mut self, pos: [usize; 2]) -> Option<&mut Cell> { fn get_mut(&mut self, pos: [usize; 2]) -> Option<&mut Cell> {
if pos[0] < self.size()[0] && pos[1] < self.size()[1] { 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]]) Some(&mut self.fb.cells[offs[1] * self.fb.size[0] as usize + offs[0]])
} else { } else {
None None
} }
} }
pub fn with<R>(&mut self, f: impl FnOnce(&mut Rect) -> R) -> R { f(self) } pub fn with<R>(&mut self, f: impl FnOnce(&mut Rect) -> R) -> R {
f(self)
}
pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect { pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect {
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: [
size[0].min((self.size[0] as usize).saturating_sub(origin[0])) as u16, 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, 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, fg: self.fg,
bg: self.bg, bg: self.bg,
fb: self.fb, fb: self.fb,
has_focus: self.has_focus,
} }
} }
pub fn with_border(&mut self, theme: &theme::BorderTheme) -> Rect { pub fn with_border(&mut self, theme: &theme::BorderTheme) -> Rect {
let edge = self.size().map(|e| e.saturating_sub(1)); let edge = self.size().map(|e| e.saturating_sub(1));
for col in 0..edge[0] { for col in 0..edge[0] {
self.get_mut([col, 0]).map(|c| c.c = theme.top); self.get_mut([col, 0]).map(|c| {
self.get_mut([col, edge[1]]).map(|c| c.c = theme.bottom); 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] { for row in 0..edge[1] {
self.get_mut([0, row]).map(|c| c.c = theme.left); self.get_mut([0, row]).map(|c| {
self.get_mut([edge[0], row]).map(|c| c.c = theme.right); 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([0, 0]).map(|c| {
self.get_mut([edge[0], 0]).map(|c| c.c = theme.top_right); c.c = theme.top_left;
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); });
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))) self.rect([1, 1], self.size().map(|e| e.saturating_sub(2)))
} }
pub fn with_fg(&mut self, fg: Color) -> Rect { pub fn with_fg(&mut self, fg: Color) -> Rect {
Rect { fg, bg: self.bg, origin: self.origin, size: self.size, fb: self.fb } Rect {
} fg,
bg: self.bg,
pub fn with_bg(&mut self, bg: Color) -> Rect { origin: self.origin,
Rect { fg: self.fg, bg, origin: self.origin, size: self.size, fb: self.fb } 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 { pub fn fill(&mut self, c: char) -> Rect {
for row in 0..self.size()[1] { for row in 0..self.size()[1] {
for col in 0..self.size()[0] { for col in 0..self.size()[0] {
let cell = Cell { c, fg: self.fg, bg: self.bg }; let cell = Cell {
if let Some(c) = self.get_mut([col, row]) { *c = 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()) self.rect([0, 0], self.size())
} }
pub fn text<C: Borrow<char>>(&mut self, origin: [usize; 2], text: impl IntoIterator<Item = C>) -> Rect { pub fn text<C: Borrow<char>>(
&mut self,
origin: [usize; 2],
text: impl IntoIterator<Item = C>,
) -> Rect {
for (idx, c) in text.into_iter().enumerate() { for (idx, c) in text.into_iter().enumerate() {
if origin[0] + idx >= self.size()[0] { if origin[0] + idx >= self.size()[0] {
break; break;
} else { } else {
let cell = Cell { c: *c.borrow(), fg: self.fg, bg: self.bg }; let cell = Cell {
if let Some(c) = self.get_mut([origin[0] + idx, origin[1]]) { *c = 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()) self.rect([0, 0], self.size())
} }
pub fn set_cursor(&mut self, cursor: [usize; 2], style: CursorStyle) -> Rect { pub fn set_cursor(&mut self, cursor: [usize; 2], style: CursorStyle) -> Rect {
self.fb.cursor = Some(( if self.has_focus {
[self.origin[0] + cursor[0] as u16, self.origin[1] + cursor[1] as u16], self.fb.cursor = Some((
style, [
)); self.origin[0] + cursor[0] as u16,
self.origin[1] + cursor[1] as u16,
],
style,
));
}
self.rect([0, 0], self.size()) self.rect([0, 0], self.size())
} }
} }
@ -145,6 +223,7 @@ impl Framebuffer {
origin: [0, 0], origin: [0, 0],
size: self.size, size: self.size,
fb: self, fb: self,
has_focus: true,
} }
} }
} }
@ -160,112 +239,128 @@ impl<'a> Terminal<'a> {
let _ = terminal::enable_raw_mode(); let _ = terminal::enable_raw_mode();
let _ = stdout.execute(terminal::EnterAlternateScreen); let _ = stdout.execute(terminal::EnterAlternateScreen);
} }
fn leave(mut stdout: impl io::Write) { fn leave(mut stdout: impl io::Write) {
let _ = terminal::disable_raw_mode(); let _ = terminal::disable_raw_mode();
let _ = stdout.execute(terminal::LeaveAlternateScreen); let _ = stdout.execute(terminal::LeaveAlternateScreen);
let _ = stdout.execute(cursor::Show); let _ = stdout.execute(cursor::Show);
} }
pub fn with<T>(f: impl FnOnce(&mut Self) -> Result<T, Error> + panic::UnwindSafe) -> Result<T, Error> { pub fn with<T>(
f: impl FnOnce(&mut Self) -> Result<T, Error> + panic::UnwindSafe,
) -> Result<T, Error> {
let size = terminal::window_size()?; let size = terminal::window_size()?;
Self::enter(io::stdout().lock()); Self::enter(io::stdout().lock());
let mut this = Self { let mut this = Self {
stdout: io::stdout().lock(), stdout: io::stdout().lock(),
size: [size.columns, size.rows], size: [size.columns, size.rows],
fb: [Framebuffer::default(), Framebuffer::default()], fb: [Framebuffer::default(), Framebuffer::default()],
}; };
let hook = panic::take_hook(); 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); let res = f(&mut this);
Self::leave(io::stdout().lock()); Self::leave(io::stdout().lock());
res res
} }
pub fn set_size(&mut self, size: [u16; 2]) { pub fn set_size(&mut self, size: [u16; 2]) {
self.size = size; self.size = size;
} }
pub fn update(&mut self, render: impl FnOnce(&mut Rect)) { pub fn update(&mut self, render: impl FnOnce(&mut Rect)) {
// Reset framebuffer // Reset framebuffer
if self.fb[0].size != self.size { if self.fb[0].size != self.size {
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; self.fb[0].cursor = None;
render(&mut self.fb[0].rect()); render(&mut self.fb[0].rect());
self.stdout.sync_update(|stdout| { self.stdout
let mut cursor_pos = [0, 0]; .sync_update(|stdout| {
let mut fg = Color::Reset; let mut cursor_pos = [0, 0];
let mut bg = Color::Reset; let mut fg = Color::Reset;
stdout let mut bg = Color::Reset;
.queue(cursor::MoveTo(cursor_pos[0], cursor_pos[1])).unwrap() stdout
.queue(style::SetForegroundColor(fg)).unwrap() .queue(cursor::MoveTo(cursor_pos[0], cursor_pos[1]))
.queue(style::SetBackgroundColor(bg)).unwrap() .unwrap()
.queue(cursor::Hide).unwrap(); .queue(style::SetForegroundColor(fg))
.unwrap()
// Write out changes .queue(style::SetBackgroundColor(bg))
for row in 0..self.size[1] { .unwrap()
for col in 0..self.size[0] { .queue(cursor::Hide)
let pos = row as usize * self.size[0] as usize + col as usize; .unwrap();
let cell = self.fb[0].cells[pos];
// Write out changes
let changed = self.fb[0].size != self.fb[1].size for row in 0..self.size[1] {
|| cell != self.fb[1].cells[pos]; for col in 0..self.size[0] {
let pos = row as usize * self.size[0] as usize + col as usize;
if changed { let cell = self.fb[0].cells[pos];
if cursor_pos != [col, row] {
// Minimise the work done to move the cursor around let changed =
if cursor_pos[1] == row { self.fb[0].size != self.fb[1].size || cell != self.fb[1].cells[pos];
stdout.queue(cursor::MoveToColumn(col)).unwrap();
} else if cursor_pos[0] == col { if changed {
stdout.queue(cursor::MoveToRow(row)).unwrap(); if cursor_pos != [col, row] {
} else { // Minimise the work done to move the cursor around
stdout.queue(cursor::MoveTo(col, row)).unwrap(); 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 {
if let Some(([col, row], style)) = self.fb[0].cursor { stdout
stdout .queue(cursor::MoveTo(col, row))
.queue(cursor::MoveTo(col, row)).unwrap() .unwrap()
.queue(style).unwrap() .queue(style)
.queue(cursor::Show).unwrap(); .unwrap()
} else { .queue(cursor::Show)
stdout.queue(cursor::Hide).unwrap(); .unwrap();
} } else {
}).unwrap(); stdout.queue(cursor::Hide).unwrap();
}
})
.unwrap();
self.stdout.flush().unwrap(); self.stdout.flush().unwrap();
// Switch front and back buffers // Switch front and back buffers
self.fb.swap(0, 1); self.fb.swap(0, 1);
} }
// Get the next pending event, if one is available. // Get the next pending event, if one is available.
pub fn get_event(&mut self) -> Option<TerminalEvent> { pub fn get_event(&mut self) -> Option<TerminalEvent> {
if event::poll(Duration::ZERO).ok()? { if event::poll(Duration::ZERO).ok()? {
@ -274,7 +369,7 @@ impl<'a> Terminal<'a> {
None None
} }
} }
// Wait for the given duration or until an event arrives. // Wait for the given duration or until an event arrives.
pub fn wait_at_least(&mut self, dur: Duration) { pub fn wait_at_least(&mut self, dur: Duration) {
event::poll(dur).unwrap(); event::poll(dur).unwrap();

View file

@ -9,6 +9,7 @@ pub struct BorderTheme {
pub top_right: char, pub top_right: char,
pub bottom_left: char, pub bottom_left: char,
pub bottom_right: char, pub bottom_right: char,
pub fg: Color,
} }
impl Default for BorderTheme { impl Default for BorderTheme {
@ -22,6 +23,7 @@ impl Default for BorderTheme {
top_right: '', top_right: '',
bottom_left: '', bottom_left: '',
bottom_right: '', bottom_right: '',
fg: Color::DarkGrey,
} }
} }
} }
@ -30,6 +32,7 @@ pub struct Theme {
pub ui_bg: Color, pub ui_bg: Color,
pub status_bg: Color, pub status_bg: Color,
pub border: BorderTheme, pub border: BorderTheme,
pub focus_border: BorderTheme,
} }
impl Default for Theme { impl Default for Theme {
@ -38,6 +41,10 @@ impl Default for Theme {
ui_bg: Color::AnsiValue(235), ui_bg: Color::AnsiValue(235),
status_bg: Color::AnsiValue(23), status_bg: Color::AnsiValue(23),
border: BorderTheme::default(), border: BorderTheme::default(),
focus_border: BorderTheme {
fg: Color::White,
..BorderTheme::default()
},
} }
} }
} }

View file

@ -16,43 +16,48 @@ impl Input {
impl Element for Input { impl Element for Input {
fn handle(&mut self, event: Event) -> Result<Resp, Event> { fn handle(&mut self, event: Event) -> Result<Resp, Event> {
match event.to_action(|e| e.to_char().map(Action::Char) match event.to_action(|e| {
.or_else(|| e.to_move().map(Action::Move))) { e.to_char()
.map(Action::Char)
.or_else(|| e.to_move().map(Action::Move))
}) {
Some(Action::Char('\x08')) => { Some(Action::Char('\x08')) => {
self.cursor = self.cursor.saturating_sub(1); self.cursor = self.cursor.saturating_sub(1);
if self.text.len() > self.cursor { if self.text.len() > self.cursor {
self.text.remove(self.cursor); self.text.remove(self.cursor);
} }
Ok(Resp::handled(None)) Ok(Resp::handled(None))
}, }
Some(Action::Char(c)) => { Some(Action::Char(c)) => {
self.text.insert(self.cursor, c); self.text.insert(self.cursor, c);
self.cursor += 1; self.cursor += 1;
Ok(Resp::handled(None)) Ok(Resp::handled(None))
}, }
Some(Action::Move(Dir::Left)) => { Some(Action::Move(Dir::Left)) => {
self.cursor = self.cursor.saturating_sub(1); self.cursor = self.cursor.saturating_sub(1);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
}, }
Some(Action::Move(Dir::Right)) => { Some(Action::Move(Dir::Right)) => {
self.cursor = (self.cursor + 1).min(self.text.len()); self.cursor = (self.cursor + 1).min(self.text.len());
Ok(Resp::handled(None)) Ok(Resp::handled(None))
}, }
_ => Err(event), _ => Err(event),
} }
} }
} }
impl Visual for Input { impl Visual for Input {
fn render(&self, state: &State, frame: &mut Rect) { fn render(&self, state: &State, frame: &mut Rect) {
frame.with(|frame| { frame.with(|frame| {
frame.fill(' '); frame.fill(' ');
frame.text([0, 0], self.preamble.chars()); frame.text([0, 0], self.preamble.chars());
frame.rect([self.preamble.chars().count(), 0], frame.size()).with(|frame| { frame
frame.text([0, 0], &self.text); .rect([self.preamble.chars().count(), 0], frame.size())
frame.set_cursor([self.cursor, 0], CursorStyle::BlinkingBar); .with(|frame| {
}); frame.text([0, 0], &self.text);
frame.set_cursor([self.cursor, 0], CursorStyle::BlinkingBar);
});
}); });
} }
} }

View file

@ -1,20 +1,20 @@
mod input;
mod panes;
mod prompt; mod prompt;
mod root; mod root;
mod panes;
mod status; mod status;
mod input;
pub use self::{ pub use self::{
prompt::{Prompt, Confirm, Show},
panes::Panes,
status::Status,
root::Root,
input::Input, input::Input,
panes::Panes,
prompt::{Confirm, Prompt, Show},
root::Root,
status::Status,
}; };
use crate::{ use crate::{
terminal::{Rect, Color}, terminal::{Color, Rect},
State, Action, Event, Dir, Action, Dir, Event, State,
}; };
pub enum CannotEnd {} pub enum CannotEnd {}
@ -32,8 +32,10 @@ impl Resp<CanEnd> {
action: action.into(), 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<T> Resp<T> { impl<T> Resp<T> {
@ -43,7 +45,7 @@ impl<T> Resp<T> {
action: action.into(), action: action.into(),
} }
} }
pub fn into_can_end(self) -> Resp<CanEnd> { pub fn into_can_end(self) -> Resp<CanEnd> {
Resp { Resp {
should_end: None, should_end: None,
@ -68,18 +70,17 @@ pub struct Label(String);
impl std::ops::Deref for Label { impl std::ops::Deref for Label {
type Target = String; type Target = String;
fn deref(&self) -> &Self::Target { &self.0 } fn deref(&self) -> &Self::Target {
&self.0
}
} }
impl Visual for Label { impl Visual for Label {
fn render(&self, state: &State, frame: &mut Rect) { fn render(&self, state: &State, frame: &mut Rect) {
frame frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| {
.with_bg(state.theme.ui_bg) for (idx, line) in self.lines().enumerate() {
.fill(' ') frame.text([0, idx], line.chars());
.with(|frame| { }
for (idx, line) in self.lines().enumerate() { });
frame.text([0, idx], line.chars());
}
});
} }
} }

View file

@ -1,12 +1,115 @@
use super::*; 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<Resp, Event> { fn handle(&mut self, event: Event) -> Result<Resp, Event> {
match event.to_action(|e| None) { match event.to_action(|e| {
//Some(_) => todo!(), 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), _ => 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<Pane>,
}
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<Resp, Event> {
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),
});
}
}
}

View file

@ -27,8 +27,14 @@ impl Prompt {
pub fn get_action(&self) -> Option<Action> { pub fn get_action(&self) -> Option<Action> {
match self.input.get_text().as_str() { match self.input.get_text().as_str() {
"quit" => Some(Action::Quit), "quit" => Some(Action::Quit),
"version" => Some(Action::Show(format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")))), "version" => Some(Action::Show(format!(
"help" => Some(Action::Show(format!("Temporary help info:\n- quit\n- version\n- help"))), "{} {}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
))),
"help" => Some(Action::Show(format!(
"Temporary help info:\n- quit\n- version\n- help"
))),
_ => None, _ => None,
} }
} }
@ -36,30 +42,36 @@ impl Prompt {
impl Element<CanEnd> for Prompt { impl Element<CanEnd> for Prompt {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| if e.is_cancel() { match event.to_action(|e| {
Some(Action::Cancel) if e.is_cancel() {
} else if e.is_go() { Some(Action::Cancel)
Some(Action::Go) } else if e.is_go() {
} else if e.is_prompt() { Some(Action::Go)
Some(Action::OpenPrompt) } else if e.is_prompt() {
} else { Some(Action::OpenPrompt)
None } else {
None
}
}) { }) {
Some(Action::Cancel /*| Action::Prompt*/) => Ok(Resp::end(None)), Some(Action::Cancel /*| Action::Prompt*/) => Ok(Resp::end(None)),
Some(Action::Go) => if let Some(action) = self.get_action() { Some(Action::Go) => {
Ok(Resp::end(action)) if let Some(action) = self.get_action() {
} else { Ok(Resp::end(action))
Ok(Resp::end(Action::Show(format!("unknown command `{}`", self.input.get_text())))) } else {
}, Ok(Resp::end(Action::Show(format!(
"unknown command `{}`",
self.input.get_text()
))))
}
}
_ => self.input.handle(event).map(Resp::into_can_end), _ => self.input.handle(event).map(Resp::into_can_end),
} }
} }
} }
impl Visual for Prompt { impl Visual for Prompt {
fn render(&self, state: &State, frame: &mut Rect) { fn render(&self, state: &State, frame: &mut Rect) {
frame frame.with(|f| self.input.render(state, f));
.with(|f| self.input.render(state, f));
} }
} }
@ -69,10 +81,12 @@ pub struct Show {
impl Element<CanEnd> for Show { impl Element<CanEnd> for Show {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| if e.is_cancel() { match event.to_action(|e| {
Some(Action::Cancel) if e.is_cancel() {
} else { Some(Action::Cancel)
None } else {
None
}
}) { }) {
Some(Action::Cancel) => Ok(Resp::end(None)), Some(Action::Cancel) => Ok(Resp::end(None)),
_ => Err(event), _ => Err(event),
@ -85,7 +99,10 @@ impl Visual for Show {
let lines = self.label.lines().count(); let lines = self.label.lines().count();
self.label.render( self.label.render(
state, 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<CanEnd> for Confirm { impl Element<CanEnd> for Confirm {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| if e.is_cancel() || e.to_char() == Some('n') { match event.to_action(|e| {
Some(Action::Cancel) if e.is_cancel() || e.to_char() == Some('n') {
} else if e.to_char() == Some('y') { Some(Action::Cancel)
Some(Action::Go) } else if e.to_char() == Some('y') {
} else { Some(Action::Go)
None } else {
None
}
}) { }) {
Some(Action::Go) => Ok(Resp::end(Some(self.action.clone()))), Some(Action::Go) => Ok(Resp::end(Some(self.action.clone()))),
Some(Action::Cancel) => Ok(Resp::end(None)), Some(Action::Cancel) => Ok(Resp::end(None)),
@ -116,7 +135,10 @@ impl Visual for Confirm {
let lines = self.label.lines().count(); let lines = self.label.lines().count();
self.label.render( self.label.render(
state, 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],
),
); );
} }
} }

View file

@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::state::BufferId;
pub struct Root { pub struct Root {
panes: Panes, panes: Panes,
@ -13,9 +14,9 @@ pub enum Task {
} }
impl Root { impl Root {
pub fn new(state: &State) -> Self { pub fn new(state: &mut State, buffers: &[BufferId]) -> Self {
Self { Self {
panes: Panes, panes: Panes::new(state, buffers),
status: Status, status: Status,
tasks: Vec::new(), tasks: Vec::new(),
} }
@ -29,24 +30,28 @@ impl Element<CanEnd> for Root {
let action = loop { let action = loop {
task_idx = match task_idx.checked_sub(1) { task_idx = match task_idx.checked_sub(1) {
Some(task_idx) => task_idx, Some(task_idx) => task_idx,
None => break match self.panes.handle(event) { None => {
Ok(resp) => resp.action, break match self.panes.handle(event) {
Err(event) => event.to_action(|e| if e.is_prompt() { Ok(resp) => resp.action,
Some(Action::OpenPrompt) Err(event) => event.to_action(|e| {
} else if e.is_cancel() { if e.is_prompt() {
Some(Action::Cancel) Some(Action::OpenPrompt)
} else { } else if e.is_cancel() {
None Some(Action::Cancel)
}), } else {
}, None
}
}),
}
}
}; };
let res = match &mut self.tasks[task_idx] { let res = match &mut self.tasks[task_idx] {
Task::Prompt(p) => p.handle(event), Task::Prompt(p) => p.handle(event),
Task::Show(s) => s.handle(event), Task::Show(s) => s.handle(event),
Task::Confirm(c) => c.handle(event), Task::Confirm(c) => c.handle(event),
}; };
match res { match res {
Ok(resp) => { Ok(resp) => {
// If the task has requested that it should end, kill it and all of its children // If the task has requested that it should end, kill it and all of its children
@ -58,16 +63,19 @@ impl Element<CanEnd> for Root {
Err(e) => event = e, Err(e) => event = e,
} }
}; };
// Handle 'top-level' actions // Handle 'top-level' actions
if let Some(action) = action { if let Some(action) = action {
match action { match action {
Action::OpenPrompt => { Action::OpenPrompt => {
self.tasks.clear(); // Prompt overrides all self.tasks.clear(); // Prompt overrides all
self.tasks.push(Task::Prompt(Prompt { self.tasks.push(Task::Prompt(Prompt {
input: Input { preamble: "> ", ..Input::default() }, input: Input {
preamble: "> ",
..Input::default()
},
})); }));
}, }
Action::Cancel => self.tasks.push(Task::Confirm(Confirm { Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
label: Label("Are you sure you wish to quit? (y/n)".to_string()), label: Label("Are you sure you wish to quit? (y/n)".to_string()),
action: Action::Quit, action: Action::Quit,
@ -77,7 +85,7 @@ impl Element<CanEnd> for Root {
action => todo!("Unhandled action {action:?}"), action => todo!("Unhandled action {action:?}"),
} }
} }
// Root element swallows all other events // Root element swallows all other events
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
@ -85,25 +93,36 @@ impl Element<CanEnd> for Root {
impl Visual for Root { impl Visual for Root {
fn render(&self, state: &State, frame: &mut Rect) { fn render(&self, state: &State, frame: &mut Rect) {
frame frame.fill(' ');
.fill(' ');
let task_has_focus = self.tasks.last().is_some();
// Display status bar // Display status bar
frame frame
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3]) .rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
.fill(' ') .with_border(if task_has_focus {
.with_border(&state.theme.border) &state.theme.focus_border
} else {
&state.theme.border
})
.with(|frame| { .with(|frame| {
if let Some(Task::Prompt(p)) = self.tasks.last() { if let Some(Task::Prompt(p)) = self.tasks.last() {
p.render(state, frame); 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() { if let Some(task) = self.tasks.last() {
match task { match task {
Task::Show(s) => s.render(state, frame), Task::Show(s) => s.render(state, frame),
Task::Confirm(c) => c.render(state, frame), Task::Confirm(c) => c.render(state, frame),
_ => {}, _ => {}
} }
} }
} }