use ratatui::text::Span;
use crossterm::style::Color;
use unicode_width::UnicodeWidthStr;
use crate::
{
colors,
options,
role::Role,
command::
{
self,
ArgValues,
CommandArg,
CommandInfo,
SubcommandInfo,
},
};
use super::theme;
pub const MAX_ROWS: usize = 8;
pub enum PaletteMode
{
Hidden, Menu(Vec<Entry>, usize), Values(Values), Signature(Entry, Option<usize>), }
#[derive(Clone, Copy)]
pub struct Entry
{
pub info: &'static CommandInfo,
pub sub: Option<&'static SubcommandInfo>,
}
pub struct Values
{
pub arg: &'static CommandArg,
pub matches: Vec<String>,
pub selected: usize,
pub start: usize, }
pub struct Palette {
pub mode: PaletteMode,
pub offset: usize,
}
impl Entry
{
pub fn command(info: &'static CommandInfo) -> Self { Self { info, sub: None } }
pub fn action(info: &'static CommandInfo, sub: &'static SubcommandInfo) -> Self { Self { info, sub: Some(sub) } }
pub fn args(&self) -> &'static [CommandArg]
{
self.sub.map_or(self.info.args, |sub| sub.args)
}
pub fn description(&self) -> &'static str
{
self.sub.map_or(self.info.description, |sub| sub.description)
}
pub fn shortcut(&self) -> String
{
match self.sub
{
Some(_) => String::new(),
None => self.info.shortcut.map(|s| format!("Ctrl+{}", s.to_ascii_uppercase())).unwrap_or_default(),
}
}
pub fn name(&self) -> String
{
let mut name = format!("{}{}", command::COMMAND_PREFIX, self.info.triggers[0].to_lowercase());
if let Some(sub) = self.sub { name.push_str(&format!(" {}", sub.triggers[0].to_lowercase())); }
name
}
pub fn signature(&self) -> String
{
let args = self.args().iter().map(format_arg).collect::<Vec<String>>().join(" ");
let separator = if args.is_empty() { "" } else { " " };
format!("{}{separator}{args}", self.name())
}
pub fn width(&self) -> usize { self.signature().width() }
pub fn spans(&self, active: Option<usize>) -> Vec<Span<'static>>
{
let mut spans = vec![Span::styled(self.name(), theme::TITLE)];
for (i, arg) in self.args().iter().enumerate()
{
let style = if active == Some(i)
{
theme::ARG_ACTIVE
} else if arg.required
{
theme::ARG_REQUIRED
} else
{
theme::ARG_OPTIONAL
};
spans.push(Span::raw(" "));
spans.push(Span::styled(format_arg(arg), style));
}
spans
}
pub fn typed(&self, input: &str) -> bool
{
let Some(rest) = input.trim().strip_prefix(command::COMMAND_PREFIX) else { return false };
match self.sub
{
None => self.info.triggers.iter().any(|t| t.eq_ignore_ascii_case(rest)),
Some(sub) => match rest.split_once(char::is_whitespace)
{
Some((word, action)) => self.info.triggers.iter().any(|t| t.eq_ignore_ascii_case(word)) &&
sub.triggers.iter().any(|t| t.eq_ignore_ascii_case(action.trim())),
None => false,
},
}
}
}
impl Values
{
pub fn selection(&self) -> Option<&str> { self.matches.get(self.selected).map(String::as_str) }
pub fn typed(&self, input: &str) -> bool
{
let typed = input.chars().skip(self.start).collect::<String>();
self.selection().is_some_and(|value| value.eq_ignore_ascii_case(typed.trim()))
}
pub fn swatch(&self, value: &str) -> Option<Color>
{
match self.arg.values
{
ArgValues::Colors => colors::by_name(value),
ArgValues::Free | ArgValues::Monitors | ArgValues::Roles => None,
}
}
}
impl Default for Palette
{
fn default() -> Self { Self::new() }
}
impl Palette
{
pub fn new() -> Self
{
Self { mode: PaletteMode::Hidden, offset: 0 }
}
pub fn is_active(&self) -> bool { matches!(self.mode, PaletteMode::Menu(..) | PaletteMode::Values(..)) }
pub fn values(&self) -> Option<&Values>
{
match &self.mode
{
PaletteMode::Values(values) => Some(values),
_ => None,
}
}
pub fn is_visible(&self) -> bool { !matches!(self.mode, PaletteMode::Hidden) }
pub fn update(&mut self, input: &str, role: Role)
{
if !options::get_sending_messages()
{
self.dismiss();
return;
}
let Some(rest) = input.strip_prefix(command::COMMAND_PREFIX) else
{
self.dismiss();
return;
};
match rest.find(char::is_whitespace)
{
None =>
{
let candidate = rest.to_lowercase();
let matches = command::COMMAND_LIST.iter()
.filter(|info| info.available(role) && info.triggers.iter().any(|t| t.to_lowercase().starts_with(&candidate)))
.map(Entry::command).collect::<Vec<Entry>>();
self.menu(matches, rest);
},
Some(split) =>
{
let (word, tail) = rest.split_at(split);
let Some(info) = command::COMMAND_LIST.iter()
.find(|info| info.available(role) && info.triggers.iter().any(|t| t.eq_ignore_ascii_case(word))) else
{
self.dismiss();
return;
};
if !info.subcommands.is_empty()
{
self.action(info, tail.trim_start(), role, input);
return;
}
if info.args.is_empty()
{
self.dismiss();
return;
}
self.hint(Entry::command(info), tail, input);
},
}
}
fn action(&mut self, info: &'static CommandInfo, tail: &str, role: Role, input: &str)
{
match tail.find(char::is_whitespace)
{
None =>
{
let candidate = tail.to_lowercase();
let matches = info.actions(role)
.filter(|sub| sub.triggers.iter().any(|t| t.to_lowercase().starts_with(&candidate)))
.map(|sub| Entry::action(info, sub)).collect::<Vec<Entry>>();
self.menu(matches, tail);
},
Some(split) =>
{
let (action, tail) = tail.split_at(split);
let Some(sub) = info.action(action).filter(|sub| sub.available(role)) else
{
self.dismiss();
return;
};
if sub.args.is_empty()
{
self.dismiss();
return;
}
self.hint(Entry::action(info, sub), tail, input);
},
}
}
fn hint(&mut self, entry: Entry, tail: &str, input: &str)
{
let args = entry.args();
let active = active_arg(args, tail);
if let Some(arg) = active.and_then(|i| args.get(i)) && arg.values != ArgValues::Free
{
let typed = partial(tail).to_lowercase();
let matches = vocabulary(arg.values).into_iter()
.filter(|value| value.to_lowercase().starts_with(&typed)).collect::<Vec<String>>();
if !matches.is_empty()
{
let exact = matches.iter().position(|value| value.eq_ignore_ascii_case(&typed));
let selected = match (exact, &self.mode)
{
(Some(exact), _) => exact,
(None, PaletteMode::Values(values)) => values.selected.min(matches.len() - 1),
(None, _) => 0,
};
self.mode = PaletteMode::Values(Values
{
arg,
matches,
selected,
start: input.chars().count() - typed.chars().count(),
});
return;
}
}
self.mode = PaletteMode::Signature(entry, active);
}
fn menu(&mut self, matches: Vec<Entry>, typed: &str)
{
if matches.is_empty()
{
self.dismiss();
return;
}
let exact = matches.iter().position(|entry| match entry.sub
{
Some(sub) => sub.triggers.iter().any(|t| t.eq_ignore_ascii_case(typed)),
None => entry.info.triggers.iter().any(|t| t.eq_ignore_ascii_case(typed)),
});
let selected = match (exact, &self.mode)
{
(Some(exact), _) => exact,
(None, PaletteMode::Menu(_, selected)) => (*selected).min(matches.len() - 1),
(None, _) => 0,
};
self.mode = PaletteMode::Menu(matches, selected);
}
pub fn dismiss(&mut self)
{
self.mode = PaletteMode::Hidden;
self.offset = 0;
}
pub fn next(&mut self)
{
match &mut self.mode
{
PaletteMode::Menu(matches, selected) => *selected = (*selected + 1) % matches.len(),
PaletteMode::Values(values) => values.selected = (values.selected + 1) % values.matches.len(),
_ => {},
}
}
pub fn previous(&mut self)
{
match &mut self.mode
{
PaletteMode::Menu(matches, selected) =>
*selected = if *selected == 0 { matches.len() - 1 } else { *selected - 1 },
PaletteMode::Values(values) =>
values.selected = if values.selected == 0 { values.matches.len() - 1 } else { values.selected - 1 },
_ => {},
}
}
pub fn selection(&self) -> Option<Entry>
{
match &self.mode
{
PaletteMode::Menu(matches, selected) => matches.get(*selected).copied(),
_ => None,
}
}
}
fn active_arg(args: &'static [CommandArg], tail: &str) -> Option<usize>
{
let given = tail.split_whitespace().count();
let index = if tail.ends_with(char::is_whitespace) { given } else { given.saturating_sub(1) };
Some(index.min(args.len() - 1))
}
fn partial(tail: &str) -> &str
{
if tail.ends_with(char::is_whitespace) { "" } else { tail.split_whitespace().next_back().unwrap_or("") }
}
fn vocabulary(values: ArgValues) -> Vec<String>
{
match values
{
ArgValues::Colors => colors::offered().into_iter().map(str::to_string).collect(),
#[cfg(feature = "client_screen")]
ArgValues::Monitors => crate::screen::capture::monitor_names(),
#[cfg(not(feature = "client_screen"))]
ArgValues::Monitors => Vec::new(),
ArgValues::Roles => Role::ALL.iter().map(Role::to_string).collect(),
ArgValues::Free => Vec::new(),
}
}
pub fn format_arg(arg: &command::CommandArg) -> String {
if arg.required
{
format!("<{}>", arg.name.to_lowercase())
} else
{
format!("[{}]", arg.name.to_lowercase())
}
}