Began work on panes and buffers
This commit is contained in:
parent
a58caed9e9
commit
4d4d6a3470
11 changed files with 590 additions and 306 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
target/
|
||||||
|
zte.log
|
||||||
111
src/action.rs
111
src/action.rs
|
|
@ -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,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
src/main.rs
35
src/main.rs
|
|
@ -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(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
76
src/state.rs
76
src/state.rs
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
331
src/terminal.rs
331
src/terminal.rs
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
src/ui/panes.rs
111
src/ui/panes.rs
|
|
@ -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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
_ => {},
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue