@jx0/jmux 0.4.0 → 0.5.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 +1 -0
- package/config/defaults.conf +3 -0
- package/config/new-session.sh +21 -7
- package/config/release-notes.sh +48 -0
- package/config/settings.sh +139 -0
- package/package.json +1 -1
- package/src/main.ts +88 -8
- package/src/sidebar.ts +50 -7
package/README.md
CHANGED
package/config/defaults.conf
CHANGED
|
@@ -63,6 +63,9 @@ bind y run-shell "tmux capture-pane -pS - -E - | grep . | pbcopy" \; display-mes
|
|
|
63
63
|
# Move window to another session (C-a m)
|
|
64
64
|
bind-key m display-popup -E -w 40% -h 50% -b heavy -S 'fg=#4f565d' "$JMUX_DIR/config/move-window.sh"
|
|
65
65
|
|
|
66
|
+
# Settings modal (C-a i)
|
|
67
|
+
bind-key i display-popup -E -w 50% -h 40% -b heavy -S 'fg=#4f565d' "$JMUX_DIR/config/settings.sh"
|
|
68
|
+
|
|
66
69
|
# Rename session (C-a r)
|
|
67
70
|
bind-key r display-popup -E -w 40% -h 8 -b heavy -S 'fg=#4f565d' "$JMUX_DIR/config/rename-session.sh"
|
|
68
71
|
|
package/config/new-session.sh
CHANGED
|
@@ -6,14 +6,24 @@ FZF_COLORS="border:#4f565d,header:#b5bcc9,prompt:#9fe8c3,label:#9fe8c3,pointer:#
|
|
|
6
6
|
|
|
7
7
|
# ─── Step 1: Pick a directory ─────────────────────────────────────────
|
|
8
8
|
|
|
9
|
+
# Read project directories from config, fall back to defaults
|
|
10
|
+
CONFIG_FILE="$HOME/.config/jmux/config.json"
|
|
11
|
+
SEARCH_DIRS=""
|
|
12
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
13
|
+
SEARCH_DIRS=$(bun -e "
|
|
14
|
+
const c = await Bun.file('$CONFIG_FILE').json().catch(() => ({}));
|
|
15
|
+
const dirs = c.projectDirs ?? [];
|
|
16
|
+
console.log(dirs.map(d => d.replace('~', process.env.HOME)).join('\n'));
|
|
17
|
+
" 2>/dev/null)
|
|
18
|
+
fi
|
|
19
|
+
if [ -z "$SEARCH_DIRS" ]; then
|
|
20
|
+
SEARCH_DIRS=$(printf "%s\n%s\n%s\n%s\n%s" \
|
|
21
|
+
"$HOME/Code" "$HOME/Projects" "$HOME/src" "$HOME/work" "$HOME/dev")
|
|
22
|
+
fi
|
|
23
|
+
|
|
9
24
|
# Build project list: find directories with .git (dir or file — worktrees use a file)
|
|
10
25
|
# Search common code directories, limit depth for speed
|
|
11
|
-
PROJECT_DIRS=$(find \
|
|
12
|
-
"$HOME/Code" \
|
|
13
|
-
"$HOME/Projects" \
|
|
14
|
-
"$HOME/src" \
|
|
15
|
-
"$HOME/work" \
|
|
16
|
-
"$HOME/dev" \
|
|
26
|
+
PROJECT_DIRS=$(echo "$SEARCH_DIRS" | xargs -I{} find "{}" \
|
|
17
27
|
-maxdepth 4 -name ".git" 2>/dev/null \
|
|
18
28
|
| sed 's|/\.git$||' \
|
|
19
29
|
| sort -u)
|
|
@@ -44,7 +54,11 @@ WORK_DIR="${SELECTED_DIR/#\~/$HOME}"
|
|
|
44
54
|
|
|
45
55
|
# Check if this is a bare repo (wtm-managed) and wtm is available
|
|
46
56
|
IS_BARE=false
|
|
47
|
-
|
|
57
|
+
WTM_ENABLED=$(bun -e "
|
|
58
|
+
const c = await Bun.file('$CONFIG_FILE').json().catch(() => ({}));
|
|
59
|
+
console.log(c.wtmIntegration ?? true);
|
|
60
|
+
" 2>/dev/null || echo "true")
|
|
61
|
+
if [ "$WTM_ENABLED" = "true" ] && command -v wtm &>/dev/null && [ -f "$WORK_DIR/.git/config" ]; then
|
|
48
62
|
if git --git-dir="$WORK_DIR/.git" config --get core.bare 2>/dev/null | grep -q "true"; then
|
|
49
63
|
IS_BARE=true
|
|
50
64
|
fi
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Show release notes for a jmux version in a formatted popup
|
|
3
|
+
# Usage: release-notes.sh <tag>
|
|
4
|
+
|
|
5
|
+
TAG="${1:-v0.0.0}"
|
|
6
|
+
REPO="jarredkenny/jmux"
|
|
7
|
+
|
|
8
|
+
# Colors
|
|
9
|
+
BOLD="\033[1m"
|
|
10
|
+
DIM="\033[2m"
|
|
11
|
+
GREEN="\033[32m"
|
|
12
|
+
YELLOW="\033[33m"
|
|
13
|
+
CYAN="\033[36m"
|
|
14
|
+
RESET="\033[0m"
|
|
15
|
+
|
|
16
|
+
# Fetch release data
|
|
17
|
+
BODY=$(gh release view "$TAG" --repo "$REPO" --json body,publishedAt,name -q '"\(.name)\n\(.publishedAt)\n\(.body)"' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
if [ -z "$BODY" ]; then
|
|
20
|
+
echo -e "\n ${DIM}jmux ${TAG}${RESET}\n"
|
|
21
|
+
echo -e " ${DIM}No release notes available.${RESET}\n"
|
|
22
|
+
echo -e " ${DIM}Press q to close${RESET}"
|
|
23
|
+
read -rsn1
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
NAME=$(echo "$BODY" | head -1)
|
|
28
|
+
DATE=$(echo "$BODY" | sed -n '2p' | cut -dT -f1)
|
|
29
|
+
NOTES=$(echo "$BODY" | tail -n +3)
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
echo ""
|
|
33
|
+
echo -e " ${BOLD}${GREEN}jmux ${NAME}${RESET}"
|
|
34
|
+
echo -e " ${DIM}Released ${DATE}${RESET}"
|
|
35
|
+
echo ""
|
|
36
|
+
# Format markdown: bold **text**, headers ##, bullet points
|
|
37
|
+
echo "$NOTES" | sed \
|
|
38
|
+
-e "s/^## \(.*\)/ $(printf '\033[1m')\1$(printf '\033[0m')/" \
|
|
39
|
+
-e "s/^- / • /" \
|
|
40
|
+
-e "s/\*\*\([^*]*\)\*\*/$(printf '\033[1m')\1$(printf '\033[0m')/g" \
|
|
41
|
+
-e "s/\`\([^\`]*\)\`/$(printf '\033[36m')\1$(printf '\033[0m')/g" \
|
|
42
|
+
-e '/^$/s/^$//'
|
|
43
|
+
echo ""
|
|
44
|
+
echo -e " ${DIM}https://github.com/${REPO}/releases/tag/${TAG}${RESET}"
|
|
45
|
+
echo ""
|
|
46
|
+
echo -e " ${DIM}Press q to close${RESET}"
|
|
47
|
+
}
|
|
48
|
+
read -rsn1
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# jmux settings modal — reads/writes ~/.config/jmux/config.json
|
|
3
|
+
# Called via: display-popup -E "settings.sh"
|
|
4
|
+
|
|
5
|
+
CONFIG_DIR="$HOME/.config/jmux"
|
|
6
|
+
CONFIG_FILE="$CONFIG_DIR/config.json"
|
|
7
|
+
FZF_COLORS="border:#4f565d,header:#b5bcc9,prompt:#9fe8c3,label:#9fe8c3,pointer:#9fe8c3,fg:#6b7280,fg+:#b5bcc9,hl:#fbd4b8,hl+:#fbd4b8"
|
|
8
|
+
|
|
9
|
+
# ─── Helpers ──────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
read_config() {
|
|
12
|
+
bun -e "
|
|
13
|
+
const c = await Bun.file('$CONFIG_FILE').json().catch(() => ({}));
|
|
14
|
+
console.log(JSON.stringify(c));
|
|
15
|
+
" 2>/dev/null || echo "{}"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
write_config() {
|
|
19
|
+
local json="$1"
|
|
20
|
+
mkdir -p "$CONFIG_DIR"
|
|
21
|
+
echo "$json" | bun -e "
|
|
22
|
+
const stdin = await Bun.stdin.text();
|
|
23
|
+
const c = JSON.parse(stdin);
|
|
24
|
+
await Bun.write('$CONFIG_FILE', JSON.stringify(c, null, 2) + '\n');
|
|
25
|
+
"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get_value() {
|
|
29
|
+
local json="$1" key="$2" default="$3"
|
|
30
|
+
echo "$json" | bun -e "
|
|
31
|
+
const stdin = await Bun.stdin.text();
|
|
32
|
+
const c = JSON.parse(stdin);
|
|
33
|
+
console.log(c['$key'] ?? '$default');
|
|
34
|
+
" 2>/dev/null || echo "$default"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set_value() {
|
|
38
|
+
local json="$1" key="$2" value="$3" type="$4"
|
|
39
|
+
echo "$json" | bun -e "
|
|
40
|
+
const stdin = await Bun.stdin.text();
|
|
41
|
+
const c = JSON.parse(stdin);
|
|
42
|
+
const v = '$value';
|
|
43
|
+
const t = '$type';
|
|
44
|
+
if (t === 'number') c['$key'] = parseInt(v, 10);
|
|
45
|
+
else if (t === 'bool') c['$key'] = v === 'true';
|
|
46
|
+
else if (t === 'array') c['$key'] = v.split(',').map(s => s.trim()).filter(Boolean);
|
|
47
|
+
else c['$key'] = v;
|
|
48
|
+
console.log(JSON.stringify(c));
|
|
49
|
+
" 2>/dev/null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ─── Main loop ────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
while true; do
|
|
55
|
+
CONFIG=$(read_config)
|
|
56
|
+
|
|
57
|
+
SIDEBAR_WIDTH=$(get_value "$CONFIG" "sidebarWidth" "26")
|
|
58
|
+
PROJECT_DIRS_RAW=$(echo "$CONFIG" | bun -e "
|
|
59
|
+
const c = JSON.parse(await Bun.stdin.text());
|
|
60
|
+
const dirs = c.projectDirs ?? ['~/Code', '~/Projects', '~/src', '~/work', '~/dev'];
|
|
61
|
+
console.log(dirs.join(', '));
|
|
62
|
+
" 2>/dev/null || echo "~/Code, ~/Projects, ~/src, ~/work, ~/dev")
|
|
63
|
+
WTM_ENABLED=$(get_value "$CONFIG" "wtmIntegration" "true")
|
|
64
|
+
|
|
65
|
+
# Format display
|
|
66
|
+
WTM_DISPLAY="on"
|
|
67
|
+
[ "$WTM_ENABLED" = "false" ] && WTM_DISPLAY="off"
|
|
68
|
+
|
|
69
|
+
SELECTION=$(printf "%s\n%s\n%s" \
|
|
70
|
+
"Sidebar Width $SIDEBAR_WIDTH" \
|
|
71
|
+
"Project Directories $PROJECT_DIRS_RAW" \
|
|
72
|
+
"wtm Integration $WTM_DISPLAY" \
|
|
73
|
+
| fzf \
|
|
74
|
+
--height=100% \
|
|
75
|
+
--layout=reverse \
|
|
76
|
+
--border=rounded \
|
|
77
|
+
--border-label=" Settings " \
|
|
78
|
+
--header="Select a setting to change" \
|
|
79
|
+
--header-first \
|
|
80
|
+
--prompt=" " \
|
|
81
|
+
--pointer="▸" \
|
|
82
|
+
--no-info \
|
|
83
|
+
--color="$FZF_COLORS")
|
|
84
|
+
|
|
85
|
+
[ -z "$SELECTION" ] && exit 0
|
|
86
|
+
|
|
87
|
+
case "$SELECTION" in
|
|
88
|
+
"Sidebar Width"*)
|
|
89
|
+
NEW_WIDTH=$(echo "" | fzf --print-query \
|
|
90
|
+
--height=100% \
|
|
91
|
+
--layout=reverse \
|
|
92
|
+
--border=rounded \
|
|
93
|
+
--border-label=" Sidebar Width " \
|
|
94
|
+
--header="Current: $SIDEBAR_WIDTH (takes effect on restart)" \
|
|
95
|
+
--header-first \
|
|
96
|
+
--prompt="Width: " \
|
|
97
|
+
--query="$SIDEBAR_WIDTH" \
|
|
98
|
+
--pointer="" \
|
|
99
|
+
--no-info \
|
|
100
|
+
--color="$FZF_COLORS" \
|
|
101
|
+
| head -1)
|
|
102
|
+
|
|
103
|
+
if [ -n "$NEW_WIDTH" ]; then
|
|
104
|
+
CONFIG=$(set_value "$CONFIG" "sidebarWidth" "$NEW_WIDTH" "number")
|
|
105
|
+
write_config "$CONFIG"
|
|
106
|
+
fi
|
|
107
|
+
;;
|
|
108
|
+
|
|
109
|
+
"Project Directories"*)
|
|
110
|
+
NEW_DIRS=$(echo "" | fzf --print-query \
|
|
111
|
+
--height=100% \
|
|
112
|
+
--layout=reverse \
|
|
113
|
+
--border=rounded \
|
|
114
|
+
--border-label=" Project Directories " \
|
|
115
|
+
--header="Comma-separated list of directories to search" \
|
|
116
|
+
--header-first \
|
|
117
|
+
--prompt="Dirs: " \
|
|
118
|
+
--query="$PROJECT_DIRS_RAW" \
|
|
119
|
+
--pointer="" \
|
|
120
|
+
--no-info \
|
|
121
|
+
--color="$FZF_COLORS" \
|
|
122
|
+
| head -1)
|
|
123
|
+
|
|
124
|
+
if [ -n "$NEW_DIRS" ]; then
|
|
125
|
+
CONFIG=$(set_value "$CONFIG" "projectDirs" "$NEW_DIRS" "array")
|
|
126
|
+
write_config "$CONFIG"
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
|
|
130
|
+
"wtm Integration"*)
|
|
131
|
+
if [ "$WTM_ENABLED" = "true" ]; then
|
|
132
|
+
CONFIG=$(set_value "$CONFIG" "wtmIntegration" "false" "bool")
|
|
133
|
+
else
|
|
134
|
+
CONFIG=$(set_value "$CONFIG" "wtmIntegration" "true" "bool")
|
|
135
|
+
fi
|
|
136
|
+
write_config "$CONFIG"
|
|
137
|
+
;;
|
|
138
|
+
esac
|
|
139
|
+
done
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { homedir } from "os";
|
|
|
12
12
|
|
|
13
13
|
// --- CLI commands (run and exit before TUI) ---
|
|
14
14
|
|
|
15
|
-
const VERSION = "0.
|
|
15
|
+
const VERSION = "0.5.0";
|
|
16
16
|
|
|
17
17
|
const HELP = `jmux — a persistent session sidebar for tmux
|
|
18
18
|
|
|
@@ -108,9 +108,22 @@ function installAgentHooks(): void {
|
|
|
108
108
|
|
|
109
109
|
// --- TUI startup ---
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
// Read sidebar width from user config, fall back to default
|
|
112
|
+
function loadUserConfig(): Record<string, any> {
|
|
113
|
+
const configPath = resolve(homedir(), ".config", "jmux", "config.json");
|
|
114
|
+
try {
|
|
115
|
+
if (existsSync(configPath)) {
|
|
116
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Invalid config — use defaults
|
|
120
|
+
}
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
const userConfig = loadUserConfig();
|
|
124
|
+
let sidebarWidth = (userConfig.sidebarWidth as number) || 26;
|
|
112
125
|
const BORDER_WIDTH = 1;
|
|
113
|
-
|
|
126
|
+
function sidebarTotal(): number { return sidebarWidth + BORDER_WIDTH; }
|
|
114
127
|
|
|
115
128
|
// Resolve paths relative to source
|
|
116
129
|
const jmuxDir = resolve(dirname(import.meta.dir));
|
|
@@ -138,7 +151,7 @@ for (let i = 2; i < process.argv.length; i++) {
|
|
|
138
151
|
const cols = process.stdout.columns || 80;
|
|
139
152
|
const rows = process.stdout.rows || 24;
|
|
140
153
|
const sidebarVisible = cols >= 80;
|
|
141
|
-
const mainCols = sidebarVisible ? cols -
|
|
154
|
+
const mainCols = sidebarVisible ? cols - sidebarTotal() : cols;
|
|
142
155
|
|
|
143
156
|
// Enter alternate screen, raw mode, enable mouse tracking
|
|
144
157
|
process.stdout.write("\x1b[?1049h");
|
|
@@ -155,13 +168,15 @@ process.stdin.resume();
|
|
|
155
168
|
const pty = new TmuxPty({ sessionName, socketName, configFile, jmuxDir, cols: mainCols, rows });
|
|
156
169
|
const bridge = new ScreenBridge(mainCols, rows);
|
|
157
170
|
const renderer = new Renderer();
|
|
158
|
-
const sidebar = new Sidebar(
|
|
171
|
+
const sidebar = new Sidebar(sidebarWidth, rows);
|
|
159
172
|
const control = new TmuxControl();
|
|
160
173
|
|
|
161
174
|
let currentSessionId: string | null = null;
|
|
162
175
|
let ptyClientName: string | null = null;
|
|
163
176
|
let sidebarShown = sidebarVisible;
|
|
164
177
|
let currentSessions: SessionInfo[] = [];
|
|
178
|
+
|
|
179
|
+
sidebar.setVersion(VERSION);
|
|
165
180
|
const lastViewedTimestamps = new Map<string, number>();
|
|
166
181
|
const sessionDetailsCache = new Map<string, { directory?: string; gitBranch?: string; project?: string }>();
|
|
167
182
|
|
|
@@ -303,12 +318,16 @@ function clearSessionIndicators(): void {
|
|
|
303
318
|
|
|
304
319
|
const inputRouter = new InputRouter(
|
|
305
320
|
{
|
|
306
|
-
sidebarCols:
|
|
321
|
+
sidebarCols: sidebarWidth,
|
|
307
322
|
onPtyData: (data) => {
|
|
308
323
|
pty.write(data);
|
|
309
324
|
clearSessionIndicators();
|
|
310
325
|
},
|
|
311
326
|
onSidebarClick: (row) => {
|
|
327
|
+
if (sidebar.isVersionRow(row)) {
|
|
328
|
+
showVersionInfo();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
312
331
|
const session = sidebar.getSessionByRow(row);
|
|
313
332
|
if (session) switchSession(session.id);
|
|
314
333
|
},
|
|
@@ -393,15 +412,76 @@ process.on("SIGWINCH", () => {
|
|
|
393
412
|
const newCols = process.stdout.columns || 80;
|
|
394
413
|
const newRows = process.stdout.rows || 24;
|
|
395
414
|
const newSidebarVisible = newCols >= 80;
|
|
396
|
-
const newMainCols = newSidebarVisible ? newCols -
|
|
415
|
+
const newMainCols = newSidebarVisible ? newCols - sidebarTotal() : newCols;
|
|
397
416
|
|
|
398
417
|
sidebarShown = newSidebarVisible;
|
|
399
418
|
inputRouter.setSidebarVisible(newSidebarVisible);
|
|
400
419
|
pty.resize(newMainCols, newRows);
|
|
401
420
|
bridge.resize(newMainCols, newRows);
|
|
402
|
-
sidebar.resize(
|
|
421
|
+
sidebar.resize(sidebarWidth, newRows);
|
|
403
422
|
});
|
|
404
423
|
|
|
424
|
+
// --- Config file watcher ---
|
|
425
|
+
|
|
426
|
+
const configPath = resolve(homedir(), ".config", "jmux", "config.json");
|
|
427
|
+
try {
|
|
428
|
+
const { watch } = await import("fs");
|
|
429
|
+
watch(configPath, () => {
|
|
430
|
+
const updated = loadUserConfig();
|
|
431
|
+
const newWidth = (updated.sidebarWidth as number) || 26;
|
|
432
|
+
if (newWidth !== sidebarWidth) {
|
|
433
|
+
sidebarWidth = newWidth;
|
|
434
|
+
const cols = process.stdout.columns || 80;
|
|
435
|
+
const rows = process.stdout.rows || 24;
|
|
436
|
+
const newSidebarVisible = cols >= 80;
|
|
437
|
+
const newMainCols = newSidebarVisible ? cols - sidebarTotal() : cols;
|
|
438
|
+
|
|
439
|
+
sidebarShown = newSidebarVisible;
|
|
440
|
+
inputRouter.setSidebarVisible(newSidebarVisible);
|
|
441
|
+
pty.resize(newMainCols, rows);
|
|
442
|
+
bridge.resize(newMainCols, rows);
|
|
443
|
+
sidebar.resize(sidebarWidth, rows);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
} catch {
|
|
447
|
+
// Config file may not exist yet — watcher will fail silently
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// --- Update check ---
|
|
451
|
+
|
|
452
|
+
async function checkForUpdates(): Promise<void> {
|
|
453
|
+
try {
|
|
454
|
+
const resp = await fetch(
|
|
455
|
+
"https://api.github.com/repos/jarredkenny/jmux/releases/latest",
|
|
456
|
+
{ headers: { "Accept": "application/vnd.github.v3+json" } },
|
|
457
|
+
);
|
|
458
|
+
if (!resp.ok) return;
|
|
459
|
+
const data = await resp.json() as { tag_name?: string };
|
|
460
|
+
const latest = data.tag_name?.replace(/^v/, "");
|
|
461
|
+
if (latest && latest !== VERSION) {
|
|
462
|
+
sidebar.setVersion(VERSION, latest);
|
|
463
|
+
scheduleRender();
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Offline or rate-limited — no problem
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function showVersionInfo(): Promise<void> {
|
|
471
|
+
if (!ptyClientName) await resolveClientName();
|
|
472
|
+
if (!ptyClientName) return;
|
|
473
|
+
const tag = `v${VERSION}`;
|
|
474
|
+
const cmd = `${jmuxDir}/config/release-notes.sh ${tag}`;
|
|
475
|
+
// Use tmux CLI directly — confirmed working from terminal tests
|
|
476
|
+
const args = ["tmux"];
|
|
477
|
+
if (socketName) args.push("-L", socketName);
|
|
478
|
+
args.push("display-popup", "-c", ptyClientName, "-E", "-w", "70%", "-h", "40%", "-b", "heavy", "-S", "fg=#4f565d", "sh", "-c", cmd);
|
|
479
|
+
Bun.spawn(args, { stdout: "ignore", stderr: "ignore" });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check for updates in the background (non-blocking)
|
|
483
|
+
checkForUpdates();
|
|
484
|
+
|
|
405
485
|
// --- Control mode events ---
|
|
406
486
|
|
|
407
487
|
control.onEvent((event: ControlEvent) => {
|
package/src/sidebar.ts
CHANGED
|
@@ -161,6 +161,11 @@ function itemHeight(item: RenderItem): number {
|
|
|
161
161
|
|
|
162
162
|
// --- Sidebar class ---
|
|
163
163
|
|
|
164
|
+
const UPDATE_AVAILABLE_ATTRS: CellAttrs = {
|
|
165
|
+
fg: 3,
|
|
166
|
+
fgMode: ColorMode.Palette,
|
|
167
|
+
};
|
|
168
|
+
|
|
164
169
|
export class Sidebar {
|
|
165
170
|
private width: number;
|
|
166
171
|
private height: number;
|
|
@@ -171,6 +176,8 @@ export class Sidebar {
|
|
|
171
176
|
private rowToSessionIndex = new Map<number, number>();
|
|
172
177
|
private activitySet = new Set<string>();
|
|
173
178
|
private scrollOffset = 0;
|
|
179
|
+
private currentVersion: string = "";
|
|
180
|
+
private latestVersion: string | null = null;
|
|
174
181
|
|
|
175
182
|
constructor(width: number, height: number) {
|
|
176
183
|
this.width = width;
|
|
@@ -212,6 +219,19 @@ export class Sidebar {
|
|
|
212
219
|
.filter(Boolean) as string[];
|
|
213
220
|
}
|
|
214
221
|
|
|
222
|
+
setVersion(current: string, latest?: string): void {
|
|
223
|
+
this.currentVersion = current;
|
|
224
|
+
this.latestVersion = latest ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
hasUpdate(): boolean {
|
|
228
|
+
return this.latestVersion !== null && this.latestVersion !== this.currentVersion;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
isVersionRow(row: number): boolean {
|
|
232
|
+
return this.currentVersion !== "" && row === this.height - 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
215
235
|
getSessionByRow(row: number): SessionInfo | null {
|
|
216
236
|
const sessionIdx = this.rowToSessionIndex.get(row);
|
|
217
237
|
if (sessionIdx === undefined) return null;
|
|
@@ -231,7 +251,7 @@ export class Sidebar {
|
|
|
231
251
|
|
|
232
252
|
scrollToActive(): void {
|
|
233
253
|
if (!this.activeSessionId) return;
|
|
234
|
-
const viewportHeight = this.
|
|
254
|
+
const viewportHeight = this.viewportHeight();
|
|
235
255
|
let vRow = 0;
|
|
236
256
|
for (const item of this.items) {
|
|
237
257
|
const h = itemHeight(item);
|
|
@@ -251,10 +271,17 @@ export class Sidebar {
|
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
|
|
274
|
+
private footerRows(): number {
|
|
275
|
+
return this.currentVersion ? 1 : 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private viewportHeight(): number {
|
|
279
|
+
return this.height - HEADER_ROWS - this.footerRows();
|
|
280
|
+
}
|
|
281
|
+
|
|
254
282
|
private clampScroll(): void {
|
|
255
283
|
const totalRows = this.items.reduce((sum, item) => sum + itemHeight(item), 0);
|
|
256
|
-
const
|
|
257
|
-
const maxOffset = Math.max(0, totalRows - viewportHeight);
|
|
284
|
+
const maxOffset = Math.max(0, totalRows - this.viewportHeight());
|
|
258
285
|
this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset));
|
|
259
286
|
}
|
|
260
287
|
|
|
@@ -266,7 +293,8 @@ export class Sidebar {
|
|
|
266
293
|
writeString(grid, 0, 1, "jmux", { ...ACCENT_ATTRS, bold: true });
|
|
267
294
|
writeString(grid, 1, 0, "\u2500".repeat(this.width), DIM_ATTRS);
|
|
268
295
|
|
|
269
|
-
const
|
|
296
|
+
const vpHeight = this.viewportHeight();
|
|
297
|
+
const contentBottom = HEADER_ROWS + vpHeight;
|
|
270
298
|
let vRow = 0;
|
|
271
299
|
let totalRows = 0;
|
|
272
300
|
|
|
@@ -281,7 +309,7 @@ export class Sidebar {
|
|
|
281
309
|
continue;
|
|
282
310
|
}
|
|
283
311
|
// Track total rows even after viewport
|
|
284
|
-
if (screenRow >=
|
|
312
|
+
if (screenRow >= contentBottom) {
|
|
285
313
|
vRow += h;
|
|
286
314
|
totalRows += h;
|
|
287
315
|
continue;
|
|
@@ -307,8 +335,23 @@ export class Sidebar {
|
|
|
307
335
|
if (this.scrollOffset > 0) {
|
|
308
336
|
writeString(grid, HEADER_ROWS, this.width - 1, "\u25b2", DIM_ATTRS);
|
|
309
337
|
}
|
|
310
|
-
if (this.scrollOffset +
|
|
311
|
-
|
|
338
|
+
if (this.scrollOffset + vpHeight < totalRows) {
|
|
339
|
+
const scrollRow = this.footerRows() ? contentBottom - 1 : this.height - 1;
|
|
340
|
+
writeString(grid, scrollRow, this.width - 1, "\u25bc", DIM_ATTRS);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Version footer
|
|
344
|
+
if (this.currentVersion) {
|
|
345
|
+
const footerRow = this.height - 1;
|
|
346
|
+
const versionText = `v${this.currentVersion}`;
|
|
347
|
+
if (this.hasUpdate()) {
|
|
348
|
+
const updateText = `v${this.latestVersion} avail`;
|
|
349
|
+
const maxLen = this.width - 2;
|
|
350
|
+
const display = updateText.length <= maxLen ? updateText : `v${this.latestVersion}`;
|
|
351
|
+
writeString(grid, footerRow, 1, display, UPDATE_AVAILABLE_ATTRS);
|
|
352
|
+
} else {
|
|
353
|
+
writeString(grid, footerRow, 1, versionText, DIM_ATTRS);
|
|
354
|
+
}
|
|
312
355
|
}
|
|
313
356
|
|
|
314
357
|
return grid;
|