code-squad-cli 1.2.22 → 1.3.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/dist/adapters/GitAdapter.js +18 -4
- package/dist/dash/InkDashboard.d.ts +13 -0
- package/dist/dash/InkDashboard.js +442 -0
- package/dist/dash/TmuxAdapter.d.ts +233 -0
- package/dist/dash/TmuxAdapter.js +520 -0
- package/dist/dash/index.d.ts +4 -0
- package/dist/dash/index.js +216 -0
- package/dist/dash/pathUtils.d.ts +27 -0
- package/dist/dash/pathUtils.js +70 -0
- package/dist/dash/threadHelpers.d.ts +9 -0
- package/dist/dash/threadHelpers.js +37 -0
- package/dist/dash/types.d.ts +42 -0
- package/dist/dash/types.js +1 -0
- package/dist/dash/useDirectorySuggestions.d.ts +23 -0
- package/dist/dash/useDirectorySuggestions.js +136 -0
- package/dist/dash/usePathValidation.d.ts +9 -0
- package/dist/dash/usePathValidation.js +34 -0
- package/dist/dash/windowHelpers.d.ts +10 -0
- package/dist/dash/windowHelpers.js +43 -0
- package/dist/index.js +1376 -78
- package/package.json +7 -3
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { exec as execCallback } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const exec = promisify(execCallback);
|
|
4
|
+
const execOptions = { maxBuffer: 1024 * 1024 };
|
|
5
|
+
/**
|
|
6
|
+
* tmux 명령어 실행 래퍼
|
|
7
|
+
*/
|
|
8
|
+
export class TmuxAdapter {
|
|
9
|
+
/**
|
|
10
|
+
* tmux 설치 여부 확인
|
|
11
|
+
*/
|
|
12
|
+
async isTmuxAvailable() {
|
|
13
|
+
try {
|
|
14
|
+
await exec('which tmux', execOptions);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 현재 tmux 세션 내부인지 확인
|
|
23
|
+
*/
|
|
24
|
+
isInsideTmux() {
|
|
25
|
+
return !!process.env.TMUX;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 세션 존재 여부 확인
|
|
29
|
+
*/
|
|
30
|
+
async hasSession(name) {
|
|
31
|
+
try {
|
|
32
|
+
await exec(`tmux has-session -t "${name}"`, execOptions);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 새 tmux 세션 생성
|
|
41
|
+
*/
|
|
42
|
+
async createSession(name, cwd) {
|
|
43
|
+
await exec(`tmux new-session -d -s "${name}" -c "${cwd}"`, execOptions);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 세션 생성 또는 기존 세션 반환
|
|
47
|
+
* @returns true if new session created, false if existing session
|
|
48
|
+
*/
|
|
49
|
+
async ensureSession(name, cwd) {
|
|
50
|
+
if (await this.hasSession(name)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
await this.createSession(name, cwd);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 세션에 attach (spawn 사용)
|
|
58
|
+
*/
|
|
59
|
+
async attachSession(name) {
|
|
60
|
+
const { spawn } = await import('child_process');
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const tmux = spawn('tmux', ['attach-session', '-t', name], { stdio: 'inherit' });
|
|
63
|
+
tmux.on('close', (code) => {
|
|
64
|
+
if (code === 0)
|
|
65
|
+
resolve();
|
|
66
|
+
else
|
|
67
|
+
reject(new Error(`tmux attach failed with code ${code}`));
|
|
68
|
+
});
|
|
69
|
+
tmux.on('error', reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 현재 pane 분할 (수직/수평)
|
|
74
|
+
* @param direction 'v' = 수직 (좌우), 'h' = 수평 (상하)
|
|
75
|
+
* @param cwd 새 pane의 작업 디렉토리
|
|
76
|
+
* @param percent 새 pane의 크기 비율 (옵션)
|
|
77
|
+
*/
|
|
78
|
+
async splitWindow(direction, cwd, percent) {
|
|
79
|
+
const flag = direction === 'v' ? '-h' : '-v'; // tmux는 반대로 해석
|
|
80
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : '';
|
|
81
|
+
const percentFlag = percent ? `-p ${percent}` : '';
|
|
82
|
+
await exec(`tmux split-window ${flag} ${cwdFlag} ${percentFlag}`, execOptions);
|
|
83
|
+
// 새로 생성된 pane ID 반환
|
|
84
|
+
const { stdout } = await exec('tmux display-message -p "#{pane_id}"', execOptions);
|
|
85
|
+
return stdout.trim();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 특정 pane 선택 (포커스)
|
|
89
|
+
*/
|
|
90
|
+
async selectPane(paneId) {
|
|
91
|
+
await exec(`tmux select-pane -t "${paneId}"`, execOptions);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* pane 목록 조회
|
|
95
|
+
*/
|
|
96
|
+
async listPanes() {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await exec('tmux list-panes -F "#{pane_id}|#{pane_index}|#{pane_active}|#{pane_current_path}"', execOptions);
|
|
99
|
+
return stdout.trim().split('\n').filter(Boolean).map(line => {
|
|
100
|
+
const [id, index, active, cwd] = line.split('|');
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
index: parseInt(index, 10),
|
|
104
|
+
active: active === '1',
|
|
105
|
+
cwd,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* pane 종료
|
|
115
|
+
*/
|
|
116
|
+
async killPane(paneId) {
|
|
117
|
+
await exec(`tmux kill-pane -t "${paneId}"`, execOptions);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* pane에 키 전송
|
|
121
|
+
*/
|
|
122
|
+
async sendKeys(paneId, keys) {
|
|
123
|
+
// 특수문자 이스케이프
|
|
124
|
+
const escaped = keys.replace(/"/g, '\\"');
|
|
125
|
+
await exec(`tmux send-keys -t "${paneId}" "${escaped}"`, execOptions);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* pane에 특수 키 전송 (C-l, C-c, Enter 등 tmux 키 이름)
|
|
129
|
+
*/
|
|
130
|
+
async sendSpecialKey(paneId, keyName) {
|
|
131
|
+
await exec(`tmux send-keys -t "${paneId}" ${keyName}`, execOptions);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* pane에 Enter 키 전송
|
|
135
|
+
*/
|
|
136
|
+
async sendEnter(paneId) {
|
|
137
|
+
await exec(`tmux send-keys -t "${paneId}" Enter`, execOptions);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 현재 window의 레이아웃 설정
|
|
141
|
+
* @param layout 'even-horizontal' | 'even-vertical' | 'main-horizontal' | 'main-vertical' | 'tiled'
|
|
142
|
+
*/
|
|
143
|
+
async setLayout(layout) {
|
|
144
|
+
await exec(`tmux select-layout ${layout}`, execOptions);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* pane 크기 조정
|
|
148
|
+
*/
|
|
149
|
+
async resizePane(paneId, direction, amount) {
|
|
150
|
+
await exec(`tmux resize-pane -t "${paneId}" -${direction} ${amount}`, execOptions);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* pane 너비를 절대값으로 설정
|
|
154
|
+
*/
|
|
155
|
+
async setPaneWidth(paneId, width) {
|
|
156
|
+
await exec(`tmux resize-pane -t "${paneId}" -x ${width}`, execOptions);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* pane을 별도 window로 분리 (백그라운드 실행 유지)
|
|
160
|
+
* @returns 새로 생성된 window ID
|
|
161
|
+
*/
|
|
162
|
+
async breakPaneToWindow(paneId) {
|
|
163
|
+
const { stdout } = await exec(`tmux break-pane -d -s "${paneId}" -P -F "#{window_id}"`, execOptions);
|
|
164
|
+
return stdout.trim();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 특정 session:index에 window 생성
|
|
168
|
+
* @returns 새로 생성된 window ID
|
|
169
|
+
*/
|
|
170
|
+
async createWindowAtIndex(sessionName, index, cwd) {
|
|
171
|
+
const { stdout } = await exec(`tmux new-window -d -t "${sessionName}:${index}" -c "${cwd}" -P -F "#{window_id}"`, execOptions);
|
|
172
|
+
return stdout.trim();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 숨겨진 window 생성 (단일 pane)
|
|
176
|
+
*/
|
|
177
|
+
async createHiddenWindow(cwd) {
|
|
178
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : '';
|
|
179
|
+
const { stdout } = await exec(`tmux new-window -d ${cwdFlag} -P -F "#{window_id}"`, execOptions);
|
|
180
|
+
return stdout.trim();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* window의 pane을 특정 pane과 swap
|
|
184
|
+
*/
|
|
185
|
+
async swapPaneWithWindow(windowId, targetPaneId) {
|
|
186
|
+
await exec(`tmux swap-pane -d -s "${windowId}.0" -t "${targetPaneId}"`, execOptions);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* window 내 단일 pane 너비 설정
|
|
190
|
+
*/
|
|
191
|
+
async setWindowPaneWidth(windowId, width) {
|
|
192
|
+
await exec(`tmux resize-pane -t "${windowId}.0" -x ${width}`, execOptions);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* pane index로 pane ID 조회 (현재 window)
|
|
196
|
+
*/
|
|
197
|
+
async getPaneIdByIndex(index) {
|
|
198
|
+
try {
|
|
199
|
+
const { stdout } = await exec('tmux list-panes -F "#{pane_index}|#{pane_id}"', execOptions);
|
|
200
|
+
const match = stdout
|
|
201
|
+
.trim()
|
|
202
|
+
.split('\n')
|
|
203
|
+
.map(line => line.split('|'))
|
|
204
|
+
.find(([idx]) => parseInt(idx, 10) === index);
|
|
205
|
+
return match?.[1] ?? null;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 숨겨진 window에서 pane을 현재 window로 가져옴
|
|
213
|
+
* @param windowId 숨겨진 window ID
|
|
214
|
+
* @param targetPaneId 옆에 배치할 target pane
|
|
215
|
+
* @returns 가져온 pane의 새 ID
|
|
216
|
+
*/
|
|
217
|
+
async joinPaneFromWindow(windowId, targetPaneId) {
|
|
218
|
+
// 수평 분할로 target pane 옆에 배치
|
|
219
|
+
await exec(`tmux join-pane -h -s "${windowId}.0" -t "${targetPaneId}"`, execOptions);
|
|
220
|
+
// 새로 join된 pane ID 반환
|
|
221
|
+
const { stdout } = await exec('tmux display-message -p "#{pane_id}"', execOptions);
|
|
222
|
+
return stdout.trim();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* pane 숨기기 (width=0) - deprecated, use breakPaneToWindow
|
|
226
|
+
*/
|
|
227
|
+
async hidePane(paneId) {
|
|
228
|
+
await exec(`tmux resize-pane -t "${paneId}" -x 0`, execOptions);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* pane 표시 (지정 너비로 복원) - deprecated, use joinPaneFromWindow
|
|
232
|
+
*/
|
|
233
|
+
async showPane(paneId, width) {
|
|
234
|
+
await exec(`tmux resize-pane -t "${paneId}" -x ${width}`, execOptions);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 현재 window 너비 조회
|
|
238
|
+
*/
|
|
239
|
+
async getWindowWidth() {
|
|
240
|
+
const { stdout } = await exec('tmux display-message -p "#{window_width}"', execOptions);
|
|
241
|
+
return parseInt(stdout.trim(), 10);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* pane 너비 조회
|
|
245
|
+
*/
|
|
246
|
+
async getPaneWidth(paneId) {
|
|
247
|
+
const { stdout } = await exec(`tmux display-message -t "${paneId}" -p "#{pane_width}"`, execOptions);
|
|
248
|
+
return parseInt(stdout.trim(), 10);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* pane 높이 조회
|
|
252
|
+
*/
|
|
253
|
+
async getPaneHeight(paneId) {
|
|
254
|
+
const { stdout } = await exec(`tmux display-message -t "${paneId}" -p "#{pane_height}"`, execOptions);
|
|
255
|
+
return parseInt(stdout.trim(), 10);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 현재 세션 이름 조회
|
|
259
|
+
*/
|
|
260
|
+
async getSessionName() {
|
|
261
|
+
try {
|
|
262
|
+
const { stdout } = await exec('tmux display-message -p "#{session_name}"', execOptions);
|
|
263
|
+
return stdout.trim();
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 현재 pane ID 조회
|
|
271
|
+
*/
|
|
272
|
+
async getCurrentPaneId() {
|
|
273
|
+
try {
|
|
274
|
+
const { stdout } = await exec('tmux display-message -p "#{pane_id}"', execOptions);
|
|
275
|
+
return stdout.trim();
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* UX 개선 설정 적용
|
|
283
|
+
* - 마우스 모드 활성화 (클릭 선택, 드래그 리사이징)
|
|
284
|
+
* - 활성 pane 테두리 강조
|
|
285
|
+
* - pane swap 단축키 (Ctrl+b m)
|
|
286
|
+
*/
|
|
287
|
+
async applyUXSettings() {
|
|
288
|
+
const settings = [
|
|
289
|
+
'set -g mouse on',
|
|
290
|
+
// extended-keys 활성화 (Shift+Tab 등 modifier key 조합 인식에 필요)
|
|
291
|
+
'set -g extended-keys on',
|
|
292
|
+
"set -g pane-active-border-style 'fg=cyan,bold'",
|
|
293
|
+
"set -g pane-border-style 'fg=#444444'",
|
|
294
|
+
'set -g pane-border-lines single',
|
|
295
|
+
'set -g pane-border-status top',
|
|
296
|
+
"set -g pane-border-format ' #{pane_current_path} '",
|
|
297
|
+
// pane 번호 표시 시간 늘리기
|
|
298
|
+
'set -g display-panes-time 5000',
|
|
299
|
+
// pane 번호 색상
|
|
300
|
+
"set -g display-panes-colour '#444444'",
|
|
301
|
+
"set -g display-panes-active-colour cyan",
|
|
302
|
+
];
|
|
303
|
+
for (const setting of settings) {
|
|
304
|
+
try {
|
|
305
|
+
await exec(`tmux ${setting}`, execOptions);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// 일부 설정이 실패해도 계속 진행
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Ctrl+b m = pane 이동 메뉴 (대시보드 pane 0 제외)
|
|
312
|
+
try {
|
|
313
|
+
// 간단한 메뉴 - pane 0에서는 동작 안 함, Move Left는 pane 1에서 동작 안 함
|
|
314
|
+
await exec(`tmux bind-key m if-shell -F "#{!=:#{pane_index},0}" "display-menu -T 'Move Pane' -x P -y P 'Move Left' l 'if-shell -F \\"#{>:#{pane_index},1}\\" \\"swap-pane -U\\"' 'Move Right' r 'swap-pane -D' '' 'Swap #1' 1 'swap-pane -t 1' 'Swap #2' 2 'swap-pane -t 2' 'Swap #3' 3 'swap-pane -t 3' 'Swap #4' 4 'swap-pane -t 4' '' 'Cancel' q ''"`, execOptions);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// 실패해도 계속 진행
|
|
318
|
+
}
|
|
319
|
+
// Shift+Tab = 대시보드/터미널 간 포커스 토글 (prefix 없이)
|
|
320
|
+
// pane 0(대시보드)이면 pane 1(터미널)로, 그 외면 pane 0(대시보드)으로 이동
|
|
321
|
+
try {
|
|
322
|
+
await exec(`tmux bind-key -n BTab if-shell -F "#{==:#{pane_index},0}" "select-pane -t 1" "select-pane -t 0"`, execOptions);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// 실패해도 계속 진행
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* 대시보드 pane 리사이즈 훅 설정
|
|
330
|
+
*/
|
|
331
|
+
async setDashboardResizeHook(paneId, width) {
|
|
332
|
+
try {
|
|
333
|
+
await exec(`tmux set -g @csq_dash_pane "${paneId}"`, execOptions);
|
|
334
|
+
await exec(`tmux set -g @csq_dash_width "${width}"`, execOptions);
|
|
335
|
+
await exec(`tmux set-hook after-resize-pane 'resize-pane -t "#{@csq_dash_pane}" -x #{@csq_dash_width}'`, execOptions);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// 실패해도 계속 진행
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* main-vertical 레이아웃 적용 (왼쪽 고정, 오른쪽 분할)
|
|
343
|
+
* @param mainWidth 왼쪽 메인 pane 너비 (columns)
|
|
344
|
+
*/
|
|
345
|
+
async applyMainVerticalLayout(mainWidth = 30) {
|
|
346
|
+
try {
|
|
347
|
+
await exec(`tmux set-window-option main-pane-width ${mainWidth}`, execOptions);
|
|
348
|
+
await exec('tmux select-layout main-vertical', execOptions);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// 실패해도 계속 진행
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* 특정 pane 기준으로 분할
|
|
356
|
+
* @param targetPaneId 분할할 대상 pane
|
|
357
|
+
* @param direction 'v' = 수직 (좌우), 'h' = 수평 (상하)
|
|
358
|
+
* @param cwd 새 pane의 작업 디렉토리
|
|
359
|
+
*/
|
|
360
|
+
async splitPane(targetPaneId, direction, cwd) {
|
|
361
|
+
const flag = direction === 'v' ? '-h' : '-v';
|
|
362
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : '';
|
|
363
|
+
await exec(`tmux split-window ${flag} -t "${targetPaneId}" ${cwdFlag}`, execOptions);
|
|
364
|
+
const { stdout } = await exec('tmux display-message -p "#{pane_id}"', execOptions);
|
|
365
|
+
return stdout.trim();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 현재 세션 종료
|
|
369
|
+
*/
|
|
370
|
+
async killCurrentSession() {
|
|
371
|
+
await exec('tmux kill-session', execOptions);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 현재 클라이언트 detach (세션은 유지)
|
|
375
|
+
*/
|
|
376
|
+
async detachClient() {
|
|
377
|
+
await exec('tmux detach-client', execOptions);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Detach client and kill a window in one atomic tmux command.
|
|
381
|
+
* Prevents flash of another window between kill and detach.
|
|
382
|
+
*/
|
|
383
|
+
async detachAndKillWindow(sessionName, windowIndex) {
|
|
384
|
+
await exec(`tmux detach-client \\; kill-window -t "${sessionName}:${windowIndex}"`, execOptions);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 특정 window 종료
|
|
388
|
+
*/
|
|
389
|
+
async killWindow(windowId) {
|
|
390
|
+
await exec(`tmux kill-window -t "${windowId}"`, execOptions);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* pane 화면 클리어 (scrollback 포함)
|
|
394
|
+
*/
|
|
395
|
+
async clearPane(paneId) {
|
|
396
|
+
await exec(`tmux send-keys -t "${paneId}" -R`, execOptions);
|
|
397
|
+
await exec(`tmux clear-history -t "${paneId}"`, execOptions);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 클라이언트 강제 리프레시
|
|
401
|
+
*/
|
|
402
|
+
async refreshClient() {
|
|
403
|
+
await exec('tmux refresh-client -S', execOptions);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* pane 위치 swap
|
|
407
|
+
* @param paneId 이동할 pane
|
|
408
|
+
* @param direction 'left' | 'right' | 'up' | 'down'
|
|
409
|
+
*/
|
|
410
|
+
async swapPane(paneId, direction) {
|
|
411
|
+
const dirMap = {
|
|
412
|
+
left: '-U', // swap with pane above/left
|
|
413
|
+
right: '-D', // swap with pane below/right
|
|
414
|
+
up: '-U',
|
|
415
|
+
down: '-D',
|
|
416
|
+
};
|
|
417
|
+
await exec(`tmux swap-pane -t "${paneId}" ${dirMap[direction]}`, execOptions);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 대시보드 제외하고 오른쪽 pane들 균등 분할
|
|
421
|
+
* @param dashPaneId 대시보드 pane ID
|
|
422
|
+
* @param dashWidth 대시보드 너비 (columns)
|
|
423
|
+
*/
|
|
424
|
+
async distributeRightPanes(dashPaneId, dashWidth = 35) {
|
|
425
|
+
try {
|
|
426
|
+
// 전체 window 너비 가져오기
|
|
427
|
+
const { stdout: widthStr } = await exec('tmux display-message -p "#{window_width}"', execOptions);
|
|
428
|
+
const totalWidth = parseInt(widthStr.trim(), 10);
|
|
429
|
+
// 모든 pane 가져오기
|
|
430
|
+
const panes = await this.listPanes();
|
|
431
|
+
const rightPanes = panes.filter(p => p.id !== dashPaneId);
|
|
432
|
+
if (rightPanes.length === 0)
|
|
433
|
+
return;
|
|
434
|
+
// 오른쪽 영역 너비 계산 (전체 - 대시보드 - 구분선)
|
|
435
|
+
const rightAreaWidth = totalWidth - dashWidth - 1;
|
|
436
|
+
const paneWidth = Math.floor(rightAreaWidth / rightPanes.length);
|
|
437
|
+
// 각 pane 크기 조정
|
|
438
|
+
for (const pane of rightPanes) {
|
|
439
|
+
await exec(`tmux resize-pane -t "${pane.id}" -x ${paneWidth}`, execOptions);
|
|
440
|
+
}
|
|
441
|
+
// 대시보드 크기 복원
|
|
442
|
+
await exec(`tmux resize-pane -t "${dashPaneId}" -x ${dashWidth}`, execOptions);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// 실패해도 계속 진행
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// ============================================================
|
|
449
|
+
// Window 관련 메서드 (Task 1, 8)
|
|
450
|
+
// ============================================================
|
|
451
|
+
/**
|
|
452
|
+
* 현재 세션의 모든 window 목록 조회
|
|
453
|
+
*/
|
|
454
|
+
async listWindows() {
|
|
455
|
+
try {
|
|
456
|
+
const { stdout } = await exec('tmux list-windows -F "#{window_id}|#{window_index}|#{window_name}|#{pane_current_path}|#{window_active}"', execOptions);
|
|
457
|
+
return stdout.trim().split('\n').filter(Boolean).map(line => {
|
|
458
|
+
const [id, index, name, cwd, active] = line.split('|');
|
|
459
|
+
return {
|
|
460
|
+
id,
|
|
461
|
+
index: parseInt(index, 10),
|
|
462
|
+
name,
|
|
463
|
+
cwd,
|
|
464
|
+
active: active === '1',
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* 특정 window로 포커스 전환
|
|
474
|
+
*/
|
|
475
|
+
async selectWindow(windowId) {
|
|
476
|
+
await exec(`tmux select-window -t "${windowId}"`, execOptions);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* 새 window 생성
|
|
480
|
+
* @param cwd 작업 디렉토리 (옵션)
|
|
481
|
+
* @param name window 이름 (옵션)
|
|
482
|
+
* @returns 생성된 window ID
|
|
483
|
+
*/
|
|
484
|
+
async createNewWindow(cwd, name) {
|
|
485
|
+
const cwdFlag = cwd ? `-c "${cwd}"` : '';
|
|
486
|
+
const nameFlag = name ? `-n "${name}"` : '';
|
|
487
|
+
const { stdout } = await exec(`tmux new-window -d ${cwdFlag} ${nameFlag} -P -F "#{window_id}"`, execOptions);
|
|
488
|
+
return stdout.trim();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 현재 window ID 조회
|
|
492
|
+
*/
|
|
493
|
+
async getCurrentWindowId() {
|
|
494
|
+
try {
|
|
495
|
+
const { stdout } = await exec('tmux display-message -p "#{window_id}"', execOptions);
|
|
496
|
+
return stdout.trim();
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* 현재 window index 조회
|
|
504
|
+
*/
|
|
505
|
+
async getCurrentWindowIndex() {
|
|
506
|
+
try {
|
|
507
|
+
const { stdout } = await exec('tmux display-message -p "#{window_index}"', execOptions);
|
|
508
|
+
return parseInt(stdout.trim(), 10);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* window 이름 변경
|
|
516
|
+
*/
|
|
517
|
+
async renameWindow(windowId, name) {
|
|
518
|
+
await exec(`tmux rename-window -t "${windowId}" "${name}"`, execOptions);
|
|
519
|
+
}
|
|
520
|
+
}
|