diff --git a/.icons/zellij.svg b/.icons/zellij.svg new file mode 100644 index 000000000..ef77c1dd2 --- /dev/null +++ b/.icons/zellij.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/registry/jang2162/.images/avatar.png b/registry/jang2162/.images/avatar.png new file mode 100644 index 000000000..331be9af9 Binary files /dev/null and b/registry/jang2162/.images/avatar.png differ diff --git a/registry/jang2162/README.md b/registry/jang2162/README.md new file mode 100644 index 000000000..4427ac733 --- /dev/null +++ b/registry/jang2162/README.md @@ -0,0 +1,11 @@ +--- +display_name: "Byeong-Hyun" +bio: "ㅎㅇ means Hi" +avatar: "./.images/avatar.png" +github: "jang2162" +status: "community" +--- + +# Byeong-Hyun + +ㅎㅇ means "Hi" diff --git a/registry/jang2162/modules/zellij/README.md b/registry/jang2162/modules/zellij/README.md new file mode 100644 index 000000000..f2707c0cc --- /dev/null +++ b/registry/jang2162/modules/zellij/README.md @@ -0,0 +1,113 @@ +--- +display_name: Zellij +description: Modern terminal workspace with session management +icon: ../../../../.icons/zellij.svg +verified: false +tags: [zellij, terminal, multiplexer] +--- + +# Zellij + +Automatically install and configure [zellij](https://github.com/zellij-org/zellij), a modern terminal workspace with session management. Supports terminal and web modes, custom configuration, and session persistence. + +```tf +module "zellij" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/jang2162/zellij/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Features + +- Installs zellij if not already present (version configurable, default `0.43.1`) +- Configures zellij with sensible defaults +- Supports custom configuration (KDL format) +- Session serialization enabled by default +- **Two modes**: `terminal` (Coder built-in terminal) and `web` (browser-based via subdomain proxy) +- Cross-platform architecture support (x86_64, aarch64) + +## Examples + +### Basic Usage (Terminal Mode) + +```tf +module "zellij" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/jang2162/zellij/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +### Web Mode + +```tf +module "zellij" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/jang2162/zellij/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + mode = "web" + web_port = 8082 + group = "Terminal" + order = 1 +} +``` + +### Custom Configuration + +```tf +module "zellij" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/jang2162/zellij/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + zellij_config = <<-EOT + keybinds { + normal { + bind "Ctrl t" { NewTab; } + } + } + theme "dracula" + EOT +} +``` + +## How It Works + +### Installation & Setup (scripts/run.sh) + +1. **Version Check**: Checks if zellij is already installed with the correct version +2. **Architecture Detection**: Detects system architecture (x86_64 or aarch64) +3. **Download**: Downloads the appropriate zellij binary from GitHub releases +4. **Installation**: Installs zellij to `/usr/local/bin/zellij` +5. **Configuration**: Creates default or custom configuration at `~/.config/zellij/config.kdl` +6. **Web Mode Only**: + - Prepends a `TERM` fix to `~/.bashrc` (sets `TERM=xterm-256color` inside zellij when `TERM=dumb`) + - Starts the zellij web server as a daemon and creates an authentication token + +### Session Access + +- **Terminal mode**: Opens zellij in the Coder built-in terminal via `zellij attach --create default` +- **Web mode**: Accesses zellij through a subdomain proxy in the browser (authentication token required on first visit) + +## Default Configuration + +The default configuration includes: + +- Session serialization enabled for persistence +- 10,000 line scroll buffer +- Copy on select enabled (system clipboard) +- Rounded pane frames +- Key bindings: `Ctrl+s` (new pane), `Ctrl+q` (quit) +- Default theme +- Web mode: web server IP/port automatically appended + +> [!IMPORTANT] +> +> - Custom `zellij_config` replaces the default configuration entirely +> - Requires `curl` and `tar` for installation +> - Uses `sudo` to install to `/usr/local/bin/` +> - Supported architectures: x86_64, aarch64 diff --git a/registry/jang2162/modules/zellij/main.test.ts b/registry/jang2162/modules/zellij/main.test.ts new file mode 100644 index 000000000..687f7ce92 --- /dev/null +++ b/registry/jang2162/modules/zellij/main.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("zellij", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("default mode should be terminal", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + const terminalApp = state.resources.find( + (r) => r.type === "coder_app" && r.name === "zellij_terminal", + ); + const webApp = state.resources.find( + (r) => r.type === "coder_app" && r.name === "zellij_web", + ); + expect(terminalApp).toBeDefined(); + expect(terminalApp!.instances.length).toBe(1); + expect(terminalApp!.instances[0].attributes.command).toBe( + "zellij attach --create default", + ); + expect(webApp).toBeUndefined(); + }); + + it("web mode should create web app", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + mode: "web", + }); + const webApp = state.resources.find( + (r) => r.type === "coder_app" && r.name === "zellij_web", + ); + const terminalApp = state.resources.find( + (r) => r.type === "coder_app" && r.name === "zellij_terminal", + ); + expect(webApp).toBeDefined(); + expect(webApp!.instances.length).toBe(1); + expect(webApp!.instances[0].attributes.subdomain).toBe(true); + expect(webApp!.instances[0].attributes.url).toBe("http://localhost:8082"); + expect(terminalApp).toBeUndefined(); + }); + + it("web mode should use custom port", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + mode: "web", + web_port: 9090, + }); + const webApp = state.resources.find( + (r) => r.type === "coder_app" && r.name === "zellij_web", + ); + expect(webApp).toBeDefined(); + expect(webApp!.instances[0].attributes.url).toBe("http://localhost:9090"); + }); +}); diff --git a/registry/jang2162/modules/zellij/main.tf b/registry/jang2162/modules/zellij/main.tf new file mode 100644 index 000000000..86712c910 --- /dev/null +++ b/registry/jang2162/modules/zellij/main.tf @@ -0,0 +1,104 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "zellij_version" { + type = string + description = "The version of zellij to install." + default = "0.43.1" +} + +variable "zellij_config" { + type = string + description = "Custom zellij configuration to apply." + default = "" +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/zellij.svg" +} + +variable "mode" { + type = string + description = "How to run zellij: 'web' for web client with subdomain proxy, 'terminal' for Coder built-in terminal." + default = "terminal" + + validation { + condition = contains(["web", "terminal"], var.mode) + error_message = "mode must be 'web' or 'terminal'." + } +} + +variable "web_port" { + type = number + description = "The port for the zellij web server. Only used when mode is 'web'." + default = 8082 +} + + +resource "coder_script" "zellij" { + agent_id = var.agent_id + display_name = "Zellij" + icon = "/icon/zellij.svg" + script = templatefile("${path.module}/scripts/run.sh", { + ZELLIJ_VERSION = var.zellij_version + ZELLIJ_CONFIG = var.zellij_config + MODE = var.mode + WEB_PORT = var.web_port + }) + run_on_start = true + run_on_stop = false +} + +# Web mode: subdomain proxy to zellij web server +resource "coder_app" "zellij_web" { + count = var.mode == "web" ? 1 : 0 + + agent_id = var.agent_id + slug = "zellij" + display_name = "Zellij" + icon = var.icon + order = var.order + group = var.group + url = "http://localhost:${var.web_port}" + subdomain = true +} + +# Terminal mode: run zellij in Coder built-in terminal +resource "coder_app" "zellij_terminal" { + count = var.mode == "terminal" ? 1 : 0 + + agent_id = var.agent_id + slug = "zellij" + display_name = "Zellij" + icon = var.icon + order = var.order + group = var.group + command = "zellij attach --create default" +} diff --git a/registry/jang2162/modules/zellij/scripts/run.sh b/registry/jang2162/modules/zellij/scripts/run.sh new file mode 100644 index 000000000..14ebf69fc --- /dev/null +++ b/registry/jang2162/modules/zellij/scripts/run.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash + +BOLD='\033[0;1m' + +# Convert templated variables to shell variables +ZELLIJ_VERSION="${ZELLIJ_VERSION}" +ZELLIJ_CONFIG="${ZELLIJ_CONFIG}" +MODE="${MODE}" +WEB_PORT="${WEB_PORT}" + +# Function to check if zellij is already installed +is_installed() { + command -v zellij > /dev/null 2>&1 +} + +# Function to get installed version +get_installed_version() { + if is_installed; then + zellij --version | grep -oP 'zellij \K[0-9]+\.[0-9]+\.[0-9]+' + else + echo "" + fi +} + +# Function to install zellij +install_zellij() { + printf "Checking for zellij installation\n" + + INSTALLED_VERSION=$(get_installed_version) + + if [ -n "$INSTALLED_VERSION" ]; then + if [ "$INSTALLED_VERSION" = "$ZELLIJ_VERSION" ]; then + printf "zellij version $ZELLIJ_VERSION is already installed \n\n" + return 0 + else + printf "zellij version $INSTALLED_VERSION is installed, but version $ZELLIJ_VERSION is required\n" + fi + fi + + printf "Installing zellij version $ZELLIJ_VERSION \n\n" + + # Detect architecture + ARCH=$(uname -m) + case "$ARCH" in + x86_64) + ARCH="x86_64" + ;; + aarch64 | arm64) + ARCH="aarch64" + ;; + *) + printf "ERROR: Unsupported architecture: $ARCH\n" + exit 1 + ;; + esac + + # Download and install zellij + DOWNLOAD_URL="https://github.com/zellij-org/zellij/releases/download/v$${ZELLIJ_VERSION}/zellij-$${ARCH}-unknown-linux-musl.tar.gz" + TEMP_DIR=$(mktemp -d) + + printf "Downloading zellij version $ZELLIJ_VERSION for $ARCH...\n" + printf "URL: $DOWNLOAD_URL\n" + + if ! curl -fsSL "$DOWNLOAD_URL" -o "$TEMP_DIR/zellij.tar.gz"; then + printf "ERROR: Failed to download zellij\n" + rm -rf "$TEMP_DIR" + exit 1 + fi + + printf "Extracting zellij...\n" + tar -xzf "$TEMP_DIR/zellij.tar.gz" -C "$TEMP_DIR" + + printf "Installing zellij to /usr/local/bin...\n" + sudo mv "$TEMP_DIR/zellij" /usr/local/bin/zellij + sudo chmod +x /usr/local/bin/zellij + + # Cleanup + rm -rf "$TEMP_DIR" + + # Verify installation + if is_installed; then + FINAL_VERSION=$(get_installed_version) + printf "✓ zellij version $FINAL_VERSION installed successfully\n" + else + printf "ERROR: zellij installation failed\n" + exit 1 + fi +} + +# Function to setup zellij configuration +setup_zellij_config() { + printf "Setting up zellij configuration \n" + + local config_dir="$HOME/.config/zellij" + local config_file="$config_dir/config.kdl" + + mkdir -p "$config_dir" + + if [ -n "$ZELLIJ_CONFIG" ]; then + printf "$ZELLIJ_CONFIG" > "$config_file" + printf "$${BOLD}Custom zellij configuration applied at $config_file \n\n" + else + cat > "$config_file" << 'CONFIGEOF' +// Zellij Configuration File + +keybinds { + normal { + // Session management + bind "Ctrl s" { NewPane; } + bind "Ctrl q" { Quit; } + } +} + +// UI configuration +ui { + pane_frames { + rounded_corners true + } +} + +// Session configuration +session_serialization true +pane_frames true +simplified_ui false + +// Scroll settings +scroll_buffer_size 10000 +copy_on_select true +copy_clipboard "system" + +// Theme +theme "default" +CONFIGEOF + + # Append web server config only in web mode + if [ "$MODE" = "web" ]; then + cat >> "$config_file" << EOF + +// Web server configuration +web_server_ip "127.0.0.1" +web_server_port $WEB_PORT +EOF + fi + printf "zellij configuration created at $config_file \n\n" + fi +} + +# Function to fix TERM for zellij web client (sets TERM=dumb) +# Must be prepended to .bashrc so it runs BEFORE prompt color detection +fix_term_for_web() { + local bashrc="$HOME/.bashrc" + local marker="# zellij-term-fix" + + if ! grep -q "$marker" "$bashrc" 2> /dev/null; then + printf "Prepending TERM fix for Zellij web client to $bashrc\n" + local fix + fix=$( + cat << 'TERMFIX' +# Fix TERM for Zellij web client (TERM=dumb breaks colors) # zellij-term-fix +if [ -n "$ZELLIJ" ] && [ "$TERM" = "dumb" ]; then + export TERM=xterm-256color +fi + +TERMFIX + ) + # Prepend to .bashrc so TERM is set before prompt color detection + if [ -f "$bashrc" ]; then + local tmp + tmp=$(mktemp) + printf '%s\n' "$fix" | cat - "$bashrc" > "$tmp" && mv "$tmp" "$bashrc" + else + printf '%s\n' "$fix" > "$bashrc" + fi + fi +} + +# Function to start zellij web server and create auth token +start_web_server() { + printf "Starting zellij web server on port $WEB_PORT...\n" + + # Stop any existing web server + zellij web --stop 2> /dev/null || true + + # Start web server in daemon mode + zellij web -d + + # Wait for web server to be ready + sleep 2 + + # Create auth token if not exists or invalid + local token_file="$HOME/.zellij-web-token" + local need_token=false + + if [ ! -f "$token_file" ]; then + need_token=true + elif ! grep -qP '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' "$token_file"; then + printf "Invalid token file detected, regenerating...\n" + rm -f "$token_file" + need_token=true + fi + + if [ "$need_token" = true ]; then + printf "Creating authentication token...\n" + # Extract UUID token from output (format: "token_N: ") + TOKEN=$(zellij web --create-token 2>&1 | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') + echo "$TOKEN" > "$token_file" + printf "$${BOLD}===========================================\n" + printf "$${BOLD} Zellij Web Token: $TOKEN\n" + printf "$${BOLD} Saved to: ~/.zellij-web-token\n" + printf "$${BOLD} Enter this token on first browser visit.\n" + printf "$${BOLD}===========================================\n\n" + else + printf "Auth token: $(cat "$token_file")\n\n" + fi +} + +# Main execution +main() { + printf "$${BOLD}🛠️ Setting up zellij! \n\n" + printf "" + + # Install zellij + install_zellij + + # Setup zellij configuration + setup_zellij_config + + # Web mode: fix TERM and start web server + if [ "$MODE" = "web" ]; then + fix_term_for_web + start_web_server + fi + + printf "$${BOLD}✅ zellij setup complete! \n\n" + printf "$${BOLD}Access zellij via the Coder dashboard.\n" +} + +# Run main function +main diff --git a/registry/jang2162/modules/zellij/zellij.tftest.hcl b/registry/jang2162/modules/zellij/zellij.tftest.hcl new file mode 100644 index 000000000..8acdda0c0 --- /dev/null +++ b/registry/jang2162/modules/zellij/zellij.tftest.hcl @@ -0,0 +1,136 @@ +run "required_variables" { + command = plan + + variables { + agent_id = "test-agent-id" + } +} + +run "default_terminal_mode" { + command = plan + + variables { + agent_id = "test-agent-id" + } + + assert { + condition = resource.coder_app.zellij_terminal[0].command == "zellij attach --create default" + error_message = "Terminal mode should use 'zellij attach --create default' command" + } + + assert { + condition = resource.coder_app.zellij_terminal[0].slug == "zellij" + error_message = "Terminal app slug should be 'zellij'" + } + + assert { + condition = resource.coder_app.zellij_terminal[0].display_name == "Zellij" + error_message = "Terminal app display name should be 'Zellij'" + } + + assert { + condition = length(resource.coder_app.zellij_web) == 0 + error_message = "Web app should not be created in terminal mode" + } +} + +run "web_mode" { + command = plan + + variables { + agent_id = "test-agent-id" + mode = "web" + } + + assert { + condition = resource.coder_app.zellij_web[0].url == "http://localhost:8082" + error_message = "Web app should use default port 8082" + } + + assert { + condition = resource.coder_app.zellij_web[0].subdomain == true + error_message = "Web app should use subdomain" + } + + assert { + condition = resource.coder_app.zellij_web[0].slug == "zellij" + error_message = "Web app slug should be 'zellij'" + } + + assert { + condition = length(resource.coder_app.zellij_terminal) == 0 + error_message = "Terminal app should not be created in web mode" + } +} + +run "web_mode_custom_port" { + command = plan + + variables { + agent_id = "test-agent-id" + mode = "web" + web_port = 9090 + } + + assert { + condition = resource.coder_app.zellij_web[0].url == "http://localhost:9090" + error_message = "Web app should use custom port 9090" + } +} + +run "custom_order_and_group" { + command = plan + + variables { + agent_id = "test-agent-id" + order = 3 + group = "Terminal" + } + + assert { + condition = resource.coder_app.zellij_terminal[0].order == 3 + error_message = "App order should be 3" + } + + assert { + condition = resource.coder_app.zellij_terminal[0].group == "Terminal" + error_message = "App group should be 'Terminal'" + } +} + +run "custom_icon" { + command = plan + + variables { + agent_id = "test-agent-id" + icon = "/icon/custom.svg" + } + + assert { + condition = resource.coder_app.zellij_terminal[0].icon == "/icon/custom.svg" + error_message = "App should use custom icon" + } +} + +run "coder_script_config" { + command = plan + + variables { + agent_id = "test-agent-id" + } + + assert { + condition = resource.coder_script.zellij.display_name == "Zellij" + error_message = "Script display name should be 'Zellij'" + } + + assert { + condition = resource.coder_script.zellij.run_on_start == true + error_message = "Script should run on start" + } + + assert { + condition = resource.coder_script.zellij.run_on_stop == false + error_message = "Script should not run on stop" + } +}