Introduction
In this post, I will write about the story I made a CLI tool and distributed it, also serves as note to self.
This post includes following contents:
- A brief explanation of Rust
- Notes on how to distribute your tools via homebrew
The tool I made
TodoScanner
A fast, friendly tool to scan your codebase for TODOs, FIXMEs, and other actionable comments. Built in Rust for speed and reliability.
Features
- Scan directories recursively for annotated comments (e.g.,
TODO,FIXME,NOTE) - Supports common source file extensions (Rust, JS/TS, Python, etc.)
- Configurable markers and ignore patterns
- Clean, readable output with file paths and line numbers
- Exit codes suitable for CI workflows
Installation
brew tap I-Dieod/tap
brew install tds
Usage
# Scan current directory
tds
# Scan a specific path with default markers
tds path/to/project
Example Output
Roadmap
- Markdown/HTML report output
- Interactive TUI
Contributing
Issues and PRs are welcome. Please open an issue with a clear description and minimal repro if you find a bug.
License
MIT License. See LICENSE for details.
Acknowledgments
Inspired by the need to keep TODOs visible and actionable during daily development and CI.
Target readers
- Someone who wants to distribute their own tools
- Someone who wants to know how to read files and extract comments using Rust
Background
These days I was programming a logic to extract comment lines(Like "TODO:", "FIX:" or something else) for Zed extension, similar to TodoTree.
However I realized it would be more difficult than I thought, and a similar extension already existed. Another reason was that zed_extension_api couldn't interact Zed's UI from extension. So I froze that project indefinitely, and I thought, "Why not distribute the byproduct logic via Homebrew?"
There is already a leading project doing this, but I thought "My logic might run faster because it's made with Rust" and "the leading project stopped updating 4 years ago." You know what, I just decided to do it anyway.
How does the tool work?
Actually it's simple logic.
The args value receives arguments for command, at the same time, the tool will get current directory the tool is running by env library, and declares to scan from current directory if args is empty.
let args: Vec<String> = env::args().collect();
let cdu: String = env::current_dir().unwrap().to_string_lossy().to_string();
let root_path = if args.len() > 1 { &args[1] } else { &cdu };
println!("🔍 Scanning for COMMENT items in: {}", root_path);
The tool determine if it's directory or not, by is_dir() method in Path Object, and scan files recursively.
path.extension() checks the file extension.
if path.is_dir() {
scan_directory(&path, todos)?;
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
// Check if it's a supported file type
match ext {
"rs" | "js" | "ts" | "jsx" | "tsx" | "py" | "java" | "go" | "c" | "cpp" | "h"
| "hpp" | "php" | "rb" | "swift" | "kt" | "scala" | "html" | "css" | "vue"
| "md" | "txt" | "yaml" | "yml" | "json" | "toml" | "xml" | "sh" | "bash"
| "zsh" => {
scan_file(&path, todos)?;
}
_ => {}
}
}
Whan the tool finds a file with a matching extension, it reads the file contents using the scan_file function.
Then it extracts lines start with TODO:, FIX:, NOTE:, etc. from comment lines beginning with //, #, or other comment markers, and adds them to a struct.
After scanning all directories, the tool declares a BTreeMap object to sort items by comment type, and pushes items into it one by one.
Finally, it iterates through a predefined order array using a for loop, and prints the content of each item grouped by comment type using println!.
for comment_type in &type_order {
if let Some(files) = by_type.get(*comment_type) {
println!("🏷️ {} Items:", comment_type);
println!(" {}", "=".repeat(comment_type.len() + 7));
for (file, file_todos) in files {
println!(" 📁 {}", file);
for todo in file_todos {
println!(" 📌 Line {}: {}", todo.line + 1, todo.text);
}
println!();
}
}
}
Distribute via Homebrew
To distribute the tool via Homebrew, I created a new repository named "homebrew-tap", and put a Formula file written by Ruby into the repo.
At this time, make sure that the package name defined in
Cargo.toml, the Formula file name, and the class name inside it are all the same.If they don't match, you'll get an error when running
brew tap, which I'll mention later.I left the coding of the
Formula file to Claude Sonnet 4.5.# !This class name!
class Tds < Formula
desc "Fast TODO comment scanner for your codebase"
homepage "https://github.com/I-Dieod/TodoScanner"
url "https://github.com/I-Dieod/TodoScanner/archive/refs/tags/v0.2.0.tar.gz"
sha256 "f2afe11e469036e8af3344e546d85df70fbe6340241c156309b6b904f520befa"
license "MIT"
depends_on "rust" => :build
def install
system "cargo", "install", *std_cargo_args
end
test do
# Create a test file with a TODO comment
(testpath/"test.rs").write <<~EOS
// TODO: Test comment
fn main() {}
EOS
output = shell_output("#{bin}/tds #{testpath}")
assert_match "Found 1 TODO items", output
end
end
For the actual content, you need to prepare the tarball link you can get from your repository releases and SHA256 hash you can get from that tarball link.
You need to write them in url and sha256 fields.
If you're using Nushell or Bash shells, you can get the hash by running the following command:
curl -L [tarball_url] | shasum -a 256
Treatment of hashes, and URLs
The hash is validation information, not a secret.
In fact, by publishing it, users can verify that the downloaded file hasn't been tampered with.
This is a security-enhancing mechanism.The thing you should never publish
On the other hand, you must never include the following things in your
Fomula.
❌ API keys, tokens
❌ Password
❌ Access tokens of private repository
❌ Secret keys
After you push the Formula file you created to the remote repository, run these commands in your CLI:
brew tap [your_user_name]/tap
brew install [your_user_name]/[package_name_you_want]
# or
brew install [your_user_name]/tap/[package_name_you_want]
Now you can use the tool you made in your CLI via Homebrew.
How to update brew packages
Basically, you only have to replace
urlandsha256inFormula, to newest version.
Final Thoughts
I really wanted to make Zed extension, but I feel frustrated that it already existed. My idea was beaten.
I just did it anyway, thinking I should start creating tools and contributing OSS to OSS, taking this as an opportunity.
I'm grad if this post helps someone who wants to distribute something via Homebrew.
I'm always open to complaints about my tool, PRs on GitHub, or corrections if I said something wrong. Also, I'm Japanese, and I'm writing this post in English as much as I can by myself. If you can't understand some parts, feel free to ask questions in the comments.
Thank you for reading!


Top comments (0)