@yuriteixeira/pi-muxr 0.1.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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli/dashboard-component.d.ts +41 -0
- package/dist/cli/dashboard-component.js +236 -0
- package/dist/cli/dashboard-component.js.map +1 -0
- package/dist/cli/dashboard-icons.d.ts +10 -0
- package/dist/cli/dashboard-icons.js +40 -0
- package/dist/cli/dashboard-icons.js.map +1 -0
- package/dist/cli/dashboard-layout.d.ts +20 -0
- package/dist/cli/dashboard-layout.js +50 -0
- package/dist/cli/dashboard-layout.js.map +1 -0
- package/dist/cli/dashboard-theme.d.ts +43 -0
- package/dist/cli/dashboard-theme.js +56 -0
- package/dist/cli/dashboard-theme.js.map +1 -0
- package/dist/cli/dashboard.d.ts +4 -0
- package/dist/cli/dashboard.js +186 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/format.d.ts +4 -0
- package/dist/cli/format.js +35 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +74 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/rows.d.ts +2 -0
- package/dist/cli/rows.js +27 -0
- package/dist/cli/rows.js.map +1 -0
- package/dist/config/config.d.ts +3 -0
- package/dist/config/config.js +37 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/paths.d.ts +5 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/domain/sorting.d.ts +3 -0
- package/dist/domain/sorting.js +28 -0
- package/dist/domain/sorting.js.map +1 -0
- package/dist/domain/status.d.ts +60 -0
- package/dist/domain/status.js +17 -0
- package/dist/domain/status.js.map +1 -0
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +2 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/pi-muxr.d.ts +7 -0
- package/dist/extension/pi-muxr.js +262 -0
- package/dist/extension/pi-muxr.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/desktop.d.ts +2 -0
- package/dist/notifications/desktop.js +29 -0
- package/dist/notifications/desktop.js.map +1 -0
- package/dist/state/dashboard-presence.d.ts +6 -0
- package/dist/state/dashboard-presence.js +23 -0
- package/dist/state/dashboard-presence.js.map +1 -0
- package/dist/state/database.d.ts +3 -0
- package/dist/state/database.js +12 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/read-statuses.d.ts +3 -0
- package/dist/state/read-statuses.js +5 -0
- package/dist/state/read-statuses.js.map +1 -0
- package/dist/state/rows.d.ts +23 -0
- package/dist/state/rows.js +23 -0
- package/dist/state/rows.js.map +1 -0
- package/dist/state/schema.d.ts +1 -0
- package/dist/state/schema.js +31 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/state/write-status.d.ts +10 -0
- package/dist/state/write-status.js +59 -0
- package/dist/state/write-status.js.map +1 -0
- package/dist/tmux/focus.d.ts +3 -0
- package/dist/tmux/focus.js +19 -0
- package/dist/tmux/focus.js.map +1 -0
- package/dist/tmux/list-panes.d.ts +5 -0
- package/dist/tmux/list-panes.js +21 -0
- package/dist/tmux/list-panes.js.map +1 -0
- package/dist/tmux/sidebar.d.ts +16 -0
- package/dist/tmux/sidebar.js +171 -0
- package/dist/tmux/sidebar.js.map +1 -0
- package/dist/web/assets.d.ts +5 -0
- package/dist/web/assets.js +19 -0
- package/dist/web/assets.js.map +1 -0
- package/dist/web/base16-theme.d.ts +23 -0
- package/dist/web/base16-theme.js +68 -0
- package/dist/web/base16-theme.js.map +1 -0
- package/dist/web/notifications.d.ts +7 -0
- package/dist/web/notifications.js +47 -0
- package/dist/web/notifications.js.map +1 -0
- package/dist/web/page.d.ts +1 -0
- package/dist/web/page.js +155 -0
- package/dist/web/page.js.map +1 -0
- package/dist/web/protocol.d.ts +25 -0
- package/dist/web/protocol.js +2 -0
- package/dist/web/protocol.js.map +1 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.js +51 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/terminal-gateway.d.ts +2 -0
- package/dist/web/terminal-gateway.js +110 -0
- package/dist/web/terminal-gateway.js.map +1 -0
- package/dist/web/tmux-session.d.ts +4 -0
- package/dist/web/tmux-session.js +50 -0
- package/dist/web/tmux-session.js.map +1 -0
- package/docs/ARCHITECTURE.md +287 -0
- package/docs/BUILDING.md +78 -0
- package/docs/CONTRIBUTING.md +32 -0
- package/docs/resources/fullscreen.png +0 -0
- package/docs/resources/sidebar.png +0 -0
- package/docs/resources/web.png +0 -0
- package/examples/config.json +13 -0
- package/package.json +76 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
export function focusPane(pane) {
|
|
3
|
+
const commands = buildFocusCommands(pane);
|
|
4
|
+
if (!process.env.TMUX)
|
|
5
|
+
return commands;
|
|
6
|
+
execFileSync("tmux", ["switch-client", "-t", `${pane.sessionName}:${pane.windowIndex}`], { stdio: "ignore" });
|
|
7
|
+
execFileSync("tmux", ["select-pane", "-t", pane.paneId], { stdio: "ignore" });
|
|
8
|
+
return commands;
|
|
9
|
+
}
|
|
10
|
+
export function buildFocusCommands(pane) {
|
|
11
|
+
return [
|
|
12
|
+
`tmux switch-client -t '${escapeSingleQuotes(`${pane.sessionName}:${pane.windowIndex}`)}'`,
|
|
13
|
+
`tmux select-pane -t '${escapeSingleQuotes(pane.paneId)}'`,
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
function escapeSingleQuotes(value) {
|
|
17
|
+
return value.replaceAll("'", "'\\''");
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.js","sourceRoot":"","sources":["../../src/tmux/focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,UAAU,SAAS,CAAC,IAA8D;IACtF,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvC,YAAY,CAAC,MAAM,EAAE,CAAC,eAAe,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9G,YAAY,CAAC,MAAM,EAAE,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9E,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAA8D;IAC/F,OAAO;QACL,0BAA0B,kBAAkB,CAAC,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG;QAC1F,wBAAwB,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG;KAC3D,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TmuxPane } from "../domain/status.js";
|
|
2
|
+
export declare const TMUX_PANES_FORMAT = "#{session_name}\t#{window_index}\t#{window_name}\t#{pane_id}\t#{pane_active}\t#{pane_current_command}\t#{pane_title}\t#{pane_pid}";
|
|
3
|
+
export declare function parseTmuxPanes(output: string): TmuxPane[];
|
|
4
|
+
export declare function parseTmuxPaneLine(line: string): TmuxPane | null;
|
|
5
|
+
export declare function listTmuxPanes(): TmuxPane[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
export const TMUX_PANES_FORMAT = "#{session_name}\t#{window_index}\t#{window_name}\t#{pane_id}\t#{pane_active}\t#{pane_current_command}\t#{pane_title}\t#{pane_pid}";
|
|
3
|
+
export function parseTmuxPanes(output) {
|
|
4
|
+
return output.split(/\r?\n/).filter(Boolean).map(parseTmuxPaneLine).filter((pane) => pane !== null);
|
|
5
|
+
}
|
|
6
|
+
export function parseTmuxPaneLine(line) {
|
|
7
|
+
const [sessionName, windowIndex, windowName, paneId, paneActive, currentCommand, paneTitle, panePid] = line.split("\t");
|
|
8
|
+
if (!sessionName || !windowIndex || !paneId)
|
|
9
|
+
return null;
|
|
10
|
+
return { sessionName, windowIndex, windowName: windowName ?? "", paneId, paneActive: paneActive === "1", currentCommand: currentCommand ?? "", paneTitle: paneTitle ?? "", panePid: Number(panePid ?? 0) };
|
|
11
|
+
}
|
|
12
|
+
export function listTmuxPanes() {
|
|
13
|
+
try {
|
|
14
|
+
const output = execFileSync("tmux", ["list-panes", "-a", "-F", TMUX_PANES_FORMAT], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
15
|
+
return parseTmuxPanes(output);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=list-panes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-panes.js","sourceRoot":"","sources":["../../src/tmux/list-panes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,CAAC,MAAM,iBAAiB,GAAG,mIAAmI,CAAC;AAErK,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAoB,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACxH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,CAAC,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxH,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,IAAI,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,KAAK,GAAG,EAAE,cAAc,EAAE,cAAc,IAAI,EAAE,EAAE,SAAS,EAAE,SAAS,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC;AAC7M,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC9I,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SidebarSide = "left" | "right";
|
|
2
|
+
export type SidebarToggleResult = "created" | "destroyed";
|
|
3
|
+
export interface SidebarPaneInventory {
|
|
4
|
+
sidebarPaneIds: string[];
|
|
5
|
+
targetPaneIds: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function parseSidebarSide(argv: string[]): SidebarSide | null;
|
|
8
|
+
export declare function parseSidebarPaneInventory(output: string): SidebarPaneInventory;
|
|
9
|
+
export declare function buildSidebarSplitArgs(side: SidebarSide, targetPaneId: string, command: string): string[];
|
|
10
|
+
export declare function parseSidebarWindowTarget(argv: string[]): string | null;
|
|
11
|
+
export declare function buildPiMuxrCommand(execPath: string, entrypoint: string): string;
|
|
12
|
+
export declare function buildSidebarHookCommand(execPath: string, entrypoint: string): string;
|
|
13
|
+
export declare function buildSidebarHookArgs(hookCommand: string): string[][];
|
|
14
|
+
export declare function buildSidebarUnhookArgs(): string[][];
|
|
15
|
+
export declare function toggleSidebar(side: SidebarSide): SidebarToggleResult;
|
|
16
|
+
export declare function ensurePinnedSidebar(targetWindowId: string): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const SIDEBAR_OPTION = "@pi-muxr-sidebar";
|
|
3
|
+
const SIDEBAR_SIDE_OPTION = "@pi-muxr-sidebar-side";
|
|
4
|
+
const SIDEBAR_HOOKS = ["after-new-window[731]", "after-new-session[731]"];
|
|
5
|
+
const SIDEBAR_PANES_FORMAT = `#{pane_id}\t#{window_id}\t#{${SIDEBAR_OPTION}}`;
|
|
6
|
+
export function parseSidebarSide(argv) {
|
|
7
|
+
const optionIndex = argv.indexOf("--sidebar");
|
|
8
|
+
if (optionIndex === -1)
|
|
9
|
+
return null;
|
|
10
|
+
const side = argv[optionIndex + 1];
|
|
11
|
+
if (side === undefined || side.startsWith("--"))
|
|
12
|
+
return "right";
|
|
13
|
+
if (side === "left" || side === "right")
|
|
14
|
+
return side;
|
|
15
|
+
throw new Error("--sidebar accepts only 'left' or 'right'.");
|
|
16
|
+
}
|
|
17
|
+
export function parseSidebarPaneInventory(output) {
|
|
18
|
+
const sidebarPaneIds = [];
|
|
19
|
+
const targetPaneIds = [];
|
|
20
|
+
const seenWindowIds = new Set();
|
|
21
|
+
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
|
22
|
+
const [paneId, windowId, marker] = line.split("\t");
|
|
23
|
+
if (!paneId || !windowId)
|
|
24
|
+
continue;
|
|
25
|
+
if (marker === "left" || marker === "right")
|
|
26
|
+
sidebarPaneIds.push(paneId);
|
|
27
|
+
if (seenWindowIds.has(windowId))
|
|
28
|
+
continue;
|
|
29
|
+
seenWindowIds.add(windowId);
|
|
30
|
+
targetPaneIds.push(paneId);
|
|
31
|
+
}
|
|
32
|
+
return { sidebarPaneIds, targetPaneIds };
|
|
33
|
+
}
|
|
34
|
+
export function buildSidebarSplitArgs(side, targetPaneId, command) {
|
|
35
|
+
const args = ["split-window", "-d", "-f", "-h", "-l", "25%"];
|
|
36
|
+
if (side === "left")
|
|
37
|
+
args.push("-b");
|
|
38
|
+
args.push("-t", targetPaneId, "-P", "-F", "#{pane_id}", command);
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
export function parseSidebarWindowTarget(argv) {
|
|
42
|
+
const optionIndex = argv.indexOf("--sidebar-window");
|
|
43
|
+
if (optionIndex === -1)
|
|
44
|
+
return null;
|
|
45
|
+
const target = argv[optionIndex + 1];
|
|
46
|
+
if (!target || !/^@\d+$/.test(target))
|
|
47
|
+
throw new Error("--sidebar-window requires a tmux window ID.");
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
50
|
+
export function buildPiMuxrCommand(execPath, entrypoint) {
|
|
51
|
+
return `${quoteShellArgument(execPath)} ${quoteShellArgument(entrypoint)}`;
|
|
52
|
+
}
|
|
53
|
+
export function buildSidebarHookCommand(execPath, entrypoint) {
|
|
54
|
+
const command = `${buildPiMuxrCommand(execPath, entrypoint)} --sidebar-window '#{window_id}'`;
|
|
55
|
+
return `run-shell ${quoteShellArgument(command)}`;
|
|
56
|
+
}
|
|
57
|
+
export function buildSidebarHookArgs(hookCommand) {
|
|
58
|
+
return SIDEBAR_HOOKS.map((hook) => ["set-hook", "-g", hook, hookCommand]);
|
|
59
|
+
}
|
|
60
|
+
export function buildSidebarUnhookArgs() {
|
|
61
|
+
return SIDEBAR_HOOKS.map((hook) => ["set-hook", "-g", "-u", hook]);
|
|
62
|
+
}
|
|
63
|
+
export function toggleSidebar(side) {
|
|
64
|
+
if (!process.env.TMUX)
|
|
65
|
+
throw new Error("--sidebar must be run inside tmux.");
|
|
66
|
+
const inventory = readSidebarPaneInventory();
|
|
67
|
+
if (readPinnedSidebarSide() || inventory.sidebarPaneIds.length > 0) {
|
|
68
|
+
unpinSidebar();
|
|
69
|
+
destroySidebarPanes(inventory.sidebarPaneIds);
|
|
70
|
+
return "destroyed";
|
|
71
|
+
}
|
|
72
|
+
const entrypoint = resolveEntrypoint();
|
|
73
|
+
if (inventory.targetPaneIds.length === 0)
|
|
74
|
+
throw new Error("No tmux windows are available.");
|
|
75
|
+
const command = buildPiMuxrCommand(process.execPath, entrypoint);
|
|
76
|
+
createSidebarPanes(side, inventory.targetPaneIds, command);
|
|
77
|
+
try {
|
|
78
|
+
pinSidebar(side, buildSidebarHookCommand(process.execPath, entrypoint));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
destroySidebarPanes(readSidebarPaneInventory().sidebarPaneIds);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return "created";
|
|
85
|
+
}
|
|
86
|
+
export function ensurePinnedSidebar(targetWindowId) {
|
|
87
|
+
const side = readPinnedSidebarSide();
|
|
88
|
+
if (!side)
|
|
89
|
+
return false;
|
|
90
|
+
const inventory = readSidebarPaneInventory(targetWindowId);
|
|
91
|
+
if (inventory.sidebarPaneIds.length > 0)
|
|
92
|
+
return false;
|
|
93
|
+
const targetPaneId = inventory.targetPaneIds[0];
|
|
94
|
+
if (!targetPaneId)
|
|
95
|
+
throw new Error(`No pane is available in tmux window ${targetWindowId}.`);
|
|
96
|
+
createSidebarPanes(side, [targetPaneId], buildPiMuxrCommand(process.execPath, resolveEntrypoint()));
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
function resolveEntrypoint() {
|
|
100
|
+
const entrypoint = process.argv[1];
|
|
101
|
+
if (!entrypoint)
|
|
102
|
+
throw new Error("Cannot resolve the pi-muxr entrypoint.");
|
|
103
|
+
return entrypoint;
|
|
104
|
+
}
|
|
105
|
+
function readSidebarPaneInventory(targetWindowId) {
|
|
106
|
+
const targetArgs = targetWindowId ? ["-t", targetWindowId] : ["-a"];
|
|
107
|
+
const output = execFileSync("tmux", ["list-panes", ...targetArgs, "-F", SIDEBAR_PANES_FORMAT], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
110
|
+
});
|
|
111
|
+
return parseSidebarPaneInventory(output);
|
|
112
|
+
}
|
|
113
|
+
function readPinnedSidebarSide() {
|
|
114
|
+
try {
|
|
115
|
+
const side = execFileSync("tmux", ["show-option", "-gv", SIDEBAR_SIDE_OPTION], {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
118
|
+
}).trim();
|
|
119
|
+
return side === "left" || side === "right" ? side : null;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function pinSidebar(side, hookCommand) {
|
|
126
|
+
execFileSync("tmux", ["set-option", "-g", SIDEBAR_SIDE_OPTION, side], { stdio: "ignore" });
|
|
127
|
+
try {
|
|
128
|
+
for (const args of buildSidebarHookArgs(hookCommand))
|
|
129
|
+
execFileSync("tmux", args, { stdio: "ignore" });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
removeSidebarHooks();
|
|
133
|
+
execFileSync("tmux", ["set-option", "-gu", SIDEBAR_SIDE_OPTION], { stdio: "ignore" });
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function unpinSidebar() {
|
|
138
|
+
removeSidebarHooks();
|
|
139
|
+
execFileSync("tmux", ["set-option", "-gu", SIDEBAR_SIDE_OPTION], { stdio: "ignore" });
|
|
140
|
+
}
|
|
141
|
+
function removeSidebarHooks() {
|
|
142
|
+
for (const args of buildSidebarUnhookArgs())
|
|
143
|
+
execFileSync("tmux", args, { stdio: "ignore" });
|
|
144
|
+
}
|
|
145
|
+
function createSidebarPanes(side, targetPaneIds, command) {
|
|
146
|
+
const createdPaneIds = [];
|
|
147
|
+
try {
|
|
148
|
+
for (const targetPaneId of targetPaneIds) {
|
|
149
|
+
const paneId = execFileSync("tmux", buildSidebarSplitArgs(side, targetPaneId, command), {
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
152
|
+
}).trim();
|
|
153
|
+
if (!paneId)
|
|
154
|
+
throw new Error("tmux did not return the new sidebar pane ID.");
|
|
155
|
+
execFileSync("tmux", ["set-option", "-p", "-t", paneId, SIDEBAR_OPTION, side], { stdio: "ignore" });
|
|
156
|
+
createdPaneIds.push(paneId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
destroySidebarPanes(createdPaneIds);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function destroySidebarPanes(paneIds) {
|
|
165
|
+
for (const paneId of paneIds)
|
|
166
|
+
execFileSync("tmux", ["kill-pane", "-t", paneId], { stdio: "ignore" });
|
|
167
|
+
}
|
|
168
|
+
function quoteShellArgument(value) {
|
|
169
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=sidebar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../src/tmux/sidebar.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
export function readVendorAsset(pathname) {
|
|
5
|
+
if (pathname === "/vendor/xterm.js")
|
|
6
|
+
return javascriptAsset("@xterm/xterm/lib/xterm.js");
|
|
7
|
+
if (pathname === "/vendor/addon-fit.js")
|
|
8
|
+
return javascriptAsset("@xterm/addon-fit/lib/addon-fit.js");
|
|
9
|
+
if (pathname === "/vendor/xterm.css")
|
|
10
|
+
return textAsset("@xterm/xterm/css/xterm.css", "text/css; charset=utf-8");
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function javascriptAsset(specifier) {
|
|
14
|
+
return textAsset(specifier, "application/javascript; charset=utf-8");
|
|
15
|
+
}
|
|
16
|
+
function textAsset(specifier, contentType) {
|
|
17
|
+
return { contentType, body: readFileSync(require.resolve(specifier), "utf8") };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.js","sourceRoot":"","sources":["../../src/web/assets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAO/C,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,IAAI,QAAQ,KAAK,kBAAkB;QAAE,OAAO,eAAe,CAAC,2BAA2B,CAAC,CAAC;IACzF,IAAI,QAAQ,KAAK,sBAAsB;QAAE,OAAO,eAAe,CAAC,mCAAmC,CAAC,CAAC;IACrG,IAAI,QAAQ,KAAK,mBAAmB;QAAE,OAAO,SAAS,CAAC,4BAA4B,EAAE,yBAAyB,CAAC,CAAC;IAChH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,SAAiB;IACxC,OAAO,SAAS,CAAC,SAAS,EAAE,uCAAuC,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,SAAS,CAAC,SAAiB,EAAE,WAAmB;IACvD,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type TerminalTheme = {
|
|
2
|
+
name: string;
|
|
3
|
+
foreground: string;
|
|
4
|
+
background: string;
|
|
5
|
+
cursor: string;
|
|
6
|
+
black: string;
|
|
7
|
+
red: string;
|
|
8
|
+
green: string;
|
|
9
|
+
yellow: string;
|
|
10
|
+
blue: string;
|
|
11
|
+
magenta: string;
|
|
12
|
+
cyan: string;
|
|
13
|
+
white: string;
|
|
14
|
+
brightBlack: string;
|
|
15
|
+
brightRed: string;
|
|
16
|
+
brightGreen: string;
|
|
17
|
+
brightYellow: string;
|
|
18
|
+
brightBlue: string;
|
|
19
|
+
brightMagenta: string;
|
|
20
|
+
brightCyan: string;
|
|
21
|
+
brightWhite: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function getBase16TerminalTheme(): Promise<TerminalTheme | null>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const XTERM_COLOR_NAMES = [
|
|
5
|
+
"black",
|
|
6
|
+
"red",
|
|
7
|
+
"green",
|
|
8
|
+
"yellow",
|
|
9
|
+
"blue",
|
|
10
|
+
"magenta",
|
|
11
|
+
"cyan",
|
|
12
|
+
"white",
|
|
13
|
+
"brightBlack",
|
|
14
|
+
"brightRed",
|
|
15
|
+
"brightGreen",
|
|
16
|
+
"brightYellow",
|
|
17
|
+
"brightBlue",
|
|
18
|
+
"brightMagenta",
|
|
19
|
+
"brightCyan",
|
|
20
|
+
"brightWhite",
|
|
21
|
+
];
|
|
22
|
+
export async function getBase16TerminalTheme() {
|
|
23
|
+
const themeName = process.env.BASE16_THEME?.trim();
|
|
24
|
+
if (!themeName)
|
|
25
|
+
return null;
|
|
26
|
+
const script = await readBase16Script(themeName);
|
|
27
|
+
if (!script)
|
|
28
|
+
return null;
|
|
29
|
+
const colors = parseBase16Colors(script);
|
|
30
|
+
if (!hasRequiredColors(colors))
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
name: themeName,
|
|
34
|
+
foreground: colors.color_foreground,
|
|
35
|
+
background: colors.color_background,
|
|
36
|
+
cursor: colors.color_foreground,
|
|
37
|
+
...Object.fromEntries(XTERM_COLOR_NAMES.map((name, index) => [name, colors[`color${String(index).padStart(2, "0")}`]])),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function readBase16Script(themeName) {
|
|
41
|
+
const path = join(homedir(), ".config", "base16-shell", "scripts", `base16-${themeName}.sh`);
|
|
42
|
+
try {
|
|
43
|
+
return await readFile(path, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseBase16Colors(script) {
|
|
50
|
+
const colors = {};
|
|
51
|
+
for (const line of script.split("\n")) {
|
|
52
|
+
const match = line.match(/^(color(?:\d{2}|_foreground|_background))=(?:"([0-9a-fA-F]{2}\/[0-9a-fA-F]{2}\/[0-9a-fA-F]{2})"|\$(color\d{2}))/);
|
|
53
|
+
if (!match)
|
|
54
|
+
continue;
|
|
55
|
+
const [, name, literal, reference] = match;
|
|
56
|
+
const resolved = literal ? toHexColor(literal) : colors[reference];
|
|
57
|
+
if (resolved)
|
|
58
|
+
colors[name] = resolved;
|
|
59
|
+
}
|
|
60
|
+
return colors;
|
|
61
|
+
}
|
|
62
|
+
function toHexColor(base16Color) {
|
|
63
|
+
return `#${base16Color.replaceAll("/", "")}`;
|
|
64
|
+
}
|
|
65
|
+
function hasRequiredColors(colors) {
|
|
66
|
+
return Boolean(colors.color_foreground && colors.color_background && XTERM_COLOR_NAMES.every((_, index) => colors[`color${String(index).padStart(2, "0")}`]));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=base16-theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base16-theme.js","sourceRoot":"","sources":["../../src/web/base16-theme.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DashboardConfig } from "../domain/status.js";
|
|
2
|
+
import type { Database } from "../state/database.js";
|
|
3
|
+
import type { ServerMessage } from "./protocol.js";
|
|
4
|
+
export interface NotificationMonitor {
|
|
5
|
+
poll(): ServerMessage[];
|
|
6
|
+
}
|
|
7
|
+
export declare function createNotificationMonitor(db: Database, config: DashboardConfig): NotificationMonitor;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { projectName } from "../domain/status.js";
|
|
2
|
+
import { readStatuses } from "../state/read-statuses.js";
|
|
3
|
+
import { listTmuxPanes } from "../tmux/list-panes.js";
|
|
4
|
+
import { buildDashboardRows } from "../cli/rows.js";
|
|
5
|
+
export function createNotificationMonitor(db, config) {
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
seedSeenEvents(seen, readActionableRows(db, config));
|
|
8
|
+
return { poll: () => pollNotifications(db, config, seen) };
|
|
9
|
+
}
|
|
10
|
+
function pollNotifications(db, config, seen) {
|
|
11
|
+
const messages = [];
|
|
12
|
+
for (const row of readActionableRows(db, config)) {
|
|
13
|
+
const key = eventKey(row);
|
|
14
|
+
if (seen.has(key))
|
|
15
|
+
continue;
|
|
16
|
+
seen.add(key);
|
|
17
|
+
messages.push({ type: "notification", title: notificationTitle(row), body: row.summary, row: notificationRow(row) });
|
|
18
|
+
}
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
function readActionableRows(db, config) {
|
|
22
|
+
return buildDashboardRows(readStatuses(db), listTmuxPanes(), config).filter((row) => row.actionable && row.unread && !row.dismissed);
|
|
23
|
+
}
|
|
24
|
+
function seedSeenEvents(seen, rows) {
|
|
25
|
+
for (const row of rows)
|
|
26
|
+
seen.add(eventKey(row));
|
|
27
|
+
}
|
|
28
|
+
function eventKey(row) {
|
|
29
|
+
return `${row.id}:${row.lastEventAt}`;
|
|
30
|
+
}
|
|
31
|
+
function notificationTitle(row) {
|
|
32
|
+
return `pi-muxr: ${row.state} in ${projectName(row.cwd)}`;
|
|
33
|
+
}
|
|
34
|
+
function notificationRow(row) {
|
|
35
|
+
return {
|
|
36
|
+
id: row.id,
|
|
37
|
+
state: row.state,
|
|
38
|
+
severity: row.severity,
|
|
39
|
+
summary: row.summary,
|
|
40
|
+
cwd: row.cwd,
|
|
41
|
+
tmuxSession: row.tmuxSession,
|
|
42
|
+
tmuxWindowIndex: row.tmuxWindowIndex,
|
|
43
|
+
paneId: row.paneId,
|
|
44
|
+
lastEventAt: row.lastEventAt,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/web/notifications.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAOpD,MAAM,UAAU,yBAAyB,CAAC,EAAY,EAAE,MAAuB;IAC7E,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,cAAc,CAAC,IAAI,EAAE,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,SAAS,iBAAiB,CAAC,EAAY,EAAE,MAAuB,EAAE,IAAiB;IACjF,MAAM,QAAQ,GAAoB,EAAE,CAAC;IACrC,KAAK,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC;QACjD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,EAAY,EAAE,MAAuB;IAC/D,OAAO,kBAAkB,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AACvI,CAAC;AAED,SAAS,cAAc,CAAC,IAAiB,EAAE,IAAoB;IAC7D,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,QAAQ,CAAC,GAAiB;IACjC,OAAO,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAiB;IAC1C,OAAO,YAAY,GAAG,CAAC,KAAK,OAAO,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,SAAS,eAAe,CAAC,GAAiB;IACxC,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,WAAW,EAAE,GAAG,CAAC,WAAW;KAC7B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WEB_PAGE = "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>pi-muxr web</title>\n <link rel=\"stylesheet\" href=\"/vendor/xterm.css\" />\n <style>\n :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: #0f1218; color: #eef2ff; }\n * { box-sizing: border-box; }\n body { margin: 0; min-height: 100vh; }\n header { align-items: center; background: #171b24; border-bottom: 1px solid #2d3342; display: flex; gap: 1rem; justify-content: space-between; padding: 0.8rem 1rem; }\n h1 { font-size: 1.05rem; margin: 0; }\n main { display: grid; grid-template-rows: auto 1fr; gap: 0.75rem; height: calc(100vh - 58px); padding: 0.75rem; }\n button { background: #2563eb; border: 0; border-radius: 8px; color: #fff; cursor: pointer; font: inherit; padding: 0.55rem 0.8rem; }\n button.secondary { background: #374151; }\n button:disabled { cursor: not-allowed; opacity: 0.6; }\n .toolbar { align-items: center; display: flex; gap: 0.75rem; justify-content: space-between; }\n .status { color: #cbd5e1; font-size: 0.9rem; }\n .terminal-container { background: var(--terminal-background, #000); border: 1px solid #2d3342; border-radius: 10px; min-height: 0; overflow: hidden; padding: 0.4rem; }\n .terminal-container .xterm-viewport { scrollbar-width: none; }\n .terminal-container .xterm-viewport::-webkit-scrollbar { display: none; }\n .toasts { bottom: 1rem; display: grid; gap: 0.75rem; max-width: min(420px, calc(100vw - 2rem)); position: fixed; right: 1rem; z-index: 10; }\n .toast { background: #1f2937; border: 1px solid #475569; border-left: 4px solid #f59e0b; border-radius: 10px; box-shadow: 0 10px 35px #0008; padding: 0.8rem; }\n .toast strong { display: block; margin-bottom: 0.25rem; }\n .toast small { color: #cbd5e1; display: block; margin-top: 0.35rem; }\n .error { color: #fecaca; }\n </style>\n</head>\n<body>\n <header>\n <h1>pi-muxr web</h1>\n <div class=\"status\" id=\"session-label\"></div>\n </header>\n <main>\n <div class=\"toolbar\">\n <div><span id=\"status\">Connecting\u2026</span> <span class=\"error\" id=\"error\"></span></div>\n <button id=\"notifications\" class=\"secondary\">Enable notifications</button>\n </div>\n <div id=\"terminal\" class=\"terminal-container\"></div>\n </main>\n <div id=\"toasts\" class=\"toasts\"></div>\n <script src=\"/vendor/xterm.js\"></script>\n <script src=\"/vendor/addon-fit.js\"></script>\n <script>\n const terminalElement = document.getElementById('terminal');\n const statusElement = document.getElementById('status');\n const errorElement = document.getElementById('error');\n const toastsElement = document.getElementById('toasts');\n const notificationsButton = document.getElementById('notifications');\n const params = new URLSearchParams(location.search);\n const session = params.get('session') || 'pi-muxr-web';\n document.getElementById('session-label').textContent = 'tmux: ' + session;\n\n let terminal;\n let fitAddon;\n let socket;\n let animationFrame;\n let lastSize = { cols: 0, rows: 0 };\n\n initializeTerminal();\n notificationsButton.addEventListener('click', requestNotifications);\n updateNotificationButton();\n\n async function initializeTerminal() {\n const terminalTheme = await loadTerminalTheme();\n if (terminalTheme?.background) terminalElement.style.setProperty('--terminal-background', terminalTheme.background);\n\n terminal = new Terminal({ cursorBlink: true, convertEol: true, fontFamily: 'JetBrainsMono Nerd Font, JetBrains Mono, Menlo, Monaco, Consolas, monospace', fontSize: 14, theme: terminalTheme });\n fitAddon = new FitAddon.FitAddon();\n terminal.loadAddon(fitAddon);\n terminal.open(terminalElement);\n\n socket = new WebSocket(buildTerminalUrl(session));\n socket.addEventListener('open', () => { statusElement.textContent = 'Connected'; fitAndSync(); terminal.focus(); });\n socket.addEventListener('message', (event) => handleServerMessage(JSON.parse(String(event.data))));\n socket.addEventListener('close', () => { statusElement.textContent = 'Disconnected \u2014 refresh to reconnect.'; });\n socket.addEventListener('error', () => { errorElement.textContent = ' Terminal websocket failed.'; });\n terminal.onData((data) => send({ type: 'input', data }));\n\n const resizeObserver = new ResizeObserver(fitAndSync);\n resizeObserver.observe(terminalElement);\n window.addEventListener('beforeunload', () => socket.close());\n fitAndSync();\n }\n\n function handleServerMessage(message) {\n if (message.type === 'output') terminal.write(message.data);\n if (message.type === 'error') { errorElement.textContent = ' ' + message.message; terminal.writeln('\\r\\n' + message.message); }\n if (message.type === 'exit') statusElement.textContent = 'Disconnected' + (message.code === undefined ? '' : ' (' + message.code + ')');\n if (message.type === 'notification') showAttention(message);\n }\n\n function showAttention(message) {\n const toast = document.createElement('div');\n toast.className = 'toast';\n toast.innerHTML = '<strong></strong><div></div><small></small>';\n toast.querySelector('strong').textContent = message.title;\n toast.querySelector('div').textContent = message.body;\n toast.querySelector('small').textContent = formatLocation(message.row);\n toastsElement.prepend(toast);\n setTimeout(() => toast.remove(), 12000);\n if (Notification.permission === 'granted') new Notification(message.title, { body: message.body });\n }\n\n function fitAndSync() {\n if (!terminal || !fitAddon || !socket || animationFrame !== undefined) return;\n animationFrame = requestAnimationFrame(() => {\n animationFrame = undefined;\n fitAddon.fit();\n if (!terminal.cols || !terminal.rows || socket.readyState !== WebSocket.OPEN) return;\n if (terminal.cols === lastSize.cols && terminal.rows === lastSize.rows) return;\n lastSize = { cols: terminal.cols, rows: terminal.rows };\n send({ type: 'resize', cols: terminal.cols, rows: terminal.rows });\n });\n }\n\n function send(message) {\n if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));\n }\n\n async function loadTerminalTheme() {\n try {\n const response = await fetch('/api/terminal-theme');\n const data = await response.json();\n return data.theme || undefined;\n } catch {\n return undefined;\n }\n }\n\n function buildTerminalUrl(sessionName) {\n const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';\n return protocol + '//' + location.host + '/ws/terminal?session=' + encodeURIComponent(sessionName);\n }\n\n async function requestNotifications() {\n if (!('Notification' in window)) return;\n await Notification.requestPermission();\n updateNotificationButton();\n }\n\n function updateNotificationButton() {\n if (!('Notification' in window)) { notificationsButton.disabled = true; notificationsButton.textContent = 'Notifications unavailable'; return; }\n if (Notification.permission === 'granted') { notificationsButton.disabled = true; notificationsButton.textContent = 'Notifications enabled'; return; }\n if (Notification.permission === 'denied') { notificationsButton.disabled = true; notificationsButton.textContent = 'Notifications blocked'; }\n }\n\n function formatLocation(row) {\n return [row.tmuxSession || '?', row.tmuxWindowIndex || '?', row.paneId || '?'].join(':');\n }\n </script>\n</body>\n</html>";
|
package/dist/web/page.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export const WEB_PAGE = `<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>pi-muxr web</title>
|
|
7
|
+
<link rel="stylesheet" href="/vendor/xterm.css" />
|
|
8
|
+
<style>
|
|
9
|
+
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0f1218; color: #eef2ff; }
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body { margin: 0; min-height: 100vh; }
|
|
12
|
+
header { align-items: center; background: #171b24; border-bottom: 1px solid #2d3342; display: flex; gap: 1rem; justify-content: space-between; padding: 0.8rem 1rem; }
|
|
13
|
+
h1 { font-size: 1.05rem; margin: 0; }
|
|
14
|
+
main { display: grid; grid-template-rows: auto 1fr; gap: 0.75rem; height: calc(100vh - 58px); padding: 0.75rem; }
|
|
15
|
+
button { background: #2563eb; border: 0; border-radius: 8px; color: #fff; cursor: pointer; font: inherit; padding: 0.55rem 0.8rem; }
|
|
16
|
+
button.secondary { background: #374151; }
|
|
17
|
+
button:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
18
|
+
.toolbar { align-items: center; display: flex; gap: 0.75rem; justify-content: space-between; }
|
|
19
|
+
.status { color: #cbd5e1; font-size: 0.9rem; }
|
|
20
|
+
.terminal-container { background: var(--terminal-background, #000); border: 1px solid #2d3342; border-radius: 10px; min-height: 0; overflow: hidden; padding: 0.4rem; }
|
|
21
|
+
.terminal-container .xterm-viewport { scrollbar-width: none; }
|
|
22
|
+
.terminal-container .xterm-viewport::-webkit-scrollbar { display: none; }
|
|
23
|
+
.toasts { bottom: 1rem; display: grid; gap: 0.75rem; max-width: min(420px, calc(100vw - 2rem)); position: fixed; right: 1rem; z-index: 10; }
|
|
24
|
+
.toast { background: #1f2937; border: 1px solid #475569; border-left: 4px solid #f59e0b; border-radius: 10px; box-shadow: 0 10px 35px #0008; padding: 0.8rem; }
|
|
25
|
+
.toast strong { display: block; margin-bottom: 0.25rem; }
|
|
26
|
+
.toast small { color: #cbd5e1; display: block; margin-top: 0.35rem; }
|
|
27
|
+
.error { color: #fecaca; }
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<header>
|
|
32
|
+
<h1>pi-muxr web</h1>
|
|
33
|
+
<div class="status" id="session-label"></div>
|
|
34
|
+
</header>
|
|
35
|
+
<main>
|
|
36
|
+
<div class="toolbar">
|
|
37
|
+
<div><span id="status">Connecting…</span> <span class="error" id="error"></span></div>
|
|
38
|
+
<button id="notifications" class="secondary">Enable notifications</button>
|
|
39
|
+
</div>
|
|
40
|
+
<div id="terminal" class="terminal-container"></div>
|
|
41
|
+
</main>
|
|
42
|
+
<div id="toasts" class="toasts"></div>
|
|
43
|
+
<script src="/vendor/xterm.js"></script>
|
|
44
|
+
<script src="/vendor/addon-fit.js"></script>
|
|
45
|
+
<script>
|
|
46
|
+
const terminalElement = document.getElementById('terminal');
|
|
47
|
+
const statusElement = document.getElementById('status');
|
|
48
|
+
const errorElement = document.getElementById('error');
|
|
49
|
+
const toastsElement = document.getElementById('toasts');
|
|
50
|
+
const notificationsButton = document.getElementById('notifications');
|
|
51
|
+
const params = new URLSearchParams(location.search);
|
|
52
|
+
const session = params.get('session') || 'pi-muxr-web';
|
|
53
|
+
document.getElementById('session-label').textContent = 'tmux: ' + session;
|
|
54
|
+
|
|
55
|
+
let terminal;
|
|
56
|
+
let fitAddon;
|
|
57
|
+
let socket;
|
|
58
|
+
let animationFrame;
|
|
59
|
+
let lastSize = { cols: 0, rows: 0 };
|
|
60
|
+
|
|
61
|
+
initializeTerminal();
|
|
62
|
+
notificationsButton.addEventListener('click', requestNotifications);
|
|
63
|
+
updateNotificationButton();
|
|
64
|
+
|
|
65
|
+
async function initializeTerminal() {
|
|
66
|
+
const terminalTheme = await loadTerminalTheme();
|
|
67
|
+
if (terminalTheme?.background) terminalElement.style.setProperty('--terminal-background', terminalTheme.background);
|
|
68
|
+
|
|
69
|
+
terminal = new Terminal({ cursorBlink: true, convertEol: true, fontFamily: 'JetBrainsMono Nerd Font, JetBrains Mono, Menlo, Monaco, Consolas, monospace', fontSize: 14, theme: terminalTheme });
|
|
70
|
+
fitAddon = new FitAddon.FitAddon();
|
|
71
|
+
terminal.loadAddon(fitAddon);
|
|
72
|
+
terminal.open(terminalElement);
|
|
73
|
+
|
|
74
|
+
socket = new WebSocket(buildTerminalUrl(session));
|
|
75
|
+
socket.addEventListener('open', () => { statusElement.textContent = 'Connected'; fitAndSync(); terminal.focus(); });
|
|
76
|
+
socket.addEventListener('message', (event) => handleServerMessage(JSON.parse(String(event.data))));
|
|
77
|
+
socket.addEventListener('close', () => { statusElement.textContent = 'Disconnected — refresh to reconnect.'; });
|
|
78
|
+
socket.addEventListener('error', () => { errorElement.textContent = ' Terminal websocket failed.'; });
|
|
79
|
+
terminal.onData((data) => send({ type: 'input', data }));
|
|
80
|
+
|
|
81
|
+
const resizeObserver = new ResizeObserver(fitAndSync);
|
|
82
|
+
resizeObserver.observe(terminalElement);
|
|
83
|
+
window.addEventListener('beforeunload', () => socket.close());
|
|
84
|
+
fitAndSync();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleServerMessage(message) {
|
|
88
|
+
if (message.type === 'output') terminal.write(message.data);
|
|
89
|
+
if (message.type === 'error') { errorElement.textContent = ' ' + message.message; terminal.writeln('\\r\\n' + message.message); }
|
|
90
|
+
if (message.type === 'exit') statusElement.textContent = 'Disconnected' + (message.code === undefined ? '' : ' (' + message.code + ')');
|
|
91
|
+
if (message.type === 'notification') showAttention(message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function showAttention(message) {
|
|
95
|
+
const toast = document.createElement('div');
|
|
96
|
+
toast.className = 'toast';
|
|
97
|
+
toast.innerHTML = '<strong></strong><div></div><small></small>';
|
|
98
|
+
toast.querySelector('strong').textContent = message.title;
|
|
99
|
+
toast.querySelector('div').textContent = message.body;
|
|
100
|
+
toast.querySelector('small').textContent = formatLocation(message.row);
|
|
101
|
+
toastsElement.prepend(toast);
|
|
102
|
+
setTimeout(() => toast.remove(), 12000);
|
|
103
|
+
if (Notification.permission === 'granted') new Notification(message.title, { body: message.body });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fitAndSync() {
|
|
107
|
+
if (!terminal || !fitAddon || !socket || animationFrame !== undefined) return;
|
|
108
|
+
animationFrame = requestAnimationFrame(() => {
|
|
109
|
+
animationFrame = undefined;
|
|
110
|
+
fitAddon.fit();
|
|
111
|
+
if (!terminal.cols || !terminal.rows || socket.readyState !== WebSocket.OPEN) return;
|
|
112
|
+
if (terminal.cols === lastSize.cols && terminal.rows === lastSize.rows) return;
|
|
113
|
+
lastSize = { cols: terminal.cols, rows: terminal.rows };
|
|
114
|
+
send({ type: 'resize', cols: terminal.cols, rows: terminal.rows });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function send(message) {
|
|
119
|
+
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadTerminalTheme() {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch('/api/terminal-theme');
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
return data.theme || undefined;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildTerminalUrl(sessionName) {
|
|
133
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
134
|
+
return protocol + '//' + location.host + '/ws/terminal?session=' + encodeURIComponent(sessionName);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function requestNotifications() {
|
|
138
|
+
if (!('Notification' in window)) return;
|
|
139
|
+
await Notification.requestPermission();
|
|
140
|
+
updateNotificationButton();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function updateNotificationButton() {
|
|
144
|
+
if (!('Notification' in window)) { notificationsButton.disabled = true; notificationsButton.textContent = 'Notifications unavailable'; return; }
|
|
145
|
+
if (Notification.permission === 'granted') { notificationsButton.disabled = true; notificationsButton.textContent = 'Notifications enabled'; return; }
|
|
146
|
+
if (Notification.permission === 'denied') { notificationsButton.disabled = true; notificationsButton.textContent = 'Notifications blocked'; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatLocation(row) {
|
|
150
|
+
return [row.tmuxSession || '?', row.tmuxWindowIndex || '?', row.paneId || '?'].join(':');
|
|
151
|
+
}
|
|
152
|
+
</script>
|
|
153
|
+
</body>
|
|
154
|
+
</html>`;
|
|
155
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/web/page.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAyJhB,CAAC"}
|