DEV Community

Cover image for peektea: brewing a terminal file browser with Bubble Tea
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

peektea: brewing a terminal file browser with Bubble Tea

Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback for improving the project.


What I actually want is an ncdu-style file browser fully keyboard-driven, where I can hop through directories and open files straight into Nautilus, vim, Nemo, whatever, even preview images right in the terminal.

No mouse, no leaving the keyboard.

But that's a big pot to brew.

So I started small and used it as an excuse to learn Bubble Tea, Charmbracelet's Go TUI framework.

The result is peektea: a minimal version that, for now, just browses.

Run it, arrow your way through directories, pop back up to the parent.

That's the whole cup of tea so far.

~140 lines of Go: the opening-files-and-previewing-images part is where it's headed next.

So what's Bubble Tea?

Bubble Tea is a Go framework for building terminal UIs, built on the Elm Architecture.

Which is a fancy way of saying your entire app boils down to three things:

  • Model — the state of your program
  • Update — a function that takes events and returns a new model
  • View — a function that renders the model to a string

No mutation, no spilled state.

Update and View are pure functions.

The framework handles the event loop, terminal I/O, and all the redrawing so you don't have to.

You just describe what things should look like and it does the dishes.

The model

type model struct {
    dir     string
    entries []os.DirEntry
    cursor  int
    err     error
}
Enter fullscreen mode Exit fullscreen mode

That's the entire state of peektea.

The current directory, what's in it, where the cursor sits, and whether something went sideways.

When you navigate into a new directory you don't mutate anything in place, Update hands back a fresh model with the new values.

The update loop (where the tea gets stirred)

Every keypress shows up as a tea.KeyMsg.

You pattern-match on it and return the next model:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "right", "l", "enter":
            if len(m.entries) > 0 && m.entries[m.cursor].IsDir() {
                next := filepath.Join(m.dir, m.entries[m.cursor].Name())
                entries, err := os.ReadDir(next)
                if err == nil {
                    m.dir = next
                    m.entries = entries
                    m.cursor = 0
                }
            }
        // ...
        }
    }
    return m, nil
}
Enter fullscreen mode Exit fullscreen mode

That second return value, tea.Cmd, is a function that runs asynchronously and produces the next message.

Here Update is fully synchronous, so we just return nil.

But the moment you want to fetch data or read files in the background, Cmd is how you do it without blocking the UI.

Think of it as putting the kettle on and getting pinged when it's ready, instead of standing there watching it.

One small touch worth calling out: when you go back up to the parent directory, the cursor lands right back on the folder you just came out of, instead of snapping to the top.

It's just a little loop over the entries to find the matching name but it makes navigation feel natural instead of jarring.

The view

Rendering is a pure string transformation.

Bubble Tea calls View() after every Update and redraws the terminal for you.

Styling comes from Lipgloss, also from Charmbracelet (they really committed to the tea house aesthetic).

You define styles once at the package level:

var (
    cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)
    dirStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
    fileStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
)
Enter fullscreen mode Exit fullscreen mode

Then call .Render(s) on them anywhere in your view. Directories get a trailing / and a cyan tint, the cursor row gets a background highlight, and the selected entry gets a so you always know where you are.

Wiring it up

func main() {
    dir, _ := os.Getwd()
    p := tea.NewProgram(newModel(dir), tea.WithAltScreen())
    p.Run()
}
Enter fullscreen mode Exit fullscreen mode

The one line worth highlighting is tea.WithAltScreen().

It flips the terminal into the alternate screen buffer — the same trick vim and htop use.

Your TUI takes over the whole terminal, and when you quit, your shell session comes back clean like nothing happened.

Without it, your UI just dumps into the scrollback like any other output.

Nobody wants tea stains on their terminal history.

image

What I'd steep next

This is where the ncdu-style dream actually starts brewing:

  • Open files in your editor/appenter on a file shells out to vim, or hands it to Nautilus/Nemo/xdg-open based on type
  • Image previews in the terminal — render images inline via Kitty/iTerm protocols or chafa
  • File previews — split the screen, show the selected file's content on the right
  • Filtering — type to narrow the list (Bubble Tea ships a textinput in bubbles)
  • Hidden file toggleh to show/hide dotfiles

All of it stays keyboard-driven no reaching for the mouse. As they land, they'll be in the repo.

The nice thing is the framework makes each one refreshingly easy: you extend the model and handle new keys in Update.

That's the whole pattern.


AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*

Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

GitHub logo HexmosTech / git-lrc

Free, Micro AI Code Reviews That Run on Commit




AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs -- without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

See It In Action

See git-lrc catch serious security issues such as leaked credentials, expensive cloud operations, and sensitive material in log statements

git-lrc-intro-60s.mp4

Why

  • 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won't notice until production.
  • 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
  • 🔁 Build a

Top comments (0)