From 805b65067b82316c25927603c755774b781f288b Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 9 Jul 2025 18:54:54 +0300 Subject: [PATCH] feat(move): make completions case agnostic --- Cargo.lock | 59 ++++++++++++++++++-------------------- Cargo.toml | 4 +-- src/app/mod.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/handler.rs | 25 ++++++++-------- src/main.rs | 2 +- 5 files changed, 121 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2353696..5bde3be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1464,9 +1464,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] @@ -1771,44 +1771,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "toml_write", + "toml_parser", + "toml_writer", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_datetime" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b679217f2848de74cabd3e8fc5e6d66f40b7da40f8e1954d92054d9010690fd5" [[package]] name = "tower" @@ -2336,9 +2334,6 @@ name = "winnow" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" -dependencies = [ - "memchr", -] [[package]] name = "wit-bindgen-rt" diff --git a/Cargo.toml b/Cargo.toml index 57a7d8a..32ab689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,13 @@ edition = "2021" color-eyre = "0.6" crossterm = "0.29" ratatui = { version = "0.29" } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } thiserror = "2.0" tracing-appender = "0.2" serde = { version = "1.0", features = ["derive"] } -toml = "0.8" +toml = "0.9" dirs = "6.0" transmission-rpc = "0.5" url = "2.5" diff --git a/src/app/mod.rs b/src/app/mod.rs index e57d5a7..c27840e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -8,6 +8,8 @@ pub mod utils; use crate::config::Config; use color_eyre::Result; use ratatui::widgets::TableState; +use std::path::{Path, PathBuf}; +use tokio::fs; use types::Selected; pub use {tab::Tab, torrent::Torrents}; @@ -24,6 +26,8 @@ pub struct App<'a> { pub input: String, pub cursor_position: usize, pub input_mode: bool, + pub completions: Vec, + pub completion_idx: usize, } impl<'a> App<'a> { @@ -41,9 +45,22 @@ impl<'a> App<'a> { input: String::new(), cursor_position: 0, input_mode: false, + completions: Vec::new(), + completion_idx: 0, }) } + pub async fn complete_input(&mut self) -> Result<()> { + let path = PathBuf::from(&self.input); + let (base_path, partial_name) = split_path_components(path); + let matches = find_matching_entries(&base_path, &partial_name).await?; + + self.update_completions(matches); + self.update_input_with_matches(); + + Ok(()) + } + /// Handles the tick event of the terminal. pub async fn tick(&mut self) -> Result<()> { self.torrents.update().await?; @@ -196,4 +213,64 @@ impl<'a> App<'a> { .collect(); Selected::List(selected_torrents) } + + fn update_completions(&mut self, matches: Vec) { + if matches.is_empty() { + self.completions.clear(); + self.completion_idx = 0; + return; + } + + if matches != self.completions { + self.completions = matches; + self.completion_idx = 0; + return; + } + + self.completion_idx = (self.completion_idx + 1) % self.completions.len(); + } + + fn update_input_with_matches(&mut self) { + if let Some(completion) = self.completions.get(self.completion_idx) { + self.input = completion.clone(); + self.cursor_position = self.input.len(); + } + } +} + +fn split_path_components(path: PathBuf) -> (PathBuf, String) { + if path.is_dir() { + return (path, String::new()); + } + + let partial = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + let base = path + .parent() + .unwrap_or_else(|| Path::new("/")) + .to_path_buf(); + + (base, partial) +} + +async fn find_matching_entries(base_path: &Path, partial_name: &str) -> Result> { + let mut entries = fs::read_dir(&base_path).await?; + let mut matches = Vec::new(); + + while let Some(entry) = entries.next_entry().await? { + let file_name = entry.file_name().to_string_lossy().to_string(); + + if file_name + .to_lowercase() + .starts_with(&partial_name.to_lowercase()) + { + matches.push(format!("{}/{}", base_path.to_string_lossy(), file_name)); + } + } + + Ok(matches) } diff --git a/src/handler.rs b/src/handler.rs index 9d4357b..a61b817 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,29 +3,33 @@ use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use tracing::{event, info_span, Level}; -fn handle_input(key_event: KeyEvent, app: &mut App) -> Option { +async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result> { match key_event.code { - KeyCode::Enter => Some(Action::Submit), + KeyCode::Enter => Ok(Some(Action::Submit)), + KeyCode::Tab => { + app.complete_input().await?; + Ok(None) + } KeyCode::Char(c) => { app.input.push(c); app.cursor_position = app.input.len(); - None + Ok(None) } KeyCode::Backspace => { app.input.pop(); app.cursor_position = app.input.len(); - None + Ok(None) } - KeyCode::Esc => Some(Action::Cancel), - _ => None, + KeyCode::Esc => Ok(Some(Action::Cancel)), + _ => Ok(None), } } /// Handles the key events of [`App`]. #[tracing::instrument] -pub fn get_action(key_event: KeyEvent, app: &mut App) -> Option { +pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result> { if app.input_mode { - return handle_input(key_event, app); + return handle_input(key_event, app).await; } let span = info_span!("get_action"); @@ -54,10 +58,10 @@ pub fn get_action(key_event: KeyEvent, app: &mut App) -> Option { for (action, keybind) in actions { if matches_keybind(&key_event, keybind) { - return Some(action); + return Ok(Some(action)); } } - None + Ok(None) } /// Handles the updates of [`App`]. @@ -140,7 +144,6 @@ fn parse_keybind(key_str: &str) -> (KeyModifiers, Option) { key_code = Some(KeyCode::F(num)); } } - single_char if single_char.len() == 1 => { if let Some(c) = single_char.chars().next() { key_code = Some(KeyCode::Char(c)); diff --git a/src/main.rs b/src/main.rs index 13c0fea..d4a59f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ async fn main() -> Result<()> { match tui.events.next()? { Event::Tick => app.tick().await?, Event::Key(key_event) => { - if let Some(action) = get_action(key_event, &mut app) { + if let Some(action) = get_action(key_event, &mut app).await? { update(&mut app, action).await?; } }