peektea: 140 Lines of Go for a Keyboard-Driven File Browser
Maneshwar wanted an ncdu-style file browser — fully keyboard-driven, able to hop directories, open files in vim or Nautilus, and preview images in the terminal. Instead of building that whole thing at once, he used it as an excuse to learn Bubble Tea, Charmbracelet's Go TUI framework. The result is peektea: a minimal version that currently only browses directories. You run it, arrow through directories, and pop back to the parent. That's it — for now. The source is on GitHub.
What is Bubble Tea?
Bubble Tea is a Go framework for building terminal UIs, based on the Elm Architecture. 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 redrawing. You just describe what things should look like.
The Model
type model struct {
dir string
entries []os.DirEntry
cursor int
err error
}
That's the entire state of peektea. The current directory, its entries, the cursor position, and any error. When you navigate into a new directory, Update returns a fresh model with new values — no mutation.
The Update Loop
Every keypress arrives 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
}
The second return value, tea.Cmd, is a function that runs asynchronously and produces the next message. Here Update is synchronous, so it returns nil. But for background file reads or data fetching, Cmd is how you do it without blocking the UI.
One small touch: when you go up to the parent directory, the cursor lands back on the folder you came out of, instead of snapping to the top. A simple loop over entries to find the matching name makes navigation feel natural.
The View
Rendering is a pure string transformation. Bubble Tea calls View() after every Update and redraws the terminal. Styling comes from Lipgloss, also from Charmbracelet:
var (
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)
dirStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
fileStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
)
Directories get a trailing / and 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()
}
tea.WithAltScreen() flips the terminal into the alternate screen buffer — the same trick vim and htop use. Your TUI takes over the terminal, and when you quit, your shell session comes back clean.
What's Next
Maneshwar plans to add:
- Open files in editor/app (enter on a file shells out to vim, or hands it to Nautilus/xdg-open)
- Image previews in terminal (via Kitty/iTerm protocols or chafa)
- File previews (split screen showing file content)
- Filtering (type to narrow the list)
- Hidden file toggle (h to show/hide dotfiles)
All keyboard-driven. The framework makes each feature easy: extend the model and handle new keys in Update.
Why This Matters
Bubble Tea is a powerful framework for building terminal UIs in Go. Peektea shows how little code is needed to get a functional file browser. Developers looking to build TUIs should study this pattern. The 140-line codebase is a perfect starting point for learning the Elm Architecture in Go.
Maneshwar is also building git-lrc, a free AI code reviewer that runs on every commit. You can find both projects on his GitHub.





