feat(move): make completions case agnostic

This commit is contained in:
Kristofers Solo 2025-07-09 18:54:54 +03:00
parent f2650c7090
commit 805b65067b
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
5 changed files with 121 additions and 46 deletions

59
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<String>,
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<String>) {
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<Vec<String>> {
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)
}

View File

@ -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<Action> {
async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
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<Action> {
pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
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<Action> {
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<KeyCode>) {
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));

View File

@ -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?;
}
}