Unify prompt and doc input
This commit is contained in:
parent
84056b5a74
commit
67dac456af
9 changed files with 337 additions and 292 deletions
|
|
@ -17,7 +17,8 @@ pub enum Action {
|
||||||
PaneMove(Dir), // Move panes
|
PaneMove(Dir), // Move panes
|
||||||
PaneOpen(Dir), // Create a new pane
|
PaneOpen(Dir), // Create a new pane
|
||||||
PaneClose, // Close the current pane
|
PaneClose, // Close the current pane
|
||||||
Cancel, // Cancels the current context
|
Cancel, // Cancels the current action
|
||||||
|
Continue, // Continue past an info-only element (like a help screen)
|
||||||
Go, // Search, accept, or select the current option
|
Go, // Search, accept, or select the current option
|
||||||
Yes, // A binary confirmation is answered 'yes'
|
Yes, // A binary confirmation is answered 'yes'
|
||||||
No, // A binary confirmation is answered 'no'
|
No, // A binary confirmation is answered 'no'
|
||||||
|
|
@ -234,6 +235,22 @@ impl RawEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_continue(&self) -> Option<Action> {
|
||||||
|
if matches!(
|
||||||
|
&self.0,
|
||||||
|
TerminalEvent::Key(KeyEvent {
|
||||||
|
code: KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' '),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Some(Action::Continue)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_no(&self) -> Option<Action> {
|
pub fn to_no(&self) -> Option<Action> {
|
||||||
if matches!(self.to_char(), Some('n' | 'N')) {
|
if matches!(self.to_char(), Some('n' | 'N')) {
|
||||||
Some(Action::No)
|
Some(Action::No)
|
||||||
|
|
|
||||||
28
src/state.rs
28
src/state.rs
|
|
@ -1,8 +1,4 @@
|
||||||
use crate::{
|
use crate::{Args, Dir, Error, theme};
|
||||||
Action, Args, Color, Dir, Error, Event, theme,
|
|
||||||
ui::{self, Element as _, Resp},
|
|
||||||
};
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
||||||
use slotmap::{HopSlotMap, new_key_type};
|
use slotmap::{HopSlotMap, new_key_type};
|
||||||
use std::{io, ops::Range, path::PathBuf};
|
use std::{io, ops::Range, path::PathBuf};
|
||||||
|
|
||||||
|
|
@ -33,10 +29,17 @@ impl Cursor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
chars: Vec<char>,
|
chars: Vec<char>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToString for Text {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.chars.iter().copied().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Text {
|
impl Text {
|
||||||
pub fn to_coord(&self, pos: usize) -> [isize; 2] {
|
pub fn to_coord(&self, pos: usize) -> [isize; 2] {
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
|
|
@ -53,7 +56,7 @@ impl Text {
|
||||||
[(pos - last_n) as isize, i.saturating_sub(1) as isize]
|
[(pos - last_n) as isize, i.saturating_sub(1) as isize]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_pos(&self, mut coord: [isize; 2]) -> usize {
|
pub fn to_pos(&self, coord: [isize; 2]) -> usize {
|
||||||
if coord[1] < 0 {
|
if coord[1] < 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -98,14 +101,15 @@ impl Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
pub path: PathBuf,
|
pub path: Option<PathBuf>,
|
||||||
pub text: Text,
|
pub text: Text,
|
||||||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
pub fn from_file(path: PathBuf) -> Result<Self, Error> {
|
||||||
let chars = match std::fs::read_to_string(&path) {
|
let chars = match std::fs::read_to_string(&path) {
|
||||||
Ok(s) => s.chars().collect(),
|
Ok(s) => s.chars().collect(),
|
||||||
// If the file doesn't exist, create a new file
|
// If the file doesn't exist, create a new file
|
||||||
|
|
@ -113,7 +117,7 @@ impl Buffer {
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path: Some(path),
|
||||||
text: Text { chars },
|
text: Text { chars },
|
||||||
cursors: HopSlotMap::default(),
|
cursors: HopSlotMap::default(),
|
||||||
})
|
})
|
||||||
|
|
@ -139,7 +143,7 @@ impl Buffer {
|
||||||
cursor.reset_desired_col(&self.text);
|
cursor.reset_desired_col(&self.text);
|
||||||
}
|
}
|
||||||
Dir::Up => {
|
Dir::Up => {
|
||||||
let mut coord = self.text.to_coord(cursor.pos);
|
let coord = self.text.to_coord(cursor.pos);
|
||||||
// Special case: pressing 'up' at the top of the screen resets the cursor to the beginning
|
// Special case: pressing 'up' at the top of the screen resets the cursor to the beginning
|
||||||
if coord[1] <= 0 {
|
if coord[1] <= 0 {
|
||||||
cursor.pos = 0;
|
cursor.pos = 0;
|
||||||
|
|
@ -151,7 +155,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Dir::Down => {
|
Dir::Down => {
|
||||||
let mut coord = self.text.to_coord(cursor.pos);
|
let coord = self.text.to_coord(cursor.pos);
|
||||||
cursor.pos = self
|
cursor.pos = self
|
||||||
.text
|
.text
|
||||||
.to_pos([cursor.desired_col, coord[1] + dist[1] as isize]);
|
.to_pos([cursor.desired_col, coord[1] + dist[1] as isize]);
|
||||||
|
|
@ -258,7 +262,7 @@ impl TryFrom<Args> for State {
|
||||||
};
|
};
|
||||||
|
|
||||||
for path in args.paths {
|
for path in args.paths {
|
||||||
this.buffers.insert(Buffer::new(path)?);
|
this.buffers.insert(Buffer::from_file(path)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(this)
|
Ok(this)
|
||||||
|
|
|
||||||
85
src/ui/doc.rs
Normal file
85
src/ui/doc.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
state::{BufferId, CursorId},
|
||||||
|
terminal::CursorStyle,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Doc {
|
||||||
|
buffer: BufferId,
|
||||||
|
// Remember the cursor we use for each buffer
|
||||||
|
cursors: HashMap<BufferId, CursorId>,
|
||||||
|
input: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Doc {
|
||||||
|
pub fn new(state: &mut State, buffer: BufferId) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer,
|
||||||
|
// TODO: Don't index directly
|
||||||
|
cursors: [(buffer, state.buffers[buffer].start_session())]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
input: Input::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self, state: &State) -> Option<String> {
|
||||||
|
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(buffer.path.as_ref()?.display().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self, state: &mut State) {
|
||||||
|
for (buffer, cursor) in self.cursors {
|
||||||
|
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
buffer.end_session(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Doc {
|
||||||
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||||
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
|
return Err(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
match event.to_action(|e| e.to_open_switcher()) {
|
||||||
|
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
||||||
|
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||||
|
self.buffer = new_buffer;
|
||||||
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
|
return Err(event);
|
||||||
|
};
|
||||||
|
// Start a new cursor session for this buffer if one doesn't exist
|
||||||
|
let cursor_id = *self
|
||||||
|
.cursors
|
||||||
|
.entry(self.buffer)
|
||||||
|
.or_insert_with(|| buffer.start_session());
|
||||||
|
self.input.refocus(buffer, cursor_id);
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
|
return Err(event);
|
||||||
|
};
|
||||||
|
let cursor_id = self.cursors[&self.buffer];
|
||||||
|
self.input.handle(buffer, cursor_id, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visual for Doc {
|
||||||
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cursor_id = self.cursors[&self.buffer];
|
||||||
|
self.input.render(state, buffer, cursor_id, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/ui/input.rs
210
src/ui/input.rs
|
|
@ -1,59 +1,193 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::terminal::CursorStyle;
|
use crate::{
|
||||||
|
state::{Buffer, CursorId},
|
||||||
|
terminal::CursorStyle,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
|
enum Mode {
|
||||||
|
#[default]
|
||||||
|
Doc,
|
||||||
|
Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
pub text: Vec<char>,
|
pub mode: Mode,
|
||||||
pub cursor: usize,
|
// x/y location in the buffer that the pane is trying to focus on
|
||||||
pub preamble: &'static str,
|
pub focus: [isize; 2],
|
||||||
|
// Remember the last known size for things like scrolling
|
||||||
|
pub last_size: [usize; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
pub fn get_text(&self) -> String {
|
pub fn prompt() -> Self {
|
||||||
self.text.iter().copied().collect()
|
Self {
|
||||||
|
mode: Mode::Prompt,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for Input {
|
pub fn refocus(&mut self, buffer: &mut Buffer, cursor_id: CursorId) {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||||
|
for i in 0..2 {
|
||||||
|
self.focus[i] = self.focus[i].clamp(
|
||||||
|
cursor_coord[i] - self.last_size[i] as isize + 1,
|
||||||
|
cursor_coord[i],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(
|
||||||
|
&mut self,
|
||||||
|
buffer: &mut Buffer,
|
||||||
|
cursor_id: CursorId,
|
||||||
|
event: Event,
|
||||||
|
) -> Result<Resp, Event> {
|
||||||
match event.to_action(|e| e.to_char().map(Action::Char).or_else(|| e.to_move())) {
|
match event.to_action(|e| e.to_char().map(Action::Char).or_else(|| e.to_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)) => {
|
Some(Action::Char(c)) => {
|
||||||
self.text.insert(self.cursor, c);
|
if c == '\x08' {
|
||||||
self.cursor += 1;
|
buffer.backspace(cursor_id);
|
||||||
|
} else if c == '\x7F' {
|
||||||
|
buffer.delete(cursor_id);
|
||||||
|
} else {
|
||||||
|
buffer.enter(cursor_id, c);
|
||||||
|
}
|
||||||
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Move(Dir::Left, _, _)) => {
|
Some(Action::Move(dir, page, retain_base)) => {
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
let dist = if page {
|
||||||
Ok(Resp::handled(None))
|
self.last_size.map(|s| s.saturating_sub(3).max(1))
|
||||||
}
|
} else {
|
||||||
Some(Action::Move(Dir::Right, _, _)) => {
|
[1, 1]
|
||||||
self.cursor = (self.cursor + 1).min(self.text.len());
|
};
|
||||||
|
buffer.move_cursor(cursor_id, dir, dist, retain_base);
|
||||||
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
_ => Err(event),
|
_ => Err(event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Visual for Input {
|
pub fn render(
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
&mut self,
|
||||||
frame.with(|frame| {
|
state: &State,
|
||||||
frame.fill(' ');
|
buffer: &Buffer,
|
||||||
frame.text([0, 0], self.preamble.chars());
|
cursor_id: CursorId,
|
||||||
|
frame: &mut Rect,
|
||||||
|
) {
|
||||||
|
let title = if let Some(path) = &buffer.path {
|
||||||
|
Some(path.display().to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
frame
|
// Add frame
|
||||||
.rect([self.preamble.chars().count(), 0], frame.size())
|
let mut frame = frame.with_border(
|
||||||
.with(|frame| {
|
if frame.has_focus() {
|
||||||
frame.text([0, 0], &self.text);
|
&state.theme.focus_border
|
||||||
frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
|
} else {
|
||||||
});
|
&state.theme.border
|
||||||
});
|
},
|
||||||
|
title.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||||
|
|
||||||
|
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
|
||||||
|
let margin_w = match self.mode {
|
||||||
|
Mode::Prompt => 2,
|
||||||
|
Mode::Doc => line_num_w + 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]];
|
||||||
|
|
||||||
|
let mut pos = 0;
|
||||||
|
for (i, (line_num, (line_pos, line))) in buffer
|
||||||
|
.text
|
||||||
|
.lines()
|
||||||
|
.map(move |line| {
|
||||||
|
let line_pos = pos;
|
||||||
|
pos += line.len();
|
||||||
|
(line_pos, line)
|
||||||
|
})
|
||||||
|
.enumerate()
|
||||||
|
.skip(self.focus[1].max(0) as usize)
|
||||||
|
.enumerate()
|
||||||
|
.take(frame.size()[1])
|
||||||
|
{
|
||||||
|
// Margin
|
||||||
|
match self.mode {
|
||||||
|
Mode::Prompt => frame
|
||||||
|
.rect([0, i], [1, 1])
|
||||||
|
.with_bg(state.theme.margin_bg)
|
||||||
|
.with_fg(state.theme.margin_line_num)
|
||||||
|
.fill(' ')
|
||||||
|
.text([0, 0], ">".chars()),
|
||||||
|
Mode::Doc => frame
|
||||||
|
.rect([0, i], [margin_w, 1])
|
||||||
|
.with_bg(state.theme.margin_bg)
|
||||||
|
.with_fg(state.theme.margin_line_num)
|
||||||
|
.fill(' ')
|
||||||
|
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Line
|
||||||
|
{
|
||||||
|
let mut frame = frame.rect([margin_w, i], [!0, 1]);
|
||||||
|
for i in 0..frame.size()[0] {
|
||||||
|
let coord = self.focus[0] + i as isize;
|
||||||
|
if (0..line.len() as isize).contains(&coord) {
|
||||||
|
let pos = line_pos + coord as usize;
|
||||||
|
let selected = cursor.selection().map_or(false, |s| s.contains(&pos));
|
||||||
|
let (fg, c) = match line[coord as usize] {
|
||||||
|
'\n' if selected => (state.theme.whitespace, '⮠'),
|
||||||
|
c => (state.theme.text, c),
|
||||||
|
};
|
||||||
|
frame
|
||||||
|
.with_bg(if selected {
|
||||||
|
state.theme.select_bg
|
||||||
|
} else {
|
||||||
|
Color::Reset
|
||||||
|
})
|
||||||
|
.with_fg(fg)
|
||||||
|
.text([i as isize, 0], &[c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cursor position
|
||||||
|
if cursor_coord[1] == line_num as isize {
|
||||||
|
frame.set_cursor(
|
||||||
|
[cursor_coord[0] - self.focus[0], 0],
|
||||||
|
CursorStyle::BlinkingBar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += line.len();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// impl Visual for Input {
|
||||||
|
// fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
// frame.with(|frame| {
|
||||||
|
// frame.fill(' ');
|
||||||
|
// frame.text([0, 0], self.preamble.chars());
|
||||||
|
|
||||||
|
// frame
|
||||||
|
// .rect([self.preamble.chars().count(), 0], frame.size())
|
||||||
|
// .with(|frame| {
|
||||||
|
// frame.text([0, 0], &self.text);
|
||||||
|
// frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod doc;
|
||||||
mod input;
|
mod input;
|
||||||
mod panes;
|
mod panes;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
|
|
@ -5,6 +6,7 @@ mod root;
|
||||||
mod status;
|
mod status;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
|
doc::Doc,
|
||||||
input::Input,
|
input::Input,
|
||||||
panes::Panes,
|
panes::Panes,
|
||||||
prompt::{Confirm, Prompt, Show, Switcher},
|
prompt::{Confirm, Prompt, Show, Switcher},
|
||||||
|
|
|
||||||
209
src/ui/panes.rs
209
src/ui/panes.rs
|
|
@ -1,186 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::state::BufferId;
|
||||||
state::{Buffer, BufferId, Cursor, CursorId},
|
|
||||||
terminal::CursorStyle,
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Doc {
|
|
||||||
buffer: BufferId,
|
|
||||||
// Remember the cursor we use for each buffer
|
|
||||||
cursors: HashMap<BufferId, CursorId>,
|
|
||||||
// x/y location in the buffer that the pane is trying to focus on
|
|
||||||
focus: [isize; 2],
|
|
||||||
// Remember the last known size for things like scrolling
|
|
||||||
last_size: [usize; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Doc {
|
|
||||||
pub fn new(state: &mut State, buffer: BufferId) -> Self {
|
|
||||||
Self {
|
|
||||||
buffer,
|
|
||||||
// TODO: Don't index directly
|
|
||||||
cursors: [(buffer, state.buffers[buffer].start_session())]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
focus: [0, 0],
|
|
||||||
last_size: [1, 1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn refocus(&mut self, state: &mut State) {
|
|
||||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
|
||||||
for i in 0..2 {
|
|
||||||
self.focus[i] = self.focus[i].clamp(
|
|
||||||
cursor_coord[i] - self.last_size[i] as isize + 1,
|
|
||||||
cursor_coord[i],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(self, state: &mut State) {
|
|
||||||
for (buffer, cursor) in self.cursors {
|
|
||||||
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
buffer.end_session(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for Doc {
|
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
|
||||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
|
||||||
return Err(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
match event.to_action(|e| {
|
|
||||||
e.to_char()
|
|
||||||
.map(Action::Char)
|
|
||||||
.or_else(|| e.to_move())
|
|
||||||
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
|
||||||
.or_else(|| e.to_open_switcher())
|
|
||||||
}) {
|
|
||||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
|
||||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
|
||||||
self.buffer = new_buffer;
|
|
||||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
|
||||||
return Err(event);
|
|
||||||
};
|
|
||||||
// Start a new cursor session for this buffer if one doesn't exist
|
|
||||||
self.cursors
|
|
||||||
.entry(self.buffer)
|
|
||||||
.or_insert_with(|| buffer.start_session());
|
|
||||||
self.refocus(state);
|
|
||||||
Ok(Resp::handled(None))
|
|
||||||
}
|
|
||||||
Some(Action::Char(c)) => {
|
|
||||||
let cursor_id = self.cursors[&self.buffer];
|
|
||||||
if c == '\x08' {
|
|
||||||
buffer.backspace(cursor_id);
|
|
||||||
} else if c == '\x7F' {
|
|
||||||
buffer.delete(cursor_id);
|
|
||||||
} else {
|
|
||||||
buffer.enter(cursor_id, c);
|
|
||||||
}
|
|
||||||
self.refocus(state);
|
|
||||||
Ok(Resp::handled(None))
|
|
||||||
}
|
|
||||||
Some(Action::Move(dir, page, retain_base)) => {
|
|
||||||
let dist = if page {
|
|
||||||
self.last_size.map(|s| s.saturating_sub(3).max(1))
|
|
||||||
} else {
|
|
||||||
[1, 1]
|
|
||||||
};
|
|
||||||
buffer.move_cursor(self.cursors[&self.buffer], dir, dist, retain_base);
|
|
||||||
self.refocus(state);
|
|
||||||
Ok(Resp::handled(None))
|
|
||||||
}
|
|
||||||
_ => Err(event),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Visual for Doc {
|
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
|
||||||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
|
||||||
|
|
||||||
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
|
|
||||||
let margin_w = line_num_w + 2;
|
|
||||||
|
|
||||||
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]];
|
|
||||||
|
|
||||||
let mut pos = 0;
|
|
||||||
for (i, (line_num, (line_pos, line))) in buffer
|
|
||||||
.text
|
|
||||||
.lines()
|
|
||||||
.map(move |line| {
|
|
||||||
let line_pos = pos;
|
|
||||||
pos += line.len();
|
|
||||||
(line_pos, line)
|
|
||||||
})
|
|
||||||
.enumerate()
|
|
||||||
.skip(self.focus[1].max(0) as usize)
|
|
||||||
.enumerate()
|
|
||||||
.take(frame.size()[1])
|
|
||||||
{
|
|
||||||
// Margin
|
|
||||||
frame
|
|
||||||
.rect([0, i], [margin_w, 1])
|
|
||||||
.with_bg(state.theme.margin_bg)
|
|
||||||
.with_fg(state.theme.margin_line_num)
|
|
||||||
.fill(' ')
|
|
||||||
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars());
|
|
||||||
|
|
||||||
// Line
|
|
||||||
{
|
|
||||||
let mut frame = frame.rect([margin_w, i], [!0, 1]);
|
|
||||||
for i in 0..frame.size()[0] {
|
|
||||||
let coord = self.focus[0] + i as isize;
|
|
||||||
if (0..line.len() as isize).contains(&coord) {
|
|
||||||
let pos = line_pos + coord as usize;
|
|
||||||
let selected = cursor.selection().map_or(false, |s| s.contains(&pos));
|
|
||||||
let (fg, c) = match line[coord as usize] {
|
|
||||||
'\n' if selected => (state.theme.whitespace, '⮠'),
|
|
||||||
c => (state.theme.text, c),
|
|
||||||
};
|
|
||||||
frame
|
|
||||||
.with_bg(if selected {
|
|
||||||
state.theme.select_bg
|
|
||||||
} else {
|
|
||||||
Color::Reset
|
|
||||||
})
|
|
||||||
.with_fg(fg)
|
|
||||||
.text([i as isize, 0], &[c]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cursor position
|
|
||||||
if cursor_coord[1] == line_num as isize {
|
|
||||||
frame.set_cursor(
|
|
||||||
[cursor_coord[0] - self.focus[0], 0],
|
|
||||||
CursorStyle::BlinkingBar,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pos += line.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Pane {
|
pub enum Pane {
|
||||||
|
|
@ -188,20 +7,6 @@ pub enum Pane {
|
||||||
Doc(Doc),
|
Doc(Doc),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pane {
|
|
||||||
fn title(&self, state: &State) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Empty => None,
|
|
||||||
Self::Doc(doc) => {
|
|
||||||
let Some(buffer) = state.buffers.get(doc.buffer) else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
Some(buffer.path.display().to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Panes {
|
pub struct Panes {
|
||||||
selected: usize,
|
selected: usize,
|
||||||
panes: Vec<Pane>,
|
panes: Vec<Pane>,
|
||||||
|
|
@ -262,7 +67,7 @@ impl Element for Panes {
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
// Pass anything else through to the active pane
|
// Pass anything else through to the active pane
|
||||||
err => {
|
_ => {
|
||||||
if let Some(pane) = self.panes.get_mut(self.selected) {
|
if let Some(pane) = self.panes.get_mut(self.selected) {
|
||||||
// Pass to pane
|
// Pass to pane
|
||||||
match pane {
|
match pane {
|
||||||
|
|
@ -287,18 +92,10 @@ impl Visual for Panes {
|
||||||
for (i, pane) in self.panes.iter_mut().enumerate() {
|
for (i, pane) in self.panes.iter_mut().enumerate() {
|
||||||
let (x0, x1) = (boundary(i), boundary(i + 1));
|
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
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draw pane contents
|
// Draw pane contents
|
||||||
frame
|
frame
|
||||||
.rect([x0, 0], [x1 - x0, frame.size()[1]])
|
.rect([x0, 0], [x1 - x0, frame.size()[1]])
|
||||||
.with_border(border_theme, pane.title(state).as_deref())
|
.with_focus(self.selected == i)
|
||||||
.with_focus(is_selected)
|
|
||||||
.with(|frame| match pane {
|
.with(|frame| match pane {
|
||||||
Pane::Empty => {}
|
Pane::Empty => {}
|
||||||
Pane::Doc(doc) => doc.render(state, frame),
|
Pane::Doc(doc) => doc.render(state, frame),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,24 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::state::BufferId;
|
use crate::state::{Buffer, BufferId, CursorId};
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
pub struct Prompt {
|
pub struct Prompt {
|
||||||
pub input: Input,
|
buffer: Buffer,
|
||||||
|
cursor_id: CursorId,
|
||||||
|
input: Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut buffer = Buffer::default();
|
||||||
|
Self {
|
||||||
|
cursor_id: buffer.start_session(),
|
||||||
|
buffer,
|
||||||
|
input: Input::prompt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_action(&self) -> Option<Action> {
|
pub fn get_action(&self) -> Option<Action> {
|
||||||
match self.input.get_text().as_str() {
|
match self.buffer.text.to_string().as_str() {
|
||||||
// The root sees 'cancel' as an initiator for quitting
|
// The root sees 'cancel' as an initiator for quitting
|
||||||
"q" | "quit" => Some(Action::Cancel),
|
"q" | "quit" => Some(Action::Cancel),
|
||||||
"version" => Some(Action::Show(format!(
|
"version" => Some(Action::Show(format!(
|
||||||
|
|
@ -41,18 +51,21 @@ impl Element<CanEnd> for Prompt {
|
||||||
} else {
|
} else {
|
||||||
Ok(Resp::end(Action::Show(format!(
|
Ok(Resp::end(Action::Show(format!(
|
||||||
"unknown command `{}`",
|
"unknown command `{}`",
|
||||||
self.input.get_text()
|
self.buffer.text.to_string()
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => self.input.handle(state, event).map(Resp::into_can_end),
|
_ => self
|
||||||
|
.input
|
||||||
|
.handle(&mut self.buffer, self.cursor_id, event)
|
||||||
|
.map(Resp::into_can_end),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Prompt {
|
impl Visual for Prompt {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.with(|f| self.input.render(state, f));
|
frame.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,9 +75,9 @@ pub struct Show {
|
||||||
|
|
||||||
impl Element<CanEnd> for Show {
|
impl Element<CanEnd> for Show {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||||
match event.to_action(|e| e.to_cancel()) {
|
match event.to_action(|e| e.to_continue()) {
|
||||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
Some(Action::Continue) => Ok(Resp::end(None)),
|
||||||
_ => Err(event),
|
_ => Ok(Resp::handled(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +160,10 @@ impl Visual for Switcher {
|
||||||
let Some(buffer) = state.buffers.get(*buffer) else {
|
let Some(buffer) = state.buffers.get(*buffer) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let buffer_name = match &buffer.path {
|
||||||
|
Some(path) => path.display().to_string(),
|
||||||
|
None => format!("<Untitled>"),
|
||||||
|
};
|
||||||
frame
|
frame
|
||||||
.rect(
|
.rect(
|
||||||
[
|
[
|
||||||
|
|
@ -161,7 +178,7 @@ impl Visual for Switcher {
|
||||||
state.theme.ui_bg
|
state.theme.ui_bg
|
||||||
})
|
})
|
||||||
.fill(' ')
|
.fill(' ')
|
||||||
.text([0, 0], buffer.path.display().to_string().chars());
|
.text([0, 0], buffer_name.chars());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,7 @@ impl Element<CanEnd> for Root {
|
||||||
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::new()));
|
||||||
input: Input {
|
|
||||||
preamble: "> ",
|
|
||||||
..Input::default()
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Action::OpenSwitcher => {
|
Action::OpenSwitcher => {
|
||||||
self.tasks.clear(); // Prompt overrides all
|
self.tasks.clear(); // Prompt overrides all
|
||||||
|
|
@ -106,24 +101,20 @@ impl Visual for Root {
|
||||||
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
||||||
|
|
||||||
// Display status bar
|
// Display status bar
|
||||||
frame
|
let status_size = if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
|
||||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
frame
|
||||||
.with_border(
|
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||||
if task_has_focus {
|
.with(|frame| p.render(state, frame));
|
||||||
&state.theme.focus_border
|
3
|
||||||
} else {
|
} else {
|
||||||
&state.theme.border
|
0
|
||||||
},
|
};
|
||||||
Some("Prompt (press alt + enter)"),
|
|
||||||
)
|
|
||||||
.with(|frame| {
|
|
||||||
if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
|
|
||||||
p.render(state, frame);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frame
|
frame
|
||||||
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
|
.rect(
|
||||||
|
[0, 0],
|
||||||
|
[frame.size()[0], frame.size()[1].saturating_sub(status_size)],
|
||||||
|
)
|
||||||
.with_focus(!task_has_focus)
|
.with_focus(!task_has_focus)
|
||||||
.with(|frame| {
|
.with(|frame| {
|
||||||
self.panes.render(state, frame);
|
self.panes.render(state, frame);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub struct Status;
|
pub struct Status;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue