app
The Headless Deskman
Public Unreviewedby barrett-land
A browser desktop for a headless Adom container. Runs Xvfb+fluxbox+x11vnc+noVNC inside the container and maps a *.adom.cloud port so any browser (or a Hydrogen webview tab) shows the container's GUI — no SSH tunnel, no local viewer. The container hosts the page itself. (Formerly Headview.)
← Commit history
headviewdeleted−306@@ -1,306 +0,0 @@-#!/usr/bin/env bash-# headview — turn a headless Adom container into a browser-accessible desktop.-#-# Spins up Xvfb + fluxbox + x11vnc + noVNC inside the container, maps a-# *.adom.cloud port so Cloudflare serves it, and prints a URL you can open-# from any browser (or as a Hydrogen webview tab). The container hosts the-# site itself — no SSH tunnel, no local viewer.-#-# Design: AI-oriented output — every line starts with OK: or ERROR:, errors-# carry a Hint:. Colors only on a TTY (stripped when piped/logged).--set -uo pipefail--HEADVIEW_VERSION="0.1.0"--# ---------------------------------------------------------------- output helpers-if [ -t 1 ]; then C_OK=$'\033[32m\033[1m'; C_CYAN=$'\033[36m'; C_YEL=$'\033[33m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'-else C_OK=""; C_CYAN=""; C_YEL=""; C_DIM=""; C_RST=""; fi-if [ -t 2 ]; then E_ERR=$'\033[31m\033[1m'; E_DIM=$'\033[2m'; E_RST=$'\033[0m'; else E_ERR=""; E_DIM=""; E_RST=""; fi--ok() { printf '%sOK:%s %s\n' "$C_OK" "$C_RST" "$*"; }-note() { printf '%s%s%s\n' "$C_DIM" "$*" "$C_RST"; }-err() { printf '%sERROR:%s %s\n' "$E_ERR" "$E_RST" "$*" >&2; }-hint() { printf '%sHint:%s %s\n' "$E_DIM" "$E_RST" "$*" >&2; }-die() { err "$1"; [ -n "${2:-}" ] && hint "$2"; exit 1; }--# ---------------------------------------------------------------- state-STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/headview"-STATE_FILE="$STATE_DIR/state.env"-NOVNC_HOME="$HOME/.local/share/novnc" # git-clone fallback web root-mkdir -p "$STATE_DIR"--save_state() { # key value- mkdir -p "$STATE_DIR"; touch "$STATE_FILE"- grep -v "^$1=" "$STATE_FILE" 2>/dev/null > "$STATE_FILE.tmp" || true- echo "$1=$2" >> "$STATE_FILE.tmp"; mv "$STATE_FILE.tmp" "$STATE_FILE"-}-load_state() { [ -f "$STATE_FILE" ] && set -a && . "$STATE_FILE" && set +a || true; }--# ---------------------------------------------------------------- defaults-DISPLAY_NUM=":99"-GEOMETRY="1600x1000"-DEPTH="24"-VNC_PORT="5900"-WEB_PORT="6080"-PREFIX="desktop"-APP=""-USE_PASSWORD="1"--# ---------------------------------------------------------------- deps-have() { command -v "$1" >/dev/null 2>&1; }--novnc_webroot() {- if [ -f /usr/share/novnc/vnc.html ]; then echo /usr/share/novnc- elif [ -f "$NOVNC_HOME/vnc.html" ]; then echo "$NOVNC_HOME"- else echo ""; fi-}--ensure_deps() {- local need=()- have Xvfb || need+=(xvfb)- have x11vnc || need+=(x11vnc)- have fluxbox || need+=(fluxbox)- have xdpyinfo|| need+=(x11-utils)- # noVNC + websockify: prefer apt packages- if [ -z "$(novnc_webroot)" ] || ! have websockify; then need+=(novnc websockify); fi- if [ ${#need[@]} -gt 0 ]; then- note "installing: ${need[*]}"- if have sudo; then sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${need[@]}" >/dev/null 2>&1- else apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${need[@]}" >/dev/null 2>&1; fi- fi- # git-clone fallback if apt didn't provide a web root- if [ -z "$(novnc_webroot)" ]; then- note "apt novnc unavailable — cloning noVNC from GitHub"- have git || die "git not available and noVNC not installed" "sudo apt-get install -y git novnc"- [ -d "$NOVNC_HOME" ] || git clone --depth 1 -q https://github.com/novnc/noVNC "$NOVNC_HOME" || \- die "failed to fetch noVNC" "check the container has outbound network to github.com"- [ -d "$NOVNC_HOME/utils/websockify" ] || git clone --depth 1 -q https://github.com/novnc/websockify "$NOVNC_HOME/utils/websockify" || true- fi- for b in Xvfb x11vnc fluxbox; do have "$b" || die "$b still missing after install" "install it manually: sudo apt-get install -y $b"; done-}--# ---------------------------------------------------------------- self slug-container_slug() {- local j; j="$(adom-cli carbon containers current 2>/dev/null)" || return 1- printf '%s' "$j" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("slug",""))' 2>/dev/null-}--# find an existing mapped hostname whose internal port == WEB_PORT-existing_hostname_for_port() {- local port="$1" j- j="$(adom-cli carbon containers current 2>/dev/null)" || return 1- printf '%s' "$j" | python3 -c '-import sys,json-p=int(sys.argv[1]); d=json.load(sys.stdin)-for host,port in (d.get("port_mappings") or {}).items():- if int(port)==p: print(host); break-' "$port" 2>/dev/null-}--start_websockify() {- local webroot="$1"- pkill -f "websockify.*:$WEB_PORT" 2>/dev/null; pkill -f "novnc_proxy.*$WEB_PORT" 2>/dev/null; sleep 1- if have websockify; then- nohup websockify --web="$webroot" "0.0.0.0:$WEB_PORT" "localhost:$VNC_PORT" >/tmp/headview-web.log 2>&1 &- else- nohup "$NOVNC_HOME/utils/novnc_proxy" --vnc "localhost:$VNC_PORT" --listen "$WEB_PORT" >/tmp/headview-web.log 2>&1 &- fi-}--# ---------------------------------------------------------------- commands-cmd_up() {- ensure_deps- local webroot; webroot="$(novnc_webroot)"- [ -n "$webroot" ] || die "noVNC web root not found" "run: sudo apt-get install -y novnc"-- export DISPLAY="$DISPLAY_NUM"-- # 1) Xvfb- if ! pgrep -f "Xvfb $DISPLAY_NUM" >/dev/null; then- nohup Xvfb "$DISPLAY_NUM" -screen 0 "${GEOMETRY}x${DEPTH}" >/tmp/headview-xvfb.log 2>&1 &- sleep 2- fi- pgrep -f "Xvfb $DISPLAY_NUM" >/dev/null || die "Xvfb failed to start" "check /tmp/headview-xvfb.log"-- # 2) window manager- pgrep -f "fluxbox" >/dev/null || { nohup fluxbox >/tmp/headview-fluxbox.log 2>&1 & sleep 1; }-- # 3) password- local pw=""- if [ "$USE_PASSWORD" = "1" ]; then- pw="${HEADVIEW_PASSWORD:-}"- if [ -z "$pw" ]; then pw="hv$(od -An -N4 -tx1 /dev/urandom | tr -d ' \n')"; fi- mkdir -p "$HOME/.vnc"- x11vnc -storepasswd "$pw" "$HOME/.vnc/passwd" >/dev/null 2>&1- save_state PASSWORD "$pw"- else- save_state PASSWORD ""- fi-- # 4) x11vnc (loopback only — noVNC is the public face)- pkill -f "x11vnc.*:$VNC_PORT" 2>/dev/null; pkill -f "x11vnc -display $DISPLAY_NUM" 2>/dev/null; sleep 1- if [ "$USE_PASSWORD" = "1" ]; then- nohup x11vnc -display "$DISPLAY_NUM" -localhost -rfbauth "$HOME/.vnc/passwd" -forever -shared -rfbport "$VNC_PORT" -bg >/tmp/headview-x11vnc.log 2>&1- else- nohup x11vnc -display "$DISPLAY_NUM" -localhost -nopw -forever -shared -rfbport "$VNC_PORT" -bg >/tmp/headview-x11vnc.log 2>&1- fi- sleep 2- grep -q "Listening for VNC" /tmp/headview-x11vnc.log || die "x11vnc failed to listen on $VNC_PORT" "check /tmp/headview-x11vnc.log"-- # 5) noVNC web bridge- start_websockify "$webroot"; sleep 3- curl -s -o /dev/null --max-time 6 "http://127.0.0.1:$WEB_PORT/vnc.html" || die "noVNC not serving on $WEB_PORT" "check /tmp/headview-web.log"-- # 6) port mapping- local host; host="$(existing_hostname_for_port "$WEB_PORT")"- if [ -z "$host" ]; then- local slug; slug="$(container_slug)"- [ -n "$slug" ] || die "could not determine this container's slug" "run inside an Adom container; check: adom-cli carbon containers current"- note "adding port mapping ${WEB_PORT} -> ${PREFIX}-*.adom.cloud"- local out; out="$(adom-cli carbon containers port-add --port "$WEB_PORT" --prefix "$PREFIX" "$slug" 2>&1)" || die "port-add failed: $out" "check adom-cli auth"- host="$(printf '%s' "$out" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("hostname",""))' 2>/dev/null)"- fi- [ -n "$host" ] || die "no mapped hostname" "run: adom-cli carbon containers current # to inspect mappings"-- # 7) optional app- if [ -n "$APP" ]; then DISPLAY="$DISPLAY_NUM" nohup $APP >/tmp/headview-app.log 2>&1 & note "launched app: $APP"; fi-- save_state DISPLAY_NUM "$DISPLAY_NUM"- save_state GEOMETRY "$GEOMETRY"- save_state VNC_PORT "$VNC_PORT"- save_state WEB_PORT "$WEB_PORT"- save_state HV_HOST "$host"-- local url="https://$host/vnc.html?autoconnect=1&resize=scale"- ok "Container desktop is live and hosted by the container itself."- note " URL: $url"- [ "$USE_PASSWORD" = "1" ] && note " Password: $pw"- note " Display: $DISPLAY_NUM (${GEOMETRY}) VNC:$VNC_PORT web:$WEB_PORT"- note "Open it in any browser, or as a Hydrogen tab: headview open"- note "Launch apps onto it with: DISPLAY=$DISPLAY_NUM your-app &"-}--cmd_down() {- load_state- local removed_map=""- pkill -f "websockify.*:${WEB_PORT:-6080}" 2>/dev/null- pkill -f "novnc_proxy" 2>/dev/null- pkill -f "x11vnc.*:${VNC_PORT:-5900}" 2>/dev/null- pkill -f "x11vnc -display ${DISPLAY_NUM:-:99}" 2>/dev/null- pkill -f "fluxbox" 2>/dev/null- pkill -f "Xvfb ${DISPLAY_NUM:-:99}" 2>/dev/null- if [ "${1:-}" = "--keep-mapping" ]; then :; else- if [ -n "${HV_HOST:-}" ]; then- local slug sub; slug="$(container_slug)"; sub="${HV_HOST%.adom.cloud}"- if [ -n "$slug" ] && [ -n "$sub" ]; then- adom-cli carbon containers port-remove "$slug" "$sub" >/dev/null 2>&1 && removed_map=" and removed port mapping ($sub)" || true- fi- fi- fi- ok "Tore down headview desktop$removed_map."-}--cmd_status() {- load_state- local up=0- printf 'Xvfb: ' ; pgrep -f "Xvfb ${DISPLAY_NUM:-:99}" >/dev/null && { echo running; up=1; } || echo stopped- printf 'x11vnc: ' ; pgrep -f "x11vnc" >/dev/null && echo running || echo stopped- printf 'websockify: ' ; pgrep -f "websockify|novnc_proxy" >/dev/null && echo running || echo stopped- if [ -n "${HV_HOST:-}" ]; then note "URL: https://$HV_HOST/vnc.html?autoconnect=1&resize=scale"; [ -n "${PASSWORD:-}" ] && note "Password: $PASSWORD"; fi- [ "$up" = 1 ] && ok "headview is up." || ok "headview is not running."-}--cmd_url() {- load_state- [ -n "${HV_HOST:-}" ] || die "no desktop is running" "start one with: headview up"- echo "https://$HV_HOST/vnc.html?autoconnect=1&resize=scale"-}--cmd_password() {- load_state- [ -n "${PASSWORD:-}" ] && ok "VNC password: $PASSWORD" || ok "no password set (passwordless mode)"-}--cmd_open() {- load_state- [ -n "${HV_HOST:-}" ] || die "no desktop is running" "start one with: headview up"- [ -n "${VSCODE_PROXY_URI:-}" ] || die "Hydrogen editor not detected (VSCODE_PROXY_URI unset)" "run this from a Hydrogen workspace terminal, or open the URL in a browser: headview url"- local url="https://$HV_HOST/vnc.html?autoconnect=1&resize=scale"- adom-cli hydrogen webview open-or-refresh --name "Container Desktop" --url "$url" && ok "Opened 'Container Desktop' webview tab." || die "failed to open webview" "check adom-cli hydrogen webview"-}--cmd_install() {- local skill_dir="$HOME/.claude/skills/headview"- mkdir -p "$skill_dir"- local url="https://wiki.adom.inc/static/apps/headview/SKILL.md"- if curl -fsSL --max-time 5 "$url" -o "$skill_dir/SKILL.md" 2>/dev/null; then- ok "Installed headview skill (fetched from wiki)."- elif [ -f "$(dirname "$0")/SKILL.md" ]; then- cp "$(dirname "$0")/SKILL.md" "$skill_dir/SKILL.md"; ok "Installed headview skill (bundled copy; wiki unreachable)."- else- die "could not install skill" "SKILL.md not found next to the binary and wiki unreachable"- fi-}--usage() {- cat <<EOF-headview v$HEADVIEW_VERSION — a browser desktop for a headless Adom container--USAGE: headview <command> [options]--COMMANDS:- up Start the desktop (Xvfb+fluxbox+x11vnc+noVNC), map a port, print the URL- down Stop everything and remove the port mapping (--keep-mapping to keep it)- status Show what's running and the URL/password- url Print the desktop URL- password Print the VNC password- open Open the desktop as a Hydrogen webview tab (needs a Hydrogen terminal)- install Install the headview skill into ~/.claude/skills/- version Print version--'up' OPTIONS:- --display :N X display (default :99)- --geometry WxH screen size (default 1600x1000)- --vnc-port N internal VNC port (default 5900)- --web-port N internal noVNC/web port to map (default 6080)- --prefix NAME subdomain prefix for the mapping (default desktop)- --app "CMD" launch CMD on the desktop after starting- --no-password run passwordless (only do this behind other access control)--EXAMPLES:- headview up- headview up --geometry 1920x1080 --app "kicad"- headview url- headview down-EOF-}--# ---------------------------------------------------------------- arg parse-cmd="${1:-}"; shift || true-while [ $# -gt 0 ]; do- case "$1" in- --display) DISPLAY_NUM="$2"; shift 2;;- --geometry) GEOMETRY="$2"; shift 2;;- --vnc-port) VNC_PORT="$2"; shift 2;;- --web-port) WEB_PORT="$2"; shift 2;;- --prefix) PREFIX="$2"; shift 2;;- --app) APP="$2"; shift 2;;- --no-password) USE_PASSWORD="0"; shift;;- --keep-mapping) KEEP_MAPPING="--keep-mapping"; shift;;- *) shift;;- esac-done--case "$cmd" in- up) cmd_up;;- down) cmd_down "${KEEP_MAPPING:-}";;- status) cmd_status;;- url) cmd_url;;- password) cmd_password;;- open) cmd_open;;- install) cmd_install;;- version|--version|-V) ok "headview v$HEADVIEW_VERSION";;- ""|help|-h|--help) usage;;- *) err "unknown command: $cmd"; hint "run 'headview help' for usage"; exit 1;;-esac