diff --git a/clide/app.py b/clide/app.py index 1721c7ca..09a0b9bb 100644 --- a/clide/app.py +++ b/clide/app.py @@ -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.""" diff --git a/deploy/clide-launcher b/deploy/clide-launcher new file mode 100755 index 00000000..ad253625 --- /dev/null +++ b/deploy/clide-launcher @@ -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 diff --git a/deploy/clide-web.service b/deploy/clide-web.service new file mode 100644 index 00000000..e5a7b6f7 --- /dev/null +++ b/deploy/clide-web.service @@ -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 diff --git a/deploy/install-clide-web.sh b/deploy/install-clide-web.sh new file mode 100644 index 00000000..b303ee7f --- /dev/null +++ b/deploy/install-clide-web.sh @@ -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" diff --git a/deploy/zellij/bare.kdl b/deploy/zellij/bare.kdl new file mode 100644 index 00000000..7b887b02 --- /dev/null +++ b/deploy/zellij/bare.kdl @@ -0,0 +1,4 @@ +// Bare layout - no UI, just the pane +layout { + pane +} diff --git a/deploy/zellij/config.kdl b/deploy/zellij/config.kdl new file mode 100644 index 00000000..9f6f65be --- /dev/null +++ b/deploy/zellij/config.kdl @@ -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: +// +// 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 " diff --git a/docs/web-deployment.md b/docs/web-deployment.md new file mode 100644 index 00000000..6b4cf0f1 --- /dev/null +++ b/docs/web-deployment.md @@ -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-` +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) +```