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 +59 -0
- package/bin/setup.js +156 -0
- package/claude/settings-statusline.json +6 -0
- package/claude/statusline-command.sh +162 -0
- package/install.sh +151 -0
- package/package.json +15 -0
- package/wezterm.lua +259 -0
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,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
|