Initial upload with CI/CD workflow
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-02-05 19:31:53 +00:00
parent 63035cbf08
commit 241486619b

267
src/parsers/tmuxParser.ts Normal file
View 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();