This commit is contained in:
267
src/parsers/tmuxParser.ts
Normal file
267
src/parsers/tmuxParser.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import { shellUtils } from '../utils/shellUtils';
|
||||||
|
import { Layout, Session, Window, Pane, ParserResult, TerminalType } from '../models/types';
|
||||||
|
|
||||||
|
interface TmuxPaneInfo {
|
||||||
|
pane_index: string;
|
||||||
|
pane_current_path: string;
|
||||||
|
pane_current_command: string;
|
||||||
|
pane_title: string;
|
||||||
|
pane_active: string;
|
||||||
|
layout: string;
|
||||||
|
x: string;
|
||||||
|
y: string;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TmuxWindowInfo {
|
||||||
|
window_index: string;
|
||||||
|
window_name: string;
|
||||||
|
window_active: string;
|
||||||
|
panes: TmuxPaneInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TmuxSessionInfo {
|
||||||
|
session_name: string;
|
||||||
|
session_windows: TmuxWindowInfo[];
|
||||||
|
session_active: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TmuxParser {
|
||||||
|
private tmuxPath: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tmuxPath = 'tmux';
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAvailable(): Promise<boolean> {
|
||||||
|
return shellUtils.isCommandAvailable('tmux');
|
||||||
|
}
|
||||||
|
|
||||||
|
async listSessions(): Promise<string[]> {
|
||||||
|
const result = await shellUtils.exec('tmux list-sessions -F "#{session_name}"');
|
||||||
|
if (!result.success) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return result.stdout.split('\n').filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async captureSession(sessionName?: string): Promise<ParserResult<Layout>> {
|
||||||
|
try {
|
||||||
|
const sessionInfo = await this.getSessionInfo(sessionName);
|
||||||
|
if (!sessionInfo) {
|
||||||
|
return { success: false, error: `Session not found: ${sessionName}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = this.convertToLayout(sessionInfo);
|
||||||
|
return { success: true, data: layout };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSessionInfo(sessionName?: string): Promise<TmuxSessionInfo | null> {
|
||||||
|
let command = 'tmux list-windows -a -F "#{window_index}:#{window_name}:#{window_active}"';
|
||||||
|
if (sessionName) {
|
||||||
|
command = `tmux list-windows -t "${sessionName}" -F "#{window_index}:#{window_name}:#{window_active}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowsResult = await shellUtils.exec(command);
|
||||||
|
if (!windowsResult.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windows: TmuxWindowInfo[] = [];
|
||||||
|
const windowLines = windowsResult.stdout.split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
for (const line of windowLines) {
|
||||||
|
const parts = line.split(':');
|
||||||
|
if (parts.length < 3) continue;
|
||||||
|
|
||||||
|
const windowIndex = parts[0].trim();
|
||||||
|
const windowName = parts[1].trim();
|
||||||
|
const isActive = parts[2].trim() === '1';
|
||||||
|
|
||||||
|
const panes = await this.getPanesForWindow(sessionName, windowIndex);
|
||||||
|
windows.push({
|
||||||
|
window_index: windowIndex,
|
||||||
|
window_name: windowName,
|
||||||
|
window_active: isActive ? '1' : '0',
|
||||||
|
panes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionNameResult = sessionName || await this.getActiveSessionName();
|
||||||
|
|
||||||
|
return {
|
||||||
|
session_name: sessionNameResult || 'unknown',
|
||||||
|
session_windows: windows,
|
||||||
|
session_active: windows.find(w => w.window_active === '1')?.window_index || '0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getActiveSessionName(): Promise<string | null> {
|
||||||
|
const result = await shellUtils.exec('tmux display-message -p "#{session_name}"');
|
||||||
|
return result.success ? result.stdout : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPanesForWindow(sessionName: string | undefined, windowIndex: string): Promise<TmuxPaneInfo[]> {
|
||||||
|
const target = sessionName ? `${sessionName}:${windowIndex}` : windowIndex;
|
||||||
|
const result = await shellUtils.exec(
|
||||||
|
`tmux list-panes -t "${target}" -F "#{pane_index}:#{pane_current_path}:#{pane_current_command}:#{pane_title}:#{pane_active}:#{layout}:#{pane_width}:#{pane_height}:#{pane_x}:#{pane_y}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const panes: TmuxPaneInfo[] = [];
|
||||||
|
const paneLines = result.stdout.split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
for (const line of paneLines) {
|
||||||
|
const parts = line.split(':');
|
||||||
|
if (parts.length < 10) continue;
|
||||||
|
|
||||||
|
panes.push({
|
||||||
|
pane_index: parts[0],
|
||||||
|
pane_current_path: parts[1],
|
||||||
|
pane_current_command: parts[2],
|
||||||
|
pane_title: parts[3],
|
||||||
|
pane_active: parts[4],
|
||||||
|
layout: parts[5],
|
||||||
|
width: parts[6],
|
||||||
|
height: parts[7],
|
||||||
|
x: parts[8],
|
||||||
|
y: parts[9],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return panes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertToLayout(sessionInfo: TmuxSessionInfo): Layout {
|
||||||
|
const activeWindowIndex = parseInt(sessionInfo.session_active, 10) || 0;
|
||||||
|
|
||||||
|
const windows: Window[] = sessionInfo.session_windows.map((w) => {
|
||||||
|
const panes: Pane[] = w.panes.map((p) => ({
|
||||||
|
id: `p${p.pane_index}`,
|
||||||
|
index: parseInt(p.pane_index, 10),
|
||||||
|
layout: {
|
||||||
|
x: parseInt(p.x, 10) || 0,
|
||||||
|
y: parseInt(p.y, 10) || 0,
|
||||||
|
width: parseInt(p.width, 10) || 80,
|
||||||
|
height: parseInt(p.height, 10) || 24,
|
||||||
|
},
|
||||||
|
command: p.pane_current_command !== 'zsh' && p.pane_current_command !== 'bash' ? p.pane_current_command : undefined,
|
||||||
|
cwd: p.pane_current_path,
|
||||||
|
title: p.pane_title,
|
||||||
|
isActive: p.pane_active === '1',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `w${w.window_index}`,
|
||||||
|
index: parseInt(w.window_index, 10),
|
||||||
|
name: w.window_name,
|
||||||
|
panes,
|
||||||
|
layout: w.panes[0]?.layout,
|
||||||
|
activePaneIndex: panes.findIndex((p) => p.isActive),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
id: `s-${sessionInfo.session_name}`,
|
||||||
|
name: sessionInfo.session_name,
|
||||||
|
windows,
|
||||||
|
activeWindowIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1.0.0',
|
||||||
|
terminalType: 'tmux' as TerminalType,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
session,
|
||||||
|
metadata: {
|
||||||
|
description: `Captured tmux session: ${sessionInfo.session_name}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateRestoreCommands(layout: Layout, sessionName?: string): Promise<string[]> {
|
||||||
|
const commands: string[] = [];
|
||||||
|
const targetSession = sessionName || layout.session.name;
|
||||||
|
|
||||||
|
commands.push(`tmux new-session -d -s "${targetSession}"`);
|
||||||
|
|
||||||
|
for (let i = 0; i < layout.session.windows.length; i++) {
|
||||||
|
const window = layout.session.windows[i];
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
commands.push(`tmux rename-window -t "${targetSession}:0" "${window.name}"`);
|
||||||
|
} else {
|
||||||
|
commands.push(`tmux new-window -t "${targetSession}" -n "${window.name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < window.panes.length; j++) {
|
||||||
|
const pane = window.panes[j];
|
||||||
|
|
||||||
|
if (j === 0 && i === 0) {
|
||||||
|
commands.push(`tmux select-pane -t "${targetSession}:${i}.${j}"`);
|
||||||
|
} else if (j === 0) {
|
||||||
|
commands.push(`tmux select-pane -t "${targetSession}:${i}.${j}"`);
|
||||||
|
} else {
|
||||||
|
commands.push(`tmux split-window -t "${targetSession}:${i}" -${this.getSplitDirection(layout, i, j)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pane.cwd) {
|
||||||
|
commands.push(`tmux send-keys -t "${targetSession}:${i}.${j}" "cd '${pane.cwd}'" C-m`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pane.command) {
|
||||||
|
commands.push(`tmux send-keys -t "${targetSession}:${i}.${j}" "${pane.command}" C-m`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.layout) {
|
||||||
|
commands.push(`tmux select-layout -t "${targetSession}:${i}" "${window.layout}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.session.activeWindowIndex > 0) {
|
||||||
|
commands.push(`tmux select-window -t "${targetSession}:${layout.session.activeWindowIndex}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSplitDirection(layout: Layout, windowIndex: number, paneIndex: number): string {
|
||||||
|
if (paneIndex === 0) return 'h';
|
||||||
|
|
||||||
|
const prevPane = layout.session.windows[windowIndex].panes[paneIndex - 1];
|
||||||
|
const currentPane = layout.session.windows[windowIndex].panes[paneIndex];
|
||||||
|
|
||||||
|
if (currentPane.layout.x > prevPane.layout.x) {
|
||||||
|
return 'v';
|
||||||
|
}
|
||||||
|
return 'h';
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreLayout(layout: Layout, sessionName?: string): Promise<ParserResult<void>> {
|
||||||
|
try {
|
||||||
|
const commands = await this.generateRestoreCommands(layout, sessionName);
|
||||||
|
|
||||||
|
for (const command of commands) {
|
||||||
|
const result = await shellUtils.exec(command);
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, error: `Failed to execute: ${command}\n${result.stderr}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tmuxParser = new TmuxParser();
|
||||||
Reference in New Issue
Block a user