Skip to content

Hal Overlay

A keyboard-only "notch" that hangs from the top of the screen and drives opencode from one line of text: type, Enter, watch the answer stream in. Built 2026-10-24 in ~/Projects/hal-overlay/, inspired by Void minus voice.

Overview

┌──────────────────────────────────────────────┐
│  ◉  HAL                                esc   │
│     idle                                     │
│  ❯ fix the failing tests                     │
│  ⚙ bash                                      │
│  Done — the assertion in test_x was stale…   │
│  ❯ ask hal…                            enter │
└──────────────────────────────────────────────┘
piece implementation
window GTK4 + gtk4-layer-shell, layer OVERLAY, anchored top, keyboard grabbed only while visible
face WebKit view of face.html (Hal eye as inline SVG, CSS states: idle/thinking/working/error)
agent opencode run --format json --auto [--session sid] <prompt>, NDJSON streamed
session sessionID from the first event reused for follow-ups
height ResizeObserver in the page reports content height; the window resizes to fit

Details

Usage

~/Projects/hal-overlay/hal                  # toggle
~/Projects/hal-overlay/hal --start-hidden   # bind socket, no surface (autostart)
~/Projects/hal-overlay/hal --quit
~/Projects/hal-overlay/hal --dir ~/Projects/foo

Single instance via $XDG_RUNTIME_DIR/hal-overlay.sock, so a compositor keybind is instant. It appears on whichever monitor is focused.

env default meaning
HAL_DIR ~ working directory for opencode run
HAL_MODEL opencode default passed as --model provider/model
HAL_TOP_MARGIN -48 Hyprland offsets top-anchored layer surfaces 48 px; -48 = flush to y=0, 26 = hang under the 74 px bar

Wayland gotchas (the expensive part)

  1. LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so is mandatory. Otherwise the surface silently degrades to a normal toplevel with Failed to initialize layer surface, GTK4 Layer Shell may have been linked after libwayland. The hal wrapper sets it and strips it again before spawning opencode.
  2. webkit2gtk-4.1 is GTK3 — with GTK4 you must use WebKit 6.0 (webkitgtk-6.0).
  3. GTK4 has no Gtk.main() — use GLib.MainLoop().
  4. Hyprland puts top-anchored layer surfaces 48 px down; a negative margin is the workaround.

Design decisions

  • No voice. Dropped faster-whisper/Kokoro/PTT entirely — keyboard only.
  • --auto is hardcoded, so tool calls are never prompted for. An approval bubble is the obvious next step if it starts being pointed at unfamiliar repos.
  • The face is HTML because Void's is; that makes the eye trivially animatable and swappable (see the portrait in ~/Pictures/hal-portrait.svg).

Status / next ideas

Done: overlay, streaming replies, session continuity, auto-height, --start-hidden, --quit. Open: keybind in keybinds.lua, approval bubble, prompt history, /model + /agent switches, tool-call detail (command + diffstat), theme sync with Noctalia's palette, selection → prompt.

  • [[Index]]
  • [[Kitty Custom Shaders]]
  • [[Hyprland Tweaks]]
  • [[OpenCode Voice Input]] — the voice-based idea this deliberately skips