Skip to content

Commit c64fb08

Browse files
committed
feat: add fuzzy search command (v1.1.0)
- new `search <query>` command with alias `s` - fuzzy matches on snippet names, substring matches on content lines - results ranked by relevance score - optional --tags flag to scope search to a tag - matching lines shown inline under each result
1 parent be7907a commit c64fb08

5 files changed

Lines changed: 140 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Changelog
2+
3+
All notable changes to sinbo will be documented here.
4+
5+
---
6+
7+
## 1.1.0 - 2026-04-01
8+
9+
### Added
10+
- `sinbo search <query>` command — fuzzy search across snippet names and content
11+
- Results are ranked by relevance score, with matching content lines shown inline
12+
- `--tags` / `-t` flag on `search` to scope results to a specific tag
13+
14+
---
15+
16+
## 1.0.0 - Initial release
17+
18+
### Added
19+
- `sinbo add` — add snippets via editor, stdin, or file
20+
- `sinbo get` — print or copy a snippet to clipboard
21+
- `sinbo list` — list all snippets, with tag filtering and `--show` for inline content
22+
- `sinbo edit` — edit an existing snippet in `$EDITOR`, with optional tag update
23+
- `sinbo remove` — delete a snippet
24+
- `--ext` flag for syntax-highlighted temp files in editor
25+
- Tag support across add, edit, and list
26+
- Plain-file storage under system config directory
27+
- Cross-platform: Linux, macOS, Windows

Cargo.lock

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sinbo"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2024"
55
description = "A CLI snippet manager. Store code once, retrieve it anywhere."
66
license = "MIT"
@@ -16,5 +16,6 @@ atty = "0.2.14"
1616
clap = { version = "4.6.0", features = ["derive"] }
1717
colored = "3.1.1"
1818
dirs = "6.0.0"
19+
fuzzy-matcher = "0.3.7"
1920
serde = { version = "1.0.228", features = ["derive"] }
2021
serde_json = "1.0.149"

readme.md

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
**sinbo is a CLI snippet manager. Store code once, retrieve it anywhere.**
1414

15-
</div>
16-
1715
---
1816

1917
## Installation
@@ -44,6 +42,7 @@ cargo install sinbo
4442
sinbo add rust-test -e rs # opens your editor, saves as a Rust snippet
4543
sinbo get rust-test # prints the snippet
4644
sinbo get rust-test --copy # copies to clipboard
45+
sinbo search "hello" # fuzzy search across all snippets
4746
```
4847

4948
---
@@ -63,7 +62,9 @@ export EDITOR="pycharm --wait" # PyCharm
6362
export EDITOR="subl --wait" # Sublime Text
6463
```
6564

66-
The temp file uses the `--ext` flag for syntax highlighting `sinbo add my-snippet -e rs` opens the editor with a `.rs` file so your editor formats the language syntax correctly.
65+
The temp file uses the `--ext` flag for syntax highlighting. `sinbo add my-snippet -e rs` opens the editor with a `.rs` file so your editor formats the language syntax correctly.
66+
67+
---
6768

6869
## Commands
6970

@@ -95,9 +96,9 @@ sinbo get rust-test # print to stdout
9596
sinbo get rust-test --copy # copy to clipboard
9697
```
9798

98-
| Flag | Short | Description |
99-
| -------- | ----- | ----------------- |
100-
| `--copy` | `-c` | Copy to clipboard |
99+
| Flag | Short | Description |
100+
| -------- | ----- | ----------------------------- |
101+
| `--copy` | `-c` | Copy to clipboard |
101102

102103
---
103104

@@ -108,12 +109,30 @@ List all saved snippets.
108109
```bash
109110
sinbo list # list all
110111
sinbo list -t docker # filter by tag
112+
sinbo list -s # show snippet content
113+
```
114+
115+
| Flag | Short | Description |
116+
| -------- | ----- | ---------------------- |
117+
| `--tags` | `-t` | Filter by tags |
118+
| `--show` | `-s` | Show snippet content |
119+
120+
---
121+
122+
### `sinbo search <query>`
123+
124+
Fuzzy search across snippet names and content. Results are ranked by relevance. Optionally scope the search to a tag.
125+
126+
```bash
127+
sinbo search "docker run" # search all snippets
128+
sinbo search "deploy" -t infra # search within a tag
111129
```
112130

113-
| Flag | Short | Description |
114-
| -------- | ----- | ------------------------- |
115-
| `--tags` | `-t` | Filter by tags |
116-
| `--show` | `-s` | SHow the snippets content |
131+
| Flag | Short | Description |
132+
| -------- | ----- | ---------------------------------- |
133+
| `--tags` | `-t` | Scope search to snippets with tag |
134+
135+
Matching content lines are shown inline under each result.
117136

118137
---
119138

@@ -145,7 +164,8 @@ Tags let you group and filter snippets:
145164
```bash
146165
sinbo add nginx-conf -t infra server
147166
sinbo add k8s-deploy -t infra k8s
148-
sinbo list -t infra # shows both
167+
sinbo list -t infra # shows both
168+
sinbo search "deploy" -t infra # search within infra tag
149169
```
150170

151171
---
@@ -162,4 +182,4 @@ sinbo list -t infra # shows both
162182

163183
## License
164184

165-
MIT - [LICENSE](LICENSE)
185+
MIT - [LICENSE](LICENSE)

src/main.rs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::{
77
use anyhow::{Context, Result, anyhow};
88
use clap::{Parser, Subcommand};
99
use colored::Colorize;
10+
use fuzzy_matcher::FuzzyMatcher;
11+
use fuzzy_matcher::skim::SkimMatcherV2;
1012

1113
mod storage;
1214

@@ -58,6 +60,12 @@ enum Action {
5860
#[arg(short, long, num_args = 1.., help = "Update tags")]
5961
tags: Option<Vec<String>>,
6062
},
63+
#[command(about = "Search a query in snippets", alias = "s")]
64+
Search {
65+
#[arg(short, long, num_args = 1.., help = "search in tags")]
66+
tags: Option<Vec<String>>,
67+
query: String,
68+
},
6169
}
6270

6371
fn open_editor(initial_content: Option<&str>, ext: Option<&str>) -> Result<String> {
@@ -118,7 +126,6 @@ fn main() -> Result<()> {
118126
print!("{}", snippet.content);
119127
}
120128
}
121-
122129
Action::Add {
123130
name,
124131
file_name,
@@ -154,7 +161,6 @@ fn main() -> Result<()> {
154161
storage.save(&name, &content, meta)?;
155162
eprintln!("{} saved '{}'", "sinbo".cyan().bold(), name.yellow());
156163
}
157-
158164
Action::List { tags, show } => {
159165
let snippets = storage.list(tags.as_ref())?;
160166

@@ -185,12 +191,10 @@ fn main() -> Result<()> {
185191
}
186192
}
187193
}
188-
189194
Action::Remove { name } => {
190195
storage.remove(&name)?;
191196
eprintln!("{} removed '{}'", "sinbo".cyan().bold(), name.yellow());
192197
}
193-
194198
Action::Edit { name, tags } => {
195199
let snippet = storage
196200
.get(&name)
@@ -219,6 +223,57 @@ fn main() -> Result<()> {
219223
storage.save(&name, &content, meta)?;
220224
eprintln!("{} updated '{}'", "sinbo".cyan().bold(), name.yellow());
221225
}
226+
Action::Search { query, tags } => {
227+
let all_snippets = storage.list(tags.as_ref())?;
228+
let matcher = SkimMatcherV2::default().ignore_case();
229+
let query_lower = query.to_lowercase();
230+
231+
let mut results: Vec<(i64, &_)> = all_snippets
232+
.iter()
233+
.filter_map(|s| {
234+
let name_score = matcher.fuzzy_match(&s.name, &query);
235+
let content_match = s
236+
.content
237+
.lines()
238+
.any(|l| l.to_lowercase().contains(&query_lower));
239+
match (name_score, content_match) {
240+
(Some(score), _) => Some((score, s)),
241+
(None, true) => Some((0, s)),
242+
_ => None,
243+
}
244+
})
245+
.collect();
246+
247+
results.sort_by(|a, b| b.0.cmp(&a.0));
248+
249+
if results.is_empty() {
250+
eprintln!("{} no matches found", "sinbo".cyan().bold());
251+
} else {
252+
eprintln!("{} {} results\n", "sinbo".cyan().bold(), results.len());
253+
for s in results {
254+
let tags_str = if s.1.meta.tags.is_empty() {
255+
String::new()
256+
} else {
257+
format!(" [{}]", s.1.meta.tags.join(", ").dimmed())
258+
};
259+
let ext_str = s
260+
.1.meta
261+
.ext
262+
.as_deref()
263+
.map(|e| format!(" .{}", e.bright_black()))
264+
.unwrap_or_default();
265+
266+
println!("{}{}{}", s.1.name.cyan().bold(), tags_str, ext_str);
267+
268+
for line in s.1.content.lines() {
269+
if line.to_lowercase().contains(&query_lower) {
270+
println!(" {} {}", ">".yellow().bold(), line.dimmed());
271+
}
272+
}
273+
println!();
274+
}
275+
}
276+
}
222277
}
223278

224279
Ok(())

0 commit comments

Comments
 (0)