---
title: "peektea: brewing a terminal file browser with Bubble Tea"
author: "Athreya aka Maneshwar"
published_at: "2026-05-31T19:00:22+00:00"
link: "https://dev.to/lovestaco/peektea-brewing-a-terminal-file-browser-with-bubble-tea-4jg1"
feed: "DEV Community"
clawfeed: "https://agent.clawfeeds.com/feed/d5v8-87f6-e3id.md"
feed_url: "https://agent.clawfeeds.com/feed/d5v8-87f6-e3id.md"
categories: ["webdev","programming","beginners","go"]
---

# 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](https://github.com/HexmosTech/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](https://github.com/charmbracelet/bubbletea), Charmbracelet's Go TUI framework.

The result is **[peektea](https://github.com/lovestaco/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](https://guide.elm-lang.org/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 
-----------

```
<span>type</span> <span>model</span> <span>struct</span> <span>{</span>
    <span>dir</span>     <span>string</span>
    <span>entries</span> <span>[]</span><span>os</span><span>.</span><span>DirEntry</span>
    <span>cursor</span>  <span>int</span>
    <span>err</span>     <span>error</span>
<span>}</span>

```

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:

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

```

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](https://github.com/charmbracelet/lipgloss), also from Charmbracelet (they really committed to the tea house aesthetic).

You define styles once at the package level:

```
<span>var</span> <span>(</span>
    <span>cursorStyle</span> <span>=</span> <span>lipgloss</span><span>.</span><span>NewStyle</span><span>()</span><span>.</span><span>Foreground</span><span>(</span><span>lipgloss</span><span>.</span><span>Color</span><span>(</span><span>"212"</span><span>))</span><span>.</span><span>Bold</span><span>(</span><span>true</span><span>)</span>
    <span>dirStyle</span>    <span>=</span> <span>lipgloss</span><span>.</span><span>NewStyle</span><span>()</span><span>.</span><span>Foreground</span><span>(</span><span>lipgloss</span><span>.</span><span>Color</span><span>(</span><span>"39"</span><span>))</span>
    <span>fileStyle</span>   <span>=</span> <span>lipgloss</span><span>.</span><span>NewStyle</span><span>()</span><span>.</span><span>Foreground</span><span>(</span><span>lipgloss</span><span>.</span><span>Color</span><span>(</span><span>"252"</span><span>))</span>
<span>)</span>

```

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 
--------------

```
<span>func</span> <span>main</span><span>()</span> <span>{</span>
    <span>dir</span><span>,</span> <span>_</span> <span>:=</span> <span>os</span><span>.</span><span>Getwd</span><span>()</span>
    <span>p</span> <span>:=</span> <span>tea</span><span>.</span><span>NewProgram</span><span>(</span><span>newModel</span><span>(</span><span>dir</span><span>),</span> <span>tea</span><span>.</span><span>WithAltScreen</span><span>())</span>
    <span>p</span><span>.</span><span>Run</span><span>()</span>
<span>}</span>

```

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](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4ojscj47l67inis37tl.png)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4ojscj47l67inis37tl.png)

 What I'd steep next 
---------------------

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

- **Open files in your editor/app** — `enter` 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](https://github.com/charmbracelet/bubbles))
- **Hidden file toggle** — `h` 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.

[![ ](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed6ratvd5eb5bp0ep9ck.png)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed6ratvd5eb5bp0ep9ck.png)

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](https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg) [ HexmosTech ](https://github.com/HexmosTech) / [ git-lrc ](https://github.com/HexmosTech/git-lrc) 
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

###  Free, Micro AI Code Reviews That Run on Commit 

 

| [🇩🇰 Dansk](https://github.com/HexmosTech/git-lrc/readme/README.da.md) | [🇪🇸 Español](https://github.com/HexmosTech/git-lrc/readme/README.es.md) | [🇮🇷 Farsi](https://github.com/HexmosTech/git-lrc/readme/README.fa.md) | [🇫🇮 Suomi](https://github.com/HexmosTech/git-lrc/readme/README.fi.md) | [🇯🇵 日本語](https://github.com/HexmosTech/git-lrc/readme/README.ja.md) | [🇳🇴 Norsk](https://github.com/HexmosTech/git-lrc/readme/README.nn.md) | [🇵🇹 Português](https://github.com/HexmosTech/git-lrc/readme/README.pt.md) | [🇷🇺 Русский](https://github.com/HexmosTech/git-lrc/readme/README.ru.md) | [🇦🇱 Shqip](https://github.com/HexmosTech/git-lrc/readme/README.sq.md) | [🇨🇳 中文](https://github.com/HexmosTech/git-lrc/readme/README.zh.md) |

[![git-lrc logo](https://camo.githubusercontent.com/948c8f2d5cf41b48985cd364d48c3a2dc9bfbfd42eab3e0a9a1b3e61f5f17ce3/68747470733a2f2f6865786d6f732e636f6d2f66726565646576746f6f6c732f7075626c69632f6c725f6c6f676f2e737667)](https://camo.githubusercontent.com/948c8f2d5cf41b48985cd364d48c3a2dc9bfbfd42eab3e0a9a1b3e61f5f17ce3/68747470733a2f2f6865786d6f732e636f6d2f66726565646576746f6f6c732f7075626c69632f6c725f6c6f676f2e737667)
git-lrc
=======

Free, Micro AI Code Reviews That Run on Commit
----------------------------------------------

[![git-lrc - Free, unlimited AI code reviews that run on commit | Product Hunt](https://camo.githubusercontent.com/87bf2d4283c1e0aa99e254bd17fefb1c67c0c0d39300043a243a4aa633b6cecc/68747470733a2f2f6170692e70726f6475637468756e742e636f6d2f776964676574732f656d6265642d696d6167652f76312f746f702d706f73742d62616467652e7376673f706f73745f69643d31303739323632267468656d653d6c6967687426706572696f643d6461696c7926743d31373731373439313730383638)](https://www.producthunt.com/products/git-lrc?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_campaign=badge-git-lrc)

[ ![Discord Community](https://camo.githubusercontent.com/b8f979318aaabc8dec512b9d4e6e2a12431fba3c8a3b8738e1a97a0722d4e4bf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446973636f72642d436f6d6d756e6974792d3538363546323f6c6f676f3d646973636f7264266c6162656c436f6c6f723d7768697465)](https://discord.gg/sGdnKwB3qq) [![Go Report Card](https://camo.githubusercontent.com/e74c0651c3ee9165a2ed01cb0f6842c494029960df30eb9c24cf622d3d21bf46/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f4865786d6f73546563682f6769742d6c7263)](https://goreportcard.com/report/github.com/HexmosTech/git-lrc) [![confidence.yml](https://github.com/HexmosTech/git-lrc/actions/workflows/confidence.yml/badge.svg)](https://github.com/HexmosTech/git-lrc/actions/workflows/confidence.yml) [![status-doc-link-check.yml](https://github.com/HexmosTech/git-lrc/actions/workflows/status-doc-link-check.yml/badge.svg)](https://github.com/HexmosTech/git-lrc/actions/workflows/status-doc-link-check.yml) [![gitleaks.yml](https://github.com/HexmosTech/git-lrc/actions/workflows/gitleaks.yml/badge.svg)](https://github.com/HexmosTech/git-lrc/actions/workflows/gitleaks.yml) [![osv-scanner.yml](https://github.com/HexmosTech/git-lrc/actions/workflows/osv-scanner.yml/badge.svg)](https://github.com/HexmosTech/git-lrc/actions/workflows/osv-scanner.yml) [![govulncheck.yml](https://github.com/HexmosTech/git-lrc/actions/workflows/govulncheck.yml/badge.svg)](https://github.com/HexmosTech/git-lrc/actions/workflows/govulncheck.yml) [![semgrep.yml](https://github.com/HexmosTech/git-lrc/actions/workflows/semgrep.yml/badge.svg)](https://github.com/HexmosTech/git-lrc/actions/workflows/semgrep.yml) [![dependabot-enabled](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FHexmosTech%2Fgit-lrc%2FHEAD%2F.%2Fgfx%2Fdependabot-enabled.svg)](https://github.com/HexmosTech/git-lrc/gfx/dependabot-enabled.svg)

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.mp4Why
---

- 🤖 **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**…

 

[View on GitHub](https://github.com/HexmosTech/git-lrc)
