file navigation menu

This commit is contained in:
Crispy 2023-03-05 18:06:29 +01:00
parent f41c23bbf8
commit 15beff91b3
3 changed files with 158 additions and 62 deletions

View file

@ -9,6 +9,7 @@ use std::{
fs::{self, File}, fs::{self, File},
io::{stdout, Write}, io::{stdout, Write},
ops::Range, ops::Range,
path::PathBuf,
vec, vec,
}; };
@ -24,7 +25,7 @@ pub struct Editor {
cursor: Cursor, cursor: Cursor,
marker: Option<usize>, marker: Option<usize>,
clipboard: Clipboard, clipboard: Clipboard,
path: Option<String>, path: Option<PathBuf>,
active: bool, active: bool,
unsaved_changes: bool, unsaved_changes: bool,
} }
@ -39,9 +40,9 @@ struct Cursor {
type Line = Range<usize>; type Line = Range<usize>;
impl Editor { impl Editor {
pub fn new(clipboard: Clipboard, path: String) -> Self { pub fn open_file(clipboard: Clipboard, path: PathBuf) -> Option<Self> {
let text = fs::read_to_string(&path).unwrap_or_default(); let text = fs::read_to_string(&path).ok()?;
let mut this = Editor { Some(Editor {
text, text,
lines: Vec::new(), lines: Vec::new(),
scroll: 0, scroll: 0,
@ -51,9 +52,7 @@ impl Editor {
path: Some(path), path: Some(path),
active: false, active: false,
unsaved_changes: false, unsaved_changes: false,
}; })
this.find_lines();
this
} }
pub fn new_empty(clipboard: Clipboard) -> Self { pub fn new_empty(clipboard: Clipboard) -> Self {
@ -70,16 +69,36 @@ impl Editor {
} }
} }
pub fn name(&self) -> &str { pub fn new_named(clipboard: Clipboard, path: PathBuf) -> Self {
self.path.as_ref().map_or("untitled", |s| s) Editor {
text: String::new(),
lines: vec![0..0],
scroll: 0,
cursor: Cursor { line: 0, column: 0 },
marker: None,
clipboard,
path: Some(path),
active: false,
unsaved_changes: true,
}
}
pub fn name(&self) -> String {
if let Some(path) = &self.path {
if let Some(name) = path.file_name() {
return name.to_string_lossy().to_string();
}
}
"untitled".into()
} }
pub fn has_unsaved_changes(&self) -> bool { pub fn has_unsaved_changes(&self) -> bool {
self.unsaved_changes self.unsaved_changes
} }
pub fn open(&mut self) { pub fn enter(&mut self) {
self.active = true; self.active = true;
self.find_lines();
while self.active { while self.active {
self.draw(); self.draw();
@ -88,31 +107,28 @@ impl Editor {
} }
fn input(&mut self) { fn input(&mut self) {
match event::read() { if let Ok(Event::Key(event)) = event::read() {
Ok(Event::Key(event)) => { if self.input_movement(&event) {
if self.input_movement(&event) { return;
return; }
} match event.modifiers {
match event.modifiers { KeyModifiers::NONE => match event.code {
KeyModifiers::NONE => match event.code { KeyCode::Esc => self.active = false,
KeyCode::Esc => self.active = false, KeyCode::Char(ch) => self.insert_char(ch),
KeyCode::Char(ch) => self.insert_char(ch), KeyCode::Enter => self.insert_char('\n'),
KeyCode::Enter => self.insert_char('\n'), KeyCode::Backspace => self.backspace(),
KeyCode::Backspace => self.backspace(), KeyCode::Delete => self.delete(),
KeyCode::Delete => self.delete(), _ => (),
_ => (), },
}, KeyModifiers::CONTROL => match event.code {
KeyModifiers::CONTROL => match event.code { KeyCode::Char('s') => self.save(),
KeyCode::Char('s') => self.save(), KeyCode::Char('c') => self.copy(),
KeyCode::Char('c') => self.copy(), KeyCode::Char('x') => self.cut(),
KeyCode::Char('x') => self.cut(), KeyCode::Char('v') => self.paste(),
KeyCode::Char('v') => self.paste(), _ => (),
_ => (), },
}, _ => (),
_ => (),
}
} }
_ => (),
} }
} }
@ -330,11 +346,8 @@ impl Editor {
fn selection(&self) -> Option<Range<usize>> { fn selection(&self) -> Option<Range<usize>> {
let cursor = self.char_index(); let cursor = self.char_index();
if let Some(marker) = self.marker { self.marker
Some(marker.min(cursor)..(marker.max(cursor))) .map(|marker| marker.min(cursor)..(marker.max(cursor)))
} else {
None
}
} }
fn selection_or_line(&self) -> Range<usize> { fn selection_or_line(&self) -> Range<usize> {
@ -359,6 +372,7 @@ impl Editor {
text += "\n"; text += "\n";
end += 1; end += 1;
} }
end = end.min(self.text.len());
self.clipboard.set(text); self.clipboard.set(text);
self.text = self.text[..start].to_owned() + &self.text[end..]; self.text = self.text[..start].to_owned() + &self.text[end..];
self.find_lines(); self.find_lines();
@ -411,7 +425,7 @@ impl Editor {
fn save(&mut self) { fn save(&mut self) {
if self.path.is_none() { if self.path.is_none() {
self.path = read_line("Enter path: "); self.path = read_line("Enter path: ").map(PathBuf::from);
if self.path.is_none() { if self.path.is_none() {
return; return;
} }

View file

@ -9,8 +9,9 @@ use crossterm::{
}, },
}; };
use std::{ use std::{
env, env, fs,
io::{stdout, Write}, io::{stdout, Write},
path::PathBuf,
process::exit, process::exit,
}; };
@ -25,25 +26,46 @@ fn main() {
} }
struct Navigator { struct Navigator {
editors: Vec<Editor>,
selected: Option<usize>,
clipboard: Clipboard, clipboard: Clipboard,
editors: Vec<Editor>,
files: Vec<PathBuf>,
selected: usize,
path: PathBuf,
immediate_open: bool,
} }
impl Navigator { impl Navigator {
fn new() -> Self { fn new() -> Self {
let clipboard = Clipboard::new(); let clipboard = Clipboard::new();
let mut editors: Vec<Editor> = env::args() let mut editors = Vec::new();
.skip(1)
.map(|path| Editor::new(clipboard.clone(), path)) let args: Vec<String> = env::args().skip(1).collect();
.collect();
if editors.is_empty() { let mut path = env::current_dir().unwrap();
for arg in args.iter().map(PathBuf::from) {
if arg.is_dir() {
path = arg.canonicalize().unwrap();
break;
} else if arg.is_file() {
if let Some(editor) = Editor::open_file(clipboard.clone(), arg) {
editors.push(editor);
}
} else {
editors.push(Editor::new_named(clipboard.clone(), arg))
}
}
if args.is_empty() {
editors.push(Editor::new_empty(clipboard.clone())); editors.push(Editor::new_empty(clipboard.clone()));
} }
let immediate_open = editors.len() == 1;
Self { Self {
editors,
selected: Some(0),
clipboard, clipboard,
editors,
selected: 0,
files: Vec::new(),
path,
immediate_open,
} }
} }
@ -51,7 +73,12 @@ impl Navigator {
execute!(stdout(), EnterAlternateScreen, Clear(ClearType::All)).unwrap(); execute!(stdout(), EnterAlternateScreen, Clear(ClearType::All)).unwrap();
enable_raw_mode().unwrap(); enable_raw_mode().unwrap();
if self.immediate_open {
self.enter();
}
loop { loop {
self.get_files();
self.draw(); self.draw();
self.input(); self.input();
} }
@ -62,7 +89,7 @@ impl Navigator {
print!("Open editors: {}", self.editors.len()); print!("Open editors: {}", self.editors.len());
for (index, editor) in self.editors.iter().enumerate() { for (index, editor) in self.editors.iter().enumerate() {
if Some(index) == self.selected { if index == self.selected {
queue!(stdout(), SetColors(Colors::new(Color::Black, Color::White))).unwrap(); queue!(stdout(), SetColors(Colors::new(Color::Black, Color::White))).unwrap();
} }
queue!(stdout(), MoveTo(1, index as u16 + 1)).unwrap(); queue!(stdout(), MoveTo(1, index as u16 + 1)).unwrap();
@ -74,6 +101,26 @@ impl Navigator {
queue!(stdout(), ResetColor).unwrap(); queue!(stdout(), ResetColor).unwrap();
} }
let offset = self.editors.len() as u16 + 2;
queue!(stdout(), MoveTo(0, offset)).unwrap();
print!("Current dir: {}", self.path.to_string_lossy());
for (index, path) in self.files.iter().enumerate() {
if index == self.selected.wrapping_sub(self.editors.len()) {
queue!(stdout(), SetColors(Colors::new(Color::Black, Color::White))).unwrap();
}
queue!(stdout(), MoveTo(1, index as u16 + 1 + offset)).unwrap();
if let Some(name) = path.file_name() {
print!("{}", name.to_string_lossy());
} else {
print!("{}", path.to_string_lossy());
}
if path.is_dir() {
print!("/");
}
queue!(stdout(), ResetColor).unwrap();
}
stdout().flush().unwrap(); stdout().flush().unwrap();
} }
@ -83,7 +130,8 @@ impl Navigator {
KeyCode::Char('q') => self.quit(), KeyCode::Char('q') => self.quit(),
KeyCode::Up => self.nav_up(), KeyCode::Up => self.nav_up(),
KeyCode::Down => self.nav_down(), KeyCode::Down => self.nav_down(),
KeyCode::Enter => self.open_selected(), KeyCode::Enter => self.enter(),
KeyCode::Home => self.path = env::current_dir().unwrap(),
KeyCode::Char('n') => { KeyCode::Char('n') => {
if event.modifiers == KeyModifiers::CONTROL { if event.modifiers == KeyModifiers::CONTROL {
self.new_editor(); self.new_editor();
@ -95,31 +143,60 @@ impl Navigator {
} }
fn nav_up(&mut self) { fn nav_up(&mut self) {
if self.selected > Some(0) { self.selected = self.selected.saturating_sub(1);
self.selected = Some(self.selected.unwrap() - 1);
}
} }
fn nav_down(&mut self) { fn nav_down(&mut self) {
if let Some(index) = self.selected.as_mut() { self.selected = (self.selected + 1).min(self.editors.len() + self.files.len() - 1);
if *index < self.editors.len() - 1 { }
*index += 1;
fn enter(&mut self) {
if self.selected < self.editors.len() {
self.editors[self.selected].enter();
} else {
let i = self.selected - self.editors.len();
if i == 0 {
if let Some(parent) = self.path.parent() {
self.path = parent.to_owned()
}
} else {
let path = &self.files[i];
if path.is_dir() {
self.path = self.path.join(path);
self.selected = self.editors.len();
} else if path.is_file() {
if let Some(editor) =
Editor::open_file(self.clipboard.clone(), path.canonicalize().unwrap())
{
self.selected = self.editors.len();
self.editors.push(editor);
self.open_selected()
}
}
} }
} }
} }
fn open_selected(&mut self) { fn open_selected(&mut self) {
if let Some(index) = self.selected { if self.selected < self.editors.len() {
self.editors[index].open(); self.editors[self.selected].enter();
} }
} }
fn new_editor(&mut self) { fn new_editor(&mut self) {
self.selected = Some(self.editors.len()); self.selected = self.editors.len();
self.editors.push(Editor::new_empty(self.clipboard.clone())); self.editors.push(Editor::new_empty(self.clipboard.clone()));
self.open_selected(); self.open_selected();
} }
fn get_files(&mut self) {
self.files.clear();
self.files.push(PathBuf::from(".."));
for file in fs::read_dir(&self.path).unwrap().flatten() {
self.files.push(file.path());
}
}
fn quit(&self) { fn quit(&self) {
disable_raw_mode().unwrap(); disable_raw_mode().unwrap();
execute!(stdout(), LeaveAlternateScreen, cursor::Show).unwrap(); execute!(stdout(), LeaveAlternateScreen, cursor::Show).unwrap();

View file

@ -9,7 +9,10 @@ pub fn read_line(prompt: &str) -> Option<String> {
let mut response = String::new(); let mut response = String::new();
let size = terminal::size().unwrap(); let size = terminal::size().unwrap();
let start_pos = cursor::MoveTo(0, size.1); let start_pos = cursor::MoveTo(0, size.1);
let width = size.0 as usize;
queue!(stdout(), start_pos).unwrap();
print!("{:width$}", " ");
queue!(stdout(), start_pos).unwrap(); queue!(stdout(), start_pos).unwrap();
print!("{prompt}"); print!("{prompt}");
stdout().flush().unwrap(); stdout().flush().unwrap();
@ -20,6 +23,8 @@ pub fn read_line(prompt: &str) -> Option<String> {
KeyCode::Enter => break, KeyCode::Enter => break,
KeyCode::Char(ch) => response.push(ch), KeyCode::Char(ch) => response.push(ch),
KeyCode::Backspace => { KeyCode::Backspace => {
queue!(stdout(), start_pos).unwrap();
print!("{:width$}", " ");
response.pop(); response.pop();
} }
KeyCode::Esc => return None, KeyCode::Esc => return None,
@ -27,7 +32,7 @@ pub fn read_line(prompt: &str) -> Option<String> {
} }
} }
queue!(stdout(), start_pos).unwrap(); queue!(stdout(), start_pos).unwrap();
print!("{prompt}{response} "); print!("{prompt}{response}");
stdout().flush().unwrap(); stdout().flush().unwrap();
} }
Some(response.trim().into()) Some(response.trim().into())