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},
|
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,8 +107,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -112,8 +130,6 @@ impl Editor {
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cursor movement logic, returns true if cursor moved (so consider the event consumed in that case)
|
/// 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>> {
|
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;
|
||||||
}
|
}
|
||||||
|
|
119
src/main.rs
119
src/main.rs
|
@ -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();
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue