tmux-ide 1.0.0 → 1.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/src/inspect.js ADDED
@@ -0,0 +1,127 @@
1
+ import { resolve, basename } from "node:path";
2
+ import { readConfig } from "./lib/yaml-io.js";
3
+ import { validateConfig } from "./validate.js";
4
+ import { outputError } from "./lib/output.js";
5
+ import { getSessionState, listPanes } from "./lib/tmux.js";
6
+
7
+ export function buildInspection(dir, { config, configPath, running, panes }) {
8
+ const errors = validateConfig(config);
9
+ const rows = Array.isArray(config?.rows) ? config.rows : [];
10
+ const resolvedRows = rows.map((row, rowIndex) => ({
11
+ index: rowIndex,
12
+ size: row.size ?? null,
13
+ panes: (Array.isArray(row?.panes) ? row.panes : []).map((pane, paneIndex) => ({
14
+ index: paneIndex,
15
+ title: pane.title ?? null,
16
+ command: pane.command ?? null,
17
+ dir: pane.dir ?? ".",
18
+ size: pane.size ?? null,
19
+ focus: pane.focus === true,
20
+ role: pane.role ?? null,
21
+ task: pane.task ?? null,
22
+ env: pane.env ?? {},
23
+ })),
24
+ }));
25
+
26
+ const focusPane =
27
+ resolvedRows
28
+ .flatMap((row) => row.panes.map((pane) => ({ row: row.index, pane })))
29
+ .find(({ pane }) => pane.focus) ?? null;
30
+ const session = config?.name ?? basename(dir);
31
+
32
+ return {
33
+ dir,
34
+ configPath,
35
+ valid: errors.length === 0,
36
+ errors,
37
+ session,
38
+ before: config?.before ?? null,
39
+ summary: {
40
+ rows: resolvedRows.length,
41
+ panes: resolvedRows.reduce((sum, row) => sum + row.panes.length, 0),
42
+ focus: focusPane ? `rows.${focusPane.row}.panes.${focusPane.pane.index}` : null,
43
+ },
44
+ team: config?.team ?? null,
45
+ theme: config?.theme ?? null,
46
+ focus: focusPane
47
+ ? {
48
+ row: focusPane.row,
49
+ pane: focusPane.pane.index,
50
+ title: focusPane.pane.title,
51
+ }
52
+ : null,
53
+ rows: resolvedRows,
54
+ rawConfig: config,
55
+ tmux: {
56
+ running,
57
+ panes,
58
+ },
59
+ };
60
+ }
61
+
62
+ export async function inspect(targetDir, { json } = {}) {
63
+ const dir = resolve(targetDir ?? ".");
64
+
65
+ let config;
66
+ let configPath;
67
+ try {
68
+ ({ config, configPath } = readConfig(dir));
69
+ } catch (error) {
70
+ outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR", { json });
71
+ return;
72
+ }
73
+
74
+ const session = config?.name ?? basename(dir);
75
+ const state = getSessionState(session);
76
+ const panes = state.running ? listPanes(session) : [];
77
+ const data = buildInspection(dir, { config, configPath, running: state.running, panes });
78
+
79
+ if (json) {
80
+ console.log(JSON.stringify(data, null, 2));
81
+ return;
82
+ }
83
+
84
+ console.log(`Directory: ${data.dir}`);
85
+ console.log(`Config: ${data.configPath}`);
86
+ console.log(`Valid: ${data.valid ? "yes" : "no"}`);
87
+ console.log(`Session: ${data.session}`);
88
+ console.log(`Running: ${data.tmux.running ? "yes" : "no"}`);
89
+ console.log(`Rows: ${data.summary.rows}`);
90
+ console.log(`Panes: ${data.summary.panes}`);
91
+ console.log(`Team: ${data.team ? data.team.name : "disabled"}`);
92
+
93
+ if (data.focus) {
94
+ console.log(
95
+ `Focus: row ${data.focus.row}, pane ${data.focus.pane}${data.focus.title ? ` (${data.focus.title})` : ""}`,
96
+ );
97
+ }
98
+
99
+ if (!data.valid) {
100
+ console.log("\nValidation Errors:");
101
+ for (const error of data.errors) {
102
+ console.log(` - ${error}`);
103
+ }
104
+ }
105
+
106
+ console.log("\nResolved Layout:");
107
+ for (const row of data.rows) {
108
+ console.log(` Row ${row.index}${row.size ? ` (${row.size})` : ""}`);
109
+ for (const pane of row.panes) {
110
+ const parts = [];
111
+ if (pane.title) parts.push(pane.title);
112
+ if (pane.command) parts.push(`cmd=${pane.command}`);
113
+ if (pane.dir && pane.dir !== ".") parts.push(`dir=${pane.dir}`);
114
+ if (pane.role) parts.push(`role=${pane.role}`);
115
+ if (pane.focus) parts.push("focus");
116
+ console.log(` - pane ${pane.index}: ${parts.join(" | ") || "shell"}`);
117
+ }
118
+ }
119
+
120
+ if (data.tmux.running && data.tmux.panes.length > 0) {
121
+ console.log("\nLive Panes:");
122
+ for (const pane of data.tmux.panes) {
123
+ const active = pane.active ? " (active)" : "";
124
+ console.log(` ${pane.index}: ${pane.title} [${pane.width}x${pane.height}]${active}`);
125
+ }
126
+ }
127
+ }
package/src/launch.js CHANGED
@@ -2,66 +2,159 @@ import { resolve } from "node:path";
2
2
  import { execSync } from "node:child_process";
3
3
  import { readConfig, getSessionName } from "./lib/yaml-io.js";
4
4
  import { computeSizes, toSplitPercents } from "./lib/sizes.js";
5
-
6
- function tmux(cmd) {
7
- execSync(`tmux ${cmd}`, { stdio: "inherit" });
5
+ import { outputError } from "./lib/output.js";
6
+ import { buildThemeOptions, collectPaneStartupPlan } from "./lib/launch-plan.js";
7
+ import {
8
+ attachSession,
9
+ createDetachedSession,
10
+ getPaneCurrentCommand,
11
+ hasSession,
12
+ runSessionCommand,
13
+ selectPane,
14
+ sendLiteral,
15
+ setPaneTitle,
16
+ setSessionEnvironment,
17
+ splitPane,
18
+ } from "./lib/tmux.js";
19
+ import { validateConfig } from "./validate.js";
20
+
21
+ function sleepMs(ms) {
22
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
8
23
  }
9
24
 
10
- function tmuxQuiet(cmd) {
11
- try {
12
- execSync(`tmux ${cmd}`, { stdio: "ignore" });
13
- } catch {}
25
+ export function waitForPaneCommand(
26
+ targetPane,
27
+ expectedCommands,
28
+ { attempts = 20, delayMs = 100, getCurrentCommand = getPaneCurrentCommand, sleep = sleepMs } = {},
29
+ ) {
30
+ const allowed = new Set(expectedCommands.map((command) => command.toLowerCase()));
31
+
32
+ for (let attempt = 0; attempt < attempts; attempt++) {
33
+ try {
34
+ const current = getCurrentCommand(targetPane)?.trim().toLowerCase();
35
+ if (allowed.has(current)) return true;
36
+ } catch {
37
+ // Fall through to retry; tmux can briefly report transitional state.
38
+ }
39
+
40
+ if (attempt < attempts - 1) {
41
+ sleep(delayMs);
42
+ }
43
+ }
44
+
45
+ return false;
14
46
  }
15
47
 
16
- export async function launch(targetDir) {
17
- const dir = resolve(targetDir ?? ".");
48
+ export function buildPaneMap(rows, dir, rootPaneId, splitPane) {
49
+ const rowSizes = computeSizes(rows);
50
+ const rowSplitPercents = toSplitPercents(rowSizes);
51
+
52
+ // Create all rows vertically first so each row spans the full width.
53
+ const rowPaneIds = [rootPaneId];
54
+ for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
55
+ const splitFrom = rowPaneIds[rowIdx - 1];
56
+ const newPaneId = splitPane({
57
+ targetPane: splitFrom,
58
+ direction: "vertical",
59
+ cwd: dir,
60
+ percent: rowSplitPercents[rowIdx - 1],
61
+ });
62
+ rowPaneIds.push(newPaneId);
63
+ }
64
+
65
+ const paneMap = [];
66
+ const firstPanesOfRows = new Set(rowPaneIds);
18
67
 
68
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
69
+ const row = rows[rowIdx];
70
+ const panes = row.panes ?? [];
71
+ const rowPaneId = rowPaneIds[rowIdx];
72
+ const rowPanes = [rowPaneId];
73
+
74
+ const paneSizes = computeSizes(panes);
75
+ const paneSplitPercents = toSplitPercents(paneSizes);
76
+
77
+ for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
78
+ const pane = panes[paneIdx];
79
+ const targetPane = rowPanes[paneIdx - 1];
80
+ const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
81
+ const newPaneId = splitPane({
82
+ targetPane,
83
+ direction: "horizontal",
84
+ cwd: paneDir,
85
+ percent: paneSplitPercents[paneIdx - 1],
86
+ });
87
+ rowPanes.push(newPaneId);
88
+ }
89
+
90
+ paneMap.push(rowPanes);
91
+ }
92
+
93
+ return { paneMap, firstPanesOfRows };
94
+ }
95
+
96
+ function loadLaunchConfig(dir, { json } = {}) {
19
97
  let config;
98
+
20
99
  try {
21
100
  ({ config } = readConfig(dir));
22
- } catch {
23
- console.error(`No ide.yml found in ${dir}. Run "tmux-ide init" to create one.`);
24
- process.exit(1);
101
+ } catch (error) {
102
+ if (error?.code === "ENOENT") {
103
+ outputError(
104
+ `No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`,
105
+ "CONFIG_NOT_FOUND",
106
+ { json },
107
+ );
108
+ }
109
+
110
+ outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR", { json });
25
111
  }
26
112
 
27
- // Validate config structure before building tmux session
28
- if (!Array.isArray(config.rows) || config.rows.length === 0) {
29
- console.error("Invalid ide.yml: 'rows' must be a non-empty array");
30
- console.error("Run: tmux-ide validate");
31
- process.exit(1);
113
+ const errors = validateConfig(config);
114
+ if (errors.length > 0) {
115
+ outputError(
116
+ `Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`,
117
+ "INVALID_CONFIG",
118
+ { json },
119
+ );
32
120
  }
33
- for (const row of config.rows) {
34
- if (!Array.isArray(row.panes) || row.panes.length === 0) {
35
- console.error("Invalid ide.yml: each row must have a non-empty 'panes' array");
36
- console.error("Run: tmux-ide validate");
37
- process.exit(1);
38
- }
121
+
122
+ return config;
123
+ }
124
+
125
+ function runBeforeHook(command, dir, { json } = {}) {
126
+ if (!command) return;
127
+
128
+ console.log(`Running: ${command}`);
129
+
130
+ try {
131
+ execSync(command, { cwd: dir, stdio: "inherit" });
132
+ } catch {
133
+ const message = json
134
+ ? `The "before" hook failed: ${command}`
135
+ : `The before hook failed: ${command}`;
136
+ outputError(message, "BEFORE_HOOK_FAILED", { json });
39
137
  }
138
+ }
139
+
140
+ export async function launch(targetDir, { json, attach = true } = {}) {
141
+ const dir = resolve(targetDir ?? ".");
142
+ const config = loadLaunchConfig(dir, { json });
40
143
 
41
144
  const session = config.name ?? getSessionName(dir);
42
145
  const rows = config.rows;
43
146
  const theme = config.theme ?? {};
44
147
  const team = config.team ?? null;
45
148
 
46
- // Run before hook
47
- if (config.before) {
48
- console.log(`Running: ${config.before}`);
49
- try {
50
- execSync(config.before, { cwd: dir, stdio: "inherit" });
51
- } catch (e) {
52
- console.error(`"before" hook failed: ${config.before}`);
53
- process.exit(1);
54
- }
55
- }
149
+ runBeforeHook(config.before, dir, { json });
56
150
 
57
151
  // If session already exists, just attach to it
58
- try {
59
- execSync(`tmux has-session -t "${session}"`, { stdio: "ignore" });
152
+ if (hasSession(session)) {
60
153
  console.log(`Session "${session}" is already running. Attaching...`);
61
- tmux(`attach -t "${session}"`);
154
+ if (attach) {
155
+ attachSession(session);
156
+ }
62
157
  return;
63
- } catch {
64
- // Session doesn't exist, continue with creation
65
158
  }
66
159
 
67
160
  // Get terminal dimensions
@@ -69,170 +162,71 @@ export async function launch(targetDir) {
69
162
  const lines = process.stdout.rows ?? 50;
70
163
 
71
164
  // Create session with first pane
72
- tmux(`new-session -d -s "${session}" -c "${dir}" -x ${cols} -y ${lines}`);
165
+ const rootPaneId = createDetachedSession(session, dir, { cols, lines });
73
166
 
74
167
  // Set agent teams env var if team config is present
75
168
  if (team) {
76
- tmux(`set-environment -t "${session}" CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 1`);
169
+ setSessionEnvironment(session, "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", "1");
77
170
  }
78
171
 
79
- // Compute row sizes and convert to tmux split percentages
80
- const rowSizes = computeSizes(rows);
81
- const rowSplitPercents = toSplitPercents(rowSizes);
82
-
83
- // Phase 1: Create all rows (vertical splits) BEFORE any horizontal splits.
84
- // This ensures each row spans the full window width.
85
- const rowPaneIndices = [0]; // rowPaneIndices[i] = tmux pane index for row i
86
- let nextPaneIndex = 1;
87
-
88
- for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
89
- const splitFrom = rowPaneIndices[rowIdx - 1];
90
- tmux(
91
- `split-window -t "${session}:0.${splitFrom}" -v -c "${dir}" -p ${rowSplitPercents[rowIdx - 1]}`
92
- );
93
- rowPaneIndices.push(nextPaneIndex);
94
- nextPaneIndex++;
95
- }
96
-
97
- // Phase 2: Create panes within each row (horizontal splits).
98
- // paneMap[row][col] = tmux pane index
99
- const paneMap = [];
100
- const firstPanesOfRows = new Set(rowPaneIndices);
101
-
102
- for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
103
- const row = rows[rowIdx];
104
- const panes = row.panes ?? [];
105
- const rowPane = rowPaneIndices[rowIdx];
106
- const rowPanes = [rowPane];
107
-
108
- // Compute pane sizes within this row and convert to split percentages
109
- const paneSizes = computeSizes(panes);
110
- const paneSplitPercents = toSplitPercents(paneSizes);
111
-
112
- // Split this row's panes horizontally
113
- for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
114
- const pane = panes[paneIdx];
115
- const targetPane = rowPanes[paneIdx - 1];
172
+ const { paneMap, firstPanesOfRows } = buildPaneMap(
173
+ rows,
174
+ dir,
175
+ rootPaneId,
176
+ ({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent),
177
+ );
116
178
 
117
- // Per-pane working directory
118
- const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
179
+ const { focusPane, leadPane, paneActions, teammateCommands } = collectPaneStartupPlan(
180
+ rows,
181
+ paneMap,
182
+ firstPanesOfRows,
183
+ dir,
184
+ team,
185
+ );
119
186
 
120
- tmux(
121
- `split-window -t "${session}:0.${targetPane}" -h -c "${paneDir}" -p ${paneSplitPercents[paneIdx - 1]}`
122
- );
123
- rowPanes.push(nextPaneIndex);
124
- nextPaneIndex++;
187
+ for (const action of paneActions) {
188
+ if (action.title) {
189
+ setPaneTitle(action.targetPane, action.title);
125
190
  }
126
191
 
127
- paneMap.push(rowPanes);
128
- }
129
-
130
- // Build the command for a pane, transforming claude commands for team roles
131
- function buildCommand(p) {
132
- if (!p.command) return null;
133
- if (!team || !p.role) return p.command;
134
-
135
- if (p.role === "lead") {
136
- return `claude --team "${team.name}"`;
137
- } else if (p.role === "teammate") {
138
- let cmd = `claude --teammate-mode in-process --team "${team.name}"`;
139
- if (p.task) {
140
- cmd += ` --task "${p.task.replace(/"/g, '\\"')}"`;
141
- }
142
- return cmd;
192
+ if (action.chdir) {
193
+ sendLiteral(action.targetPane, `cd ${action.chdir}`);
143
194
  }
144
- return p.command;
145
- }
146
195
 
147
- // Send commands, set titles, handle env/focus/dir for first panes
148
- // Two-pass approach for teams: lead + non-teammates first, then teammates
149
- let focusPane = paneMap[0][0];
150
- const teammateCommands = [];
196
+ for (const exportCommand of action.exports) {
197
+ sendLiteral(action.targetPane, exportCommand);
198
+ }
151
199
 
152
- for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
153
- const row = rows[rowIdx];
154
- const panes = row.panes ?? [];
155
- for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
156
- const p = panes[paneIdx];
157
- const tmuxPane = paneMap[rowIdx][paneIdx];
158
-
159
- if (p.title) {
160
- tmux(`select-pane -t "${session}:0.${tmuxPane}" -T "${p.title}"`);
161
- }
162
-
163
- // Per-pane directory for first panes of each row (created with project root)
164
- if (p.dir && firstPanesOfRows.has(tmuxPane)) {
165
- const paneDir = resolve(dir, p.dir);
166
- tmux(`send-keys -t "${session}:0.${tmuxPane}" "cd ${paneDir}" C-m`);
167
- }
168
-
169
- // Environment variables
170
- if (p.env && typeof p.env === "object") {
171
- for (const [key, val] of Object.entries(p.env)) {
172
- tmux(`send-keys -t "${session}:0.${tmuxPane}" "export ${key}=${val}" C-m`);
173
- }
174
- }
175
-
176
- const cmd = buildCommand(p);
177
- if (cmd) {
178
- if (team && p.role === "teammate") {
179
- // Defer teammate launches to second pass
180
- teammateCommands.push({ pane: tmuxPane, cmd });
181
- } else {
182
- tmux(`send-keys -t "${session}:0.${tmuxPane}" "${cmd}" C-m`);
183
- }
184
- }
185
-
186
- if (p.focus) {
187
- focusPane = tmuxPane;
188
- }
200
+ if (action.command) {
201
+ sendLiteral(action.targetPane, action.command);
189
202
  }
190
203
  }
191
204
 
192
- // Second pass: launch teammates after a brief delay for the lead to initialize
205
+ // Keep a second pass hook available for future staged startup behavior.
193
206
  if (teammateCommands.length > 0) {
194
- execSync("sleep 2");
207
+ if (leadPane) {
208
+ waitForPaneCommand(leadPane, ["claude"]);
209
+ }
195
210
  for (const { pane: p, cmd } of teammateCommands) {
196
- tmux(`send-keys -t "${session}:0.${p}" "${cmd}" C-m`);
211
+ sendLiteral(p, cmd);
197
212
  }
198
213
  }
199
214
 
200
- // Apply styling
201
- const accent = theme.accent ?? "colour75";
202
- const border = theme.border ?? "colour238";
203
- const bg = theme.bg ?? "colour235";
204
- const fg = theme.fg ?? "colour248";
205
-
206
- tmux(`set-option -t "${session}" pane-border-status top`);
207
- tmux(
208
- `set-option -t "${session}" pane-border-format " #{?pane_active,#[bold]▸,·} #T "`
209
- );
210
- tmux(`set-option -t "${session}" pane-border-style "fg=${border}"`);
211
- tmux(`set-option -t "${session}" pane-active-border-style "fg=${accent}"`);
212
-
213
- tmux(`set-option -t "${session}" status-style "bg=${bg},fg=${fg}"`);
214
- tmux(
215
- `set-option -t "${session}" status-left "#[fg=colour0,bg=${accent},bold] ${session.toUpperCase()} IDE #[default] "`
216
- );
217
- tmux(`set-option -t "${session}" status-left-length 30`);
218
- tmux(
219
- `set-option -t "${session}" status-right "#[fg=colour243]%H:%M #[fg=${accent}]│ #[fg=${fg}]%b %d "`
220
- );
221
- tmux(`set-option -t "${session}" status-justify centre`);
222
- tmux(
223
- `set-option -t "${session}" window-status-current-format "#[fg=${accent},bold]●"`
224
- );
225
- tmux(
226
- `set-option -t "${session}" window-status-format "#[fg=${border}]○"`
227
- );
215
+ for (const command of buildThemeOptions(session, theme)) {
216
+ runSessionCommand(command);
217
+ }
228
218
 
229
219
  // Focus the correct pane
230
- tmux(`select-pane -t "${session}:0.${focusPane}"`);
220
+ selectPane(focusPane);
231
221
 
232
222
  // Launch summary
233
223
  const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
234
- console.log(`Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`);
224
+ console.log(
225
+ `Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`,
226
+ );
235
227
 
236
228
  // Attach
237
- tmux(`attach -t "${session}"`);
229
+ if (attach) {
230
+ attachSession(session);
231
+ }
238
232
  }
@@ -0,0 +1,83 @@
1
+ import { resolve } from "node:path";
2
+
3
+ export function buildPaneCommand(pane, _team) {
4
+ if (!pane.command) return null;
5
+ return pane.command;
6
+ }
7
+
8
+ export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir, team) {
9
+ let focusPane = paneMap[0][0];
10
+ const teammateCommands = [];
11
+ const paneActions = [];
12
+
13
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
14
+ const row = rows[rowIdx];
15
+ const panes = row.panes ?? [];
16
+
17
+ for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
18
+ const pane = panes[paneIdx];
19
+ const tmuxPane = paneMap[rowIdx][paneIdx];
20
+ const action = {
21
+ targetPane: tmuxPane,
22
+ title: pane.title ?? null,
23
+ chdir: null,
24
+ exports: [],
25
+ command: null,
26
+ };
27
+
28
+ if (pane.dir && firstPanesOfRows.has(tmuxPane)) {
29
+ action.chdir = resolve(dir, pane.dir);
30
+ }
31
+
32
+ if (pane.env && typeof pane.env === "object") {
33
+ action.exports = Object.entries(pane.env).map(([key, value]) => `export ${key}=${value}`);
34
+ }
35
+
36
+ const command = buildPaneCommand(pane, team);
37
+ if (command) {
38
+ action.command = command;
39
+ }
40
+
41
+ if (pane.focus) {
42
+ focusPane = tmuxPane;
43
+ }
44
+
45
+ paneActions.push(action);
46
+ }
47
+ }
48
+
49
+ return { focusPane, leadPane: null, paneActions, teammateCommands };
50
+ }
51
+
52
+ export function buildThemeOptions(session, theme = {}) {
53
+ const accent = theme.accent ?? "colour75";
54
+ const border = theme.border ?? "colour238";
55
+ const bg = theme.bg ?? "colour235";
56
+ const fg = theme.fg ?? "colour248";
57
+
58
+ return [
59
+ ["set-option", "-t", session, "pane-border-status", "top"],
60
+ ["set-option", "-t", session, "pane-border-format", " #{?pane_active,#[bold]▸,·} #T "],
61
+ ["set-option", "-t", session, "pane-border-style", `fg=${border}`],
62
+ ["set-option", "-t", session, "pane-active-border-style", `fg=${accent}`],
63
+ ["set-option", "-t", session, "status-style", `bg=${bg},fg=${fg}`],
64
+ [
65
+ "set-option",
66
+ "-t",
67
+ session,
68
+ "status-left",
69
+ `#[fg=colour0,bg=${accent},bold] ${session.toUpperCase()} IDE #[default] `,
70
+ ],
71
+ ["set-option", "-t", session, "status-left-length", "30"],
72
+ [
73
+ "set-option",
74
+ "-t",
75
+ session,
76
+ "status-right",
77
+ `#[fg=colour243]%H:%M #[fg=${accent}]│ #[fg=${fg}]%b %d `,
78
+ ],
79
+ ["set-option", "-t", session, "status-justify", "centre"],
80
+ ["set-option", "-t", session, "window-status-current-format", `#[fg=${accent},bold]●`],
81
+ ["set-option", "-t", session, "window-status-format", `#[fg=${border}]○`],
82
+ ];
83
+ }
package/src/lib/output.js CHANGED
@@ -8,6 +8,17 @@ export function output(data, { json } = {}) {
8
8
  }
9
9
  }
10
10
 
11
+ export class CommandError extends Error {
12
+ constructor(message, { code, exitCode = 1, details, json = false } = {}) {
13
+ super(message);
14
+ this.name = "CommandError";
15
+ this.code = code;
16
+ this.exitCode = exitCode;
17
+ this.details = details;
18
+ this.json = json;
19
+ }
20
+ }
21
+
11
22
  export function printLayout(config) {
12
23
  const INNER = 40;
13
24
  const rows = config.rows ?? [];
@@ -62,11 +73,17 @@ export function printLayout(config) {
62
73
  }
63
74
  }
64
75
 
65
- export function outputError(message, code, { json } = {}) {
76
+ export function outputError(message, code, { json, exitCode = 1, details } = {}) {
77
+ throw new CommandError(message, { code, exitCode, details, json });
78
+ }
79
+
80
+ export function printCommandError(error, { json = error?.json ?? false } = {}) {
66
81
  if (json) {
67
- console.error(JSON.stringify({ error: message, code }, null, 2));
82
+ const payload = { error: error.message, code: error.code };
83
+ if (error.details !== undefined) payload.details = error.details;
84
+ console.error(JSON.stringify(payload, null, 2));
68
85
  } else {
69
- console.error(message);
86
+ console.error(error.message);
70
87
  }
71
- process.exit(code ?? 1);
88
+ process.exit(error.exitCode ?? 1);
72
89
  }