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.
- 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 likeviandless, 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.
Build your project without leaving the editor — the output streams into the same window, with colours intact.
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.
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.
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 -jSame flow, swap apt-get for brew.
The following must be installed on the system
- libsdl2-dev or libsdl3-dev
- libyaml-dev
- testrunner (from https://github.com/gnilk/testrunner) - for unit testing
sudo apt install libsdl2-dev libsdl3-dev libyaml-devThe 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.
Everything lives in the gedit namespace. A few landmarks:
main.cpp— picks a backend, thenEditor::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 ofLines; aLineis astd::u32stringplus token attributes).src/Core/UI/- UI base with graphics, input and controllerssrc/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 theScreenBase/DrawContextinterfaces.src/Core/Language/— the tokenizer and per-language configs.src/Core/JSEngine/— Duktape host + the JS API wrappers; plugin scripts live insrc/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.
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 (seeCPPLanguage/JSONLanguageas 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 fromcmake-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.
1) Build (release recommended for distribution):
cmake --preset release
cmake --build --preset release --target goatedit2a) .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>.deb2b) 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.dmgCMAKE_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.shNotes on the AppImage script:
./scripts/build-appimage.sh [BUILD_DIR]— first arg (or$BUILD_DIR) overrides the build dir (defaultcmake-build-release);$OUTPUT_DIRoverrides where the.AppImagelands;$VERSION, if set, is folded into the filename (goatedit-<VERSION>-<arch>.AppImage).- Needs
curl(fetcheslinuxdeployonce, cached in<BUILD_DIR>/appimage-tools/) and FUSE — the script setsAPPIMAGE_EXTRACT_AND_RUN=1as 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 customAppRun.
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.
BSD 3-Clause © 2023 Fredrik Kling. See LICENSE.


