Files
oh-my-openagent/src/features/tmux-subagent/pane-state-parser.ts
YeonGyu-Kim 599ce0c283 fix(tmux): strict integer parsing and isActive validation in pane-state-parser
- parseInteger() now rejects malformed input like '120oops' using /^\d+$/ regex
- New parseActiveValue() validates active flag is exactly '0' or '1'
- Added regression tests for malformed integers, negative values, empty fields, non-binary active flags
2026-03-11 21:45:30 +09:00

136 lines
3.3 KiB
TypeScript

import type { TmuxPaneInfo } from "./types"
const MANDATORY_PANE_FIELD_COUNT = 8
type ParsedPaneState = {
windowWidth: number
windowHeight: number
panes: TmuxPaneInfo[]
}
type ParsedPaneLine = {
pane: TmuxPaneInfo
windowWidth: number
windowHeight: number
}
type MandatoryPaneFields = [
paneId: string,
widthString: string,
heightString: string,
leftString: string,
topString: string,
activeString: string,
windowWidthString: string,
windowHeightString: string,
]
export function parsePaneStateOutput(stdout: string): ParsedPaneState | null {
const lines = stdout
.split("\n")
.map((line) => line.replace(/\r$/, ""))
.filter((line) => line.length > 0)
if (lines.length === 0) return null
const parsedPaneLines = lines
.map(parsePaneLine)
.filter((parsedPaneLine): parsedPaneLine is ParsedPaneLine => parsedPaneLine !== null)
if (parsedPaneLines.length === 0) return null
const latestPaneLine = parsedPaneLines[parsedPaneLines.length - 1]
if (!latestPaneLine) return null
return {
windowWidth: latestPaneLine.windowWidth,
windowHeight: latestPaneLine.windowHeight,
panes: parsedPaneLines.map(({ pane }) => pane),
}
}
function parsePaneLine(line: string): ParsedPaneLine | null {
const fields = line.split("\t")
const mandatoryFields = getMandatoryPaneFields(fields)
if (!mandatoryFields) return null
const [paneId, widthString, heightString, leftString, topString, activeString, windowWidthString, windowHeightString] = mandatoryFields
const width = parseInteger(widthString)
const height = parseInteger(heightString)
const left = parseInteger(leftString)
const top = parseInteger(topString)
const isActive = parseActiveValue(activeString)
const windowWidth = parseInteger(windowWidthString)
const windowHeight = parseInteger(windowHeightString)
if (
width === null ||
height === null ||
left === null ||
top === null ||
isActive === null ||
windowWidth === null ||
windowHeight === null
) {
return null
}
return {
pane: {
paneId,
width,
height,
left,
top,
title: fields.slice(MANDATORY_PANE_FIELD_COUNT).join("\t"),
isActive,
},
windowWidth,
windowHeight,
}
}
function getMandatoryPaneFields(fields: string[]): MandatoryPaneFields | null {
if (fields.length < MANDATORY_PANE_FIELD_COUNT) return null
const [paneId, widthString, heightString, leftString, topString, activeString, windowWidthString, windowHeightString] = fields
if (
paneId === undefined ||
widthString === undefined ||
heightString === undefined ||
leftString === undefined ||
topString === undefined ||
activeString === undefined ||
windowWidthString === undefined ||
windowHeightString === undefined
) {
return null
}
return [
paneId,
widthString,
heightString,
leftString,
topString,
activeString,
windowWidthString,
windowHeightString,
]
}
function parseInteger(value: string): number | null {
if (!/^\d+$/.test(value)) return null
const parsedValue = Number.parseInt(value, 10)
return Number.isNaN(parsedValue) ? null : parsedValue
}
function parseActiveValue(value: string): boolean | null {
if (value === "1") return true
if (value === "0") return false
return null
}