use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::state::View;
#[derive(Debug, Clone, Default)]
pub struct CommandInput {
buffer: String,
cursor: usize,
}
impl CommandInput {
pub fn new() -> Self {
Self::default()
}
pub fn clear(&mut self) {
self.buffer.clear();
self.cursor = 0;
}
pub fn buffer(&self) -> &str {
&self.buffer
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn handle_key(&mut self, key: KeyEvent) -> CommandKeyResult {
match (key.code, key.modifiers) {
(KeyCode::Enter, _) => {
let cmd = self.buffer.clone();
self.clear();
CommandKeyResult::Execute(cmd)
}
(KeyCode::Esc, _) => {
self.clear();
CommandKeyResult::Cancel
}
(KeyCode::Backspace, _) => {
if self.cursor > 0 {
self.cursor -= 1;
self.buffer.remove(self.cursor);
CommandKeyResult::Continue
} else if self.buffer.is_empty() {
CommandKeyResult::Cancel
} else {
CommandKeyResult::Continue
}
}
(KeyCode::Delete, _) => {
if self.cursor < self.buffer.len() {
self.buffer.remove(self.cursor);
}
CommandKeyResult::Continue
}
(KeyCode::Left, _) => {
self.cursor = self.cursor.saturating_sub(1);
CommandKeyResult::Continue
}
(KeyCode::Right, _) => {
self.cursor = (self.cursor + 1).min(self.buffer.len());
CommandKeyResult::Continue
}
(KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
self.cursor = 0;
CommandKeyResult::Continue
}
(KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
self.cursor = self.buffer.len();
CommandKeyResult::Continue
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
self.clear();
CommandKeyResult::Continue
}
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
self.buffer.insert(self.cursor, c);
self.cursor += 1;
CommandKeyResult::Continue
}
_ => CommandKeyResult::Continue,
}
}
}
#[derive(Debug, Clone)]
pub enum CommandKeyResult {
Continue,
Cancel,
Execute(String),
}
#[derive(Debug, Clone)]
pub enum CommandAction {
None,
Quit,
Refresh,
NavigateTo(String),
GoToRoot,
NavigateBack,
ShowHelp,
SwitchView(View),
ClearMarks,
ToggleDetails,
SetTheme(ThemeCommand),
SetLayout(LayoutCommand),
SetSort(SortCommand),
ShowBookmarks,
SetBookmark(Option<char>),
JumpToBookmark(Option<char>),
DeleteBookmark(char),
Yank,
Cut,
Paste,
Delete,
Rename(Option<String>),
CreateFile(Option<String>),
CreateDirectory(Option<String>),
Take(Option<String>),
Undo,
BulkRename,
Extract(Option<String>),
Compress(String),
}
#[derive(Debug, Clone, Copy)]
pub enum ThemeCommand {
Dark,
Light,
Toggle,
}
#[derive(Debug, Clone, Copy)]
pub enum LayoutCommand {
Tree,
Miller,
Toggle,
}
#[derive(Debug, Clone, Copy)]
pub enum SortCommand {
SizeDesc,
SizeAsc,
NameAsc,
NameDesc,
DateDesc,
DateAsc,
CountDesc,
CountAsc,
Cycle,
Reverse,
}
pub fn parse_command(cmd: &str) -> CommandAction {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() {
return CommandAction::None;
}
match parts[0] {
"q" | "quit" | "exit" => CommandAction::Quit,
"r" | "refresh" | "rescan" => CommandAction::Refresh,
"cd" => {
if parts.len() > 1 {
CommandAction::NavigateTo(parts[1..].join(" "))
} else {
CommandAction::GoToRoot
}
}
"root" | "top" => CommandAction::GoToRoot,
"back" | "up" | ".." => CommandAction::NavigateBack,
"help" | "?" => CommandAction::ShowHelp,
"explorer" | "e" | "tree" => CommandAction::SwitchView(View::Explorer),
"duplicates" | "dups" | "d" => CommandAction::SwitchView(View::Duplicates),
"age" | "a" => CommandAction::SwitchView(View::Age),
"errors" | "err" => CommandAction::SwitchView(View::Errors),
"clear" | "unmark" => CommandAction::ClearMarks,
"details" | "info" | "i" => CommandAction::ToggleDetails,
"theme" | "t" => {
if parts.len() > 1 {
match parts[1] {
"dark" => CommandAction::SetTheme(ThemeCommand::Dark),
"light" => CommandAction::SetTheme(ThemeCommand::Light),
"toggle" => CommandAction::SetTheme(ThemeCommand::Toggle),
_ => CommandAction::None,
}
} else {
CommandAction::SetTheme(ThemeCommand::Toggle)
}
}
"dark" => CommandAction::SetTheme(ThemeCommand::Dark),
"light" => CommandAction::SetTheme(ThemeCommand::Light),
"layout" | "view" => {
if parts.len() > 1 {
match parts[1] {
"tree" => CommandAction::SetLayout(LayoutCommand::Tree),
"miller" | "columns" => CommandAction::SetLayout(LayoutCommand::Miller),
"toggle" => CommandAction::SetLayout(LayoutCommand::Toggle),
_ => CommandAction::None,
}
} else {
CommandAction::SetLayout(LayoutCommand::Toggle)
}
}
"miller" | "columns" => CommandAction::SetLayout(LayoutCommand::Miller),
"bookmark" | "mark" | "bm" => {
if parts.len() > 1 {
let key = parts[1].chars().next();
CommandAction::SetBookmark(key)
} else {
CommandAction::ShowBookmarks
}
}
"bookmarks" | "marks" | "bms" => CommandAction::ShowBookmarks,
"delmark" | "delbookmark" | "unbookmark" => {
if parts.len() > 1 {
if let Some(key) = parts[1].chars().next() {
CommandAction::DeleteBookmark(key)
} else {
CommandAction::None
}
} else {
CommandAction::None
}
}
"go" | "'" => {
if parts.len() > 1 {
let key = parts[1].chars().next();
CommandAction::JumpToBookmark(key)
} else {
CommandAction::JumpToBookmark(None)
}
}
"sort" | "s" => {
if parts.len() > 1 {
match parts[1].to_lowercase().as_str() {
"size" | "sz" => CommandAction::SetSort(SortCommand::SizeDesc),
"size-" | "sz-" | "size-desc" => CommandAction::SetSort(SortCommand::SizeDesc),
"size+" | "sz+" | "size-asc" => CommandAction::SetSort(SortCommand::SizeAsc),
"name" | "nm" => CommandAction::SetSort(SortCommand::NameAsc),
"name-" | "nm-" | "name-desc" => CommandAction::SetSort(SortCommand::NameDesc),
"name+" | "nm+" | "name-asc" => CommandAction::SetSort(SortCommand::NameAsc),
"date" | "dt" | "modified" | "mod" => {
CommandAction::SetSort(SortCommand::DateDesc)
}
"date-" | "dt-" | "date-desc" => CommandAction::SetSort(SortCommand::DateDesc),
"date+" | "dt+" | "date-asc" => CommandAction::SetSort(SortCommand::DateAsc),
"count" | "ct" | "children" => CommandAction::SetSort(SortCommand::CountDesc),
"count-" | "ct-" | "count-desc" => {
CommandAction::SetSort(SortCommand::CountDesc)
}
"count+" | "ct+" | "count-asc" => CommandAction::SetSort(SortCommand::CountAsc),
"reverse" | "rev" => CommandAction::SetSort(SortCommand::Reverse),
_ => CommandAction::SetSort(SortCommand::Cycle),
}
} else {
CommandAction::SetSort(SortCommand::Cycle)
}
}
"yank" | "y" | "copy" | "cp" => CommandAction::Yank,
"cut" | "x" => CommandAction::Cut,
"paste" | "p" => CommandAction::Paste,
"delete" | "del" | "rm" => CommandAction::Delete,
"rename" | "mv" => {
if parts.len() > 1 {
CommandAction::Rename(Some(parts[1..].join(" ")))
} else {
CommandAction::Rename(None)
}
}
"touch" | "new" | "create" => {
if parts.len() > 1 {
CommandAction::CreateFile(Some(parts[1..].join(" ")))
} else {
CommandAction::CreateFile(None)
}
}
"mkdir" | "md" => {
if parts.len() > 1 {
CommandAction::CreateDirectory(Some(parts[1..].join(" ")))
} else {
CommandAction::CreateDirectory(None)
}
}
"take" => {
if parts.len() > 1 {
CommandAction::Take(Some(parts[1..].join(" ")))
} else {
CommandAction::Take(None)
}
}
"undo" | "u" => CommandAction::Undo,
"rename-bulk" | "bulk-rename" | "bulkrename" | "brn" => CommandAction::BulkRename,
"extract" | "unzip" | "untar" | "decompress" => {
if parts.len() > 1 {
CommandAction::Extract(Some(parts[1..].join(" ")))
} else {
CommandAction::Extract(None)
}
}
"compress" | "zip" | "archive" => {
if parts.len() > 1 {
CommandAction::Compress(parts[1..].join(" "))
} else {
CommandAction::None }
}
_ => CommandAction::None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_quit() {
assert!(matches!(parse_command("q"), CommandAction::Quit));
assert!(matches!(parse_command("quit"), CommandAction::Quit));
assert!(matches!(parse_command("exit"), CommandAction::Quit));
}
#[test]
fn test_parse_cd() {
assert!(matches!(parse_command("cd"), CommandAction::GoToRoot));
match parse_command("cd /some/path") {
CommandAction::NavigateTo(path) => assert_eq!(path, "/some/path"),
_ => panic!("Expected NavigateTo"),
}
}
#[test]
fn test_parse_view() {
assert!(matches!(
parse_command("explorer"),
CommandAction::SwitchView(View::Explorer)
));
assert!(matches!(
parse_command("d"),
CommandAction::SwitchView(View::Duplicates)
));
}
}