EP 01 / BUILDING CAT IN RUST · BUILD
Building cat in Rust
May 22, 2026 · 18 min
We build a real cat replacement — with syntax highlighting, line numbers, and proper terminal detection. The kind of tool you'd actually use. Along the way we cover the ownership concepts and iterator patterns that come up constantly in real Rust.
What this episode covers
- Ownership, borrowing, and when the compiler says no
- Error handling with the
?operator andBox<dyn Error> - Iterators and trait objects in practice
- Terminal detection with
is_terminal - Syntax highlighting via the
syntectcrate - CLI argument parsing with
clap
Code
// - [x] CLI arguments parsing
// - [x] List themes
// - [x] reading files
// - [x] syntax highlighting
// - [x] real themes
// - [x] line numbers
// - [x] stdin and tty detection
// - [x] refactor
use clap::Parser;
use syntect::easy::HighlightLines;
use syntect::highlighting::Style;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use syntect::util::as_24_bit_terminal_escaped;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use is_terminal::IsTerminal;
const DEFAULT_THEME: &str = "base16-ocean.dark";
#[derive(Debug, Parser)]
struct Args {
#[arg(short = 'n', long)]
line_numbers: bool,
#[arg(long, default_value = DEFAULT_THEME)]
theme: String,
#[arg(long)]
no_color: bool,
#[arg(long)]
list_themes: bool,
files: Vec<String>,
}
fn load_syntax_assets() -> (SyntaxSet, ThemeSet) {
(
SyntaxSet::load_defaults_newlines(),
ThemeSet::load_defaults(),
)
}
fn build_highlighter<'a>(
filename: &str,
syntax_set: &'a SyntaxSet,
theme_set: &'a ThemeSet,
theme: &str,
) -> HighlightLines<'a> {
let syntax = std::path::Path::new(filename)
.extension()
.and_then(|ext| syntax_set.find_syntax_by_extension(&ext.to_string_lossy()))
.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
let theme = &theme_set
.themes
.get(theme)
.unwrap_or(&theme_set.themes[DEFAULT_THEME]);
HighlightLines::new(syntax, theme)
}
fn list_themes() {
let (_, theme_set) = load_syntax_assets();
println!("Available themes:");
for theme in theme_set.themes.keys() {
println!(" - {theme}")
}
}
fn print_file(
filename: &str,
args: &Args,
syntax_set: &SyntaxSet,
theme_set: &ThemeSet,
use_color: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let input: Box<dyn Read> = if filename == "-" {
Box::new(std::io::stdin())
} else {
Box::new(File::open(filename)?)
};
let mut reader = BufReader::new(input);
let mut line = String::new();
let mut line_number = 0;
let mut highlighter: Option<HighlightLines> = if use_color {
Some(build_highlighter(filename, &syntax_set, &theme_set, &args.theme))
} else {
None
};
while reader.read_line(&mut line)? > 0 {
line_number += 1;
if args.line_numbers {
print!("{:>6} ", line_number);
}
if let Some(ref mut h) = highlighter {
let ranges: Vec<(Style, &str)> = h.highlight_line(&line, &syntax_set)?;
let colored = as_24_bit_terminal_escaped(&ranges[..], false);
print!("{colored}");
} else {
print!("{line}");
}
line.clear();
}
Ok(())
}
fn process(args: Args) {
let (syntax_set, theme_set) = load_syntax_assets();
let use_color = std::io::stdout().is_terminal() && !args.no_color;
let files = if args.files.is_empty() {
vec!["-".to_string()]
} else {
args.files.clone()
};
for filename in files.iter() {
if let Err(e) = print_file(filename, &args, &syntax_set, &theme_set, use_color) {
eprintln!("unable to process file {filename}: {e}");
}
}
}
fn main() {
let args = Args::parse();
if args.list_themes {
list_themes();
} else {
process(args);
}
}