implement command root

This commit is contained in:
dusk 2025-10-07 21:59:26 +00:00
parent c1ed7487d7
commit 15ffd16c01
No known key found for this signature in database
11 changed files with 107 additions and 170 deletions

View file

@ -1 +0,0 @@

View file

@ -3,6 +3,7 @@ use super::*;
pub fn cmds() -> impl Iterator<Item = Command> {
let help = ("help", ["h"]);
[
command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list"),
command!(("dashboard", ["dash"]) => "dashboard"),
command!("explain" => "explain"),
command!(help => "help").help("Shows the help command"),

View file

@ -1,7 +1,6 @@
pub mod admin;
pub mod api;
pub mod autoproxy;
pub mod commands;
pub mod config;
pub mod debug;
pub mod fun;

View file

@ -90,13 +90,12 @@ pub fn parse_command(
None => {
let mut error = format!("Unknown command `{prefix}{input}`.");
let wrote_possible_commands = fmt_possible_commands(
&mut error,
&prefix,
&input,
local_tree.possible_commands(usize::MAX),
);
if wrote_possible_commands.not() {
let possible_commands =
rank_possible_commands(&input, local_tree.possible_commands(usize::MAX));
if possible_commands.is_empty().not() {
error.push_str(" Perhaps you meant one of the following commands:\n");
fmt_commands_list(&mut error, &prefix, possible_commands);
} else {
// add a space between the unknown command and "for a list of all possible commands"
// message if we didn't add any possible suggestions
error.push_str(" ");
@ -278,78 +277,70 @@ fn next_token<'a>(
// todo: should probably move this somewhere else
/// returns true if wrote possible commands, false if not
fn fmt_possible_commands(
f: &mut String,
prefix: &str,
fn rank_possible_commands(
input: &str,
mut possible_commands: impl Iterator<Item = &Command>,
) -> bool {
if let Some(first) = possible_commands.next() {
let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = std::iter::once(first)
.chain(possible_commands)
.filter(|cmd| cmd.show_in_suggestions)
.flat_map(|cmd| {
let versions = generate_command_versions(cmd);
versions.into_iter().map(move |(version, is_alias)| {
let similarity = strsim::jaro_winkler(&input, &version);
(cmd, version, similarity, is_alias)
})
possible_commands: impl IntoIterator<Item = &Command>,
) -> Vec<(Command, String, bool)> {
let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands
.into_iter()
.filter(|cmd| cmd.show_in_suggestions)
.flat_map(|cmd| {
let versions = generate_command_versions(cmd);
versions.into_iter().map(move |(version, is_alias)| {
let similarity = strsim::jaro_winkler(&input, &version);
(cmd, version, similarity, is_alias)
})
.collect();
})
.collect();
commands_with_scores
.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
commands_with_scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
// remove duplicate commands
let mut seen_commands = std::collections::HashSet::new();
let mut best_commands = Vec::new();
for (cmd, version, score, is_alias) in commands_with_scores {
if seen_commands.insert(cmd) {
best_commands.push((cmd, version, score, is_alias));
}
// remove duplicate commands
let mut seen_commands = std::collections::HashSet::new();
let mut best_commands = Vec::new();
for (cmd, version, score, is_alias) in commands_with_scores {
if seen_commands.insert(cmd) {
best_commands.push((cmd, version, score, is_alias));
}
const MIN_SCORE_THRESHOLD: f64 = 0.8;
if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD {
return false;
}
// if score falls off too much, don't show
let mut falloff_threshold: f64 = 0.2;
let best_score = best_commands[0].2;
let mut commands_to_show = Vec::new();
for (command, version, score, is_alias) in best_commands.iter().take(MAX_SUGGESTIONS) {
let delta = best_score - score;
falloff_threshold -= delta;
if delta > falloff_threshold {
break;
}
commands_to_show.push((command, version, score, is_alias));
}
if commands_to_show.is_empty() {
return false;
}
f.push_str(" Perhaps you meant one of the following commands:\n");
for (command, version, _score, is_alias) in commands_to_show {
writeln!(
f,
"- **{prefix}{version}**{alias} - *{help}*",
help = command.help,
alias = is_alias
.then(|| format!(
" (alias of **{prefix}{base_version}**)",
base_version = build_command_string(command, None)
))
.unwrap_or_else(String::new),
)
.expect("oom");
}
return true;
}
return false;
const MIN_SCORE_THRESHOLD: f64 = 0.8;
if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD {
return Vec::new();
}
// if score falls off too much, don't show
let mut falloff_threshold: f64 = 0.2;
let best_score = best_commands[0].2;
let mut commands_to_show = Vec::new();
for (command, version, score, is_alias) in best_commands.into_iter().take(MAX_SUGGESTIONS) {
let delta = best_score - score;
falloff_threshold -= delta;
if delta > falloff_threshold {
break;
}
commands_to_show.push((command.clone(), version, is_alias));
}
commands_to_show
}
fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Command, String, bool)>) {
for (command, version, is_alias) in commands_to_show {
writeln!(
f,
"- **{prefix}{version}**{alias} - *{help}*",
help = command.help,
alias = is_alias
.then(|| format!(
" (alias of **{prefix}{base_version}**)",
base_version = build_command_string(&command, None)
))
.unwrap_or_else(String::new),
)
.expect("oom");
}
}
fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> {

View file

@ -36,10 +36,10 @@ pub enum TokenMatchResult {
// a: because we want to differentiate between no match and match failure (it matched with an error)
// "no match" has a different charecteristic because we want to continue matching other tokens...
// ...while "match failure" means we should stop matching and return the error
type TryMatchResult = Option<TokenMatchResult>;
pub type TryMatchResult = Option<TokenMatchResult>;
impl Token {
pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult {
pub fn try_match(&self, input: Option<&str>) -> TryMatchResult {
let input = match input {
Some(input) => input,
None => {

View file

@ -1,5 +1,6 @@
namespace commands {
CommandResult parse_command(string prefix, string input);
string get_related_commands(string prefix, string input);
};
[Enum]
interface CommandResult {

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::{collections::HashMap, fmt::Write, usize};
use command_parser::{parameter::ParameterValue, Tree};
use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree};
uniffi::include_scaffolding!("commands");
@ -143,3 +143,22 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult {
},
)
}
pub fn get_related_commands(prefix: String, input: String) -> String {
let mut s = String::new();
for command in command_definitions::all() {
if command.tokens.first().map_or(false, |token| {
token
.try_match(Some(&input))
.map_or(false, |r| matches!(r, TokenMatchResult::MatchedValue))
}) {
writeln!(
&mut s,
"- **{prefix}{command}** - *{help}*",
help = command.help
)
.unwrap();
}
}
s
}

View file

@ -4,6 +4,16 @@ use command_parser::Tree;
use commands::COMMAND_TREE;
fn main() {
parse();
}
fn related() {
let cmd = std::env::args().nth(1).unwrap();
let related = commands::get_related_commands("pk;".to_string(), cmd);
println!("Related commands:\n{related}");
}
fn parse() {
let cmd = std::env::args()
.skip(1)
.intersperse(" ".to_string())