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},
io::{stdout, Write},
ops::Range,
path::PathBuf,
vec,
};
@ -24,7 +25,7 @@ pub struct Editor {
cursor: Cursor,
marker: Option<usize>,
clipboard: Clipboard,
path: Option<String>,
path: Option<PathBuf>,
active: bool,
unsaved_changes: bool,
}
@ -39,9 +40,9 @@ struct Cursor {
type Line = Range<usize>;
impl Editor {
pub fn new(clipboard: Clipboard, path: String) -> Self {
let text = fs::read_to_string(&path).unwrap_or_default();
let mut this = Editor {
pub fn open_file(clipboard: Clipboard, path: PathBuf) -> Option<Self> {
let text = fs::read_to_string(&path).ok()?;
Some(Editor {
text,
lines: Vec::new(),
scroll: 0,
@ -51,9 +52,7 @@ impl Editor {
path: Some(path),
active: false,
unsaved_changes: false,
};
this.find_lines();
this
})
}
pub fn new_empty(clipboard: Clipboard) -> Self {
@ -70,16 +69,36 @@ impl Editor {
}
}
pub fn name(&self) -> &str {
self.path.as_ref().map_or("untitled", |s| s)
pub fn new_named(clipboard: Clipboard, path: PathBuf) -> Self {
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 {
self.unsaved_changes
}
pub fn open(&mut self) {
pub fn enter(&mut self) {
self.active = true;
self.find_lines();
while self.active {
self.draw();
@ -88,8 +107,7 @@ impl Editor {
}
fn input(&mut self) {
match event::read() {
Ok(Event::Key(event)) => {
if let Ok(Event::Key(event)) = event::read() {
if self.input_movement(&event) {
return;
}
@ -112,8 +130,6 @@ impl Editor {
_ => (),
}
}
_ => (),
}
}
/// Cursor movement logic, returns true if cursor moved (so consider the event consumed in that case)
@ -330,11 +346,8 @@ impl Editor {
fn selection(&self) -> Option<Range<usize>> {
let cursor = self.char_index();
if let Some(marker) = self.marker {
Some(marker.min(cursor)..(marker.max(cursor)))
} else {
None
}
self.marker
.map(|marker| marker.min(cursor)..(marker.max(cursor)))
}
fn selection_or_line(&self) -> Range<usize> {
@ -359,6 +372,7 @@ impl Editor {
text += "\n";
end += 1;
}
end = end.min(self.text.len());
self.clipboard.set(text);
self.text = self.text[..start].to_owned() + &self.text[end..];
self.find_lines();
@ -411,7 +425,7 @@ impl Editor {
fn save(&mut self) {
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() {
return;
}

View file

@ -9,8 +9,9 @@ use crossterm::{
},
};
use std::{
env,
env, fs,
io::{stdout, Write},
path::PathBuf,
process::exit,
};
@ -25,25 +26,46 @@ fn main() {
}
struct Navigator {
editors: Vec<Editor>,
selected: Option<usize>,
clipboard: Clipboard,
editors: Vec<Editor>,
files: Vec<PathBuf>,
selected: usize,
path: PathBuf,
immediate_open: bool,
}
impl Navigator {
fn new() -> Self {
let clipboard = Clipboard::new();
let mut editors: Vec<Editor> = env::args()
.skip(1)
.map(|path| Editor::new(clipboard.clone(), path))
.collect();
if editors.is_empty() {
let mut editors = Vec::new();
let args: Vec<String> = env::args().skip(1).collect();
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()));
}
let immediate_open = editors.len() == 1;
Self {
editors,
selected: Some(0),
clipboard,
editors,
selected: 0,
files: Vec::new(),
path,
immediate_open,
}
}
@ -51,7 +73,12 @@ impl Navigator {
execute!(stdout(), EnterAlternateScreen, Clear(ClearType::All)).unwrap();
enable_raw_mode().unwrap();
if self.immediate_open {
self.enter();
}
loop {
self.get_files();
self.draw();
self.input();
}
@ -62,7 +89,7 @@ impl Navigator {
print!("Open editors: {}", self.editors.len());
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(), MoveTo(1, index as u16 + 1)).unwrap();
@ -74,6 +101,26 @@ impl Navigator {
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();
}
@ -83,7 +130,8 @@ impl Navigator {
KeyCode::Char('q') => self.quit(),
KeyCode::Up => self.nav_up(),
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') => {
if event.modifiers == KeyModifiers::CONTROL {
self.new_editor();
@ -95,31 +143,60 @@ impl Navigator {
}
fn nav_up(&mut self) {
if self.selected > Some(0) {
self.selected = Some(self.selected.unwrap() - 1);
}
self.selected = self.selected.saturating_sub(1);
}
fn nav_down(&mut self) {
if let Some(index) = self.selected.as_mut() {
if *index < self.editors.len() - 1 {
*index += 1;
self.selected = (self.selected + 1).min(self.editors.len() + self.files.len() - 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) {
if let Some(index) = self.selected {
self.editors[index].open();
if self.selected < self.editors.len() {
self.editors[self.selected].enter();
}
}
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.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) {
disable_raw_mode().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 size = terminal::size().unwrap();
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();
print!("{prompt}");
stdout().flush().unwrap();
@ -20,6 +23,8 @@ pub fn read_line(prompt: &str) -> Option<String> {
KeyCode::Enter => break,
KeyCode::Char(ch) => response.push(ch),
KeyCode::Backspace => {
queue!(stdout(), start_pos).unwrap();
print!("{:width$}", " ");
response.pop();
}
KeyCode::Esc => return None,
@ -27,7 +32,7 @@ pub fn read_line(prompt: &str) -> Option<String> {
}
}
queue!(stdout(), start_pos).unwrap();
print!("{prompt}{response} ");
print!("{prompt}{response}");
stdout().flush().unwrap();
}
Some(response.trim().into())