-
Notifications
You must be signed in to change notification settings - Fork 128
feat: add Zellij module #777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| display_name: "Byeong-Hyun" | ||
| bio: "ㅎㅇ means Hi" | ||
| avatar: "./.images/avatar.png" | ||
| github: "jang2162" | ||
| status: "community" | ||
| --- | ||
|
|
||
| # Byeong-Hyun | ||
|
|
||
| ㅎㅇ means "Hi" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 | ||||||||||||||||||||
|
||||||||||||||||||||
| subdomain = true | |
| healthcheck { | |
| url = "/" | |
| interval = "10s" | |
| timeout = "5s" | |
| } | |
| subdomain = true |
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both coder_app resources hardcode slug and display_name, and the web app (zellij_web) also lacks a configurable share variable. Other modules in this registry (e.g., code-server, filebrowser, kasmvnc) expose slug, display_name, and share (with a default of "owner") as configurable variables. Without slug being configurable, users cannot run multiple instances of this module in the same workspace. Consider adding slug, display_name, and share variables to match the established convention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
IMPORTANTnote says "Customzellij_configreplaces the default configuration entirely" but does not warn that inwebmode, the user must also includeweb_server_ipandweb_server_portin their custom config, since those are only appended to the default config. Without these settings, the zellij web server won't listen on the configured port when using a custom config. This is a significant omission that should be documented here.