Added buffer switching
This commit is contained in:
parent
8c0a033f3c
commit
ebc4d97dbc
8 changed files with 232 additions and 91 deletions
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{state::State, terminal::TerminalEvent};
|
||||
use crate::{state::BufferId, terminal::TerminalEvent};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -11,15 +11,19 @@ pub enum Dir {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
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
|
||||
OpenPrompt, // Open the command prompt
|
||||
Show(String), // Display some arbitrary text to the user
|
||||
Char(char), // Insert a character
|
||||
Backspace, // Backspace a character
|
||||
Move(Dir), // Move the cursor
|
||||
PaneMove(Dir), // Move panes
|
||||
Cancel, // Cancels the current context
|
||||
Go, // Search, accept, or select the current option
|
||||
Yes, // A binary confirmation is answered 'yes'
|
||||
No, // A binary confirmation is answered 'no'
|
||||
Quit, // Quit the application
|
||||
OpenPrompt, // Open the command prompt
|
||||
OpenSwitcher, // Open the buffer switcher
|
||||
Show(String), // Display some arbitrary text to the user
|
||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
|
|
@ -117,8 +121,8 @@ impl RawEvent {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn is_prompt(&self) -> bool {
|
||||
matches!(
|
||||
pub fn to_open(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
|
|
@ -126,11 +130,49 @@ impl RawEvent {
|
|||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
)
|
||||
) {
|
||||
Some(Action::OpenPrompt)
|
||||
} else if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::OpenSwitcher)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancel(&self) -> bool {
|
||||
matches!(
|
||||
pub fn to_go(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::Go)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_yes(&self) -> Option<Action> {
|
||||
if matches!(self.to_char(), Some('y' | 'Y')) {
|
||||
Some(Action::Yes)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_cancel(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Esc,
|
||||
|
|
@ -138,6 +180,18 @@ impl RawEvent {
|
|||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
)
|
||||
) {
|
||||
Some(Action::Cancel)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_no(&self) -> Option<Action> {
|
||||
if matches!(self.to_char(), Some('n' | 'N')) {
|
||||
Some(Action::No)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ impl Buffer {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn begin_session(&mut self) -> CursorId {
|
||||
self.cursors.insert(Cursor::default())
|
||||
}
|
||||
|
||||
pub fn end_session(&mut self, cursor: CursorId) {
|
||||
self.cursors.remove(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ impl<'a> Rect<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_border(&mut self, theme: &theme::BorderTheme) -> Rect {
|
||||
pub fn with_border(&mut self, theme: &theme::BorderTheme, title: Option<&str>) -> Rect {
|
||||
let edge = self.size().map(|e| e.saturating_sub(1));
|
||||
for col in 0..edge[0] {
|
||||
self.get_mut([col, 0]).map(|c| {
|
||||
|
|
@ -112,6 +112,19 @@ impl<'a> Rect<'a> {
|
|||
c.c = theme.bottom_right;
|
||||
c.fg = theme.fg;
|
||||
});
|
||||
if let Some(title) = title {
|
||||
for (i, c) in [theme.join_right, ' ']
|
||||
.into_iter()
|
||||
.chain(title.chars())
|
||||
.chain([' ', theme.join_left])
|
||||
.enumerate()
|
||||
{
|
||||
self.get_mut([2 + i, 0]).map(|cell| {
|
||||
cell.fg = theme.fg;
|
||||
cell.c = c
|
||||
});
|
||||
}
|
||||
}
|
||||
self.rect([1, 1], self.size().map(|e| e.saturating_sub(2)))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ pub struct BorderTheme {
|
|||
pub top_right: char,
|
||||
pub bottom_left: char,
|
||||
pub bottom_right: char,
|
||||
pub join_left: char,
|
||||
pub join_right: char,
|
||||
pub fg: Color,
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +25,8 @@ impl Default for BorderTheme {
|
|||
top_right: '╮',
|
||||
bottom_left: '╰',
|
||||
bottom_right: '╯',
|
||||
join_left: '├',
|
||||
join_right: '┤',
|
||||
fg: Color::DarkGrey,
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +34,7 @@ impl Default for BorderTheme {
|
|||
|
||||
pub struct Theme {
|
||||
pub ui_bg: Color,
|
||||
pub status_bg: Color,
|
||||
pub select_bg: Color,
|
||||
pub border: BorderTheme,
|
||||
pub focus_border: BorderTheme,
|
||||
}
|
||||
|
|
@ -39,7 +43,7 @@ impl Default for Theme {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
ui_bg: Color::AnsiValue(235),
|
||||
status_bg: Color::AnsiValue(23),
|
||||
select_bg: Color::AnsiValue(23),
|
||||
border: BorderTheme::default(),
|
||||
focus_border: BorderTheme {
|
||||
fg: Color::White,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ mod status;
|
|||
pub use self::{
|
||||
input::Input,
|
||||
panes::Panes,
|
||||
prompt::{Confirm, Prompt, Show},
|
||||
prompt::{Confirm, Prompt, Show, Switcher},
|
||||
root::Root,
|
||||
status::Status,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ impl Doc {
|
|||
pub fn new(state: &mut State, buffer: BufferId) -> Self {
|
||||
Self {
|
||||
buffer,
|
||||
cursor: state.buffers[buffer].cursors.insert(Cursor::default()),
|
||||
// TODO: Don't index directly
|
||||
cursor: state.buffers[buffer].begin_session(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +35,15 @@ impl Element for Doc {
|
|||
.or_else(|| e.to_move().map(Action::Move))
|
||||
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
||||
}) {
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
buffer.end_session(self.cursor);
|
||||
self.buffer = new_buffer;
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
self.cursor = buffer.begin_session();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Char(c)) => {
|
||||
buffer.insert(cursor.pos, c);
|
||||
Ok(Resp::handled(None))
|
||||
|
|
@ -70,6 +80,19 @@ pub enum Pane {
|
|||
Doc(Doc),
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
fn title(&self, state: &State) -> Option<String> {
|
||||
match self {
|
||||
Self::Doc(doc) => {
|
||||
let Some(buffer) = state.buffers.get(doc.buffer) else {
|
||||
return None;
|
||||
};
|
||||
Some(buffer.path.display().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Panes {
|
||||
selected: usize,
|
||||
panes: Vec<Pane>,
|
||||
|
|
@ -127,9 +150,11 @@ impl Visual for Panes {
|
|||
} else {
|
||||
&state.theme.border
|
||||
};
|
||||
|
||||
// Draw pane contents
|
||||
frame
|
||||
.rect([x0, 0], [x1 - x0, frame.size()[1]])
|
||||
.with_border(border_theme)
|
||||
.with_border(border_theme, pane.title(state).as_deref())
|
||||
.with_focus(is_selected)
|
||||
.with(|frame| match pane {
|
||||
Pane::Doc(doc) => doc.render(state, frame),
|
||||
|
|
|
|||
131
src/ui/prompt.rs
131
src/ui/prompt.rs
|
|
@ -1,24 +1,7 @@
|
|||
use super::*;
|
||||
use crate::state::BufferId;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub enum Command {
|
||||
Quit,
|
||||
Help,
|
||||
Version,
|
||||
}
|
||||
|
||||
impl FromStr for Command {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"q" | "quit" => Ok(Command::Quit),
|
||||
"version" => Ok(Command::Version),
|
||||
"?" | "help" => Ok(Command::Help),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Prompt {
|
||||
pub input: Input,
|
||||
}
|
||||
|
|
@ -26,15 +9,23 @@ pub struct Prompt {
|
|||
impl Prompt {
|
||||
pub fn get_action(&self) -> Option<Action> {
|
||||
match self.input.get_text().as_str() {
|
||||
"quit" => Some(Action::Quit),
|
||||
// The root sees 'cancel' as an initiator for quitting
|
||||
"q" | "quit" => Some(Action::Cancel),
|
||||
"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"
|
||||
"?" | "help" => Some(Action::Show(format!(
|
||||
"Temporary help info:\n\
|
||||
- quit\n\
|
||||
- version\n\
|
||||
- pane_move_left\n\
|
||||
- pane_move_right\n\
|
||||
- help"
|
||||
))),
|
||||
"pane_move_left" => Some(Action::PaneMove(Dir::Left)),
|
||||
"pane_move_right" => Some(Action::PaneMove(Dir::Right)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -42,18 +33,8 @@ impl Prompt {
|
|||
|
||||
impl Element<CanEnd> for Prompt {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
match event.to_action(|e| {
|
||||
if e.is_cancel() {
|
||||
Some(Action::Cancel)
|
||||
} else if e.is_go() {
|
||||
Some(Action::Go)
|
||||
} else if e.is_prompt() {
|
||||
Some(Action::OpenPrompt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
Some(Action::Cancel /*| Action::Prompt*/) => Ok(Resp::end(None)),
|
||||
match event.to_action(|e| e.to_go().or_else(|| e.to_cancel())) {
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
Some(Action::Go) => {
|
||||
if let Some(action) = self.get_action() {
|
||||
Ok(Resp::end(action))
|
||||
|
|
@ -81,13 +62,7 @@ pub struct Show {
|
|||
|
||||
impl Element<CanEnd> for Show {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
match event.to_action(|e| {
|
||||
if e.is_cancel() {
|
||||
Some(Action::Cancel)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
match event.to_action(|e| e.to_cancel()) {
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
_ => Err(event),
|
||||
}
|
||||
|
|
@ -114,18 +89,11 @@ pub struct Confirm {
|
|||
|
||||
impl Element<CanEnd> for Confirm {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
match event.to_action(|e| {
|
||||
if e.is_cancel() || e.to_char() == Some('n') {
|
||||
Some(Action::Cancel)
|
||||
} else if e.to_char() == Some('y') {
|
||||
Some(Action::Go)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
Some(Action::Go) => Ok(Resp::end(Some(self.action.clone()))),
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
_ => Err(event),
|
||||
match event.to_action(|e| e.to_yes().or_else(|| e.to_no()).or_else(|| e.to_cancel())) {
|
||||
Some(Action::Yes) => Ok(Resp::end(Some(self.action.clone()))),
|
||||
Some(Action::No | Action::Cancel) => Ok(Resp::end(None)),
|
||||
// All other events get swallowed
|
||||
_ => Ok(Resp::handled(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,3 +110,62 @@ impl Visual for Confirm {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Switcher {
|
||||
pub selected: usize,
|
||||
pub options: Vec<BufferId>,
|
||||
}
|
||||
|
||||
impl Element<CanEnd> for Switcher {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
match event.to_action(|e| {
|
||||
e.to_cancel()
|
||||
.or_else(|| e.to_go())
|
||||
.or_else(|| e.to_move().map(Action::Move))
|
||||
}) {
|
||||
Some(Action::Move(Dir::Up)) => {
|
||||
self.selected = (self.selected + self.options.len() - 1) % self.options.len();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(Dir::Down)) => {
|
||||
self.selected = (self.selected + 1) % self.options.len();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Go) => Ok(Resp::end(
|
||||
if let Some(buffer) = self.options.get(self.selected) {
|
||||
Some(Action::SwitchBuffer(*buffer))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)),
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
// All other events get swallowed
|
||||
_ => Ok(Resp::handled(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Switcher {
|
||||
fn render(&self, state: &State, frame: &mut Rect) {
|
||||
for (i, buffer) in self.options.iter().enumerate() {
|
||||
let Some(buffer) = state.buffers.get(*buffer) else {
|
||||
continue;
|
||||
};
|
||||
frame
|
||||
.rect(
|
||||
[
|
||||
0,
|
||||
frame.size()[1].saturating_sub(3 + self.options.len()) + i,
|
||||
],
|
||||
[frame.size()[0], 1],
|
||||
)
|
||||
.with_bg(if self.selected == i {
|
||||
state.theme.select_bg
|
||||
} else {
|
||||
state.theme.ui_bg
|
||||
})
|
||||
.fill(' ')
|
||||
.text([0, 0], buffer.path.display().to_string().chars());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub enum Task {
|
|||
Prompt(Prompt),
|
||||
Show(Show),
|
||||
Confirm(Confirm),
|
||||
Switcher(Switcher),
|
||||
}
|
||||
|
||||
impl Root {
|
||||
|
|
@ -33,15 +34,7 @@ impl Element<CanEnd> for Root {
|
|||
None => {
|
||||
break match self.panes.handle(state, event) {
|
||||
Ok(resp) => resp.action,
|
||||
Err(event) => event.to_action(|e| {
|
||||
if e.is_prompt() {
|
||||
Some(Action::OpenPrompt)
|
||||
} else if e.is_cancel() {
|
||||
Some(Action::Cancel)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
Err(event) => event.to_action(|e| e.to_open().or_else(|| e.to_cancel())),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -50,6 +43,7 @@ impl Element<CanEnd> for Root {
|
|||
Task::Prompt(p) => p.handle(state, event),
|
||||
Task::Show(s) => s.handle(state, event),
|
||||
Task::Confirm(c) => c.handle(state, event),
|
||||
Task::Switcher(s) => s.handle(state, event),
|
||||
};
|
||||
|
||||
match res {
|
||||
|
|
@ -76,13 +70,25 @@ impl Element<CanEnd> for Root {
|
|||
},
|
||||
}));
|
||||
}
|
||||
Action::OpenSwitcher => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks.push(Task::Switcher(Switcher {
|
||||
selected: 0,
|
||||
options: state.buffers.keys().collect(),
|
||||
}));
|
||||
}
|
||||
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
|
||||
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
|
||||
action: Action::Quit,
|
||||
})),
|
||||
Action::Show(text) => self.tasks.push(Task::Show(Show { label: Label(text) })),
|
||||
Action::Quit => return Ok(Resp::end(None)),
|
||||
action => todo!("Unhandled action {action:?}"),
|
||||
action => {
|
||||
return self
|
||||
.panes
|
||||
.handle(state, Event::Action(action))
|
||||
.map(|r| r.into_can_end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,16 +101,19 @@ impl Visual for Root {
|
|||
fn render(&self, state: &State, frame: &mut Rect) {
|
||||
frame.fill(' ');
|
||||
|
||||
let task_has_focus = self.tasks.last().is_some();
|
||||
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
||||
|
||||
// Display status bar
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with_border(if task_has_focus {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
})
|
||||
.with_border(
|
||||
if task_has_focus {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
},
|
||||
Some("Prompt (press alt + enter)"),
|
||||
)
|
||||
.with(|frame| {
|
||||
if let Some(Task::Prompt(p)) = self.tasks.last() {
|
||||
p.render(state, frame);
|
||||
|
|
@ -120,9 +129,10 @@ impl Visual for Root {
|
|||
|
||||
if let Some(task) = self.tasks.last() {
|
||||
match task {
|
||||
Task::Prompt(_) => {} // Prompt isn't rendered, it's always rendered above
|
||||
Task::Show(s) => s.render(state, frame),
|
||||
Task::Confirm(c) => c.render(state, frame),
|
||||
_ => {}
|
||||
Task::Switcher(s) => s.render(state, frame),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue