Add web deployment layer and docs for browser-based setup

Pull ttyd/zellij/clide-launcher scripts into deploy/ so the web access
layer lives with the project. Zellij is configured in locked mode with a
bare layout so it only provides session persistence without intercepting
keys or showing UI. Remove Ctrl+G goto-line binding from Clide to avoid
conflict with Zellij's unlock key.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 01:00:20 +01:00
co-authored by Claude Opus 4.6
parent aff03a2e9b
commit dba0e4805f
7 changed files with 865 additions and 5 deletions
-5
View File
@@ -175,7 +175,6 @@ class ClideApp(App[None]):
Binding("ctrl+s", "save_file", "Save", priority=True),
Binding("alt+s", "save_file", "Save", show=False, priority=True),
Binding("ctrl+z", "undo", "Undo", show=False, priority=True),
Binding("alt+l", "goto_line", "Go to Line", show=False),
# Theme
Binding("alt+t", "select_theme", "Theme", priority=True),
]
@@ -607,10 +606,6 @@ class ClideApp(App[None]):
else:
self.notify("Undo not available", severity="warning")
def action_goto_line(self) -> None:
"""Go to line dialog."""
# TODO: Implement go to line
pass
def action_quick_open(self) -> None:
"""Quick file open."""
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
set -e
export TERM=xterm-256color
export COLORTERM=truecolor
PROJECTS_DIR="/mnt/media/Projects"
CLIDE_BIN="/mnt/media/Projects/clide/.venv/bin/clide"
# Parse URL argument from ttyd (format: project=name or just name)
# ttyd passes arguments after the command
PROJECT_ARG="${1:-}"
# Extract project name (handle "project=foo" or just "foo")
if [[ "$PROJECT_ARG" == project=* ]]; then
PROJECT="${PROJECT_ARG#project=}"
elif [[ -n "$PROJECT_ARG" ]]; then
PROJECT="$PROJECT_ARG"
else
PROJECT=""
fi
# If no project specified, show selector
if [[ -z "$PROJECT" ]]; then
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ Clide Project Selector ║"
echo "╠══════════════════════════════════════════════════════════╣"
echo "║ Add ?project=NAME to URL to connect directly ║"
echo "║ Example: code.schweitz.net?project=system-management ║"
echo "╠══════════════════════════════════════════════════════════╣"
echo "║ Available projects: ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
# List git repos in Projects dir
for dir in "$PROJECTS_DIR"/*/; do
if [[ -d "$dir/.git" ]]; then
name=$(basename "$dir")
echo " • $name"
fi
done
echo ""
echo "Active sessions:"
zellij list-sessions 2>/dev/null | grep "^clide-" | sed 's/^/ • /' || echo " (none)"
echo ""
read -p "Enter project name: " PROJECT
fi
# Validate project exists
WORKDIR="$PROJECTS_DIR/$PROJECT"
if [[ ! -d "$WORKDIR" ]]; then
echo "Error: Project '$PROJECT' not found in $PROJECTS_DIR"
echo "Available: $(ls -1 "$PROJECTS_DIR" | tr '\n' ' ')"
exit 1
fi
SESSION_NAME="clide-$PROJECT"
# Check if session exists (live or dead)
cd "$WORKDIR"
if zellij list-sessions 2>/dev/null | grep -q "$SESSION_NAME"; then
# Attach to existing session (resurrects dead sessions automatically)
exec zellij attach "$SESSION_NAME"
else
# Create new session
exec zellij --session "$SESSION_NAME" options --default-shell "$CLIDE_BIN" --default-cwd "$WORKDIR"
fi
+27
View File
@@ -0,0 +1,27 @@
[Unit]
Description=Clide Web Terminal (ttyd + zellij)
After=network.target
[Service]
Type=simple
User=jpmschweitzer
Group=jpmschweitzer
WorkingDirectory=/mnt/media/Projects
# ttyd options:
# -p 8888 : Port (same as Jupyter was using)
# -W : Writable (allows input)
# -a : Allow URL arguments (passes ?project=X as command arg)
# -t fontSize=14: Terminal font size
ExecStart=/usr/local/bin/ttyd -p 8888 -W -a -t fontSize=14 /usr/local/bin/clide-launcher
Restart=on-failure
RestartSec=10
# Environment for 256-color + truecolor
Environment="TERM=xterm-256color"
Environment="COLORTERM=truecolor"
Environment="PATH=/home/jpmschweitzer/.nvm/versions/node/v24.11.0/bin:/usr/local/bin:/mnt/media/Projects/clide/.venv/bin:/usr/bin:/bin"
[Install]
WantedBy=multi-user.target
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
# Clide Web-Access Layer Installation Script
# Run with: sudo bash deploy/install-clide-web.sh
set -e
echo "=== Clide Web-Access Layer Installation ==="
echo ""
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (sudo)"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUDO_USER="${SUDO_USER:-$(logname)}"
USER_HOME=$(eval echo ~"$SUDO_USER")
echo "[1/6] Installing ttyd..."
curl -L https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64 -o /usr/local/bin/ttyd
chmod +x /usr/local/bin/ttyd
ttyd --version
echo ""
echo "[2/6] Installing zellij..."
curl -L https://github.com/zellij-org/zellij/releases/latest/download/zellij-x86_64-unknown-linux-musl.tar.gz | tar -xz -C /usr/local/bin
chmod +x /usr/local/bin/zellij
zellij --version
echo ""
echo "[3/6] Installing clide-launcher..."
cp "$SCRIPT_DIR/clide-launcher" /usr/local/bin/clide-launcher
chmod +x /usr/local/bin/clide-launcher
echo ""
echo "[4/6] Installing Zellij config..."
ZELLIJ_CONFIG_DIR="$USER_HOME/.config/zellij"
ZELLIJ_LAYOUTS_DIR="$ZELLIJ_CONFIG_DIR/layouts"
mkdir -p "$ZELLIJ_LAYOUTS_DIR"
cp "$SCRIPT_DIR/zellij/config.kdl" "$ZELLIJ_CONFIG_DIR/config.kdl"
cp "$SCRIPT_DIR/zellij/bare.kdl" "$ZELLIJ_LAYOUTS_DIR/bare.kdl"
chown -R "$SUDO_USER:$SUDO_USER" "$ZELLIJ_CONFIG_DIR"
echo ""
echo "[5/6] Installing systemd service..."
cp "$SCRIPT_DIR/clide-web.service" /etc/systemd/system/clide-web.service
echo ""
echo "[6/6] Starting service..."
systemctl daemon-reload
systemctl enable clide-web
systemctl start clide-web
echo ""
echo "=== Installation Complete ==="
echo ""
echo "Status:"
systemctl status clide-web --no-pager || true
echo ""
echo "Port check:"
ss -tlnp | grep 8888 || echo "Warning: Port 8888 not listening yet"
echo ""
echo "Access:"
echo " Local: http://localhost:8888"
echo " Direct: http://localhost:8888?project=system-management"
echo " External: https://code.schweitz.net?project=system-management"
echo ""
echo "Don't forget to update NPM to enable WebSocket support for code.schweitz.net"
+4
View File
@@ -0,0 +1,4 @@
// Bare layout - no UI, just the pane
layout {
pane
}
+539
View File
@@ -0,0 +1,539 @@
//
// THIS FILE WAS AUTOGENERATED BY ZELLIJ, THE PREVIOUS FILE AT THIS LOCATION WAS COPIED TO: /home/jpmschweitzer/.config/zellij/config.kdl.bak
//
keybinds clear-defaults=true {
locked {
bind "Ctrl g" { SwitchToMode "normal"; }
}
pane {
bind "left" { MoveFocus "left"; }
bind "down" { MoveFocus "down"; }
bind "up" { MoveFocus "up"; }
bind "right" { MoveFocus "right"; }
bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; }
bind "d" { NewPane "down"; SwitchToMode "normal"; }
bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "normal"; }
bind "f" { ToggleFocusFullscreen; SwitchToMode "normal"; }
bind "h" { MoveFocus "left"; }
bind "i" { TogglePanePinned; SwitchToMode "normal"; }
bind "j" { MoveFocus "down"; }
bind "k" { MoveFocus "up"; }
bind "l" { MoveFocus "right"; }
bind "n" { NewPane; SwitchToMode "normal"; }
bind "p" { SwitchFocus; }
bind "Ctrl p" { SwitchToMode "normal"; }
bind "r" { NewPane "right"; SwitchToMode "normal"; }
bind "s" { NewPane "stacked"; SwitchToMode "normal"; }
bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; }
bind "z" { TogglePaneFrames; SwitchToMode "normal"; }
}
tab {
bind "left" { GoToPreviousTab; }
bind "down" { GoToNextTab; }
bind "up" { GoToPreviousTab; }
bind "right" { GoToNextTab; }
bind "1" { GoToTab 1; SwitchToMode "normal"; }
bind "2" { GoToTab 2; SwitchToMode "normal"; }
bind "3" { GoToTab 3; SwitchToMode "normal"; }
bind "4" { GoToTab 4; SwitchToMode "normal"; }
bind "5" { GoToTab 5; SwitchToMode "normal"; }
bind "6" { GoToTab 6; SwitchToMode "normal"; }
bind "7" { GoToTab 7; SwitchToMode "normal"; }
bind "8" { GoToTab 8; SwitchToMode "normal"; }
bind "9" { GoToTab 9; SwitchToMode "normal"; }
bind "[" { BreakPaneLeft; SwitchToMode "normal"; }
bind "]" { BreakPaneRight; SwitchToMode "normal"; }
bind "b" { BreakPane; SwitchToMode "normal"; }
bind "h" { GoToPreviousTab; }
bind "j" { GoToNextTab; }
bind "k" { GoToPreviousTab; }
bind "l" { GoToNextTab; }
bind "n" { NewTab; SwitchToMode "normal"; }
bind "r" { SwitchToMode "renametab"; TabNameInput 0; }
bind "s" { ToggleActiveSyncTab; SwitchToMode "normal"; }
bind "Ctrl t" { SwitchToMode "normal"; }
bind "x" { CloseTab; SwitchToMode "normal"; }
bind "tab" { ToggleTab; }
}
resize {
bind "left" { Resize "Increase left"; }
bind "down" { Resize "Increase down"; }
bind "up" { Resize "Increase up"; }
bind "right" { Resize "Increase right"; }
bind "+" { Resize "Increase"; }
bind "-" { Resize "Decrease"; }
bind "=" { Resize "Increase"; }
bind "H" { Resize "Decrease left"; }
bind "J" { Resize "Decrease down"; }
bind "K" { Resize "Decrease up"; }
bind "L" { Resize "Decrease right"; }
bind "h" { Resize "Increase left"; }
bind "j" { Resize "Increase down"; }
bind "k" { Resize "Increase up"; }
bind "l" { Resize "Increase right"; }
bind "Ctrl n" { SwitchToMode "normal"; }
}
move {
bind "left" { MovePane "left"; }
bind "down" { MovePane "down"; }
bind "up" { MovePane "up"; }
bind "right" { MovePane "right"; }
bind "h" { MovePane "left"; }
bind "Ctrl h" { SwitchToMode "normal"; }
bind "j" { MovePane "down"; }
bind "k" { MovePane "up"; }
bind "l" { MovePane "right"; }
bind "n" { MovePane; }
bind "p" { MovePaneBackwards; }
bind "tab" { MovePane; }
}
scroll {
bind "e" { EditScrollback; SwitchToMode "normal"; }
bind "s" { SwitchToMode "entersearch"; SearchInput 0; }
}
search {
bind "c" { SearchToggleOption "CaseSensitivity"; }
bind "n" { Search "down"; }
bind "o" { SearchToggleOption "WholeWord"; }
bind "p" { Search "up"; }
bind "w" { SearchToggleOption "Wrap"; }
}
session {
bind "a" {
LaunchOrFocusPlugin "zellij:about" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
bind "c" {
LaunchOrFocusPlugin "configuration" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
bind "Ctrl o" { SwitchToMode "normal"; }
bind "p" {
LaunchOrFocusPlugin "plugin-manager" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
bind "s" {
LaunchOrFocusPlugin "zellij:share" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
bind "w" {
LaunchOrFocusPlugin "session-manager" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
}
shared_except "locked" {
bind "Alt left" { MoveFocusOrTab "left"; }
bind "Alt down" { MoveFocus "down"; }
bind "Alt up" { MoveFocus "up"; }
bind "Alt right" { MoveFocusOrTab "right"; }
bind "Alt +" { Resize "Increase"; }
bind "Alt -" { Resize "Decrease"; }
bind "Alt =" { Resize "Increase"; }
bind "Alt [" { PreviousSwapLayout; }
bind "Alt ]" { NextSwapLayout; }
bind "Alt f" { ToggleFloatingPanes; }
bind "Ctrl g" { SwitchToMode "locked"; }
bind "Alt h" { MoveFocusOrTab "left"; }
bind "Alt i" { MoveTab "left"; }
bind "Alt j" { MoveFocus "down"; }
bind "Alt k" { MoveFocus "up"; }
bind "Alt l" { MoveFocusOrTab "right"; }
bind "Alt n" { NewPane; }
bind "Alt o" { MoveTab "right"; }
bind "Alt p" { TogglePaneInGroup; }
bind "Alt Shift p" { ToggleGroupMarking; }
bind "Ctrl q" { Quit; }
}
shared_except "locked" "move" {
bind "Ctrl h" { SwitchToMode "move"; }
}
shared_except "locked" "session" {
bind "Ctrl o" { SwitchToMode "session"; }
}
shared_except "locked" "scroll" "search" "tmux" {
bind "Ctrl b" { SwitchToMode "tmux"; }
}
shared_except "locked" "scroll" "search" {
bind "Ctrl s" { SwitchToMode "scroll"; }
}
shared_except "locked" "tab" {
bind "Ctrl t" { SwitchToMode "tab"; }
}
shared_except "locked" "pane" {
bind "Ctrl p" { SwitchToMode "pane"; }
}
shared_except "locked" "resize" {
bind "Ctrl n" { SwitchToMode "resize"; }
}
shared_except "normal" "locked" "entersearch" {
bind "enter" { SwitchToMode "normal"; }
}
shared_except "normal" "locked" "entersearch" "renametab" "renamepane" {
bind "esc" { SwitchToMode "normal"; }
}
shared_among "pane" "tmux" {
bind "x" { CloseFocus; SwitchToMode "normal"; }
}
shared_among "scroll" "search" {
bind "PageDown" { PageScrollDown; }
bind "PageUp" { PageScrollUp; }
bind "left" { PageScrollUp; }
bind "down" { ScrollDown; }
bind "up" { ScrollUp; }
bind "right" { PageScrollDown; }
bind "Ctrl b" { PageScrollUp; }
bind "Ctrl c" { ScrollToBottom; SwitchToMode "normal"; }
bind "d" { HalfPageScrollDown; }
bind "Ctrl f" { PageScrollDown; }
bind "h" { PageScrollUp; }
bind "j" { ScrollDown; }
bind "k" { ScrollUp; }
bind "l" { PageScrollDown; }
bind "Ctrl s" { SwitchToMode "normal"; }
bind "u" { HalfPageScrollUp; }
}
entersearch {
bind "Ctrl c" { SwitchToMode "scroll"; }
bind "esc" { SwitchToMode "scroll"; }
bind "enter" { SwitchToMode "search"; }
}
renametab {
bind "esc" { UndoRenameTab; SwitchToMode "tab"; }
}
shared_among "renametab" "renamepane" {
bind "Ctrl c" { SwitchToMode "normal"; }
}
renamepane {
bind "esc" { UndoRenamePane; SwitchToMode "pane"; }
}
shared_among "session" "tmux" {
bind "d" { Detach; }
}
tmux {
bind "left" { MoveFocus "left"; SwitchToMode "normal"; }
bind "down" { MoveFocus "down"; SwitchToMode "normal"; }
bind "up" { MoveFocus "up"; SwitchToMode "normal"; }
bind "right" { MoveFocus "right"; SwitchToMode "normal"; }
bind "space" { NextSwapLayout; }
bind "\"" { NewPane "down"; SwitchToMode "normal"; }
bind "%" { NewPane "right"; SwitchToMode "normal"; }
bind "," { SwitchToMode "renametab"; }
bind "[" { SwitchToMode "scroll"; }
bind "Ctrl b" { Write 2; SwitchToMode "normal"; }
bind "c" { NewTab; SwitchToMode "normal"; }
bind "h" { MoveFocus "left"; SwitchToMode "normal"; }
bind "j" { MoveFocus "down"; SwitchToMode "normal"; }
bind "k" { MoveFocus "up"; SwitchToMode "normal"; }
bind "l" { MoveFocus "right"; SwitchToMode "normal"; }
bind "n" { GoToNextTab; SwitchToMode "normal"; }
bind "o" { FocusNextPane; }
bind "p" { GoToPreviousTab; SwitchToMode "normal"; }
bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; }
}
}
// Plugin aliases - can be used to change the implementation of Zellij
// changing these requires a restart to take effect
plugins {
about location="zellij:about"
compact-bar location="zellij:compact-bar"
configuration location="zellij:configuration"
filepicker location="zellij:strider" {
cwd "/"
}
plugin-manager location="zellij:plugin-manager"
session-manager location="zellij:session-manager"
status-bar location="zellij:status-bar"
strider location="zellij:strider"
tab-bar location="zellij:tab-bar"
welcome-screen location="zellij:session-manager" {
welcome_screen true
}
}
// Plugins to load in the background when a new session starts
// eg. "file:/path/to/my-plugin.wasm"
// eg. "https://example.com/my-plugin.wasm"
load_plugins {
}
web_client {
font "monospace"
}
// Use a simplified UI without special fonts (arrow glyphs)
// Options:
// - true
// - false (Default)
//
// simplified_ui true
// Choose the theme that is specified in the themes section.
// Default: default
//
// theme "dracula"
// Choose the base input mode of zellij.
// Default: normal
//
default_mode "locked"
// Choose the path to the default shell that zellij will use for opening new panes
// Default: $SHELL
//
default_shell "/mnt/media/Projects/clide/.venv/bin/clide"
// Choose the path to override cwd that zellij will use for opening new panes
//
default_cwd "/mnt/media/Projects/clide"
// The name of the default layout to load on startup
// Default: "default"
//
default_layout "bare"
// Disable pane frames
pane_frames false
// The folder in which Zellij will look for layouts
// (Requires restart)
//
// layout_dir "/tmp"
// The folder in which Zellij will look for themes
// (Requires restart)
//
// theme_dir "/tmp"
// Toggle enabling the mouse mode.
// On certain configurations, or terminals this could
// potentially interfere with copying text.
// Options:
// - true (default)
// - false
//
// mouse_mode false
// Toggle having pane frames around the panes
// Options:
// - true (default, enabled)
// - false
//
// pane_frames false
// When attaching to an existing session with other users,
// should the session be mirrored (true)
// or should each user have their own cursor (false)
// (Requires restart)
// Default: false
//
// mirror_session true
// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP
// eg. when terminal window with an active zellij session is closed
// (Requires restart)
// Options:
// - detach (Default)
// - quit
//
// on_force_close "quit"
// Configure the scroll back buffer size
// This is the number of lines zellij stores for each pane in the scroll back
// buffer. Excess number of lines are discarded in a FIFO fashion.
// (Requires restart)
// Valid values: positive integers
// Default value: 10000
//
// scroll_buffer_size 10000
// Provide a command to execute when copying text. The text will be piped to
// the stdin of the program to perform the copy. This can be used with
// terminal emulators which do not support the OSC 52 ANSI control sequence
// that will be used by default if this option is not set.
// Examples:
//
// copy_command "xclip -selection clipboard" // x11
// copy_command "wl-copy" // wayland
// copy_command "pbcopy" // osx
//
// copy_command "pbcopy"
// Choose the destination for copied text
// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.
// Does not apply when using copy_command.
// Options:
// - system (default)
// - primary
//
// copy_clipboard "primary"
// Enable automatic copying (and clearing) of selection when releasing mouse
// Default: true
//
// copy_on_select true
// Path to the default editor to use to edit pane scrollbuffer
// Default: $EDITOR or $VISUAL
// scrollback_editor "/usr/bin/vim"
// A fixed name to always give the Zellij session.
// Consider also setting `attach_to_session true,`
// otherwise this will error if such a session exists.
// Default: <RANDOM>
//
// session_name "My singleton session"
// When `session_name` is provided, attaches to that session
// if it is already running or creates it otherwise.
// Default: false
//
// attach_to_session true
// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible
// Options:
// - true (default)
// - false
//
// auto_layout false
// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected
// Options:
// - true (default)
// - false
//
// session_serialization false
// Whether pane viewports are serialized along with the session, default is false
// Options:
// - true
// - false (default)
//
// serialize_pane_viewport false
// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0
// defaults to the scrollback size. If this number is higher than the scrollback size, it will
// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.
//
// scrollback_lines_to_serialize 10000
// Enable or disable the rendering of styled and colored underlines (undercurl).
// May need to be disabled for certain unsupported terminals
// (Requires restart)
// Default: true
//
// styled_underlines false
// How often in seconds sessions are serialized
//
// serialization_interval 10000
// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know
// metadata info on this session)
// (Requires restart)
// Default: false
//
// disable_session_metadata false
// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)
// (Requires restart)
// Default: true (if the host terminal supports it)
//
// support_kitty_keyboard_protocol false
// Whether to make sure a local web server is running when a new Zellij session starts.
// This web server will allow creating new sessions and attaching to existing ones that have
// opted in to being shared in the browser.
// When enabled, navigate to http://127.0.0.1:8082
// (Requires restart)
//
// Note: a local web server can still be manually started from within a Zellij session or from the CLI.
// If this is not desired, one can use a version of Zellij compiled without
// `web_server_capability`
//
// Possible values:
// - true
// - false
// Default: false
//
// web_server false
// Whether to allow sessions started in the terminal to be shared through a local web server, assuming one is
// running (see the `web_server` option for more details).
// (Requires restart)
//
// Note: This is an administrative separation and not intended as a security measure.
//
// Possible values:
// - "on" (allow web sharing through the local web server if it
// is online)
// - "off" (do not allow web sharing unless sessions explicitly opt-in to it)
// - "disabled" (do not allow web sharing and do not permit sessions started in the terminal to opt-in to it)
// Default: "off"
//
// web_sharing "off"
// A path to a certificate file to be used when setting up the web client to serve the
// connection over HTTPs
//
// web_server_cert "/path/to/cert.pem"
// A path to a key file to be used when setting up the web client to serve the
// connection over HTTPs
//
// web_server_key "/path/to/key.pem"
/// Whether to enforce https connections to the web server when it is bound to localhost
/// (127.0.0.0/8)
///
/// Note: https is ALWAYS enforced when bound to non-local interfaces
///
/// Default: false
//
// enforce_https_for_localhost false
// Whether to stack panes when resizing beyond a certain size
// Default: true
//
// stacked_resize false
// Whether to show tips on startup
// Default: true
//
show_startup_tips false
// Whether to show release notes on first version run
// Default: true
//
// show_release_notes false
// Whether to enable mouse hover effects and pane grouping functionality
// default is true
// advanced_mouse_actions false
// The ip address the web server should listen on when it starts
// Default: "127.0.0.1"
// (Requires restart)
// web_server_ip "127.0.0.1"
// The port the web server should listen on when it starts
// Default: 8082
// (Requires restart)
// web_server_port 8082
// A command to run (will be wrapped with sh -c and provided the RESURRECT_COMMAND env variable)
// after Zellij attempts to discover a command inside a pane when resurrecting sessions, the STDOUT
// of this command will be used instead of the discovered RESURRECT_COMMAND
// can be useful for removing wrappers around commands
// Note: be sure to escape backslashes and similar characters properly
// post_command_discovery_hook "echo $RESURRECT_COMMAND | sed <your_regex_here>"
+159
View File
@@ -0,0 +1,159 @@
# Web Deployment
Clide runs in a browser via a three-layer stack:
```
Browser (code.schweitz.net)
└─ ttyd (web terminal server, port 8888)
└─ zellij (session persistence only)
└─ clide (Textual TUI)
└─ claude code (embedded PTY)
```
## Components
| Layer | Purpose | Config |
|-------|---------|--------|
| **ttyd** | Serves terminal over WebSocket | systemd service on port 8888 |
| **zellij** | Session reconnection (detach/reattach) | Locked mode, bare layout, no UI |
| **clide** | TUI IDE wrapper around Claude Code | Zellij's default shell |
## Installation
From the clide project root:
```bash
sudo bash deploy/install-clide-web.sh
```
This installs:
1. `ttyd` binary to `/usr/local/bin/`
2. `zellij` binary to `/usr/local/bin/`
3. `clide-launcher` script to `/usr/local/bin/`
4. Zellij config to `~/.config/zellij/` (locked mode + bare layout)
5. `clide-web.service` systemd unit
6. Enables and starts the service
### Reverse Proxy
For external access, configure your reverse proxy (e.g., Nginx Proxy Manager) to:
- Proxy `code.schweitz.net``localhost:8888`
- Enable WebSocket support (required for ttyd)
## Architecture
### ttyd
Web terminal server. Runs `clide-launcher` for each browser connection.
**Service:** `/etc/systemd/system/clide-web.service`
Key flags:
- `-p 8888` — port
- `-W` — writable (allows input)
- `-a` — allows URL arguments (passes `?project=X` to the launcher)
- `-t fontSize=14` — terminal font size
### clide-launcher
Entry point script at `/usr/local/bin/clide-launcher`. Handles:
1. **Project selection** — parses `?project=NAME` from the URL
2. **Session management** — creates or reattaches to a Zellij session named `clide-<project>`
3. **Fallback UI** — shows a project selector if no project specified
**URL patterns:**
- `code.schweitz.net` — shows project selector
- `code.schweitz.net?project=clide` — opens/attaches to the clide project
### Zellij
Used **only** for session persistence (reconnecting after browser close/refresh). All UI features are disabled.
**Config:** `deploy/zellij/config.kdl`
Key settings:
- `default_mode "locked"` — all keys pass through to Clide except `Ctrl+G`
- `default_layout "bare"` — no tab bar, no status bar
- `pane_frames false` — no pane borders
- `default_shell` — points to the clide binary
- `show_startup_tips false`
**Layout:** `deploy/zellij/bare.kdl` — single pane, zero chrome.
### Keybinding Layering
Since the stack is deeply nested, keybindings are carefully layered:
| Key | Layer | Action |
|-----|-------|--------|
| `Ctrl+G` | Zellij | Unlock Zellij (only key Zellij captures in locked mode) |
| `Ctrl+Q` | Clide | Quit Clide |
| `Ctrl+B` | Clide | Toggle sidebar |
| `Ctrl+P` | Clide | Quick open |
| `Ctrl+S` | Clide | Save file |
| All others | Clide → Claude | Pass through to Clide, then to Claude Code |
**To detach a session** (e.g., before service restart):
1. `Ctrl+G` — unlock Zellij
2. `Ctrl+O` — session mode
3. `d` — detach
Or just close the browser tab — Zellij detaches automatically.
## Operations
### Service Management
```bash
# Status
systemctl status clide-web
# Restart (disconnects all sessions)
sudo systemctl restart clide-web
# Logs
journalctl -u clide-web -f
```
### Session Management
```bash
# List sessions
zellij list-sessions
# Kill stuck sessions
zellij delete-all-sessions --force --yes
# Clear serialized session cache (if ghost sessions persist)
rm -rf ~/.cache/zellij/*/session_info/clide-*
```
### Troubleshooting
**Service stuck in `deactivating`:** A child process (usually `claude`) didn't respond to SIGTERM.
```bash
sudo systemctl kill -s SIGKILL clide-web
sudo systemctl start clide-web
```
**Old sessions ignore config changes:** Zellij serializes sessions. Delete them and restart:
```bash
zellij delete-all-sessions --force --yes
rm -rf ~/.cache/zellij/*/session_info/clide-*
sudo systemctl restart clide-web
```
**Keys not reaching Clide:** Zellij may be in normal mode. Press `Ctrl+G` to toggle back to locked mode. The status bar being visible is a sign you're unlocked (bare layout hides it in locked mode).
## Files
```
deploy/
├── install-clide-web.sh # Installation script (run with sudo)
├── clide-launcher # Session launcher (ttyd → zellij → clide)
├── clide-web.service # systemd unit file
└── zellij/
├── config.kdl # Zellij config (locked mode, no UI)
└── bare.kdl # Bare layout (single pane, no chrome)
```