Skip to content

Commit f04d7d2

Browse files
authored
feat: tmux module (#229)
Closes #203 /claim #203 ## Description Introduce the `tmux` module ## Demo https://www.loom.com/share/ec8169d34c3043f7af2163b1a1a14a4b?sid=1ea8bcb2-3db0-43ca-965a-5ed42eec3448 ## Type of Change - [x] New module - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information <!-- Delete this section if not applicable --> **Path:** `registry/anomaly/modules/tmux` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally ## Related Issues #203
1 parent 4ae6370 commit f04d7d2

File tree

9 files changed

+421
-0
lines changed

9 files changed

+421
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,6 @@ dist
145145

146146
# Generated credentials from google-github-actions/auth
147147
gha-creds-*.json
148+
149+
# IDEs
150+
.idea

.icons/tmux.svg

Lines changed: 1 addition & 0 deletions
Loading

registry/anomaly/.images/avatar.jpeg

8.85 KB
Loading

registry/anomaly/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
display_name: "Jay Kumar"
3+
bio: "I'm a Software Engineer :)"
4+
avatar_url: "./.images/avatar.png"
5+
github: "35C4n0r"
6+
linkedin: "https://www.linkedin.com/in/jaykum4r"
7+
support_email: "[email protected]"
8+
status: "community"
9+
---
10+
11+
# Your Name
12+
13+
I'm a Software Engineer :)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
display_name: "Tmux"
3+
description: "Tmux for coder agent :)"
4+
icon: "../../../../.icons/tmux.svg"
5+
verified: false
6+
tags: ["tmux", "terminal", "persistent"]
7+
---
8+
9+
# tmux
10+
11+
This module provisions and configures [tmux](https://github.com/tmux/tmux) with session persistence and plugin support
12+
for a Coder agent. It automatically installs tmux, the Tmux Plugin Manager (TPM), and a set of useful plugins, and sets
13+
up a default or custom tmux configuration with session save/restore capabilities.
14+
15+
```tf
16+
module "tmux" {
17+
source = "registry.coder.com/anomaly/tmux/coder"
18+
version = "1.0.0"
19+
agent_id = coder_agent.example.id
20+
}
21+
```
22+
23+
## Features
24+
25+
- Installs tmux if not already present
26+
- Installs TPM (Tmux Plugin Manager)
27+
- Configures tmux with plugins for sensible defaults, session persistence, and automation:
28+
- `tmux-plugins/tpm`
29+
- `tmux-plugins/tmux-sensible`
30+
- `tmux-plugins/tmux-resurrect`
31+
- `tmux-plugins/tmux-continuum`
32+
- Supports custom tmux configuration
33+
- Enables automatic session save
34+
- Configurable save interval
35+
- **Supports multiple named tmux sessions, each as a separate app in the Coder UI**
36+
37+
## Usage
38+
39+
```tf
40+
module "tmux" {
41+
source = "registry.coder.com/anomaly/tmux/coder"
42+
version = "1.0.0"
43+
agent_id = coder_agent.example.id
44+
tmux_config = "" # Optional: custom tmux.conf content
45+
save_interval = 1 # Optional: save interval in minutes
46+
sessions = ["default", "dev", "ops"] # Optional: list of tmux sessions
47+
order = 1 # Optional: UI order
48+
group = "Terminal" # Optional: UI group
49+
icon = "/icon/tmux.svg" # Optional: app icon
50+
}
51+
```
52+
53+
## Multi-Session Support
54+
55+
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.
56+
57+
- **sessions**: List of tmux session names (default: `["default"]`).
58+
59+
## How It Works
60+
61+
- **tmux Installation:**
62+
- Checks if tmux is installed; if not, installs it using the system's package manager (supports apt, yum, dnf,
63+
zypper, apk, brew).
64+
- **TPM Installation:**
65+
- Installs the Tmux Plugin Manager (TPM) to `~/.tmux/plugins/tpm` if not already present.
66+
- **tmux Configuration:**
67+
- If `tmux_config` is provided, writes it to `~/.tmux.conf`.
68+
- Otherwise, generates a default configuration with plugin support and session persistence (using tmux-resurrect and
69+
tmux-continuum).
70+
- Sets up key bindings for quick session save (`Ctrl+s`) and restore (`Ctrl+r`).
71+
- **Plugin Installation:**
72+
- Installs plugins via TPM.
73+
- **Session Persistence:**
74+
- Enables automatic session save/restore at the configured interval.
75+
76+
## Example
77+
78+
```tf
79+
module "tmux" {
80+
source = "registry.coder.com/anomaly/tmux/coder"
81+
version = "1.0.0"
82+
agent_id = var.agent_id
83+
sessions = ["default", "dev", "anomaly"]
84+
tmux_config = <<-EOT
85+
set -g mouse on
86+
set -g history-limit 10000
87+
EOT
88+
group = "Terminal"
89+
order = 2
90+
}
91+
```
92+
93+
> [!IMPORTANT]
94+
>
95+
> - If you provide a custom `tmux_config`, it will completely replace the default configuration. Ensure you include plugin
96+
> and TPM initialization lines if you want plugin support and session persistence.
97+
> - The script will attempt to install dependencies using `sudo` where required.
98+
> - If `git` is not installed, TPM installation will fail.
99+
> - If you are using custom config, you'll be responsible for setting up persistence and plugins.
100+
> - The `order`, `group`, and `icon` variables allow you to customize how tmux apps appear in the Coder UI.
101+
> - In case of session restart or shh reconnection, the tmux session will be automatically restored :)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, it, expect } from "bun:test";
2+
import {
3+
runTerraformApply,
4+
runTerraformInit,
5+
testRequiredVariables,
6+
findResourceInstance,
7+
} from "~test";
8+
import path from "path";
9+
10+
const moduleDir = path.resolve(__dirname);
11+
12+
const requiredVars = {
13+
agent_id: "dummy-agent-id",
14+
};
15+
16+
describe("tmux module", async () => {
17+
await runTerraformInit(moduleDir);
18+
19+
// 1. Required variables
20+
testRequiredVariables(moduleDir, requiredVars);
21+
22+
// 2. coder_script resource is created
23+
it("creates coder_script resource", async () => {
24+
const state = await runTerraformApply(moduleDir, requiredVars);
25+
const scriptResource = findResourceInstance(state, "coder_script");
26+
expect(scriptResource).toBeDefined();
27+
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
28+
29+
// check that the script contains expected lines
30+
expect(scriptResource.script).toContain("Installing tmux");
31+
expect(scriptResource.script).toContain("Installing Tmux Plugin Manager (TPM)");
32+
expect(scriptResource.script).toContain("tmux configuration created at");
33+
expect(scriptResource.script).toContain("✅ tmux setup complete!");
34+
});
35+
});

registry/anomaly/modules/tmux/main.tf

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
4+
required_providers {
5+
coder = {
6+
source = "coder/coder"
7+
version = ">= 2.5"
8+
}
9+
}
10+
}
11+
12+
variable "agent_id" {
13+
type = string
14+
description = "The ID of a Coder agent."
15+
}
16+
17+
variable "tmux_config" {
18+
type = string
19+
description = "Custom tmux configuration to apply."
20+
default = ""
21+
}
22+
23+
variable "save_interval" {
24+
type = number
25+
description = "Save interval (in minutes)."
26+
default = 1
27+
}
28+
29+
variable "order" {
30+
type = number
31+
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)."
32+
default = null
33+
}
34+
35+
variable "group" {
36+
type = string
37+
description = "The name of a group that this app belongs to."
38+
default = null
39+
}
40+
41+
variable "icon" {
42+
type = string
43+
description = "The icon to use for the app."
44+
default = "/icon/tmux.svg"
45+
}
46+
47+
variable "sessions" {
48+
type = list(string)
49+
description = "List of tmux sessions to create or start."
50+
default = ["default"]
51+
}
52+
53+
resource "coder_script" "tmux" {
54+
agent_id = var.agent_id
55+
display_name = "tmux"
56+
icon = "/icon/terminal.svg"
57+
script = templatefile("${path.module}/scripts/run.sh", {
58+
TMUX_CONFIG = var.tmux_config
59+
SAVE_INTERVAL = var.save_interval
60+
})
61+
run_on_start = true
62+
run_on_stop = false
63+
}
64+
65+
resource "coder_app" "tmux_sessions" {
66+
for_each = toset(var.sessions)
67+
68+
agent_id = var.agent_id
69+
slug = "tmux-${each.value}"
70+
display_name = "tmux - ${each.value}"
71+
icon = var.icon
72+
order = var.order
73+
group = var.group
74+
75+
command = templatefile("${path.module}/scripts/start.sh", {
76+
SESSION_NAME = each.value
77+
})
78+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env bash
2+
3+
BOLD='\033[0;1m'
4+
5+
# Convert templated variables to shell variables
6+
SAVE_INTERVAL="${SAVE_INTERVAL}"
7+
TMUX_CONFIG="${TMUX_CONFIG}"
8+
9+
# Function to install tmux
10+
install_tmux() {
11+
printf "Checking for tmux installation\n"
12+
13+
if command -v tmux &> /dev/null; then
14+
printf "tmux is already installed \n\n"
15+
return 0
16+
fi
17+
18+
printf "Installing tmux \n\n"
19+
20+
# Detect package manager and install tmux
21+
if command -v apt-get &> /dev/null; then
22+
sudo apt-get update
23+
sudo apt-get install -y tmux
24+
elif command -v yum &> /dev/null; then
25+
sudo yum install -y tmux
26+
elif command -v dnf &> /dev/null; then
27+
sudo dnf install -y tmux
28+
elif command -v zypper &> /dev/null; then
29+
sudo zypper install -y tmux
30+
elif command -v apk &> /dev/null; then
31+
sudo apk add tmux
32+
elif command -v brew &> /dev/null; then
33+
brew install tmux
34+
else
35+
printf "No supported package manager found. Please install tmux manually. \n"
36+
exit 1
37+
fi
38+
39+
printf "tmux installed successfully \n"
40+
}
41+
42+
# Function to install Tmux Plugin Manager (TPM)
43+
install_tpm() {
44+
local tpm_dir="$HOME/.tmux/plugins/tpm"
45+
46+
if [ -d "$tpm_dir" ]; then
47+
printf "TPM is already installed"
48+
return 0
49+
fi
50+
51+
printf "Installing Tmux Plugin Manager (TPM) \n"
52+
53+
# Create plugins directory
54+
mkdir -p "$HOME/.tmux/plugins"
55+
56+
# Clone TPM repository
57+
if command -v git &> /dev/null; then
58+
git clone https://github.com/tmux-plugins/tpm "$tpm_dir"
59+
printf "TPM installed successfully"
60+
else
61+
printf "Git is not installed. Please install git to use tmux plugins. \n"
62+
exit 1
63+
fi
64+
}
65+
66+
# Function to create tmux configuration
67+
setup_tmux_config() {
68+
printf "Setting up tmux configuration \n"
69+
70+
local config_dir="$HOME/.tmux"
71+
local config_file="$HOME/.tmux.conf"
72+
73+
mkdir -p "$config_dir"
74+
75+
if [ -n "$TMUX_CONFIG" ]; then
76+
printf "$TMUX_CONFIG" > "$config_file"
77+
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
78+
else
79+
cat > "$config_file" << EOF
80+
# Tmux Configuration File
81+
82+
# =============================================================================
83+
# PLUGIN CONFIGURATION
84+
# =============================================================================
85+
86+
# List of plugins
87+
set -g @plugin 'tmux-plugins/tpm'
88+
set -g @plugin 'tmux-plugins/tmux-sensible'
89+
set -g @plugin 'tmux-plugins/tmux-resurrect'
90+
set -g @plugin 'tmux-plugins/tmux-continuum'
91+
92+
# tmux-continuum configuration
93+
set -g @continuum-restore 'on'
94+
set -g @continuum-save-interval '$${SAVE_INTERVAL}'
95+
set -g @continuum-boot 'on'
96+
set -g status-right 'Continuum status: #{continuum_status}'
97+
98+
# =============================================================================
99+
# KEY BINDINGS FOR SESSION MANAGEMENT
100+
# =============================================================================
101+
102+
# Quick session save and restore
103+
bind C-s run-shell "~/.tmux/plugins/tmux-resurrect/scripts/save.sh"
104+
bind C-r run-shell "~/.tmux/plugins/tmux-resurrect/scripts/restore.sh"
105+
106+
# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
107+
run '~/.tmux/plugins/tpm/tpm'
108+
EOF
109+
printf "tmux configuration created at {$config_file} \n\n"
110+
fi
111+
}
112+
113+
# Function to install tmux plugins
114+
install_plugins() {
115+
printf "Installing tmux plugins"
116+
117+
# Check if TPM is installed
118+
if [ ! -d "$HOME/.tmux/plugins/tpm" ]; then
119+
printf "TPM is not installed. Cannot install plugins. \n"
120+
return 1
121+
fi
122+
123+
# Install plugins using TPM
124+
"$HOME/.tmux/plugins/tpm/bin/install_plugins"
125+
126+
printf "tmux plugins installed successfully \n"
127+
}
128+
129+
# Main execution
130+
main() {
131+
printf "$${BOLD} 🛠️Setting up tmux with session persistence! \n\n"
132+
printf ""
133+
134+
# Install dependencies
135+
install_tmux
136+
install_tpm
137+
138+
# Setup tmux configuration
139+
setup_tmux_config
140+
141+
# Install plugins
142+
install_plugins
143+
144+
printf "$${BOLD}✅ tmux setup complete! \n\n"
145+
146+
printf "$${BOLD} Attempting to restore sessions\n"
147+
tmux new-session -d \; source-file ~/.tmux.conf \; run-shell '~/.tmux/plugins/tmux-resurrect/scripts/restore.sh'
148+
printf "$${BOLD} Sessions restored: -> %s\n" "$(tmux ls)"
149+
150+
}
151+
152+
# Run main function
153+
main

0 commit comments

Comments
 (0)