Skip to content

Repository files navigation

GoatEdit 🐐

Build

A hackable, console-flavoured code editor written in modern C++20.

Someone (Peter Norton?) once said that to really become a programmer you have to write an editor. This started as that playground — and grew into something I actually use every day.

GoatEdit pairs a real text editor with a first-class embedded shell. Hit a key and the bottom of the window becomes a live terminal — run make, git, vi, anything — and the same prompt doubles as a command line for the editor itself. Think Amiga AsmOne or a game console's drop-down terminal, but driving a syntax-highlighting, multi-backend editor.

GoatEdit editing its own source, with the embedded terminal below


Why you might like it

  • The terminal is a feature, not an afterthought. A genuine PTY-backed shell lives inside the editor (bash/zsh/whatever you fancy). It handles colours, full-screen apps like vi and less, command history, tab-completion, and window-size reporting — the real deal, not a fake prompt.
  • One prompt, two brains. Type a normal command and it goes to the shell. Prefix it (configurable, default .) and it runs an editor command instead — save, load, search, or any plugin you've written. No mode-juggling.
  • Pluggable rendering backends. SDL3 (the primary, "real application" frontend with TrueType rendering via stb_truetype) and SDL2 for CI/fallback. The core knows nothing about pixels, so a fresh backend is a self-contained job — a modern-terminal backend (no NCurses baggage) is on the roadmap.
  • Terminal Compatible Modern ANSI Terminal rendering (no NCurses) - 256/truecolor terminals supported, runs well over SSH.
  • Scriptable in JavaScript. An embedded Duktape engine exposes the editor through a clean JS API (Editor, Document, View, Theme, Console…). The startup banner goat? That's a plugin.
  • Its own syntax engine. A compact stack-based tokenizer drives highlighting (C++, JSON, Make, and a default fallback), tokenizing in the background so big files don't stall the UI.
  • Runs on Linux and macOS.

A quick tour

The embedded terminal is a real terminal

Build your project without leaving the editor — the output streams into the same window, with colours intact.

Running a build inside the embedded terminal

Quick-command mode (vi/Sublime flavour)

A fast overlay for search, navigation, and plugin commands without reaching for the full terminal. Results are written straight to the terminal pane — here, a search across the buffer.

Quick-command search results in the terminal pane


Building

The build is driven by CMake 3.28+. Source dependencies (json, gnklog, dukglue, fmt) are fetched by CMake itself via FetchContent on first configure — no manual step; system dependencies you install yourself.

Linux

sudo apt-get update
sudo apt-get install -y libyaml-cpp-dev libsdl2-dev

# Configure (SDL3 is on by default)
cmake -B ./cmake-build-debug -DCMAKE_BUILD_TYPE=Debug

# ...or build the SDL2 backend instead (handy on CI / older boxes)
cmake -B ./cmake-build-debug -DCMAKE_BUILD_TYPE=Debug -DGEDIT_BUILD_SDL3=OFF -DGEDIT_BUILD_SDL2=ON

# Build the editor
cmake --build ./cmake-build-debug --config Debug --target goatedit -j

macOS

Same flow, swap apt-get for brew.

Dependencies

The following must be installed on the system

sudo apt install libsdl2-dev libsdl3-dev libyaml-dev

The following are installed during build

  • nlohmann/json
  • fmtlib
  • dukglue
  • gnklog

The following are bundled directly with the source

  • stb (various parts)
  • duktape 2.7.0

Duktape ships pre-configured in the repo (src/ext/duktape-2.7.0) so you don't have to fight its build — a scar from an old GitHub Actions battle.


Project layout

Everything lives in the gedit namespace. A few landmarks:

  • main.cpp — picks a backend, then Editor::Instance().Initialize() / OpenScreen().
  • src/Core/Editor.* — the application singleton (owns the workspace, model, plugin engine, theme, keymap, runloop).
  • src/Core/TextBuffer / Line / Workspace — the data model (a buffer is a vector of Lines; a Line is a std::u32string plus token attributes).
  • src/Core/UI/ - UI base with graphics, input and controllers
  • src/Core/Editor/UI/ — Controller and Application views (workspace, editor, terminal) + status bars etc..
  • src/Core/Controllers/ — input logic decoupled from views (EditController, TerminalController, QuickCommandController).
  • src/Core/Graphics/ — SDL2/3, GAnsi backends behind the ScreenBase / DrawContext interfaces.
  • src/Core/Language/ — the tokenizer and per-language configs.
  • src/Core/JSEngine/ — Duktape host + the JS API wrappers; plugin scripts live in src/Plugins/.
  • src/Core/Config/ — YAML config and theming.
  • src/ext/gansi/ - Terminal rendering library, written as a stand alone library

There's a much deeper architecture/coding-standards write-up in CLAUDE.md — worth a read before a first PR.


Want to hack on it?

Contributions are very welcome — this is a personal project that has outgrown one person's spare evenings, and there's a lot of low-friction surface area to jump in on:

  • 🧩 Write a plugin. The JS API is the easiest on-ramp — add a cmdlet, a theme tweak, a goat.
  • 🎨 Add a language. Implement a LanguageBase + a tokenizer config (see CPPLanguage / JSONLanguage as templates).
  • 🐚 Terminal/VT corner cases. The VT parser (VTermParser) handles a healthy subset of xterm — origin mode, bracketed paste, and friends are still open.
  • 🧪 Tests. The suite runs under trun; modules live in utests/. Run them from cmake-build-debug/:
    cd cmake-build-debug && trun --sequential ./libutests.so

If you're picking something up, open an issue or draft PR early so we can compare notes. Style and conventions are documented in CLAUDE.md; the short version is C++20, RAII everywhere, 4-space K&R, and code ordered caller-before-callee so files read top-to-bottom.


Building for Distribution

1) Build (release recommended for distribution):

cmake --preset release
cmake --build --preset release --target goatedit

2a) .deb — the CPack package target (uses the DEB generator on Linux); the package lands in the build dir:

cmake --build ./cmake-build-release --target package
# -> cmake-build-release/goatedit-<ver>-linux-<arch>.deb

2b) macOS — two separate things, neither of which is implied by the other:

# Install the bundle straight into /Applications (no packaging step, no sudo):
cmake --install ./cmake-build-release

# ..or produce a distributable disk image, to be mounted + dragged by hand:
cmake --build ./cmake-build-release --target package
# -> cmake-build-release/GoatEdit-<ver>-macOS.dmg

CMAKE_INSTALL_PREFIX defaults to /Applications on macOS (CMake's stock /usr/local would put the bundle where no Mac user looks for it, and where the goat wrapper doesn't point). Only the default is overridden, so -DCMAKE_INSTALL_PREFIX=... still wins, and CPack is unaffected — it stages the .dmg into its own temp prefix.

The .dmg is unsigned/un-notarised, so first launch needs a right-click → Open (or xattr -dr com.apple.quarantine /Applications/goatedit.app).

2c) AppImage — built separately; writes goatedit-<arch>.AppImage to the repo root by default:

./scripts/build-appimage.sh

Notes on the AppImage script:

  • ./scripts/build-appimage.sh [BUILD_DIR] — first arg (or $BUILD_DIR) overrides the build dir (default cmake-build-release); $OUTPUT_DIR overrides where the .AppImage lands; $VERSION, if set, is folded into the filename (goatedit-<VERSION>-<arch>.AppImage).
  • Needs curl (fetches linuxdeploy once, cached in <BUILD_DIR>/appimage-tools/) and FUSE — the script sets APPIMAGE_EXTRACT_AND_RUN=1 as a fallback if FUSE isn't available.
  • It runs cmake --install <BUILD_DIR> --prefix <AppDir>/usr (reusing the project's install rules), bundling the binary, SDL + yaml-cpp, the assets, the desktop file, and the icon, then sets the custom AppRun.

Command-line launch on macOS (install-cli)

A .app bundle can't be started from a shell as-is. The install-cli target drops a small wrapper script that execs the bundle binary into a directory on PATH (macOS only — on Linux the .deb already puts goatedit in /usr/bin):

cmake --build ./cmake-build-release --target install-cli
# -> ~/.local/bin/goat  ->  /Applications/goatedit.app/Contents/MacOS/goatedit

~/.local/bin is the default because it needs no sudo — on a stock Apple Silicon box /usr/local/bin is root-owned (Homebrew lives in /opt/homebrew). It is not on PATH by default on macOS though, so either add it to your shell rc or set GEDIT_CLI_BIN_DIR to somewhere that already is. The directory is created if missing.

The wrapper is generated into the build dir at configure time, so you can read it before installing it (cat cmake-build-release/goat). If the destination isn't writable the target prints the exact sudo install command and stops; it never escalates privileges on its own.

Three cache variables override the defaults:

Variable Default
GEDIT_CLI_NAME goat name of the installed command
GEDIT_CLI_BIN_DIR ~/.local/bin where it's written — e.g. /usr/local/bin (needs sudo)
GEDIT_CLI_APP_PATH /Applications/goatedit.app the bundle the wrapper launches

Because it execs (rather than forks), the wrapper blocks until the editor exits, so it works as $EDITOR/$VISUAL for git commit and friends:

export EDITOR="$HOME/.local/bin/goat"
export VISUAL="$EDITOR"

This is only the packaging half of docs/cli-install-design.md — the :install-cli in-editor self-install command is still on the back burner.

License

BSD 3-Clause © 2023 Fredrik Kling. See LICENSE.

Packages

Used by

Contributors

Languages