wezterm-setup 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # WezTerm + Claude Code Dotfiles
2
+
3
+ Terminal setup with WezTerm, Claude Code integration, and session persistence.
4
+
5
+ ## What's Included
6
+
7
+ - **WezTerm config** — Claude Code warm dark theme, JetBrains Mono Bold 20pt, session persistence via resurrect plugin, window size persistence, fancy tab bar at bottom
8
+ - **Claude Code status line** — Shows current path, git branch, model, version, and context usage bar with color coding
9
+
10
+ ## Install
11
+
12
+ Choose one:
13
+
14
+ ```bash
15
+ # Option A: npx (requires Node.js)
16
+ npx wezterm-setup
17
+
18
+ # Option B: curl (no Node.js needed)
19
+ curl -fsSL https://raw.githubusercontent.com/Barnhardt-Enterprises-Inc/wezterm-2026/main/install.sh | bash
20
+
21
+ # Option C: local clone
22
+ git clone https://github.com/Barnhardt-Enterprises-Inc/wezterm-2026.git ~/Projects/wezterm-2026
23
+ cd ~/Projects/wezterm-2026 && ./install.sh
24
+ ```
25
+
26
+ ## What Gets Installed
27
+
28
+ - `~/.wezterm.lua` → symlink to repo's `wezterm.lua`
29
+ - `~/.claude/statusline-command.sh` → symlink to repo's status line script
30
+ - `~/.claude/settings.json` → `statusLine` block merged in (non-destructive, existing settings preserved)
31
+
32
+ ## Per-Project Tab Names
33
+
34
+ Create `.wezterm/project.md` in any project directory:
35
+
36
+ ```
37
+ My Project
38
+ #FF6B6B
39
+ ```
40
+
41
+ - Line 1: Tab name (displayed in WezTerm tab bar)
42
+ - Line 2: Hex color for the tab
43
+
44
+ ## Key Bindings
45
+
46
+ | Binding | Action |
47
+ |---------|--------|
48
+ | `Shift+Enter` | Claude Code multiline |
49
+ | `Cmd+1-9` | Switch to tab |
50
+ | `Cmd+Shift+H` | Previous tab |
51
+ | `Cmd+Shift+L` | Next tab |
52
+ | `Cmd+Ctrl+Left/Right` | Move tab |
53
+ | `Cmd+D` | Split horizontal |
54
+ | `Cmd+Shift+D` | Split vertical |
55
+ | `Cmd+Opt+Arrows` | Navigate splits |
56
+ | `Cmd+Shift+S` | Save session |
57
+ | `Cmd+Shift+R` | Reload config |
58
+ | `Cmd+Shift+P` | Command palette |
59
+ | `Cmd+Shift+F` | Search |
package/bin/setup.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ // Colors
10
+ const GREEN = '\x1b[32m';
11
+ const YELLOW = '\x1b[33m';
12
+ const RED = '\x1b[31m';
13
+ const RESET = '\x1b[0m';
14
+
15
+ function ok(msg) { console.log(`${GREEN}✓${RESET} ${msg}`); }
16
+ function skip(msg) { console.log(`● ${msg}`); }
17
+ function warn(msg) { console.log(`${YELLOW}⚠${RESET} ${msg}`); }
18
+ function err(msg) { console.error(`${RED}✗${RESET} ${msg}`); }
19
+
20
+ const REPO_ROOT = path.resolve(__dirname, '..');
21
+ const HOME = os.homedir();
22
+
23
+ function symlinkTarget(linkPath) {
24
+ try {
25
+ return fs.readlinkSync(linkPath);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function isOurSymlink(linkPath, targetPath) {
32
+ const target = symlinkTarget(linkPath);
33
+ return target !== null && path.resolve(target) === path.resolve(targetPath);
34
+ }
35
+
36
+ function fileExists(p) {
37
+ try {
38
+ fs.lstatSync(p);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function isSymlink(p) {
46
+ try {
47
+ return fs.lstatSync(p).isSymbolicLink();
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function backup(filePath) {
54
+ const bakPath = filePath + '.bak';
55
+ fs.copyFileSync(filePath, bakPath);
56
+ warn(`Backed up ${filePath} → ${bakPath}`);
57
+ }
58
+
59
+ function mkdirP(dirPath) {
60
+ fs.mkdirSync(dirPath, { recursive: true });
61
+ }
62
+
63
+ function linkFile(linkPath, targetPath, label) {
64
+ if (fileExists(linkPath)) {
65
+ if (isOurSymlink(linkPath, targetPath)) {
66
+ skip(`${label} already linked — skipping`);
67
+ return;
68
+ }
69
+ if (!isSymlink(linkPath)) {
70
+ backup(linkPath);
71
+ }
72
+ fs.unlinkSync(linkPath);
73
+ }
74
+ fs.symlinkSync(targetPath, linkPath);
75
+ ok(`Linked ${linkPath} → ${targetPath}`);
76
+ }
77
+
78
+ function deepMerge(target, source) {
79
+ const result = Object.assign({}, target);
80
+ for (const key of Object.keys(source)) {
81
+ if (
82
+ typeof source[key] === 'object' &&
83
+ source[key] !== null &&
84
+ !Array.isArray(source[key]) &&
85
+ typeof target[key] === 'object' &&
86
+ target[key] !== null &&
87
+ !Array.isArray(target[key])
88
+ ) {
89
+ result[key] = deepMerge(target[key], source[key]);
90
+ } else {
91
+ result[key] = source[key];
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function objectsEqual(a, b) {
98
+ return JSON.stringify(a) === JSON.stringify(b);
99
+ }
100
+
101
+ // Step 1: Symlink ~/.wezterm.lua
102
+ const weztermSrc = path.join(REPO_ROOT, 'wezterm.lua');
103
+ const weztermDst = path.join(HOME, '.wezterm.lua');
104
+ linkFile(weztermDst, weztermSrc, '~/.wezterm.lua');
105
+
106
+ // Step 2: Ensure ~/.claude/ exists
107
+ const claudeDir = path.join(HOME, '.claude');
108
+ mkdirP(claudeDir);
109
+ ok(`Ensured ${claudeDir} exists`);
110
+
111
+ // Step 3: Symlink ~/.claude/statusline-command.sh
112
+ const statuslineSrc = path.join(REPO_ROOT, 'claude', 'statusline-command.sh');
113
+ const statuslineDst = path.join(claudeDir, 'statusline-command.sh');
114
+ linkFile(statuslineDst, statuslineSrc, '~/.claude/statusline-command.sh');
115
+
116
+ // Step 4: Non-destructive merge into ~/.claude/settings.json
117
+ const settingsMergeSrc = path.join(REPO_ROOT, 'claude', 'settings-statusline.json');
118
+ const settingsDst = path.join(claudeDir, 'settings.json');
119
+
120
+ let mergeKeys;
121
+ try {
122
+ mergeKeys = JSON.parse(fs.readFileSync(settingsMergeSrc, 'utf8'));
123
+ } catch (e) {
124
+ err(`Could not read ${settingsMergeSrc}: ${e.message}`);
125
+ process.exit(1);
126
+ }
127
+
128
+ let existing = {};
129
+ if (fileExists(settingsDst)) {
130
+ try {
131
+ existing = JSON.parse(fs.readFileSync(settingsDst, 'utf8'));
132
+ } catch (e) {
133
+ err(`Could not parse ${settingsDst}: ${e.message}`);
134
+ process.exit(1);
135
+ }
136
+
137
+ // Check if our keys already match
138
+ const alreadySet = Object.keys(mergeKeys).every(
139
+ (k) => objectsEqual(existing[k], mergeKeys[k])
140
+ );
141
+
142
+ if (alreadySet) {
143
+ skip('~/.claude/settings.json statusLine already configured — skipping');
144
+ } else {
145
+ backup(settingsDst);
146
+ const merged = deepMerge(existing, mergeKeys);
147
+ fs.writeFileSync(settingsDst, JSON.stringify(merged, null, 2) + '\n', 'utf8');
148
+ ok(`Merged statusLine into ${settingsDst}`);
149
+ }
150
+ } else {
151
+ fs.writeFileSync(settingsDst, JSON.stringify(mergeKeys, null, 2) + '\n', 'utf8');
152
+ ok(`Created ${settingsDst} with statusLine config`);
153
+ }
154
+
155
+ console.log('\nSetup complete.');
156
+ process.exit(0);
@@ -0,0 +1,6 @@
1
+ {
2
+ "statusLine": {
3
+ "type": "command",
4
+ "command": "bash ~/.claude/statusline-command.sh"
5
+ }
6
+ }
@@ -0,0 +1,162 @@
1
+ #!/bin/bash
2
+
3
+ # Claude Code Status Bar — Minimal, color-coded segments
4
+ # Designed for warm dark theme with true-color ANSI
5
+ # Target render: <50ms (no network calls, single jq, cached git)
6
+
7
+ # --- 1. Read JSON from stdin, extract ALL fields in one jq call ---
8
+ input=$(cat)
9
+ eval "$(echo "$input" | jq -r '
10
+ "cwd=" + (.workspace.current_dir // "" | @sh) + "\n" +
11
+ "project_dir=" + (.workspace.project_dir // "" | @sh) + "\n" +
12
+ "model=" + (.model.display_name // "" | @sh) + "\n" +
13
+ "version=" + (.version // "" | @sh) + "\n" +
14
+ "used_pct=" + (.context_window.used_percentage // 0 | tostring | @sh)
15
+ ')"
16
+
17
+ # Default used_pct to 0 if empty or non-numeric
18
+ [[ "$used_pct" =~ ^[0-9]+$ ]] || used_pct=0
19
+
20
+ # --- 2. Abbreviate path ---
21
+ home_dir="$HOME"
22
+ if [ -n "$project_dir" ] && [ "$project_dir" != "null" ] && [ "$project_dir" != "$home_dir" ]; then
23
+ proj_base=$(basename "$project_dir")
24
+ if [ "$cwd" = "$project_dir" ] || [ -z "$cwd" ] || [ "$cwd" = "null" ]; then
25
+ display_path="~/${proj_base}"
26
+ else
27
+ # Show relative path from project root
28
+ rel_path="${cwd#"$project_dir"}"
29
+ display_path="~/${proj_base}${rel_path}"
30
+ fi
31
+ elif [ -n "$cwd" ] && [ "$cwd" != "null" ] && [ "$cwd" != "$home_dir" ]; then
32
+ # Replace home prefix with ~
33
+ display_path="${cwd/#$home_dir/\~}"
34
+ else
35
+ display_path="~"
36
+ fi
37
+
38
+ # --- 3. Get git branch (cached, 5s TTL) ---
39
+ git_cache="/tmp/claude-statusline-git-cache"
40
+ git_dir="${project_dir:-$cwd}"
41
+ branch=""
42
+
43
+ if [ -n "$git_dir" ] && [ "$git_dir" != "null" ]; then
44
+ cache_valid=0
45
+ if [ -f "$git_cache" ]; then
46
+ # Cross-platform stat: try GNU (Linux) first, fall back to BSD (macOS)
47
+ if stat -c %Y "$git_cache" >/dev/null 2>&1; then
48
+ cache_mtime=$(stat -c %Y "$git_cache")
49
+ else
50
+ cache_mtime=$(stat -f %m "$git_cache")
51
+ fi
52
+ cache_age=$(( $(date +%s) - cache_mtime ))
53
+ # Check cache is for same directory and within TTL
54
+ cached_dir=$(head -1 "$git_cache" 2>/dev/null)
55
+ if [ "$cache_age" -le 5 ] && [ "$cached_dir" = "$git_dir" ]; then
56
+ cache_valid=1
57
+ branch=$(tail -1 "$git_cache" 2>/dev/null)
58
+ fi
59
+ fi
60
+
61
+ if [ "$cache_valid" -eq 0 ]; then
62
+ branch=$(git -C "$git_dir" --no-optional-locks branch --show-current 2>/dev/null || echo "")
63
+ printf '%s\n%s\n' "$git_dir" "$branch" > "$git_cache" 2>/dev/null
64
+ fi
65
+ fi
66
+
67
+ # --- 4. Determine context threshold color + indicator ---
68
+ # Colors (true-color hex -> R G B)
69
+ if [ "$used_pct" -ge 90 ]; then
70
+ bar_r=239; bar_g=83; bar_b=80 # Red #EF5350
71
+ indicator=" new session recommended"
72
+ elif [ "$used_pct" -ge 75 ]; then
73
+ bar_r=230; bar_g=74; bar_b=25 # Deep Orange #E64A19
74
+ indicator=" consider new session"
75
+ elif [ "$used_pct" -ge 60 ]; then
76
+ bar_r=217; bar_g=119; bar_b=87 # Orange #D97757
77
+ indicator=" compacting"
78
+ elif [ "$used_pct" -ge 50 ]; then
79
+ bar_r=255; bar_g=183; bar_b=77 # Amber #FFB74D
80
+ indicator=" compact soon"
81
+ else
82
+ bar_r=129; bar_g=199; bar_b=132 # Green #81C784
83
+ indicator=""
84
+ fi
85
+
86
+ # --- 5. Terminal width ---
87
+ term_width=${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}
88
+
89
+ # --- 6. Print Line 1: icon+path icon+branch icon+model right-aligned-version ---
90
+ # Colors
91
+ c_path="\033[38;2;217;119;87m" # Warm orange #D97757
92
+ c_branch="\033[38;2;77;182;172m" # Muted teal #4DB6AC
93
+ c_model="\033[38;2;120;113;108m" # Stone gray #78716C
94
+ c_version="\033[38;2;68;64;60m" # Very dim #44403C
95
+ c_reset="\033[0m"
96
+
97
+ # Icons (standard Unicode emoji)
98
+ icon_dir="📂" # Open folder
99
+ icon_model="🤖" # Robot head
100
+
101
+ # Build segments
102
+ seg_path="${icon_dir} ${display_path}"
103
+ seg_branch=""
104
+ [ -n "$branch" ] && seg_branch="${branch}"
105
+ seg_model="${icon_model} ${model}"
106
+ seg_version=""
107
+ [ -n "$version" ] && [ "$version" != "null" ] && seg_version="v${version}"
108
+
109
+ # Calculate plain text length for right-alignment
110
+ # Emoji are 2 display columns but ${#} counts as 1 char — add 1 per emoji
111
+ emoji_extra=1 # 📂
112
+ plain_left="${seg_path}"
113
+ [ -n "$seg_branch" ] && plain_left="${plain_left} ${seg_branch}"
114
+ plain_left="${plain_left} ${seg_model}"
115
+ emoji_extra=$((emoji_extra + 1)) # 🤖
116
+ plain_len=$(( ${#plain_left} + emoji_extra ))
117
+
118
+ # Build colored left side
119
+ colored_left="${c_reset}${icon_dir} ${c_path}${display_path}${c_reset}"
120
+ [ -n "$seg_branch" ] && colored_left="${colored_left} ${c_branch}${branch}${c_reset}"
121
+ colored_left="${colored_left} ${c_reset}${icon_model} ${c_model}${model}${c_reset}"
122
+
123
+ # Right-align version
124
+ if [ -n "$seg_version" ]; then
125
+ version_len=${#seg_version}
126
+ gap=$(( term_width - plain_len - version_len ))
127
+ [ "$gap" -lt 1 ] && gap=1
128
+ padding=$(printf '%*s' "$gap" '')
129
+ printf "%b%s%b%s%b\n" "$colored_left" "$padding" "$c_version" "$seg_version" "$c_reset"
130
+ else
131
+ printf "%b\n" "$colored_left"
132
+ fi
133
+
134
+ # --- 7. Print Line 2: colored bar + percentage + compact indicator ---
135
+ pct_label=" ${used_pct}%"
136
+ indicator_len=0
137
+ [ -n "$indicator" ] && indicator_len=${#indicator}
138
+ label_len=$(( ${#pct_label} + indicator_len ))
139
+
140
+ # Bar width = terminal width minus label space
141
+ bar_width=$(( term_width - label_len ))
142
+ [ "$bar_width" -lt 10 ] && bar_width=10
143
+
144
+ filled=$(( used_pct * bar_width / 100 ))
145
+ empty=$(( bar_width - filled ))
146
+
147
+ # Build bar string
148
+ bar_filled=""
149
+ bar_empty=""
150
+ for ((i=0; i<filled; i++)); do bar_filled+="▰"; done
151
+ for ((i=0; i<empty; i++)); do bar_empty+="▱"; done
152
+
153
+ # Empty blocks color (very dim)
154
+ c_empty="\033[38;2;68;64;60m"
155
+ c_bar="\033[38;2;${bar_r};${bar_g};${bar_b}m"
156
+
157
+ printf "%b%s%b%s%b%s%b%s%b\n" \
158
+ "$c_bar" "$bar_filled" \
159
+ "$c_empty" "$bar_empty" \
160
+ "$c_bar" "$pct_label" \
161
+ "$c_bar" "$indicator" \
162
+ "$c_reset"
package/install.sh ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # Color codes
6
+ # ---------------------------------------------------------------------------
7
+ GREEN='\033[0;32m'
8
+ YELLOW='\033[0;33m'
9
+ RED='\033[0;31m'
10
+ RESET='\033[0m'
11
+
12
+ ok() { echo -e "${GREEN}✓${RESET} $*"; }
13
+ warn() { echo -e "${YELLOW}●${RESET} $*"; }
14
+ err() { echo -e "${RED}✗${RESET} $*" >&2; }
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Determine REPO_DIR
18
+ # ---------------------------------------------------------------------------
19
+ if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ REPO_DIR="$SCRIPT_DIR"
22
+ else
23
+ # Running via curl pipe — no BASH_SOURCE available
24
+ REPO_DIR="$HOME/Projects/wezterm-2026"
25
+ if [[ ! -d "$REPO_DIR" ]]; then
26
+ warn "Running from curl pipe. Cloning repo to $REPO_DIR ..."
27
+ git clone https://github.com/Barnhardt-Enterprises-Inc/wezterm-2026.git "$REPO_DIR"
28
+ fi
29
+ fi
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Sanity check
33
+ # ---------------------------------------------------------------------------
34
+ if [[ ! -f "$REPO_DIR/wezterm.lua" ]]; then
35
+ err "wezterm.lua not found in $REPO_DIR — aborting."
36
+ exit 1
37
+ fi
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # If Node.js is available, delegate to bin/setup.js
41
+ # ---------------------------------------------------------------------------
42
+ if command -v node &>/dev/null; then
43
+ exec node "$REPO_DIR/bin/setup.js"
44
+ fi
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Full bash installer (no Node.js)
48
+ # ---------------------------------------------------------------------------
49
+
50
+ # Helper: symlink with backup-and-skip logic
51
+ # Usage: make_symlink <target> <link>
52
+ make_symlink() {
53
+ local target="$1"
54
+ local link="$2"
55
+
56
+ if [[ -L "$link" ]]; then
57
+ local current_target
58
+ current_target="$(readlink "$link")"
59
+ if [[ "$current_target" == "$target" ]]; then
60
+ ok "$(basename "$link") symlink already correct — skipping"
61
+ return
62
+ else
63
+ warn "$(basename "$link") points elsewhere; backing up to ${link}.bak"
64
+ mv "$link" "${link}.bak"
65
+ fi
66
+ elif [[ -e "$link" ]]; then
67
+ warn "$(basename "$link") exists as a regular file; backing up to ${link}.bak"
68
+ mv "$link" "${link}.bak"
69
+ fi
70
+
71
+ ln -s "$target" "$link"
72
+ ok "Symlinked $link → $target"
73
+ }
74
+
75
+ # 1. Symlink ~/.wezterm.lua
76
+ make_symlink "$REPO_DIR/wezterm.lua" "$HOME/.wezterm.lua"
77
+
78
+ # 2. Ensure ~/.claude/ exists
79
+ mkdir -p "$HOME/.claude"
80
+ ok "~/.claude/ directory exists"
81
+
82
+ # 3. Symlink ~/.claude/statusline-command.sh
83
+ make_symlink "$REPO_DIR/claude/statusline-command.sh" "$HOME/.claude/statusline-command.sh"
84
+
85
+ # 4. Non-destructive merge into ~/.claude/settings.json
86
+ SETTINGS_FILE="$HOME/.claude/settings.json"
87
+ STATUSLINE_JSON="$REPO_DIR/claude/settings-statusline.json"
88
+ STATUSLINE_INLINE='{"statusLine":{"type":"command","command":"bash ~/.claude/statusline-command.sh"}}'
89
+
90
+ if [[ ! -f "$SETTINGS_FILE" ]]; then
91
+ echo "$STATUSLINE_INLINE" > "$SETTINGS_FILE"
92
+ ok "Created $SETTINGS_FILE with statusLine config"
93
+ else
94
+ # Backup before any merge attempt
95
+ cp "$SETTINGS_FILE" "${SETTINGS_FILE}.bak"
96
+
97
+ MERGED=false
98
+
99
+ # Try python3 first
100
+ if command -v python3 &>/dev/null; then
101
+ RESULT="$(python3 -c "
102
+ import json, sys
103
+ with open(sys.argv[1]) as f:
104
+ existing = json.load(f)
105
+ with open(sys.argv[2]) as f:
106
+ new_keys = json.load(f)
107
+ if all(k in existing and existing[k] == new_keys[k] for k in new_keys):
108
+ print('SKIP')
109
+ sys.exit(0)
110
+ existing.update(new_keys)
111
+ with open(sys.argv[1], 'w') as f:
112
+ json.dump(existing, f, indent=2)
113
+ print('MERGED')
114
+ " "$SETTINGS_FILE" "$STATUSLINE_JSON" 2>/dev/null || echo "ERROR")"
115
+
116
+ case "$RESULT" in
117
+ SKIP) ok "~/.claude/settings.json already contains statusLine — skipping"; MERGED=true ;;
118
+ MERGED) ok "Merged statusLine into ~/.claude/settings.json"; MERGED=true ;;
119
+ *) warn "python3 merge failed, trying jq..." ;;
120
+ esac
121
+ fi
122
+
123
+ # Fallback: jq
124
+ if [[ "$MERGED" == false ]] && command -v jq &>/dev/null; then
125
+ MERGED_JSON="$(jq -s '.[0] * .[1]' "$SETTINGS_FILE" "$STATUSLINE_JSON" 2>/dev/null || echo "")"
126
+ if [[ -n "$MERGED_JSON" ]]; then
127
+ echo "$MERGED_JSON" > "$SETTINGS_FILE"
128
+ ok "Merged statusLine into ~/.claude/settings.json (via jq)"
129
+ MERGED=true
130
+ else
131
+ warn "jq merge failed"
132
+ fi
133
+ fi
134
+
135
+ if [[ "$MERGED" == false ]]; then
136
+ warn "Could not merge settings.json automatically (no python3 or jq). Backup at ${SETTINGS_FILE}.bak"
137
+ warn "Please manually add: $STATUSLINE_INLINE"
138
+ fi
139
+ fi
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Summary
143
+ # ---------------------------------------------------------------------------
144
+ echo ""
145
+ echo -e "${GREEN}Installation complete!${RESET}"
146
+ echo ""
147
+ echo " ~/.wezterm.lua → $REPO_DIR/wezterm.lua"
148
+ echo " ~/.claude/statusline-command.sh → $REPO_DIR/claude/statusline-command.sh"
149
+ echo " ~/.claude/settings.json (statusLine merged)"
150
+ echo ""
151
+ ok "All done. Restart WezTerm to apply changes."
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "wezterm-setup",
3
+ "version": "1.0.0",
4
+ "description": "WezTerm + Claude Code global dotfiles installer",
5
+ "bin": {
6
+ "wezterm-setup": "bin/setup.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "wezterm.lua",
11
+ "claude/",
12
+ "install.sh"
13
+ ],
14
+ "license": "MIT"
15
+ }
package/wezterm.lua ADDED
@@ -0,0 +1,259 @@
1
+ -- WezTerm Config - Claude Code warm dark theme with session persistence
2
+ local wezterm = require 'wezterm'
3
+ local act = wezterm.action
4
+ local config = wezterm.config_builder()
5
+ local resurrect = wezterm.plugin.require('https://github.com/MLFlexer/resurrect.wezterm')
6
+
7
+ -- ==========================================================================
8
+ -- SESSION PERSISTENCE (Unix domain mux server)
9
+ -- ==========================================================================
10
+ config.unix_domains = { { name = 'unix' } }
11
+ config.default_gui_startup_args = { 'connect', 'unix' }
12
+ config.window_close_confirmation = 'NeverPrompt'
13
+
14
+ wezterm.on('mux-is-process-stateful', function()
15
+ return false
16
+ end)
17
+
18
+ -- ==========================================================================
19
+ -- RESURRECT — saves session to disk, survives reboots + power outages
20
+ -- ==========================================================================
21
+ -- Auto-save every 15 minutes
22
+ resurrect.state_manager.periodic_save()
23
+
24
+ -- Auto-restore on startup (when mux server isn't already running)
25
+ wezterm.on('gui-startup', resurrect.state_manager.resurrect_on_gui_startup)
26
+
27
+ -- ==========================================================================
28
+ -- WINDOW SIZE PERSISTENCE
29
+ -- ==========================================================================
30
+ local geo_file = os.getenv('HOME') .. '/.wezterm-geometry'
31
+
32
+ wezterm.on('window-resized', function(window, pane)
33
+ local dims = window:get_dimensions()
34
+ if not dims.is_full_screen then
35
+ local f = io.open(geo_file, 'w')
36
+ if f then
37
+ f:write(string.format('%d\n%d\n', dims.pixel_width, dims.pixel_height))
38
+ f:close()
39
+ end
40
+ end
41
+ end)
42
+
43
+ local size_restored = false
44
+ wezterm.on('window-config-reloaded', function(window, pane)
45
+ if not size_restored then
46
+ size_restored = true
47
+ local f = io.open(geo_file, 'r')
48
+ if f then
49
+ local w = tonumber(f:read('*line'))
50
+ local h = tonumber(f:read('*line'))
51
+ f:close()
52
+ if w and h then
53
+ window:gui_window():set_inner_size(w, h)
54
+ end
55
+ end
56
+ end
57
+ end)
58
+
59
+ -- ==========================================================================
60
+ -- FONT
61
+ -- ==========================================================================
62
+ config.font = wezterm.font('JetBrains Mono', { weight = 'Bold' })
63
+ config.font_size = 20.0
64
+ config.line_height = 1.3
65
+
66
+ -- ==========================================================================
67
+ -- WINDOW
68
+ -- ==========================================================================
69
+ config.window_decorations = 'RESIZE'
70
+ config.window_background_opacity = 1.0
71
+ config.window_padding = { left = 12, right = 12, top = 12, bottom = 12 }
72
+
73
+ -- ==========================================================================
74
+ -- TAB BAR
75
+ -- ==========================================================================
76
+ config.use_fancy_tab_bar = true
77
+ config.tab_bar_at_bottom = true
78
+ config.hide_tab_bar_if_only_one_tab = false
79
+ config.tab_max_width = 48
80
+ config.window_frame = {
81
+ font = wezterm.font('JetBrains Mono', { weight = 'Bold' }),
82
+ font_size = 20.0,
83
+ }
84
+
85
+ -- ==========================================================================
86
+ -- CLAUDE CODE WARM DARK THEME
87
+ -- ==========================================================================
88
+ config.colors = {
89
+ foreground = '#00FF00',
90
+ background = '#1C1917',
91
+ cursor_bg = '#D97757',
92
+ cursor_fg = '#1C1917',
93
+
94
+ ansi = {
95
+ '#292524', -- black
96
+ '#EF5350', -- red
97
+ '#81C784', -- green
98
+ '#FFB74D', -- yellow
99
+ '#64B5F6', -- blue
100
+ '#CE93D8', -- magenta
101
+ '#4DB6AC', -- cyan
102
+ '#D6D3D1', -- white
103
+ },
104
+ brights = {
105
+ '#78716C', -- bright black
106
+ '#E57373', -- bright red
107
+ '#A5D6A7', -- bright green
108
+ '#FFD54F', -- bright yellow
109
+ '#90CAF9', -- bright blue
110
+ '#E1BEE7', -- bright magenta
111
+ '#80CBC4', -- bright cyan
112
+ '#F5E6D3', -- bright white
113
+ },
114
+
115
+ tab_bar = {
116
+ background = '#1C1917',
117
+ active_tab = {
118
+ bg_color = '#292524',
119
+ fg_color = '#D97757',
120
+ },
121
+ inactive_tab = {
122
+ bg_color = '#1C1917',
123
+ fg_color = '#78716C',
124
+ },
125
+ new_tab = {
126
+ bg_color = '#1C1917',
127
+ fg_color = '#D97757',
128
+ },
129
+ },
130
+ }
131
+
132
+ -- ==========================================================================
133
+ -- TAB TITLE SYSTEM (reads .wezterm/project.md per directory)
134
+ -- ==========================================================================
135
+ local function get_project_title(cwd)
136
+ if not cwd or cwd == '' then return nil end
137
+ local ok, file = pcall(io.open, cwd .. '/.wezterm/project.md', 'r')
138
+ if ok and file then
139
+ local first_line = file:read('*line')
140
+ file:close()
141
+ if first_line then
142
+ local title = first_line:gsub('^#%s*', '')
143
+ if title and #title > 0 then return title end
144
+ end
145
+ end
146
+ return nil
147
+ end
148
+
149
+ local function get_project_color(cwd)
150
+ if not cwd or cwd == '' then return nil end
151
+ local ok, file = pcall(io.open, cwd .. '/.wezterm/project.md', 'r')
152
+ if ok and file then
153
+ file:read('*line')
154
+ local second_line = file:read('*line')
155
+ file:close()
156
+ if second_line then
157
+ local color = second_line:match('^#%x%x%x%x%x%x$')
158
+ if color then return color end
159
+ end
160
+ end
161
+ return nil
162
+ end
163
+
164
+ wezterm.on('format-tab-title', function(tab)
165
+ local cwd_url = tab.active_pane.current_working_dir
166
+ local title = nil
167
+ local color = nil
168
+ local default_color = '#D97757'
169
+
170
+ if cwd_url then
171
+ local path
172
+ if type(cwd_url) == 'userdata' or type(cwd_url) == 'table' then
173
+ path = cwd_url.file_path
174
+ elseif type(cwd_url) == 'string' then
175
+ path = cwd_url:gsub('^file://[^/]*', '')
176
+ end
177
+ if path then
178
+ title = get_project_title(path)
179
+ color = get_project_color(path)
180
+ end
181
+ end
182
+
183
+ if not title or #title == 0 then
184
+ if tab.tab_title and tab.tab_title ~= '' then
185
+ title = tab.tab_title
186
+ else
187
+ local process = tab.active_pane.foreground_process_name
188
+ if process and process ~= '' then
189
+ local name = process:match('([^/]+)$')
190
+ if name and name ~= '' then title = name end
191
+ end
192
+ end
193
+ if not title or title == '' then title = 'Terminal' end
194
+ end
195
+
196
+ color = color or default_color
197
+
198
+ if tab.is_active then
199
+ return {
200
+ { Foreground = { Color = color } },
201
+ { Text = string.format(' \u{25cf} %s ', title) },
202
+ }
203
+ else
204
+ return {
205
+ { Foreground = { Color = color } },
206
+ { Text = string.format(' %s ', title) },
207
+ }
208
+ end
209
+ end)
210
+
211
+ -- ==========================================================================
212
+ -- KEY BINDINGS
213
+ -- ==========================================================================
214
+ config.keys = {
215
+ -- Claude Code multiline (CSI u sequence)
216
+ { key = 'Enter', mods = 'SHIFT', action = act.SendString('\x1b[13;2u') },
217
+
218
+ -- Tab navigation: CMD+1-9
219
+ { key = '1', mods = 'CMD', action = act.ActivateTab(0) },
220
+ { key = '2', mods = 'CMD', action = act.ActivateTab(1) },
221
+ { key = '3', mods = 'CMD', action = act.ActivateTab(2) },
222
+ { key = '4', mods = 'CMD', action = act.ActivateTab(3) },
223
+ { key = '5', mods = 'CMD', action = act.ActivateTab(4) },
224
+ { key = '6', mods = 'CMD', action = act.ActivateTab(5) },
225
+ { key = '7', mods = 'CMD', action = act.ActivateTab(6) },
226
+ { key = '8', mods = 'CMD', action = act.ActivateTab(7) },
227
+ { key = '9', mods = 'CMD', action = act.ActivateTab(-1) },
228
+
229
+ -- Relative tab navigation
230
+ { key = 'h', mods = 'CMD|SHIFT', action = act.ActivateTabRelative(-1) },
231
+ { key = 'l', mods = 'CMD|SHIFT', action = act.ActivateTabRelative(1) },
232
+
233
+ -- Move tab left/right
234
+ { key = 'LeftArrow', mods = 'CMD|CTRL', action = act.MoveTabRelative(-1) },
235
+ { key = 'RightArrow', mods = 'CMD|CTRL', action = act.MoveTabRelative(1) },
236
+
237
+ -- Splits
238
+ { key = 'd', mods = 'CMD', action = act.SplitHorizontal { domain = 'CurrentPaneDomain' } },
239
+ { key = 'd', mods = 'CMD|SHIFT', action = act.SplitVertical { domain = 'CurrentPaneDomain' } },
240
+
241
+ -- Navigate splits
242
+ { key = 'LeftArrow', mods = 'CMD|OPT', action = act.ActivatePaneDirection 'Left' },
243
+ { key = 'RightArrow', mods = 'CMD|OPT', action = act.ActivatePaneDirection 'Right' },
244
+ { key = 'UpArrow', mods = 'CMD|OPT', action = act.ActivatePaneDirection 'Up' },
245
+ { key = 'DownArrow', mods = 'CMD|OPT', action = act.ActivatePaneDirection 'Down' },
246
+
247
+ -- Session save/restore (resurrect)
248
+ { key = 's', mods = 'CMD|SHIFT', action = wezterm.action_callback(function(win, pane)
249
+ resurrect.state_manager.save_state(resurrect.workspace_state.get_workspace_state())
250
+ win:toast_notification('WezTerm', 'Session saved!', nil, 3000)
251
+ end) },
252
+
253
+ -- Utility
254
+ { key = 'r', mods = 'CMD|SHIFT', action = act.ReloadConfiguration },
255
+ { key = 'p', mods = 'CMD|SHIFT', action = act.ActivateCommandPalette },
256
+ { key = 'f', mods = 'CMD|SHIFT', action = act.Search { CaseInSensitiveString = '' } },
257
+ }
258
+
259
+ return config