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:
@@ -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."""
|
||||
|
||||
Executable
+68
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
// Bare layout - no UI, just the pane
|
||||
layout {
|
||||
pane
|
||||
}
|
||||
@@ -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>"
|
||||
@@ -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)
|
||||
```
|
||||
Reference in New Issue
Block a user