Skip to content

Commit 975a0a9

Browse files
authored
init
1 parent 3a116f1 commit 975a0a9

File tree

11 files changed

+1859
-2
lines changed

11 files changed

+1859
-2
lines changed

Cargo.lock

Lines changed: 1197 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "file-manager"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = "1.0.95"
8+
crossterm = { version = "0.28.1", features = ["event-stream"] }
9+
futures = "0.3.31"
10+
open = "5.3.2"
11+
ratatui = "0.29.0"
12+
tokio = { version = "1.40.0", features = ["full"] }
13+
trash = "5.2.1"

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
# file-manager-rust
2-
File Manager Rust
1+
# File Manager Rust

resources/help

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
------------------------------
2+
General
3+
------------------------------
4+
5+
? - open/close this help
6+
Esc/q/Ctrl-c - exit
7+
8+
------------------------------
9+
Files
10+
------------------------------
11+
12+
Ctrl-h - toggle hidden files and directories
13+
d - move file to trash
14+
Ctrl-d - delete file permanently

src/app.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use std::{env, fs, path::PathBuf};
2+
3+
use anyhow::Result;
4+
use ratatui::widgets::ListState;
5+
6+
use crate::files;
7+
8+
#[derive(Debug)]
9+
pub struct App {
10+
// General
11+
pub running: bool,
12+
13+
// Files
14+
pub current_dir: PathBuf,
15+
pub files: Vec<files::File>,
16+
pub files_state: ListState,
17+
pub show_hidden: bool,
18+
19+
// Help
20+
pub show_help: bool,
21+
pub help_offset: usize,
22+
}
23+
24+
impl App {
25+
pub fn new() -> Result<Self> {
26+
let current_dir = env::current_dir()?;
27+
28+
Ok(App {
29+
// General
30+
running: true,
31+
32+
// Files
33+
current_dir: current_dir.clone(),
34+
files: files::list_dir(current_dir, false)?,
35+
files_state: ListState::default().with_selected(Some(0)),
36+
show_hidden: false,
37+
38+
// Help
39+
show_help: false,
40+
help_offset: 0,
41+
})
42+
}
43+
44+
pub fn quit(&mut self) {
45+
self.running = false;
46+
}
47+
48+
pub fn select_next(&mut self) {
49+
if let Some(selected) = self.files_state.selected() {
50+
if selected == self.files.len() - 1 {
51+
self.files_state.select_first();
52+
return;
53+
}
54+
}
55+
56+
self.files_state.select_next();
57+
}
58+
59+
pub fn select_previous(&mut self) {
60+
if let Some(selected) = self.files_state.selected() {
61+
if selected == 0 {
62+
self.files_state.select_last();
63+
return;
64+
}
65+
}
66+
67+
self.files_state.select_previous();
68+
}
69+
70+
pub fn toggle_hidden(&mut self) -> Result<()> {
71+
self.show_hidden = !self.show_hidden;
72+
self.refresh_files(None)?;
73+
74+
Ok(())
75+
}
76+
77+
pub fn return_path(&mut self) -> Result<()> {
78+
if let Some(parent_dir) = self.current_dir.parent() {
79+
self.refresh_files(Some(PathBuf::from(parent_dir)))?;
80+
}
81+
82+
Ok(())
83+
}
84+
85+
pub fn open(&mut self) -> Result<()> {
86+
if let Some(selected) = self.files_state.selected() {
87+
let file = self.files.get(selected).unwrap();
88+
if file.is_dir {
89+
self.refresh_files(Some(self.current_dir.join(&file.name)))?;
90+
return Ok(());
91+
}
92+
93+
open::that(self.current_dir.join(&file.name))?;
94+
}
95+
96+
Ok(())
97+
}
98+
99+
pub fn move_to_trash(&mut self) -> Result<()> {
100+
if let Some(selected) = self.files_state.selected() {
101+
let file = self.files.get(selected).unwrap();
102+
trash::delete(self.current_dir.join(&file.name))?;
103+
self.refresh_files(None)?;
104+
}
105+
106+
Ok(())
107+
}
108+
109+
pub fn remove_file(&mut self) -> Result<()> {
110+
if let Some(selected) = self.files_state.selected() {
111+
let file = self.files.get(selected).unwrap();
112+
fs::remove_file(self.current_dir.join(&file.name))?;
113+
self.refresh_files(None)?;
114+
}
115+
116+
Ok(())
117+
}
118+
119+
pub fn toggle_help(&mut self) {
120+
self.show_help = !self.show_help;
121+
}
122+
123+
pub fn get_file_names(&self) -> Vec<String> {
124+
self.files.iter().map(|f| f.formated_name.clone()).collect()
125+
}
126+
127+
pub fn get_files_selected(&self) -> usize {
128+
if let Some(selected) = self.files_state.selected() {
129+
return selected;
130+
}
131+
132+
0
133+
}
134+
135+
pub fn get_preview(&self) -> String {
136+
if let Some(selected) = self.files_state.selected() {
137+
let file = self.files.get(selected).unwrap();
138+
if file.is_dir {
139+
return files::list_dir(self.current_dir.join(&file.name), self.show_hidden)
140+
.unwrap()
141+
.iter()
142+
.map(|f| f.formated_name.clone())
143+
.collect::<Vec<_>>()
144+
.join("\n");
145+
}
146+
147+
return fs::read_to_string(self.current_dir.join(&file.name))
148+
.unwrap_or("--Cannot read file--".to_string());
149+
}
150+
151+
String::new()
152+
}
153+
154+
fn refresh_files(&mut self, path: Option<PathBuf>) -> Result<()> {
155+
if let Some(path_buf) = path {
156+
self.current_dir = path_buf;
157+
}
158+
159+
self.files = files::list_dir(self.current_dir.clone(), self.show_hidden)?;
160+
self.files_state.select_first();
161+
162+
Ok(())
163+
}
164+
}

src/event.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use std::time::Duration;
2+
3+
use anyhow::Result;
4+
use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
5+
use futures::{FutureExt, StreamExt};
6+
use tokio::sync::mpsc;
7+
8+
/// Terminal events.
9+
#[derive(Clone, Copy, Debug)]
10+
pub enum Event {
11+
/// Terminal tick.
12+
Tick,
13+
/// Key press.
14+
Key(KeyEvent),
15+
/// Mouse click/scroll.
16+
#[allow(dead_code)]
17+
Mouse(MouseEvent),
18+
/// Terminal resize.
19+
#[allow(dead_code)]
20+
Resize(u16, u16),
21+
}
22+
23+
/// Terminal event handler.
24+
#[allow(dead_code)]
25+
#[derive(Debug)]
26+
pub struct EventHandler {
27+
/// Event sender channel.
28+
sender: mpsc::UnboundedSender<Event>,
29+
/// Event receiver channel.
30+
receiver: mpsc::UnboundedReceiver<Event>,
31+
/// Event handler thread.
32+
handler: tokio::task::JoinHandle<()>,
33+
}
34+
35+
impl EventHandler {
36+
/// Constructs a new instance of [`EventHandler`].
37+
pub fn new(tick_rate: u64) -> Self {
38+
let tick_rate = Duration::from_millis(tick_rate);
39+
let (sender, receiver) = mpsc::unbounded_channel();
40+
let _sender = sender.clone();
41+
let handler = tokio::spawn(async move {
42+
let mut reader = crossterm::event::EventStream::new();
43+
let mut tick = tokio::time::interval(tick_rate);
44+
loop {
45+
let tick_delay = tick.tick();
46+
let crossterm_event = reader.next().fuse();
47+
tokio::select! {
48+
_ = _sender.closed() => {
49+
break;
50+
}
51+
_ = tick_delay => {
52+
_sender.send(Event::Tick).unwrap();
53+
}
54+
Some(Ok(evt)) = crossterm_event => {
55+
match evt {
56+
CrosstermEvent::Key(key) => {
57+
if key.kind == crossterm::event::KeyEventKind::Press {
58+
_sender.send(Event::Key(key)).unwrap();
59+
}
60+
},
61+
CrosstermEvent::Mouse(mouse) => {
62+
_sender.send(Event::Mouse(mouse)).unwrap();
63+
},
64+
CrosstermEvent::Resize(x, y) => {
65+
_sender.send(Event::Resize(x, y)).unwrap();
66+
},
67+
CrosstermEvent::FocusLost => {
68+
},
69+
CrosstermEvent::FocusGained => {
70+
},
71+
CrosstermEvent::Paste(_) => {
72+
},
73+
}
74+
}
75+
};
76+
}
77+
});
78+
Self {
79+
sender,
80+
receiver,
81+
handler,
82+
}
83+
}
84+
85+
/// Receive the next event from the handler thread.
86+
///
87+
/// This function will always block the current thread if
88+
/// there is no data available and it's possible for more data to be sent.
89+
pub async fn next(&mut self) -> Result<Event> {
90+
self.receiver
91+
.recv()
92+
.await
93+
.ok_or(anyhow::anyhow!(Box::new(std::io::Error::new(
94+
std::io::ErrorKind::Other,
95+
"This is an IO error",
96+
))))
97+
}
98+
}

src/files/mod.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::{
2+
fs::{self, DirEntry},
3+
path::PathBuf,
4+
};
5+
6+
use anyhow::Result;
7+
8+
#[derive(Debug)]
9+
pub struct File {
10+
pub name: String,
11+
pub formated_name: String,
12+
pub is_dir: bool,
13+
}
14+
15+
impl File {
16+
pub fn new(name: String, is_dir: bool) -> Self {
17+
let formated_name = if is_dir {
18+
format!("🗀 {name}")
19+
} else {
20+
format!("🗎 {name}")
21+
};
22+
23+
Self {
24+
name,
25+
formated_name,
26+
is_dir,
27+
}
28+
}
29+
}
30+
31+
pub fn list_dir(path: PathBuf, show_hidden: bool) -> Result<Vec<File>> {
32+
let dir_entries: Vec<DirEntry> = fs::read_dir(path)?.filter_map(|entry| entry.ok()).collect();
33+
34+
let mut files: Vec<File> = Vec::new();
35+
for entry in dir_entries {
36+
let mut file_name = format!("{:?}", entry.file_name());
37+
38+
file_name = file_name
39+
.strip_prefix("\"")
40+
.unwrap_or(&file_name)
41+
.to_string();
42+
43+
file_name = file_name
44+
.strip_suffix("\"")
45+
.unwrap_or(&file_name)
46+
.to_string();
47+
48+
if file_name.starts_with(".") && !show_hidden {
49+
continue;
50+
}
51+
52+
files.push(File::new(file_name, entry.metadata()?.is_dir()));
53+
}
54+
55+
files.sort_unstable_by_key(|file| (!file.is_dir, file.name.clone()));
56+
57+
Ok(files)
58+
}

0 commit comments

Comments
 (0)