feat(move): make completions case agnostic

This commit is contained in:
2025-07-09 18:54:54 +03:00
parent f2650c7090
commit 805b65067b
5 changed files with 121 additions and 46 deletions

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