Project

echoes

0.01
The project is in a healthy, maintained state
Echoes is a pure-Ruby macOS terminal emulator with first-class integrations for rubish (in-process shell) and rvim (in-process vim editor) panes, plus a private OSC namespace for in-pane Ruby tools that want to drive UI features (gradient backgrounds, rectangular fills, proportional-font text) other terminals can't. Written in pure Ruby on top of AppKit via Fiddle.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 0
>= 0
 Project Readme

Echoes

A pure-Ruby AppKit-based macOS terminal emulator. Echoes aims to be a well-integrated host for Ruby tooling — embedding rubish (a Ruby shell) and rvim (a Ruby vim) as first-class panes, with native prompt rendering, structured completions, and a private OSC namespace that lets in-pane Ruby tools drive UI features (gradient backgrounds, proportional fonts) other terminals can't.

Requirements

  • macOS (uses AppKit via Fiddle; no Linux/Windows support)
  • Ruby >= 3.2

Installation

gem install echoes
echoes install

echoes install drops thin Echoes.app and EchoesEmbed.app shortcuts into ~/Applications/ so the app shows up in Spotlight, Dock, and Cmd-Space. Each shortcut is a one-line wrapper that execs into the gem-bundled launcher; re-run echoes install after each gem update echoes to refresh the path. echoes uninstall removes them.

To run from a clone instead:

git clone https://github.com/amatsuda/echoes
cd echoes
bin/setup
open Echoes.app          # GUI (PTY-spawned shell per pane)
open EchoesEmbed.app     # GUI with rubish embedded per pane
bundle exec exe/echoes -t  # TTY mode

What's in the box

  • Echoes.app — terminal mode. Spawns the user's $SHELL per pane via PTY, like any other terminal emulator.
  • EchoesEmbed.app — same window/UI, but each pane runs rubish in-process via a per-pane helper subprocess. Line editing, prompt rendering, and tab completion happen natively in Echoes (no ANSI roundtrip); only command output flows over the pty.
  • Edit File… (Cmd+Shift+E) — opens an rvim-backed editor pane. Insert mode, :w, :q, search, visual mode, undo — the full vim surface. The dialog opens at the active pane's pwd.

Terminal features

  • Images — Kitty graphics protocol (PNG via f=100, raw RGB / RGBA via f=24 / f=32, zlib-compressed payloads with o=z, file-path transmission via t=f / t=t, sub-cell pixel offsets, placements with q= / a=p / a=d, z= for layering — negative z blits the image beneath cell text, the default z=0 and any positive value blit on top) and iTerm2 inline images (OSC 1337 File= with PNG / JPEG / TIFF / GIF). SVG is also rendered by both protocols — detected by content-sniffing the payload, rasterized at the cell footprint requested (c=/r= for Kitty, width=/height= for iTerm2). Path-only SVGs (paths, basic shapes, <g>, transforms) go through a native CoreGraphics fast path — synchronous, no XPC. SVGs containing <text>, <filter>, gradients, <use>, etc. fall through transparently to a WKWebView backend (slower first paint, but full CSS / SVG surface). JavaScript is disabled and external resources blocked on both paths. Animated GIFs play — frame iteration via the retained NSBitmapImageRep, with per-image timing driven off the existing 60 Hz timer and only the image's row range marked dirty per frame.
  • Desktop notifications — OSC 9 (\e]9;message\a, iTerm2 style) and OSC 777 (\e]777;notify;title;message\a, VTE style) deliver to the macOS Notification Center.
  • Programming-font ligatures=>, !=, <=, ->, etc. shape through Core Text when the active font (Fira Code, JetBrains Mono, …) ships them.
  • Synchronized output (DEC 2026) — full-frame updates from tmux, Neovim, and IDEs land atomically instead of tearing.
  • Find — Cmd+F opens the search bar; while it's focused, Cmd+R toggles regex mode and Cmd+I toggles case-insensitive matching.
  • Pane capture — a running program can snapshot its own pane to disk as raster PNG or vector PDF via \e]7772;capture (see below).

Keyboard shortcuts

Shortcut Action
Cmd+N New window
Cmd+T New tab
Cmd+W Close tab
Cmd+Shift+W Close pane
Cmd+Shift+E Edit file… (rvim pane)
Cmd+D Split pane right
Cmd+Shift+D Split pane down
Cmd+] / Cmd+[ Next / previous pane
Cmd+Shift+] / Cmd+Shift+[ Next / previous tab
Cmd+1 … Cmd+8 Jump to tab 1 … 8
Cmd+9 Jump to the last tab
drag tab in the bar Reorder, move to another window, or tear out to a new window
Cmd++ / Cmd+- / Cmd+0 Bigger / smaller / reset font
Cmd+F Find
Cmd+G / Cmd+Shift+G Find next / previous
Cmd+Shift+P Toggle mouse pointer visibility
Cmd+Shift+C Toggle copy mode
Cmd+Ctrl+F Enter / leave full screen

Configuration

Echoes reads ~/.config/echoes/echoes.conf at startup. The file is plain Ruby instance_eval'd against Echoes.config; every setter below has the same name as its config attribute.

font_family    "JetBrains Mono"
font_size      14.0
rows           24
cols           80
shell          "/bin/zsh"
scrollback_limit  10_000
tab_position   :top                # or :bottom
window_title   "Echoes"
foreground     "#e0e0e0"
background     "#1a1a2e"
cursor_color   "#b58900"
selection_color "#586e75"

Profiles (color themes)

Two profiles ship built-in (Solarized Dark, Solarized Light), so the View → Profile submenu has alternatives without any config. Declare your own — or re-declare a built-in by the same name to override it. Switching at runtime through the menu repaints the palette, fg/bg, selection, and cursor without a restart.

profile "Tokyo Night" do
  foreground   "#c0caf5"
  background   "#1a1b26"
  cursor_color "#c0caf5"
  selection_color "#283457"
  color_palette %w[
    #15161e #f7768e #9ece6a #e0af68 #7aa2f7 #bb9af7 #7dcfff #a9b1d6
    #414868 #f7768e #9ece6a #e0af68 #7aa2f7 #bb9af7 #7dcfff #c0caf5
  ]
end

default_profile "Tokyo Night"

Custom keybinds

Override any menu shortcut by action symbol. Pass an empty string to disable a default shortcut entirely.

keybind "Cmd+Shift+T", :new_tab
keybind "Cmd+K",       :toggle_find
keybind "",            :toggle_pointer    # disable the default

Available actions (one per menu item): :new_window, :new_tab, :close_tab, :close_pane, :edit_file, :split_right, :split_down, :select_next_pane, :select_previous_pane, :show_next_tab, :show_previous_tab, :select_tab_1:select_tab_9 (jump-to-tab; :select_tab_9 is "last tab"), :increase_font_size, :decrease_font_size, :reset_font_size, :toggle_find, :find_next, :find_previous, :toggle_pointer, :toggle_copy_mode.

Modifier names are case-insensitive and accept the obvious aliases: Cmd/Command/Super, Ctrl/Control, Opt/Option/Alt, Shift.

OSC extensions

Echoes private namespace (OSC 7772)

All Echoes-specific escape sequences live under OSC code 7772. Other terminals ignore unknown OSC codes, so emitters degrade gracefully.

Background painting (pane-scoped):

\e]7772;bg-color;#rrggbb\a
\e]7772;bg-gradient;type=linear:angle=N:colors=#rrggbb,#rrggbb[,...]\a
\e]7772;bg-fill;color=#rrggbb:rect=row1,col1,row2,col2\a
\e]7772;bg-clear\a

bg-fill calls accumulate, so a presentation tool can build up a slide layout (header bar, sidebar, accent stripe) on top of a base bg-color or bg-gradient. bg-clear wipes both the base layer and all fills.

Multicell glyphs (OSC-66-shaped, with Echoes-only knobs):

\e]7772;multicell;<key=value:...>;<text>\a

Accepts the standard kitty s/w/n/d/v/h keys plus:

  • f=Family Name — render the glyph(s) in a specific font. Proportional fonts (Helvetica, Noto Serif, …) are measured per-glyph at layout time so the cells reserved match the actual rendered width — Hello in Noto Serif at 2× lays out cleanly without overflow or gaps.
  • flip=h|v|hv — mirror the rendered glyph(s) horizontally, vertically, or both. Handy for direction-having emojis (e.g. flipping 🐇 to face the other way).

h= (halign) is honored for non-fractional / proportional text: the whole string lands in an s × source_chars cell block with the renderer's center / right-align math applied.

These knobs are routed through OSC 7772 rather than OSC 66 so a future kitty-spec extension claiming the same param names can't collide with ours.

Pane snapshots, multi-display, child windows:

\e]7772;capture;<absolute-path.png|.pdf>\a
\e]7772;display-info\a
\e]7772;open-window;display=N:program=<base64-argv>:fullscreen=yes|no\a
  • capture writes a snapshot of the active pane to disk. .png rasterizes via NSBitmapImageRep; anything else (default .pdf) saves vector — usually smaller and crisper because text and background gradients stay resolution-independent.
  • display-info is a sync query: the host replies on the same pty with \e]7772;display-info;<json>\a, where <json> is an array of {index, w, h, primary, current} per NSScreen. A presentation tool uses current to pick "anywhere but here" for a second-screen slide window.
  • open-window spawns a child program in a new Echoes window on the chosen display. program is the argv JSON-encoded then base64-wrapped (e.g. Base64.strict_encode64(JSON.dump(argv))). fullscreen=yes uses a borderless, above-menu-bar window covering the full screen frame; otherwise the visible frame with default chrome.

A small Ruby helper (Echoes::Client) lets in-pane Ruby tools emit these without hand-rolling escape sequences:

require 'echoes/client'

Echoes::Client.bg_gradient(from: '#1a1a2e', to: '#16213e', angle: 90)
Echoes::Client.bg_fill('#ff6b35', row1: 0, col1: 0, row2: 2, col2: 79)
Echoes::Client.styled_text("Title", scale: 3, family: "Helvetica Neue")
Echoes::Client.capture("/tmp/slide.pdf")
Echoes::Client.bg_clear

Echoes::Client.styled_text auto-routes: OSC 66 when only standard knobs are used (portable), OSC 7772 ;multicell when an Echoes-only knob like family: is set.

Compatibility with other terminals

Code Use
OSC 4 / 10 / 11 / 12 Get/set 256-color palette + default fg / bg / cursor
OSC 7 Working directory (file://host/path)
OSC 9 iTerm2-style notification — \e]9;message\a
OSC 52 Clipboard read/write
OSC 66 Kitty multicell (standard s/w/n/d/v/h only)
OSC 133 Semantic prompts (FinalTerm / iTerm2 style)
OSC 777 VTE-style notification — \e]777;notify;title;message\a
OSC 1337 iTerm2 inline images — \e]1337;File=<args>:<base64>\a
APC _G… Kitty graphics protocol (PNG, raw RGB/RGBA, zlib, file)
(sniffed) SVG payloads — auto-detected in both image protocols
DECSET 2026 Synchronized output

OSC 9 / OSC 777 notifications go through terminal-notifier when it's on $PATH, otherwise fall back to osascript's display notification primitive. Echoes-only knobs that would otherwise extend OSC 66 (font family, mirror flips) live on OSC 7772 ;multicell instead, so portable emitters can keep OSC 66 strictly kitty-spec compatible.

Development

bin/setup
bundle exec rake test       # run all tests
bundle exec exe/echoes      # launch from the working tree
bin/console                 # irb with the gem loaded

rake app syncs CFBundleVersion in both bundles' Info.plist files with Echoes::VERSION (run after a version bump).

Contributing

Bug reports and pull requests welcome at https://github.com/amatsuda/echoes.

License

MIT.