Back to Articles

lsd: Why 16,000 Developers Replaced ls With a Rust Rewrite

[ View on GitHub ]

lsd: Why 16,000 Developers Replaced ls With a Rust Rewrite

Hook

The ls command has existed since 1971, yet a Rust rewrite from 2018 convinced 16,000 developers to replace it. What did Unix get wrong for five decades?

Context

The traditional ls command is a masterclass in minimalism: it lists files, sorts them, and stops there. For half a century, developers accepted monochrome output and cryptic permission strings as the price of command-line efficiency. Visual file managers like Finder or Nautilus offered icons and colors, but required leaving the terminal. This created a productivity gap—developers who live in tmux or vim had to mentally parse 'drwxr-xr-x' while their GUI-using colleagues got instant visual feedback about file types.

Tools like colorls (Ruby) and exa (Rust) attempted to bridge this divide, but lsd takes a different approach: it's not just a prettier ls, it's a complete reimplementation that treats visual enhancement as a first-class feature, not an afterthought. By leveraging Rust's cross-platform capabilities and Nerd Fonts' extensive glyph collection, lsd brings GUI file manager affordances to the terminal without compromising the speed or composability that makes Unix tools powerful. The project's 15,997 GitHub stars suggest developers were ready to abandon backward compatibility for better ergonomics.

Technical Insight

lsd's architecture revolves around three core components: a YAML-based configuration system, a icon rendering engine tied to Nerd Fonts, and a flexible display formatter that handles everything from simple lists to recursive tree views. Unlike GNU ls which accumulated flags organically over decades, lsd was designed from scratch with modern terminal capabilities in mind.

The configuration system demonstrates Rust's strength in structured data handling. Rather than parsing environment variables like LS_COLORS, lsd uses serde to deserialize YAML files into strongly-typed structs. Here's how the tool determines which icon to display for a file:

// Simplified from lsd's icon module
use std::path::Path;

pub struct IconTheme {
    by_name: HashMap<String, char>,
    by_extension: HashMap<String, char>,
    default_file: char,
    default_folder: char,
}

impl IconTheme {
    pub fn get_icon(&self, path: &Path, is_dir: bool) -> char {
        if is_dir {
            return self.default_folder;
        }
        
        // Check exact filename match first (e.g., "Dockerfile")
        if let Some(name) = path.file_name() {
            if let Some(icon) = self.by_name.get(name.to_str().unwrap()) {
                return *icon;
            }
        }
        
        // Fall back to extension matching
        if let Some(ext) = path.extension() {
            if let Some(icon) = self.by_extension.get(ext.to_str().unwrap()) {
                return *icon;
            }
        }
        
        self.default_file
    }
}

This lookup system is why lsd can display a Rust crab icon for .rs files or a Docker whale for Dockerfile—it's not hardcoded logic, but a data-driven mapping that users can override. The YAML configuration allows granular control:

icons:
  by-name:
    .gitignore: ""
    Dockerfile: ""
    package.json: ""
  by-extension:
    rs: ""
    go: ""
    py: ""

The performance characteristics are noteworthy. While ls is written in C and optimized over decades, lsd's Rust implementation holds its own because it avoids allocations in hot paths. The directory traversal uses std::fs::read_dir with entry filtering done through iterators, letting the compiler optimize away intermediate allocations. For large directories (1000+ files), lsd typically runs within 10-20% of GNU ls's speed—fast enough that humans can't perceive the difference.

Cross-platform support reveals Rust's ecosystem advantages. On Unix systems, lsd uses libc to query file metadata like permissions and ownership. On Windows, it translates NTFS attributes to Unix-style permission strings for consistency. The tree view feature, accessible via lsd --tree, uses a recursive algorithm that respects .gitignore patterns by integrating the ignore crate—the same library that powers ripgrep's file filtering. This means lsd --tree in a Node.js project automatically skips node_modules, something GNU ls can't do without external scripting.

The color system deserves special attention. Rather than relying on terminal-specific escape codes, lsd uses the crossterm crate to abstract color rendering across platforms. It supports 24-bit true color on modern terminals while gracefully degrading to 256-color or 16-color palettes on older systems. The configuration allows theming beyond LS_COLORS conventions:

color:
  when: auto  # auto, always, never
  theme:
    user: 230
    group: 187
    size:
      none: 245
      small: 229
      medium: 216
      large: 172

This granularity—different colors for file size ranges—is impossible with traditional ls implementations without extensive shell scripting.

Gotcha

lsd's Achilles' heel is its dependency on Nerd Fonts. These patched fonts bundle thousands of glyphs from Font Awesome, Devicons, and other icon sets, but require manual installation and terminal configuration. On a fresh macOS or Linux install, lsd displays placeholder squares instead of icons until you install something like 'Hack Nerd Font' and configure your terminal emulator. For developers who ssh into remote servers frequently, this becomes a non-starter—you can't install fonts on a machine you're accessing remotely, and lsd's icon output turns into garbage characters over ssh unless your local terminal has the right fonts.

Script compatibility is another landmine. While lsd aims for ls compatibility, subtle differences exist. Some scripts parse ls output (a practice generally discouraged but widespread) expecting specific column spacing or date formats. lsd's default format includes icons and colors that can break naive parsing. The --classic flag disables enhancements, but scripts that invoke ls without absolute paths will break if users alias ls='lsd'. Additionally, lsd doesn't implement every obscure GNU ls flag—options like --quoting-style or --time-style=full-iso may behave differently or be missing entirely. For production environments where POSIX compliance matters, this creates audit risk.

Verdict

Use if: You spend more than an hour daily navigating directories in the terminal, already use patched fonts for vim/tmux, want visual file-type feedback without opening a GUI, or need tree views that respect gitignore. lsd shines for local development on machines you control, especially if you work with polyglot codebases where file extensions matter. Skip if: You primarily work on remote servers via ssh, write scripts that parse ls output, need strict POSIX compliance for regulatory reasons, or operate in minimal environments like Alpine containers where font dependencies are excessive. The font requirement alone disqualifies lsd for many production use cases, making it a local-development luxury rather than a universal ls replacement.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/cybersecurity/lsd-rs-lsd.svg)](https://starlog.is/api/badge-click/cybersecurity/lsd-rs-lsd)