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