Skip to content

Commit ffaf273

Browse files
committed
feat: add search command
1 parent baad088 commit ffaf273

6 files changed

Lines changed: 88 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ directories = "6.0.0"
2323
jsonc-parser = { version = "0.32.1", features = ["cst", "serde", "serde_json"] }
2424
serde = { version = "1.0.228", features = ["derive"] }
2525
similar = "2.7.0"
26+
strsim = "0.11.1"
2627
thiserror = "2.0.18"
2728
toml = "1.0.7"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Please create an issue or PR if you'd like to add support for other targets.
2727
# Usage
2828
- `hue current` prints out the current set theme
2929
- `hue list` prints out available themes
30+
- `hue search <query>` prints the closest matching themes
31+
- `hue search <query> --limit <n>` limits the number of results
3032
- `hue reset` removes `~/.config/hue`, it will be lazily reinitiliased by other commands
3133
- `hue set <theme>` sets a logical theme and synchronises it across targets
3234
- `hue set <theme> --dry-run` shows a diff of the update without applying any changes

src/cli.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub enum Command {
1717
Config,
1818
#[command(about = "List available themes")]
1919
List,
20+
#[command(about = "Search available themes")]
21+
Search(SearchArgs),
2022
#[command(about = "Show current theme")]
2123
Current,
2224
#[command(about = "Resets the config for Hue")]
@@ -31,3 +33,14 @@ pub struct SetArgs {
3133
#[arg(long, help = "Preview changes")]
3234
pub dry_run: bool,
3335
}
36+
37+
#[derive(Args)]
38+
pub struct SearchArgs {
39+
pub query: String,
40+
#[arg(
41+
long,
42+
default_value_t = 10,
43+
help = "Maximum number of matches to print"
44+
)]
45+
pub limit: usize,
46+
}

src/lib.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use clap::Parser;
2+
use strsim::jaro_winkler;
23

34
pub mod catalog;
45
pub mod cli;
@@ -8,7 +9,7 @@ pub mod target;
89
pub mod ui;
910

1011
use catalog::load_catalog;
11-
use cli::{Cli, Command, SetArgs};
12+
use cli::{Cli, Command, SearchArgs, SetArgs};
1213
use config::{Paths, load_config, reset_config, save_config};
1314
use error::{Error, Result};
1415
use target::set_theme;
@@ -21,6 +22,7 @@ pub fn run() -> Result<()> {
2122
match cli.command {
2223
Command::Config => show_config_dir(&paths),
2324
Command::List => list_themes(&paths),
25+
Command::Search(args) => search_themes(&paths, args),
2426
Command::Current => show_current_theme(&paths),
2527
Command::Reset => handle_reset(&paths),
2628
Command::Set(args) => handle_set_theme(&paths, args),
@@ -39,12 +41,72 @@ fn list_themes(paths: &Paths) -> Result<()> {
3941

4042
println!(
4143
"{}",
42-
render_theme_table(config.current_theme.as_deref(), &catalog)
44+
render_theme_table(
45+
config.current_theme.as_deref(),
46+
catalog
47+
.iter()
48+
.map(|(name, mapping)| (name.as_str(), mapping)),
49+
)
4350
);
4451

4552
Ok(())
4653
}
4754

55+
struct ThemeSearchMatch<'a> {
56+
theme_name: String,
57+
mapping: &'a catalog::ThemeMapping,
58+
score: f64,
59+
}
60+
61+
fn search_themes(paths: &Paths, args: SearchArgs) -> Result<()> {
62+
let SearchArgs { query, limit } = args;
63+
64+
let config = load_config(paths)?;
65+
let catalog = load_catalog(paths)?;
66+
let query = query.trim().to_lowercase();
67+
68+
let matches = {
69+
let mut matches = catalog
70+
.iter()
71+
.filter_map(|(theme, mapping)| {
72+
const MIN_SEARCH_SCORE: f64 = 0.70;
73+
let score = jaro_winkler(&query, &theme.trim().to_lowercase());
74+
75+
(score >= MIN_SEARCH_SCORE).then(|| ThemeSearchMatch {
76+
theme_name: theme.clone(),
77+
score,
78+
mapping,
79+
})
80+
})
81+
.collect::<Vec<_>>();
82+
83+
matches.sort_by(|left, right| {
84+
right
85+
.score
86+
.total_cmp(&left.score)
87+
.then_with(|| left.theme_name.cmp(&right.theme_name))
88+
});
89+
90+
matches.truncate(limit.max(1));
91+
matches
92+
};
93+
94+
if matches.is_empty() {
95+
println!("No matches found for `{query}`.");
96+
} else {
97+
let table = render_theme_table(
98+
config.current_theme.as_deref(),
99+
matches
100+
.iter()
101+
.map(|item| (item.theme_name.as_str(), item.mapping)),
102+
);
103+
104+
println!("{table}",);
105+
}
106+
107+
Ok(())
108+
}
109+
48110
fn show_current_theme(paths: &Paths) -> Result<()> {
49111
let config = load_config(paths)?;
50112
let current = config

src/ui.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::collections::BTreeMap;
21
use std::iter::once;
32
use std::path::Path;
43

@@ -9,10 +8,10 @@ use similar::TextDiff;
98
use crate::catalog::ThemeMapping;
109
use crate::target::Target;
1110

12-
pub fn render_theme_table(
13-
current_theme: Option<&str>,
14-
catalog: &BTreeMap<String, ThemeMapping>,
15-
) -> String {
11+
pub fn render_theme_table<'a, I>(current_theme: Option<&str>, themes: I) -> String
12+
where
13+
I: IntoIterator<Item = (&'a str, &'a ThemeMapping)>,
14+
{
1615
let mut table = DisplayTable::new();
1716

1817
table
@@ -24,11 +23,11 @@ pub fn render_theme_table(
2423
.collect::<Vec<_>>(),
2524
);
2625

27-
for (name, mapping) in catalog {
28-
let theme_name = if current_theme == Some(name.as_str()) {
26+
for (name, mapping) in themes {
27+
let theme_name = if current_theme == Some(name) {
2928
format!("{name} (current)")
3029
} else {
31-
name.clone()
30+
name.to_string()
3231
};
3332

3433
table.add_row(

0 commit comments

Comments
 (0)