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::{
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 {
@ -17,6 +14,7 @@ pub enum Action {
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
@ -51,16 +49,38 @@ pub struct RawEvent(TerminalEvent);
impl RawEvent {
pub fn to_char(&self) -> Option<char> {
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<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,
@ -86,29 +106,38 @@ impl RawEvent {
}
pub fn is_go(&self) -> bool {
matches!(&self.0, TerminalEvent::Key(KeyEvent {
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 {
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 {
matches!(
&self.0,
TerminalEvent::Key(KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
}))
})
)
}
}

View file

@ -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 {
@ -33,7 +29,8 @@ fn main() -> Result<(), Error> {
println!("{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| {
loop {
@ -46,13 +43,17 @@ fn main() -> Result<(), Error> {
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(());
}
}
}
})

View file

@ -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<char>,
pub cursors: HopSlotMap<CursorId, Cursor>,
}
pub struct FileView {
line: usize,
cursor: Cursor,
// For searches
// view_cursor: Option<Cursor>,
}
pub struct File {
views: HopSlotMap<ViewId, FileView>,
}
pub struct ConsoleView {
line: usize,
}
pub struct Console {
views: HopSlotMap<ViewId, ConsoleView>,
}
pub enum Activity {
File(File),
Console(Console),
impl Buffer {
pub fn new(path: PathBuf) -> Result<Self, Error> {
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<ActivityId, Activity>,
pub buffers: HopSlotMap<BufferId, Buffer>,
pub tick: u64,
pub theme: theme::Theme,
}
@ -51,11 +45,17 @@ pub struct State {
impl TryFrom<Args> for State {
type Error = Error;
fn try_from(args: Args) -> Result<Self, Self::Error> {
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)
}
}

View file

@ -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<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 {
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 }
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 }
Rect {
fg: self.fg,
bg,
origin: self.origin,
size: self.size,
fb: self.fb,
has_focus: self.has_focus,
}
}
pub fn size(&self) -> [usize; 2] { self.size.map(|e| e as usize) }
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 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<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() {
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 {
if self.has_focus {
self.fb.cursor = Some((
[self.origin[0] + cursor[0] as u16, self.origin[1] + cursor[1] as u16],
[
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,
}
}
}
@ -167,7 +246,9 @@ impl<'a> Terminal<'a> {
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()?;
Self::enter(io::stdout().lock());
@ -179,7 +260,10 @@ impl<'a> Terminal<'a> {
};
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());
@ -195,21 +279,29 @@ impl<'a> Terminal<'a> {
// 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| {
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();
.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] {
@ -217,8 +309,8 @@ impl<'a> Terminal<'a> {
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];
let changed =
self.fb[0].size != self.fb[1].size || cell != self.fb[1].cells[pos];
if changed {
if cursor_pos != [col, row] {
@ -245,20 +337,23 @@ impl<'a> Terminal<'a> {
// 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();
.queue(cursor::MoveTo(col, row))
.unwrap()
.queue(style)
.unwrap()
.queue(cursor::Show)
.unwrap();
} else {
stdout.queue(cursor::Hide).unwrap();
}
}).unwrap();
})
.unwrap();
self.stdout.flush().unwrap();

View file

@ -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()
},
}
}
}

View file

@ -16,28 +16,31 @@ impl Input {
impl Element for Input {
fn handle(&mut self, event: Event) -> Result<Resp, Event> {
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),
}
}
@ -49,7 +52,9 @@ impl Visual for Input {
frame.fill(' ');
frame.text([0, 0], self.preamble.chars());
frame.rect([self.preamble.chars().count(), 0], frame.size()).with(|frame| {
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);
});

View file

@ -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 {}
@ -33,7 +33,9 @@ impl Resp<CanEnd> {
}
}
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> {
@ -68,15 +70,14 @@ 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| {
frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| {
for (idx, line) in self.lines().enumerate() {
frame.text([0, idx], line.chars());
}

View file

@ -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<Resp, Event> {
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<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> {
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,7 +42,8 @@ impl Prompt {
impl Element<CanEnd> for Prompt {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| if e.is_cancel() {
match event.to_action(|e| {
if e.is_cancel() {
Some(Action::Cancel)
} else if e.is_go() {
Some(Action::Go)
@ -44,13 +51,19 @@ impl Element<CanEnd> for 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() {
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()))))
},
Ok(Resp::end(Action::Show(format!(
"unknown command `{}`",
self.input.get_text()
))))
}
}
_ => self.input.handle(event).map(Resp::into_can_end),
}
}
@ -58,8 +71,7 @@ impl Element<CanEnd> for Prompt {
impl Visual for Prompt {
fn render(&self, state: &State, frame: &mut Rect) {
frame
.with(|f| self.input.render(state, f));
frame.with(|f| self.input.render(state, f));
}
}
@ -69,10 +81,12 @@ pub struct Show {
impl Element<CanEnd> for Show {
fn handle(&mut self, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| if e.is_cancel() {
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<CanEnd> for Confirm {
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| {
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],
),
);
}
}

View file

@ -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,16 +30,20 @@ impl Element<CanEnd> 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) {
None => {
break match self.panes.handle(event) {
Ok(resp) => resp.action,
Err(event) => event.to_action(|e| if e.is_prompt() {
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] {
@ -65,9 +70,12 @@ impl Element<CanEnd> for Root {
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,
@ -85,25 +93,36 @@ impl Element<CanEnd> 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),
_ => {},
_ => {}
}
}
}