Skip to main content
Cover image for Capsule: A Go TUI for File Picking and Transfer

Capsule: A Go TUI for File Picking and Transfer

golang cli tui bubbletea wormhole

I wanted a way to quickly find files in my terminal without chaining ls and cd commands, and then send them to another machine without dealing with scp syntax or cloud uploads. So I built Capsule.

It’s a terminal TUI written in Go that handles both: fast file picking and secure peer-to-peer transfer using the magic-wormhole protocol.

Why BubbleTea?

I chose BubbleTea because it implements the Elm architecture (Model-View-Update) in Go. A central Model struct manages all application state, which makes the behavior deterministic and easy to debug, even with concurrent operations like file transfers.

type Model struct {
    CurrentPage    string
    PageTitles     []string
    InputsDisabled bool
    MiscMsgData    map[string]string
    Title          string
    err            error
    quitting       bool
    filepicker     filepicker.Model
    selectedFile   string
    operations     int
}

The framework handles the event loop and updates, so I just focus on defining what the state looks like and how it changes.

File Transfer with Wormhole

The magic-wormhole protocol lets me send files or directories using a temporary code. No authentication setup, no daemon processes, no cloud storage—the sender and receiver just need to agree on the code.

From the TUI, I can select a file and generate a wormhole code in a few keystrokes. The recipient enters the same code on their end, and the transfer happens directly between machines.

The File Picker

Keyboard-driven navigation (WASD or arrow keys) makes finding files much faster than command chaining. There’s visual feedback for directory traversal and selection, and the whole thing stays snappy even in large directories.

Theming

A CLI tool you stare at for hours should look decent. Capsule uses Lipgloss for styling and has an on-demand theme switcher:

func randomTint() {
    sliceOfTints = append(sliceOfTints, tint.GetCurrentTint())
    newTint := tint.DefaultRegistry.Tints()[rand.Intn(len(tint.DefaultRegistry.Tints()))]
    tint.SetTint(newTint)
    setStyles()
}

Small detail, but it makes long terminal sessions more pleasant.

Running It

git clone https://github.com/manveerbhullar/capsule.git
cd capsule
go build .
./capsule

Standard Go toolchain, no external dependencies beyond the usual suspects.


View source: GitHub Repository