GitHub - weedonandscott/trolley: Run terminal apps anywhere

6 min read Original article ↗

NOTE: This software is pre-alpha. Functionality and design expected to be broken.

Run terminal apps anywhere.

Trolley lets you bundle any TUI executable together with a terminal emulator runtime, allowing you to distribute TUI applications to non-technical users.

Trolley targets Linux and MacOS, and Windows.

Other targets like iOS and Android are possible. Please open an issue if interested.

Although mostly simple, two recent developments make it quite powerful:

  1. Improvements in terminal functionality and performance
  2. Flourishing of easy to use, powerful TUI libraries

If you are building software that fits the textual interface style, you'll be able to create performant, cross-platform applications. Launching in under a second is typical. Combined with TUI frameworks like OpenTUI, Bubbletea & Ratatui, it is extremely easy to create apps with a developer experience not much different than a webapp's.

Giants and their shoulders

Trolley is built on top of Ghostty, which powers most of everything the end user will see and do, and enables the aforementioned functionality. Even the GUI wrappers are stripped down versions of Ghostty's.

For packaging, cargo-packager does most of the heavy lifting.

Trolley, then, is an ergonomic wrapper around those two.

Install

macOS / Linux (Homebrew):

brew install weedonandscott/tap/trolley

Linux (manual):

curl -sL https://github.com/weedonandscott/trolley/releases/latest/download/trolley-cli-x86_64-linux.tar.xz | tar xJ
mv trolley ~/.local/bin/

Nix flake (builds from source):

{
  inputs.trolley.url = "github:weedonandscott/trolley";
}

Then add inputs.trolley.packages.${system}.default to your packages.

Binaries for all platforms are available on GitHub Releases.

Quickstart

This scaffolds a trolley.toml manifest. Point it at your TUI binary:

[app]
identifier = "com.example.my-app"
display_name = "My App"
slug = "my-app"
version = "0.1.0"
icons = ["assets/icon.png"]

[linux]
binaries = { x86_64 = "target/release/my-app" }

[gui]
initial_width = 800
initial_height = 600

[fonts]
families = [{ nerdfont = "JetBrainsMono" }]

[embeds]
theme = "themes/dracula"
shaders = ["shaders/crt.glsl", "shaders/bloom.glsl"]
data = ["assets", "config/defaults.json"]

[ghostty]
font-size = 14

Then run to see how it works:

Or package to send to your end users:

How it works

Trolley bundles your TUI, assets, and config next to a terminal emulator runtime. It instructs it to launch your executable.

Trolley's runtime is a thin native wrapper around libghostty, the core library of the Ghostty terminal emulator. libghostty handles VT parsing, PTY management, GPU rendering, font shaping, and input encoding. Trolley provides the native window and kiosk behavior.

Platform Runtime language Windowing Renderer
macOS Swift (AppKit) NSWindow Metal
Linux Zig (GLFW) GLFW OpenGL
Windows Zig (Win32) Win32 OpenGL

Development Prerequisites

  • Nix with flakes enabled (provides all build tools), or:
  • Rust toolchain, Zig compiler, and platform dependencies (GLFW, X11 libs on Linux)

Manifest

The manifest file trolley.toml has the following sections:

[app] -- required

Field Description
identifier Reverse-DNS identifier (e.g. com.foo.bar)
display_name Human-readable application name
slug Filesystem-safe name (lowercase, hyphens)
version Version string
icons List of icon paths/globs (see Icons)

[linux], [macos], [windows] -- at least one required

[linux]
binaries = { x86_64 = "path/to/binary", aarch64 = "path/to/binary" }

[gui] -- optional

initial_width, initial_height, resizable, min_width, min_height, max_width, max_height.

[fonts] -- optional

[fonts]
families = [
    { nerdfont = "Inconsolata" },      # auto-downloaded from Nerd Fonts
    { path = "fonts/Custom.ttf" },     # local font file
]

[environment] -- optional

[environment]
env_file = ".env"
variables = { MY_VAR = "value" }

[embeds] -- optional

Embed portable Ghostty resources into the generated bundle. Relative paths are resolved from the directory containing trolley.toml.

[embeds]
theme = "themes/dracula"
shaders = ["shaders/crt.glsl", "shaders/bloom.glsl"]
data = ["assets", "config/defaults.json"]

theme inlines a local Ghostty theme file into the generated ghostty.conf. This is the portable way to ship a theme with your app, because it does not depend on Ghostty's external theme catalog being installed on the target machine.

shaders bundles one or more custom shader files and wires them into Ghostty as repeated custom-shader entries. Each shader path must be a clean relative path; Trolley copies every shader into the bundle at the same relative path so trolley run and packaged apps behave the same.

data copies files or directories into the bundle root at the same relative paths. This is useful for application assets or default data files that your TUI loads relative to the runtime working directory.

[ghostty] -- optional

Pass-through configuration for the Ghostty terminal engine. Accepts any Ghostty config key with a scalar value (string, integer, float, or boolean) or an array of scalars. Arrays are expanded into repeated key lines, which is how Ghostty handles multi-value options like keybind. Note that configs meant for Ghostty's GUI will not take effect (obviously). If you want to ship a theme file with your app, prefer [embeds].theme over setting theme = "..." here. If you want to bundle shaders with your app, prefer [embeds].shaders over setting custom-shader here.

[ghostty]
font-size = 14
keybind = [
    "ctrl+==increase_font_size:1",
    "ctrl+-=decrease_font_size:1",
]

Ghostty Logging

To see Ghostty log output when using trolley run, add this to your variables:

variables = { 
  GHOSTTY_LOG = "stderr" 
}

Window title

You can set a fixed window title for your application via the Ghostty title config:

[ghostty]
title = "My App"

This sets the native window title on all platforms. When set, it overrides any title escape sequences sent by your TUI program. If your TUI doesn't set a title itself, the window would otherwise show a default — so it's generally a good idea to set one.

Tip: Trolley clears all default Ghostty keybindings so they don't interfere with your TUI. If you want to re-add some of them (e.g. zoom), use the keybind array:

[ghostty]
keybind = [
    "ctrl+==increase_font_size:1",
    "ctrl+plus=increase_font_size:1",
    "ctrl+-=decrease_font_size:1",
    "ctrl+0=reset_font_size",
    "super+==increase_font_size:1",
    "super+plus=increase_font_size:1",
    "super+-=decrease_font_size:1",
    "super+0=reset_font_size",
]

See Ghostty's keybind docs for the full list of available actions.

Icons

Icons are not needed for trolley run or --bundle-only, but most package formats require them. Provide icon paths or globs in the [app] section:

[app]
icons = ["assets/icon.png"]

Different formats need different icon types:

Format Icon type Required
AppImage Square .png Yes
.deb, .rpm, pacman .png No
NSIS (Windows) .ico No
.app, .dmg (macOS) .icns No
.tar.gz -- --

To support all platforms, provide multiple icons:

icons = ["assets/icon.png", "assets/icon.ico", "assets/icon.icns"]

Glob patterns are also supported (e.g. "assets/icon.*").

Package formats

Platform Default formats
Linux AppImage, .deb, .rpm, pacman, .tar.gz
macOS .app, .dmg, .tar.gz
Windows NSIS installer

Select specific formats with --formats:

trolley package --formats appimage,deb

Use --skip-failed-formats to continue building remaining formats if one fails (e.g. when icons are missing for some formats):

trolley package --skip-failed-formats

BUNDLING != SANDBOXING

Trolley simply runs your executable inside a terminal, and in that sense, provides no extra security or sandbox guarantees.

License

MIT