Files
Cagire/src/state/file_browser.rs
2026-01-24 01:59:51 +01:00

267 lines
7.3 KiB
Rust

use std::cmp::Ordering;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Clone, PartialEq, Eq)]
pub enum FileBrowserMode {
Save,
Load,
}
#[derive(Clone, PartialEq, Eq)]
pub struct DirEntry {
pub name: String,
pub is_dir: bool,
}
#[derive(Clone, PartialEq, Eq)]
pub struct FileBrowserState {
pub mode: FileBrowserMode,
pub input: String,
pub entries: Vec<DirEntry>,
pub selected: usize,
pub scroll_offset: usize,
}
impl FileBrowserState {
pub fn new_save(initial_path: String) -> Self {
let mut state = Self {
mode: FileBrowserMode::Save,
input: initial_path,
entries: Vec::new(),
selected: 0,
scroll_offset: 0,
};
state.refresh_entries();
state
}
pub fn new_load(initial_path: String) -> Self {
let mut state = Self {
mode: FileBrowserMode::Load,
input: initial_path,
entries: Vec::new(),
selected: 0,
scroll_offset: 0,
};
state.refresh_entries();
state
}
pub fn current_dir(&self) -> PathBuf {
if self.input.is_empty() {
return std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
}
let path = Path::new(&self.input);
if self.input.ends_with('/') {
path.to_path_buf()
} else {
match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
}
}
}
fn partial_name(&self) -> &str {
if self.input.is_empty() || self.input.ends_with('/') {
""
} else if !self.input.contains('/') {
&self.input
} else {
Path::new(&self.input)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
}
}
pub fn refresh_entries(&mut self) {
let dir = self.current_dir();
let prefix = self.partial_name().to_lowercase();
let mut entries = Vec::new();
if prefix.is_empty() && dir.parent().is_some() {
entries.push(DirEntry {
name: "..".to_string(),
is_dir: true,
});
}
if let Ok(read_dir) = fs::read_dir(&dir) {
for entry in read_dir.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with('.') {
continue;
}
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
if prefix.is_empty() || name.to_lowercase().starts_with(&prefix) {
entries.push(DirEntry { name, is_dir });
}
}
}
entries.sort_by(|a, b| match (a.name.as_str(), b.name.as_str()) {
("..", _) => Ordering::Less,
(_, "..") => Ordering::Greater,
_ => match (a.is_dir, b.is_dir) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
},
});
self.entries = entries;
self.selected = 0;
self.scroll_offset = 0;
}
pub fn autocomplete(&mut self) {
let real_entries: Vec<&DirEntry> =
self.entries.iter().filter(|e| e.name != "..").collect();
if real_entries.is_empty() {
return;
}
let lcp = longest_common_prefix(&real_entries);
if lcp.is_empty() {
return;
}
let dir = self.current_dir();
let mut new_input = dir.join(&lcp).display().to_string();
if real_entries.len() == 1 && real_entries[0].is_dir && !new_input.ends_with('/') {
new_input.push('/');
}
if new_input != self.input {
self.input = new_input;
self.refresh_entries();
}
}
pub fn select_next(&mut self, visible_height: usize) {
if self.selected + 1 < self.entries.len() {
self.selected += 1;
if self.selected >= self.scroll_offset + visible_height {
self.scroll_offset = self.selected + 1 - visible_height;
}
}
}
pub fn select_prev(&mut self, _visible_height: usize) {
if self.selected > 0 {
self.selected -= 1;
if self.selected < self.scroll_offset {
self.scroll_offset = self.selected;
}
}
}
pub fn confirm(&mut self) -> Option<PathBuf> {
if !self.entries.is_empty() {
let entry = &self.entries[self.selected];
if entry.is_dir {
self.navigate_to_selected();
return None;
}
let path = self.current_dir().join(&entry.name);
if self.mode == FileBrowserMode::Save {
ensure_parent_dirs(&path);
}
return Some(path);
}
if self.mode == FileBrowserMode::Save && !self.input.is_empty() {
let path = PathBuf::from(&self.input);
ensure_parent_dirs(&path);
return Some(path);
}
None
}
pub fn enter_selected(&mut self) {
if let Some(entry) = self.entries.get(self.selected) {
if entry.is_dir {
self.navigate_to_selected();
}
}
}
pub fn go_up(&mut self) {
let dir = self.current_dir();
if let Some(parent) = dir.parent() {
self.input = format_dir_path(parent);
self.refresh_entries();
}
}
pub fn backspace(&mut self) {
if self.input.is_empty() {
return;
}
if self.input.ends_with('/') && self.input.len() > 1 {
let trimmed = &self.input[..self.input.len() - 1];
match trimmed.rfind('/') {
Some(pos) => self.input = trimmed[..=pos].to_string(),
None => self.input.clear(),
}
} else {
self.input.pop();
}
self.refresh_entries();
}
fn navigate_to_selected(&mut self) {
let entry = &self.entries[self.selected];
if entry.name == ".." {
self.go_up();
} else {
let dir = self.current_dir();
self.input = format_dir_path(&dir.join(&entry.name));
self.refresh_entries();
}
}
}
fn format_dir_path(path: &Path) -> String {
let mut s = path.display().to_string();
if !s.ends_with('/') {
s.push('/');
}
s
}
fn ensure_parent_dirs(path: &Path) {
if let Some(parent) = path.parent() {
if !parent.exists() {
let _ = fs::create_dir_all(parent);
}
}
}
fn longest_common_prefix(entries: &[&DirEntry]) -> String {
if entries.is_empty() {
return String::new();
}
let first = &entries[0].name;
let mut len = first.len();
for entry in &entries[1..] {
len = first
.chars()
.zip(entry.name.chars())
.take_while(|(a, b)| a.to_lowercase().eq(b.to_lowercase()))
.count()
.min(len);
}
first[..first
.char_indices()
.nth(len)
.map(|(i, _)| i)
.unwrap_or(first.len())]
.to_string()
}