Seems to show things

This commit is contained in:
maxstrb 2026-02-01 12:01:52 +01:00
parent d4f42ec6aa
commit 66c3ef49ac
13 changed files with 4239 additions and 2 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/mnt/removable/Projects/Rust/file-explorer" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/mnt/removable/Projects/Rust/file-explorer")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/mnt/removable/Projects/Rust/file-explorer" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/mnt/removable/Projects/Rust/file-explorer/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/mnt/removable/Projects/Rust/file-explorer/.envrc" "/mnt/removable/Projects/Rust/file-explorer/.direnv"/*.rc

View file

@ -0,0 +1 @@
/nix/store/8hx8jwa24q6rkzhca9iqy5ckk9rbphi1-source

View file

@ -0,0 +1 @@
/nix/store/a6vm6qzra5vqs1jy9k2m10fmda3pislg-source

View file

@ -0,0 +1 @@
/nix/store/b5cw92ch3f64g524j7m5w9jajyadjdi4-source

View file

@ -0,0 +1 @@
/nix/store/swz4b263qq4jdxs4fk62cnrg1v7jvmgr-nix-shell-env

File diff suppressed because it is too large Load diff

1512
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,3 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
crossterm = "0.29.0"
ratatui = "0.30.0"
rustix = { version = "1.1.3", features = ["fs"] }
# Read the optimization guideline for more details: https://ratatui.rs/recipes/apps/release-your-app/#optimizations
[profile.release]
codegen-units = 1
lto = true
opt-level = 3
strip = true

25
src/config.rs Normal file
View file

@ -0,0 +1,25 @@
pub struct Config {
pub priorities: Priorities,
pub show_hidden: bool,
}
impl Config {
pub fn new() -> Self {
Self {
priorities: Priorities {
hidden_dir: 1,
dir: 0,
hidden_file: 3,
file: 2,
},
show_hidden: false,
}
}
}
pub struct Priorities {
pub hidden_dir: u8,
pub dir: u8,
pub hidden_file: u8,
pub file: u8,
}

View file

@ -0,0 +1,28 @@
use crate::config::Config;
use rustix::fs::Access;
use std::path::{Path, PathBuf};
pub fn sort_dir(config: &Config, files: &mut Vec<PathBuf>) {
if !config.show_hidden {
files.retain(|file| !is_hidden(file.as_path()));
}
files.sort();
files.sort_by_key(|path| match (path.is_dir(), is_hidden(path.as_path())) {
(false, false) => config.priorities.file,
(false, true) => config.priorities.hidden_file,
(true, false) => config.priorities.dir,
(true, true) => config.priorities.hidden_dir,
});
}
pub fn can_read(path: &Path) -> bool {
rustix::fs::access(path, Access::READ_OK).is_ok()
}
fn is_hidden(path: &Path) -> bool {
path.file_name()
.and_then(|f| f.as_encoded_bytes().first())
.map(|&first_byte| first_byte == b'.')
.unwrap_or(false)
}

189
src/file_select.rs Normal file
View file

@ -0,0 +1,189 @@
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Line,
widgets::{StatefulWidget, Widget},
};
use std::{
cell::RefCell,
env, fs, io, mem,
path::{Path, PathBuf},
rc::Rc,
};
use crate::{config::Config, directory_operations};
pub enum ScrollingDirection {
Up,
Down,
}
pub struct FilesWidget;
pub struct FilesState {
selected: Option<usize>,
top: usize,
files: Vec<PathBuf>,
current_dir: PathBuf,
config: Rc<RefCell<Config>>,
}
impl FilesState {
pub fn get_current(&self) -> Option<&Path> {
Some(self.files[self.selected?].as_path())
}
pub fn scroll(&mut self, direction: ScrollingDirection) {
if self.files.is_empty() {
return;
}
match direction {
ScrollingDirection::Down => self.scroll_down(),
ScrollingDirection::Up => self.scroll_up(),
}
}
fn scroll_down(&mut self) {
match self.selected {
Some(v) => self.selected = Some((v + 1).min(self.files.len() - 1)),
None => self.selected = Some(0),
}
}
fn scroll_up(&mut self) {
self.selected = Some(self.selected.unwrap_or(0).saturating_sub(1));
}
pub fn go_up(&mut self) -> io::Result<()> {
let new_path = self.current_dir.parent().unwrap_or(Path::new("/"));
if !directory_operations::can_read(new_path) {
return Ok(());
}
self.current_dir.pop();
self.update_contents()
}
fn update_contents(&mut self) -> io::Result<()> {
self.files = fs::read_dir(&self.current_dir)?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.collect();
directory_operations::sort_dir(&self.config.borrow(), &mut self.files);
self.selected = if self.files.is_empty() { None } else { Some(0) };
self.top = 0;
Ok(())
}
pub fn go_inner(&mut self) -> io::Result<()> {
let Some(selected) = self.selected else {
return Ok(());
};
if !self.files[selected].is_dir() {
return Ok(());
}
if !directory_operations::can_read(self.files[selected].as_path()) {
return Ok(());
}
self.current_dir = self.files.swap_remove(selected);
self.update_contents()?;
Ok(())
}
pub fn toggle_hidden(&mut self) -> io::Result<()> {
let current_state = self.config.borrow().show_hidden;
self.set_hidden(!current_state)?;
Ok(())
}
fn set_hidden(&mut self, state: bool) -> io::Result<()> {
{
let mut cfg = self.config.borrow_mut();
cfg.show_hidden = state;
}
self.update_contents()?;
Ok(())
}
pub fn new(config: Rc<RefCell<Config>>) -> io::Result<Self> {
let current_dir = env::current_dir()?;
let mut output = FilesState {
selected: None,
files: vec![],
top: 0,
current_dir,
config,
};
output.update_contents()?;
Ok(output)
}
}
impl StatefulWidget for FilesWidget {
type State = FilesState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
let visible_rows = layout[0].height as usize;
if let Some(selected) = state.selected {
if selected < state.top {
state.top = selected;
} else if selected >= state.top + visible_rows {
state.top = selected - visible_rows + 1;
}
}
let mut items: Vec<Line> = state
.files
.iter()
.skip(state.top)
.take(visible_rows)
.map(|file| {
Line::default().spans(vec![
" ",
file.file_name()
.expect("Files should have a valid name")
.to_str()
.expect("Files should have a valid name"),
])
})
.collect();
let relative_selected = state.selected.map(|s| s - state.top);
if let Some(index) = relative_selected
&& let Some(item) = items.get_mut(index)
{
let mut old = mem::take(item);
old = old.patch_style(Style::default().fg(Color::Blue));
old.spans[0] = "> ".into();
*item = old;
}
let rows_layout =
Layout::vertical(vec![Constraint::Length(1); visible_rows]).split(layout[0]);
for (item, area) in items.into_iter().zip(rows_layout.iter()) {
item.render(*area, buf);
}
}
}

View file

@ -1,3 +1,111 @@
fn main() {
println!("Hello, world!");
mod config;
mod directory_operations;
mod file_select;
mod preview;
use config::Config;
use file_select::{FilesState, FilesWidget, ScrollingDirection};
use preview::{PreviewState, PreviewWidget};
use ratatui::{
DefaultTerminal, Frame,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Direction, Layout},
};
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
let app_result = App::new()?.run(&mut terminal);
ratatui::restore();
app_result
}
struct App {
files: FilesState,
preview: PreviewState,
exit: bool,
}
impl App {
fn new() -> io::Result<Self> {
let config = Rc::new(RefCell::new(Config::new()));
let files = FilesState::new(Rc::clone(&config))?;
let mut preview = PreviewState::new(Rc::clone(&config));
preview.change_preview(files.get_current());
Ok(Self {
files,
preview,
exit: false,
})
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
let _ = self.handle_events();
}
Ok(())
}
fn handle_events(&mut self) -> io::Result<()> {
let Event::Key(current_event) = event::read()? else {
return Ok(());
};
if current_event.kind != KeyEventKind::Press {
return Ok(());
}
match current_event.code {
KeyCode::Esc => {
self.exit = true;
}
key @ (KeyCode::Down | KeyCode::Up) => {
self.files.scroll(match key {
KeyCode::Down => ScrollingDirection::Down,
KeyCode::Up => ScrollingDirection::Up,
_ => unreachable!(),
});
self.preview.change_preview(self.files.get_current());
}
KeyCode::Left => {
self.files.go_up()?;
self.preview.change_preview(self.files.get_current());
}
KeyCode::Right => {
self.files.go_inner()?;
self.preview.change_preview(self.files.get_current());
}
KeyCode::Char('h') => {
self.files.toggle_hidden()?;
self.preview.change_preview(self.files.get_current());
}
_ => (),
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame) {
let main_area = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(frame.area());
if frame.area().width < 10 || frame.area().height < 30 {
self.exit = true;
return;
}
frame.render_stateful_widget(FilesWidget, main_area[0], &mut self.files);
frame.render_stateful_widget(PreviewWidget, main_area[1], &mut self.preview);
}
}

160
src/preview.rs Normal file
View file

@ -0,0 +1,160 @@
use ratatui::{
layout::{Constraint, Layout},
text::Line,
widgets::{Paragraph, StatefulWidget, Widget},
};
use std::path::{Path, PathBuf};
use std::{cell::RefCell, fs, rc::Rc};
use crate::{config::Config, directory_operations};
pub struct PreviewWidget;
enum PreviewType {
Nothing,
AccessDenied,
Image(String),
Pdf(String),
Binary(String),
Directory(Vec<String>),
Text { filename: String, content: String },
}
pub struct PreviewState {
preview_type: PreviewType,
config: Rc<RefCell<Config>>,
}
impl PreviewState {
pub fn new(config: Rc<RefCell<Config>>) -> Self {
Self {
preview_type: PreviewType::Nothing,
config,
}
}
pub fn change_preview(&mut self, path: Option<&Path>) {
let Some(path) = path else {
self.preview_type = PreviewType::Nothing;
return;
};
if !path.exists() {
self.preview_type = PreviewType::Nothing;
return;
}
if !directory_operations::can_read(path) {
self.preview_type = PreviewType::AccessDenied;
return;
}
if path.is_dir() {
match fs::read_dir(path) {
Ok(entries) => {
let mut items: Vec<PathBuf> = entries
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.collect();
directory_operations::sort_dir(&self.config.borrow(), &mut items);
self.preview_type = PreviewType::Directory(
items
.iter()
.filter_map(|item| {
item.file_name().map(|f| {
f.to_str()
.expect("Files should have a valid name")
.to_owned()
})
})
.collect(),
);
}
Err(_) => {
self.preview_type = PreviewType::Nothing;
}
}
return;
}
let extension = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_lowercase());
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
match extension.as_deref() {
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("bmp") | Some("webp")
| Some("svg") | Some("ico") => {
self.preview_type = PreviewType::Image(filename);
}
Some("pdf") => {
self.preview_type = PreviewType::Pdf(filename);
}
_ => {
if let Ok(content) = fs::read_to_string(path) {
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
self.preview_type = PreviewType::Text { filename, content };
} else {
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown")
.to_string();
self.preview_type = PreviewType::Binary(filename);
}
}
}
}
}
impl StatefulWidget for PreviewWidget {
type State = PreviewState;
fn render(
self,
area: ratatui::prelude::Rect,
buf: &mut ratatui::prelude::Buffer,
state: &mut Self::State,
) {
match &state.preview_type {
PreviewType::Nothing => Line::from("Nothing selected").render(area, buf),
PreviewType::Text { filename, content } => {
let areas =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(area);
Paragraph::new(content.as_str()).render(areas[1], buf);
Line::from(format!("text file: {}", filename).as_str()).render(areas[0], buf);
}
PreviewType::Image(name) | PreviewType::Pdf(name) => {
Paragraph::new(format!("{}: I need to implement this", name).as_str())
.render(area, buf);
}
PreviewType::Directory(files) => {
let visible_rows = area.height as usize;
let constraints =
Layout::vertical(vec![Constraint::Length(1); visible_rows]).split(area);
for (file, constraint) in files.iter().zip(constraints.iter()) {
Line::from(file.as_str()).render(*constraint, buf);
}
}
PreviewType::Binary(name) => {
Paragraph::new(format!("unsupported file type: {}", name)).render(area, buf)
}
PreviewType::AccessDenied => {
Paragraph::new("Access DENIED").render(area, buf);
}
}
}
}