Skip to content

feat: tmux module #229

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

Merged
merged 25 commits into from
Jul 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b66aead
feat: basic tmux module
35C4n0r Jul 14, 2025
3e894dc
fix: echo statements
35C4n0r Jul 14, 2025
b3680df
feat: replace echo with printf
35C4n0r Jul 14, 2025
9d735c8
feat: add test
35C4n0r Jul 14, 2025
b27641f
feat: update readme and format
35C4n0r Jul 14, 2025
f36ce27
feat: update readme
35C4n0r Jul 14, 2025
25857a4
Merge branch 'main' into feat-tmux
35C4n0r Jul 14, 2025
8b05a5d
feat: update readme
35C4n0r Jul 14, 2025
c031c3b
Merge branch 'main' into feat-tmux
35C4n0r Jul 14, 2025
dac1878
ci: bun fmt
35C4n0r Jul 14, 2025
200d922
Merge remote-tracking branch 'origin/feat-tmux' into feat-tmux
35C4n0r Jul 14, 2025
a06d185
Merge branch 'main' into feat-tmux
35C4n0r Jul 16, 2025
427586d
feat: add frontmatter
35C4n0r Jul 16, 2025
cf8014f
feat: add tmux svg
35C4n0r Jul 16, 2025
26e4c12
Merge branch 'main' into feat-tmux
35C4n0r Jul 18, 2025
d9f8c95
feat: update readme
35C4n0r Jul 19, 2025
0c243cc
feat: start.sh
35C4n0r Jul 22, 2025
a1b4848
feat: enhance tmux session management and configuration
35C4n0r Jul 22, 2025
6d0c19d
feat: add multi-session support for tmux in Coder UI
35C4n0r Jul 22, 2025
306bec1
Merge branch 'main' into feat-tmux
35C4n0r Jul 22, 2025
9f32bd5
docs: change NOTE to IMPORTANT and add tmux icon
35C4n0r Jul 24, 2025
8c0e7b3
Merge branch 'main' into feat-tmux
35C4n0r Jul 24, 2025
c82fe73
docs: update README with module source and example for tmux
35C4n0r Jul 24, 2025
41c8636
fix: add versions
35C4n0r Jul 24, 2025
fddc6ea
fix: fix README.md
35C4n0r Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,6 @@ dist

# Generated credentials from google-github-actions/auth
gha-creds-*.json

# IDEs
.idea
1 change: 1 addition & 0 deletions .icons/tmux.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added registry/anomaly/.images/avatar.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions registry/anomaly/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
display_name: "Jay Kumar"
bio: "I'm a Software Engineer :)"
avatar_url: "./.images/avatar.png"
github: "35C4n0r"
linkedin: "https://www.linkedin.com/in/jaykum4r"
support_email: "[email protected]"
status: "community"
---

# Your Name

I'm a Software Engineer :)
101 changes: 101 additions & 0 deletions registry/anomaly/modules/tmux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
display_name: "Tmux"
description: "Tmux for coder agent :)"
icon: "../../../../.icons/tmux.svg"
verified: false
tags: ["tmux", "terminal", "persistent"]
---

# tmux

This module provisions and configures [tmux](https://github.com/tmux/tmux) with session persistence and plugin support
for a Coder agent. It automatically installs tmux, the Tmux Plugin Manager (TPM), and a set of useful plugins, and sets
up a default or custom tmux configuration with session save/restore capabilities.

```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```

## Features

- Installs tmux if not already present
- Installs TPM (Tmux Plugin Manager)
- Configures tmux with plugins for sensible defaults, session persistence, and automation:
- `tmux-plugins/tpm`
- `tmux-plugins/tmux-sensible`
- `tmux-plugins/tmux-resurrect`
- `tmux-plugins/tmux-continuum`
- Supports custom tmux configuration
- Enables automatic session save
- Configurable save interval
- **Supports multiple named tmux sessions, each as a separate app in the Coder UI**

## Usage

```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
tmux_config = "" # Optional: custom tmux.conf content
save_interval = 1 # Optional: save interval in minutes
sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions
order = 1 # Optional: UI order
group = "Terminal" # Optional: UI group
icon = "/icon/tmux.svg" # Optional: app icon
}
```

## Multi-Session Support

This module can provision multiple tmux sessions, each as a separate app in the Coder UI. Use the `sessions` variable to specify a list of session names. For each session, a `coder_app` is created, allowing you to launch or attach to that session directly from the UI.

- **sessions**: List of tmux session names (default: `["default"]`).

## How It Works

- **tmux Installation:**
- Checks if tmux is installed; if not, installs it using the system's package manager (supports apt, yum, dnf,
zypper, apk, brew).
- **TPM Installation:**
- Installs the Tmux Plugin Manager (TPM) to `~/.tmux/plugins/tpm` if not already present.
- **tmux Configuration:**
- If `tmux_config` is provided, writes it to `~/.tmux.conf`.
- Otherwise, generates a default configuration with plugin support and session persistence (using tmux-resurrect and
tmux-continuum).
- Sets up key bindings for quick session save (`Ctrl+s`) and restore (`Ctrl+r`).
- **Plugin Installation:**
- Installs plugins via TPM.
- **Session Persistence:**
- Enables automatic session save/restore at the configured interval.

## Example

```tf
module "tmux" {
source = "registry.coder.com/anomaly/tmux/coder"
version = "1.0.0"
agent_id = var.agent_id
sessions = ["default", "dev", "anomaly"]
tmux_config = <<-EOT
set -g mouse on
set -g history-limit 10000
EOT
group = "Terminal"
order = 2
}
```

> [!IMPORTANT]
>
> - If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin
> and TPM initialization lines if you want plugin support and session persistence.
> - The script will attempt to install dependencies using `sudo` where required.
> - If `git` is not installed, TPM installation will fail.
> - If you are using custom config, you'll be responsible for setting up persistence and plugins.
> - The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
> - In case of session restart or shh reconnection, the tmux session will be automatically restored :)
35 changes: 35 additions & 0 deletions registry/anomaly/modules/tmux/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
findResourceInstance,
} from "~test";
import path from "path";

const moduleDir = path.resolve(__dirname);

const requiredVars = {
agent_id: "dummy-agent-id",
};

describe("tmux module", async () => {
await runTerraformInit(moduleDir);

// 1. Required variables
testRequiredVariables(moduleDir, requiredVars);

// 2. coder_script resource is created
it("creates coder_script resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const scriptResource = findResourceInstance(state, "coder_script");
expect(scriptResource).toBeDefined();
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);

// check that the script contains expected lines
expect(scriptResource.script).toContain("Installing tmux");
expect(scriptResource.script).toContain("Installing Tmux Plugin Manager (TPM)");
expect(scriptResource.script).toContain("tmux configuration created at");
expect(scriptResource.script).toContain("✅ tmux setup complete!");
});
});
78 changes: 78 additions & 0 deletions registry/anomaly/modules/tmux/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 "tmux_config" {
type = string
description = "Custom tmux configuration to apply."
default = ""
}

variable "save_interval" {
type = number
description = "Save interval (in minutes)."
default = 1
}

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/tmux.svg"
}

variable "sessions" {
type = list(string)
description = "List of tmux sessions to create or start."
default = ["default"]
}

resource "coder_script" "tmux" {
agent_id = var.agent_id
display_name = "tmux"
icon = "/icon/terminal.svg"
script = templatefile("${path.module}/scripts/run.sh", {
TMUX_CONFIG = var.tmux_config
SAVE_INTERVAL = var.save_interval
})
run_on_start = true
run_on_stop = false
}

resource "coder_app" "tmux_sessions" {
for_each = toset(var.sessions)

agent_id = var.agent_id
slug = "tmux-${each.value}"
display_name = "tmux - ${each.value}"
icon = var.icon
order = var.order
group = var.group

command = templatefile("${path.module}/scripts/start.sh", {
SESSION_NAME = each.value
})
}
153 changes: 153 additions & 0 deletions registry/anomaly/modules/tmux/scripts/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env bash

BOLD='\033[0;1m'

# Convert templated variables to shell variables
SAVE_INTERVAL="${SAVE_INTERVAL}"
TMUX_CONFIG="${TMUX_CONFIG}"

# Function to install tmux
install_tmux() {
printf "Checking for tmux installation\n"

if command -v tmux &> /dev/null; then
printf "tmux is already installed \n\n"
return 0
fi

printf "Installing tmux \n\n"

# Detect package manager and install tmux
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y tmux
elif command -v yum &> /dev/null; then
sudo yum install -y tmux
elif command -v dnf &> /dev/null; then
sudo dnf install -y tmux
elif command -v zypper &> /dev/null; then
sudo zypper install -y tmux
elif command -v apk &> /dev/null; then
sudo apk add tmux
elif command -v brew &> /dev/null; then
brew install tmux
else
printf "No supported package manager found. Please install tmux manually. \n"
exit 1
fi

printf "tmux installed successfully \n"
}

# Function to install Tmux Plugin Manager (TPM)
install_tpm() {
local tpm_dir="$HOME/.tmux/plugins/tpm"

if [ -d "$tpm_dir" ]; then
printf "TPM is already installed"
return 0
fi

printf "Installing Tmux Plugin Manager (TPM) \n"

# Create plugins directory
mkdir -p "$HOME/.tmux/plugins"

# Clone TPM repository
if command -v git &> /dev/null; then
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
printf "TPM installed successfully"
else
printf "Git is not installed. Please install git to use tmux plugins. \n"
exit 1
fi
}

# Function to create tmux configuration
setup_tmux_config() {
printf "Setting up tmux configuration \n"

local config_dir="$HOME/.tmux"
local config_file="$HOME/.tmux.conf"

mkdir -p "$config_dir"

if [ -n "$TMUX_CONFIG" ]; then
printf "$TMUX_CONFIG" > "$config_file"
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
else
cat > "$config_file" << EOF
# Tmux Configuration File

# =============================================================================
# PLUGIN CONFIGURATION
# =============================================================================

# List of plugins
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'

# tmux-continuum configuration
set -g @continuum-restore 'on'
set -g @continuum-save-interval '$${SAVE_INTERVAL}'
set -g @continuum-boot 'on'
set -g status-right 'Continuum status: #{continuum_status}'

# =============================================================================
# KEY BINDINGS FOR SESSION MANAGEMENT
# =============================================================================

# Quick session save and restore
bind C-s run-shell "~/.tmux/plugins/tmux-resurrect/scripts/save.sh"
bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh"

# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
run '~/.tmux/plugins/tpm/tpm'
EOF
printf "tmux configuration created at {$config_file} \n\n"
fi
}

# Function to install tmux plugins
install_plugins() {
printf "Installing tmux plugins"

# Check if TPM is installed
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
printf "TPM is not installed. Cannot install plugins. \n"
return 1
fi

# Install plugins using TPM
"$HOME/.tmux/plugins/tpm/bin/install_plugins"

printf "tmux plugins installed successfully \n"
}

# Main execution
main() {
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
printf ""

# Install dependencies
install_tmux
install_tpm

# Setup tmux configuration
setup_tmux_config

# Install plugins
install_plugins

printf "$${BOLD}✅ tmux setup complete! \n\n"

printf "$${BOLD} Attempting to restore sessions\n"
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"

}

# Run main function
main
Loading