file navigation menu
This commit is contained in:
parent
f41c23bbf8
commit
15beff91b3
3 changed files with 158 additions and 62 deletions
|
@ -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,31 +107,28 @@ impl Editor {
|
|||
}
|
||||
|
||||
fn input(&mut self) {
|
||||
match event::read() {
|
||||
Ok(Event::Key(event)) => {
|
||||
if self.input_movement(&event) {
|
||||
return;
|
||||
}
|
||||
match event.modifiers {
|
||||
KeyModifiers::NONE => match event.code {
|
||||
KeyCode::Esc => self.active = false,
|
||||
KeyCode::Char(ch) => self.insert_char(ch),
|
||||
KeyCode::Enter => self.insert_char('\n'),
|
||||
KeyCode::Backspace => self.backspace(),
|
||||
KeyCode::Delete => self.delete(),
|
||||
_ => (),
|
||||
},
|
||||
KeyModifiers::CONTROL => match event.code {
|
||||
KeyCode::Char('s') => self.save(),
|
||||
KeyCode::Char('c') => self.copy(),
|
||||
KeyCode::Char('x') => self.cut(),
|
||||
KeyCode::Char('v') => self.paste(),
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
if let Ok(Event::Key(event)) = event::read() {
|
||||
if self.input_movement(&event) {
|
||||
return;
|
||||
}
|
||||
match event.modifiers {
|
||||
KeyModifiers::NONE => match event.code {
|
||||
KeyCode::Esc => self.active = false,
|
||||
KeyCode::Char(ch) => self.insert_char(ch),
|
||||
KeyCode::Enter => self.insert_char('\n'),
|
||||
KeyCode::Backspace => self.backspace(),
|
||||
KeyCode::Delete => self.delete(),
|
||||
_ => (),
|
||||
},
|
||||
KeyModifiers::CONTROL => match event.code {
|
||||
KeyCode::Char('s') => self.save(),
|
||||
KeyCode::Char('c') => self.copy(),
|
||||
KeyCode::Char('x') => self.cut(),
|
||||
KeyCode::Char('v') => self.paste(),
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
119
src/main.rs
119
src/main.rs
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue