Added opener
This commit is contained in:
parent
58a8c5c9e5
commit
d352d04030
9 changed files with 315 additions and 61 deletions
16
README.md
16
README.md
|
|
@ -2,17 +2,17 @@
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [ ] Buffers
|
- [x] Buffers
|
||||||
- [ ] Buffer switching
|
- [x] Buffer switching
|
||||||
- [ ] Prompt
|
- [x] Prompt
|
||||||
- [ ] Cursor selection
|
- [x] Cursor selection
|
||||||
- [ ] Basic cursor movement
|
- [x] Basic cursor movement
|
||||||
- [ ] Multiple panes
|
- [x] Multiple panes
|
||||||
- [ ] Pane creation/deletion
|
- [x] Pane creation/deletion
|
||||||
|
- [x] Opener
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
|
|
||||||
- [ ] Opener
|
|
||||||
- [ ] Find
|
- [ ] Find
|
||||||
- [ ] Replace
|
- [ ] Replace
|
||||||
- [ ] Project search
|
- [ ] Project search
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{state::BufferId, terminal::TerminalEvent};
|
use crate::{state::BufferId, terminal::TerminalEvent};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Dir {
|
pub enum Dir {
|
||||||
|
|
@ -24,9 +25,11 @@ pub enum Action {
|
||||||
No, // A binary confirmation is answered 'no'
|
No, // A binary confirmation is answered 'no'
|
||||||
Quit, // Quit the application
|
Quit, // Quit the application
|
||||||
OpenPrompt, // Open the command prompt
|
OpenPrompt, // Open the command prompt
|
||||||
OpenSwitcher, // Open the buffer switcher
|
|
||||||
Show(Option<String>, String), // Display an optionally titled informational text box to the user
|
Show(Option<String>, String), // Display an optionally titled informational text box to the user
|
||||||
|
OpenSwitcher, // Open the buffer switcher
|
||||||
|
OpenOpener(PathBuf), // Open the file opener
|
||||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||||
|
OpenFile(PathBuf), // Open the file and switch the current pane to it
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -201,6 +204,22 @@ impl RawEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_open_opener(&self, path: PathBuf) -> Option<Action> {
|
||||||
|
if matches!(
|
||||||
|
&self.0,
|
||||||
|
TerminalEvent::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('o'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
Some(Action::OpenOpener(path))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_go(&self) -> Option<Action> {
|
pub fn to_go(&self) -> Option<Action> {
|
||||||
if matches!(
|
if matches!(
|
||||||
&self.0,
|
&self.0,
|
||||||
|
|
|
||||||
49
src/state.rs
49
src/state.rs
|
|
@ -1,6 +1,10 @@
|
||||||
use crate::{Args, Dir, Error, theme};
|
use crate::{Args, Dir, Error, theme};
|
||||||
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::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
new_key_type! {
|
new_key_type! {
|
||||||
pub struct BufferId;
|
pub struct BufferId;
|
||||||
|
|
@ -103,6 +107,7 @@ impl Text {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
|
pub dir: Option<PathBuf>,
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
pub text: Text,
|
pub text: Text,
|
||||||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||||
|
|
@ -110,19 +115,35 @@ pub struct Buffer {
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
pub fn from_file(path: PathBuf) -> Result<Self, Error> {
|
pub fn from_file(path: PathBuf) -> Result<Self, Error> {
|
||||||
let chars = match std::fs::read_to_string(&path) {
|
let (dir, chars) = match std::fs::read_to_string(&path) {
|
||||||
Ok(s) => s.chars().collect(),
|
Ok(s) => {
|
||||||
|
let mut path = path.canonicalize()?;
|
||||||
|
path.pop();
|
||||||
|
(Some(path), s.chars().collect())
|
||||||
|
}
|
||||||
// If the file doesn't exist, create a new file
|
// If the file doesn't exist, create a new file
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => Vec::new(),
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
|
(path.parent().map(Path::to_owned), Vec::new())
|
||||||
|
}
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
dir,
|
||||||
path: Some(path),
|
path: Some(path),
|
||||||
text: Text { chars },
|
text: Text { chars },
|
||||||
cursors: HopSlotMap::default(),
|
cursors: HopSlotMap::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> Option<&str> {
|
||||||
|
Some(
|
||||||
|
match self.path.as_ref()?.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(name) => name,
|
||||||
|
None => "<error>",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.text.chars.clear();
|
self.text.chars.clear();
|
||||||
// Reset cursors
|
// Reset cursors
|
||||||
|
|
@ -183,28 +204,34 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, pos: usize, c: char) {
|
pub fn insert(&mut self, pos: usize, chars: impl IntoIterator<Item = char>) {
|
||||||
self.text.chars.insert(pos.min(self.text.chars.len()), c);
|
let mut n = 0;
|
||||||
|
for c in chars {
|
||||||
|
self.text
|
||||||
|
.chars
|
||||||
|
.insert((pos + n).min(self.text.chars.len()), c);
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
self.cursors.values_mut().for_each(|cursor| {
|
self.cursors.values_mut().for_each(|cursor| {
|
||||||
if cursor.base >= pos {
|
if cursor.base >= pos {
|
||||||
cursor.base += 1;
|
cursor.base += n;
|
||||||
}
|
}
|
||||||
if cursor.pos >= pos {
|
if cursor.pos >= pos {
|
||||||
cursor.pos += 1;
|
cursor.pos += n;
|
||||||
cursor.reset_desired_col(&self.text);
|
cursor.reset_desired_col(&self.text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enter(&mut self, cursor_id: CursorId, c: char) {
|
pub fn enter(&mut self, cursor_id: CursorId, chars: impl IntoIterator<Item = char>) {
|
||||||
let Some(cursor) = self.cursors.get(cursor_id) else {
|
let Some(cursor) = self.cursors.get(cursor_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(selection) = cursor.selection() {
|
if let Some(selection) = cursor.selection() {
|
||||||
self.remove(selection);
|
self.remove(selection);
|
||||||
self.enter(cursor_id, c);
|
self.enter(cursor_id, chars);
|
||||||
} else {
|
} else {
|
||||||
self.insert(cursor.pos, c);
|
self.insert(cursor.pos, chars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ pub struct Theme {
|
||||||
pub focus_border: BorderTheme,
|
pub focus_border: BorderTheme,
|
||||||
pub text: Color,
|
pub text: Color,
|
||||||
pub whitespace: Color,
|
pub whitespace: Color,
|
||||||
|
pub option_dir: Color,
|
||||||
|
pub option_file: Color,
|
||||||
|
pub option_new: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Theme {
|
impl Default for Theme {
|
||||||
|
|
@ -59,6 +62,9 @@ impl Default for Theme {
|
||||||
},
|
},
|
||||||
text: Color::Reset,
|
text: Color::Reset,
|
||||||
whitespace: Color::AnsiValue(245),
|
whitespace: Color::AnsiValue(245),
|
||||||
|
option_dir: Color::AnsiValue(178),
|
||||||
|
option_file: Color::Reset,
|
||||||
|
option_new: Color::AnsiValue(148),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
state::{BufferId, CursorId},
|
state::{Buffer, BufferId, CursorId},
|
||||||
terminal::CursorStyle,
|
terminal::CursorStyle,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Doc {
|
pub struct Doc {
|
||||||
|
|
@ -25,13 +25,6 @@ impl Doc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
pub fn close(self, state: &mut State) {
|
||||||
for (buffer, cursor) in self.cursors {
|
for (buffer, cursor) in self.cursors {
|
||||||
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
||||||
|
|
@ -40,6 +33,19 @@ impl Doc {
|
||||||
buffer.end_session(cursor);
|
buffer.end_session(cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn switch_buffer(&mut self, state: &mut State, buffer: BufferId) {
|
||||||
|
self.buffer = buffer;
|
||||||
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for Doc {
|
impl Element for Doc {
|
||||||
|
|
@ -48,21 +54,25 @@ impl Element for Doc {
|
||||||
return Err(event);
|
return Err(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
match event.to_action(|e| e.to_open_switcher()) {
|
let open_path = buffer.dir.to_owned().unwrap_or(PathBuf::from("/"));
|
||||||
|
|
||||||
|
match event.to_action(|e| e.to_open_switcher().or_else(|| e.to_open_opener(open_path))) {
|
||||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))),
|
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))),
|
||||||
|
action @ Some(Action::OpenOpener(_)) => Ok(Resp::handled(action.map(Into::into))),
|
||||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||||
self.buffer = new_buffer;
|
self.switch_buffer(state, 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))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
|
Some(Action::OpenFile(path)) => match Buffer::from_file(path) {
|
||||||
|
Ok(buffer) => {
|
||||||
|
let buffer_id = state.buffers.insert(buffer);
|
||||||
|
self.switch_buffer(state, buffer_id);
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
|
Err(err) => Ok(Resp::handled(Some(
|
||||||
|
Action::Show(Some(format!("Could not open file")), format!("{err}")).into(),
|
||||||
|
))),
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
return Err(event);
|
return Err(event);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ enum Mode {
|
||||||
#[default]
|
#[default]
|
||||||
Doc,
|
Doc,
|
||||||
Prompt,
|
Prompt,
|
||||||
|
Filter,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|
@ -28,6 +29,13 @@ impl Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filter() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: Mode::Filter,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn refocus(&mut self, buffer: &mut Buffer, cursor_id: CursorId) {
|
pub fn refocus(&mut self, buffer: &mut Buffer, cursor_id: CursorId) {
|
||||||
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -54,7 +62,7 @@ impl Input {
|
||||||
} else if c == '\x7F' {
|
} else if c == '\x7F' {
|
||||||
buffer.delete(cursor_id);
|
buffer.delete(cursor_id);
|
||||||
} else {
|
} else {
|
||||||
buffer.enter(cursor_id, c);
|
buffer.enter(cursor_id, [c]);
|
||||||
}
|
}
|
||||||
self.refocus(buffer, cursor_id);
|
self.refocus(buffer, cursor_id);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
|
|
@ -80,11 +88,7 @@ impl Input {
|
||||||
cursor_id: CursorId,
|
cursor_id: CursorId,
|
||||||
frame: &mut Rect,
|
frame: &mut Rect,
|
||||||
) {
|
) {
|
||||||
let title = if let Some(path) = &buffer.path {
|
let title = buffer.name();
|
||||||
Some(path.display().to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add frame
|
// Add frame
|
||||||
let mut frame = frame.with_border(
|
let mut frame = frame.with_border(
|
||||||
|
|
@ -104,6 +108,7 @@ impl Input {
|
||||||
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
|
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
|
||||||
let margin_w = match self.mode {
|
let margin_w = match self.mode {
|
||||||
Mode::Prompt => 2,
|
Mode::Prompt => 2,
|
||||||
|
Mode::Filter => 0,
|
||||||
Mode::Doc => line_num_w + 2,
|
Mode::Doc => line_num_w + 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,6 +130,7 @@ impl Input {
|
||||||
{
|
{
|
||||||
// Margin
|
// Margin
|
||||||
match self.mode {
|
match self.mode {
|
||||||
|
Mode::Filter => frame.rect([0, 0], frame.size()),
|
||||||
Mode::Prompt => frame
|
Mode::Prompt => frame
|
||||||
.rect([0, i], [1, 1])
|
.rect([0, i], [1, 1])
|
||||||
.with_bg(state.theme.margin_bg)
|
.with_bg(state.theme.margin_bg)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub use self::{
|
||||||
doc::Doc,
|
doc::Doc,
|
||||||
input::Input,
|
input::Input,
|
||||||
panes::Panes,
|
panes::Panes,
|
||||||
prompt::{Confirm, Prompt, Show, Switcher},
|
prompt::{Confirm, Opener, Prompt, Show, Switcher},
|
||||||
root::Root,
|
root::Root,
|
||||||
status::Status,
|
status::Status,
|
||||||
};
|
};
|
||||||
|
|
@ -120,6 +120,15 @@ impl<T> Options<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_options<F: FnMut(&T) -> Option<u32>>(
|
||||||
|
&mut self,
|
||||||
|
options: impl IntoIterator<Item = T>,
|
||||||
|
mut f: F,
|
||||||
|
) {
|
||||||
|
self.options = options.into_iter().collect();
|
||||||
|
self.apply_scoring(f);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_scoring<F: FnMut(&T) -> Option<u32>>(&mut self, mut f: F) {
|
pub fn apply_scoring<F: FnMut(&T) -> Option<u32>>(&mut self, mut f: F) {
|
||||||
let mut ranking = self
|
let mut ranking = self
|
||||||
.options
|
.options
|
||||||
|
|
@ -127,8 +136,9 @@ impl<T> Options<T> {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(i, o)| Some((i, f(o)?)))
|
.filter_map(|(i, o)| Some((i, f(o)?)))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
ranking.sort_by_key(|(_, score)| *score);
|
ranking.sort_by_key(|(_, score)| std::cmp::Reverse(*score));
|
||||||
self.ranking = ranking.into_iter().map(|(i, _)| i).collect();
|
self.ranking = ranking.into_iter().map(|(i, _)| i).collect();
|
||||||
|
self.selected = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requested_height(&self) -> usize {
|
pub fn requested_height(&self) -> usize {
|
||||||
|
|
@ -136,7 +146,7 @@ impl<T> Options<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Element<T> for Options<T> {
|
impl<T: Clone> Element<T> for Options<T> {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> {
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> {
|
||||||
match event.to_action(|e| e.to_go().or_else(|| e.to_move())) {
|
match event.to_action(|e| e.to_go().or_else(|| e.to_move())) {
|
||||||
Some(Action::Move(Dir::Up, false, _)) => {
|
Some(Action::Move(Dir::Up, false, _)) => {
|
||||||
|
|
@ -150,7 +160,7 @@ impl<T> Element<T> for Options<T> {
|
||||||
Some(Action::Go) => {
|
Some(Action::Go) => {
|
||||||
if self.selected < self.ranking.len() {
|
if self.selected < self.ranking.len() {
|
||||||
Ok(Resp::end_with(
|
Ok(Resp::end_with(
|
||||||
self.options.remove(self.ranking[self.selected]),
|
self.options[self.ranking[self.selected]].clone(),
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
188
src/ui/prompt.rs
188
src/ui/prompt.rs
|
|
@ -1,5 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::state::{Buffer, BufferId, CursorId};
|
use crate::state::{Buffer, BufferId, CursorId};
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
pub struct Prompt {
|
pub struct Prompt {
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
|
|
@ -188,7 +189,7 @@ impl Switcher {
|
||||||
options: Options::new(buffers),
|
options: Options::new(buffers),
|
||||||
cursor_id: buffer.start_session(),
|
cursor_id: buffer.start_session(),
|
||||||
buffer,
|
buffer,
|
||||||
input: Input::prompt(),
|
input: Input::filter(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,10 +216,13 @@ impl Element<()> for Switcher {
|
||||||
let Some(buffer) = state.buffers.get(*b) else {
|
let Some(buffer) = state.buffers.get(*b) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
match buffer.path.as_ref() {
|
let name = buffer.name()?;
|
||||||
Some(path) if path.display().to_string().contains(&filter) => Some(1),
|
if name.starts_with(&filter) {
|
||||||
Some(_) => None,
|
Some(2)
|
||||||
None => Some(0),
|
} else if name.contains(&filter) {
|
||||||
|
Some(1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
res
|
res
|
||||||
|
|
@ -244,10 +248,174 @@ impl Visual for BufferId {
|
||||||
let Some(buffer) = state.buffers.get(*self) else {
|
let Some(buffer) = state.buffers.get(*self) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let buffer_name = match &buffer.path {
|
frame.text([0, 0], buffer.name().unwrap_or("<unknown>").chars());
|
||||||
Some(path) => path.display().to_string(),
|
}
|
||||||
None => format!("<Untitled>"),
|
}
|
||||||
};
|
|
||||||
frame.text([0, 0], buffer_name.chars());
|
pub struct Opener {
|
||||||
|
pub options: Options<FileOption>,
|
||||||
|
// Filter
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub cursor_id: CursorId,
|
||||||
|
pub input: Input,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Opener {
|
||||||
|
pub fn new(path: PathBuf) -> Self {
|
||||||
|
let mut buffer = Buffer::default();
|
||||||
|
let cursor_id = buffer.start_session();
|
||||||
|
match path.display().to_string().as_str() {
|
||||||
|
s @ "/" => buffer.enter(cursor_id, s.chars()),
|
||||||
|
s => buffer.enter(cursor_id, s.chars().chain(['/'])),
|
||||||
|
}
|
||||||
|
let mut this = Self {
|
||||||
|
options: Options::new([]),
|
||||||
|
cursor_id,
|
||||||
|
buffer,
|
||||||
|
input: Input::filter(),
|
||||||
|
};
|
||||||
|
this.update_completions();
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requested_height(&self) -> usize {
|
||||||
|
self.options.requested_height() + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_string(&mut self, s: &str) {
|
||||||
|
self.buffer.clear();
|
||||||
|
self.buffer.enter(self.cursor_id, s.chars());
|
||||||
|
self.update_completions();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_completions(&mut self) {
|
||||||
|
let path_str = self.buffer.text.to_string();
|
||||||
|
let (dir, filter) = match path_str.rsplit_once('/') {
|
||||||
|
Some(("", filter)) => ("/", filter),
|
||||||
|
Some((dir, filter)) => (dir, filter),
|
||||||
|
None => ("/", path_str.as_str()),
|
||||||
|
};
|
||||||
|
let filter = filter.to_lowercase();
|
||||||
|
match fs::read_dir(dir) {
|
||||||
|
Ok(entries) => {
|
||||||
|
let options = entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter_map(|entry| {
|
||||||
|
Some(FileOption {
|
||||||
|
path: entry.path(),
|
||||||
|
kind: if entry.file_type().ok()?.is_dir() {
|
||||||
|
FileKind::Dir
|
||||||
|
} else if entry.file_type().ok()?.is_file() {
|
||||||
|
FileKind::File
|
||||||
|
} else {
|
||||||
|
FileKind::Unknown
|
||||||
|
},
|
||||||
|
is_link: entry.file_type().ok()?.is_symlink(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.chain([FileOption {
|
||||||
|
path: [dir, &filter].into_iter().collect(),
|
||||||
|
kind: FileKind::New,
|
||||||
|
is_link: false,
|
||||||
|
}]);
|
||||||
|
// TODO
|
||||||
|
self.options.set_options(options, |e| {
|
||||||
|
let name = e.path.file_name()?.to_str()?.to_lowercase();
|
||||||
|
if matches!(e.kind, FileKind::New) {
|
||||||
|
// Special-case: the 'new file' entry always matches last
|
||||||
|
Some(0)
|
||||||
|
} else if name == filter {
|
||||||
|
Some(3)
|
||||||
|
} else if name.starts_with(&filter) {
|
||||||
|
Some(2)
|
||||||
|
} else if name.contains(&filter) {
|
||||||
|
Some(1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(err) => self.options.set_options(Vec::new(), |_| None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element<()> for Opener {
|
||||||
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||||
|
let path_str = self.buffer.text.to_string();
|
||||||
|
match event.to_action(|e| e.to_cancel().or_else(|| e.to_char().map(Action::Char))) {
|
||||||
|
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||||
|
// Backspace removes the entire path segment!
|
||||||
|
Some(Action::Char('\x08')) if path_str.ends_with("/") => {
|
||||||
|
if path_str != "/" {
|
||||||
|
self.set_string(
|
||||||
|
path_str
|
||||||
|
.trim_end_matches("/")
|
||||||
|
.trim_end_matches(|c| c != '/'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
|
_ => match self.options.handle(state, event).map(Resp::into_ended) {
|
||||||
|
// Selecting a directory enters the directory
|
||||||
|
Ok(Some(file)) if matches!(file.kind, FileKind::Dir) => {
|
||||||
|
self.set_string(&format!("{}/", file.path.display()));
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
|
Ok(Some(file)) => Ok(Resp::end(Some(Action::OpenFile(file.path).into()))),
|
||||||
|
Ok(None) => Ok(Resp::handled(None)),
|
||||||
|
Err(event) => {
|
||||||
|
let res = self
|
||||||
|
.input
|
||||||
|
.handle(&mut self.buffer, self.cursor_id, event)
|
||||||
|
.map(Resp::into_can_end);
|
||||||
|
self.update_completions();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
enum FileKind {
|
||||||
|
Unknown,
|
||||||
|
Dir,
|
||||||
|
File,
|
||||||
|
New,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FileOption {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub kind: FileKind,
|
||||||
|
pub is_link: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visual for FileOption {
|
||||||
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
let name = match self.path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(name) if matches!(self.kind, FileKind::Dir) => format!("{}/", name),
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
None => format!("<unknown>"),
|
||||||
|
};
|
||||||
|
frame
|
||||||
|
.with_fg(match self.kind {
|
||||||
|
FileKind::Dir => state.theme.option_dir,
|
||||||
|
FileKind::File | FileKind::Unknown => state.theme.option_file,
|
||||||
|
FileKind::New => state.theme.option_new,
|
||||||
|
})
|
||||||
|
.text([0, 0], name.chars());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visual for Opener {
|
||||||
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
frame
|
||||||
|
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
|
||||||
|
.with(|f| self.options.render(state, f));
|
||||||
|
frame
|
||||||
|
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||||
|
.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub enum Task {
|
||||||
Show(Show),
|
Show(Show),
|
||||||
Confirm(Confirm),
|
Confirm(Confirm),
|
||||||
Switcher(Switcher),
|
Switcher(Switcher),
|
||||||
|
Opener(Opener),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
|
|
@ -21,6 +22,7 @@ impl Task {
|
||||||
Self::Show(s) => s.requested_height(),
|
Self::Show(s) => s.requested_height(),
|
||||||
Self::Confirm(c) => c.requested_height(),
|
Self::Confirm(c) => c.requested_height(),
|
||||||
Self::Switcher(s) => s.requested_height(),
|
Self::Switcher(s) => s.requested_height(),
|
||||||
|
Self::Opener(o) => o.requested_height(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +57,7 @@ impl Element<()> for Root {
|
||||||
Task::Show(s) => s.handle(state, event),
|
Task::Show(s) => s.handle(state, event),
|
||||||
Task::Confirm(c) => c.handle(state, event),
|
Task::Confirm(c) => c.handle(state, event),
|
||||||
Task::Switcher(s) => s.handle(state, event),
|
Task::Switcher(s) => s.handle(state, event),
|
||||||
|
Task::Opener(o) => o.handle(state, event),
|
||||||
};
|
};
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
|
|
@ -83,10 +86,14 @@ impl Element<()> for Root {
|
||||||
self.tasks.push(Task::Prompt(Prompt::new()));
|
self.tasks.push(Task::Prompt(Prompt::new()));
|
||||||
}
|
}
|
||||||
Action::OpenSwitcher => {
|
Action::OpenSwitcher => {
|
||||||
self.tasks.clear(); // Prompt overrides all
|
self.tasks.clear(); // Overrides all
|
||||||
self.tasks
|
self.tasks
|
||||||
.push(Task::Switcher(Switcher::new(state.buffers.keys())));
|
.push(Task::Switcher(Switcher::new(state.buffers.keys())));
|
||||||
}
|
}
|
||||||
|
Action::OpenOpener(path) => {
|
||||||
|
self.tasks.clear(); // Overrides all
|
||||||
|
self.tasks.push(Task::Opener(Opener::new(path)));
|
||||||
|
}
|
||||||
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,
|
||||||
|
|
@ -132,6 +139,7 @@ impl Visual for Root {
|
||||||
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),
|
||||||
Task::Switcher(s) => s.render(state, frame),
|
Task::Switcher(s) => s.render(state, frame),
|
||||||
|
Task::Opener(o) => o.render(state, frame),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue