feat: add configurable VDO.Ninja overview and floating window focus
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { strict as assert } from 'node:assert'
|
||||
import { test } from 'node:test'
|
||||
import { resolveContainerTerminal } from './containerTerminal'
|
||||
|
||||
const inspection = (running = true) => JSON.stringify([{
|
||||
Id: 'container-id', State: { Running: running }, Mounts: [
|
||||
{ Source: '/home/user/ws', Destination: '/workspace' },
|
||||
{ Source: '/home/user/ws/module', Destination: '/module' }
|
||||
]
|
||||
}])
|
||||
|
||||
test('resolves module path from the most specific actual mount', () => {
|
||||
assert.deepEqual(resolveContainerTerminal(inspection(), '/home/user/ws/module'), { id: 'container-id', cwd: '/module' })
|
||||
assert.equal(resolveContainerTerminal(inspection(), '/home/user/ws/other').cwd, '/workspace/other')
|
||||
})
|
||||
|
||||
test('rejects stopped containers and unrelated workspace paths', () => {
|
||||
assert.throws(() => resolveContainerTerminal(inspection(false), '/home/user/ws/module'), /not running/)
|
||||
assert.throws(() => resolveContainerTerminal(inspection(), '/home/user/ws-other/module'), /not mounted/)
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { posix } from 'node:path'
|
||||
|
||||
export function resolveContainerTerminal(inspectJson: string, moduleRoot: string): { id: string; cwd: string } {
|
||||
const [container] = JSON.parse(inspectJson)
|
||||
if (!container?.State?.Running) throw new Error('Module container is not running')
|
||||
const root = posix.resolve(moduleRoot)
|
||||
const mounts = (container.Mounts ?? []) as { Source: string; Destination: string }[]
|
||||
const mount = mounts
|
||||
.filter((item) => root === item.Source || root.startsWith(`${item.Source.replace(/\/$/, '')}/`))
|
||||
.sort((a, b) => b.Source.length - a.Source.length)[0]
|
||||
if (!mount) throw new Error('Module workspace is not mounted in this container')
|
||||
return { id: container.Id, cwd: posix.join(mount.Destination, posix.relative(mount.Source, root)) }
|
||||
}
|
||||
+24
-4
@@ -1,5 +1,6 @@
|
||||
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
|
||||
import { checkDockerStatus } from './dockerStatus'
|
||||
import { resolveContainerTerminal } from './containerTerminal'
|
||||
import { yahboomDevicePreflight, yahboomModuleCases, yahboomRuntimeImage, yahboomRuntimeArtifacts, yahboomSimulation } from './yahboomRuntime'
|
||||
import { visualizationProgress, reportVisualizationProgress } from './visualizationProgress'
|
||||
import { moduleProgress, reportModuleProgress, createModuleProgressParser } from './moduleProgress'
|
||||
@@ -5964,17 +5965,31 @@ function terminalEnvironment(cols: number, rows: number): Record<string, string>
|
||||
}
|
||||
|
||||
function attachTerminalProcess(sender: WebContents, sessionId: string, terminalKey: string, proc: IPty): void {
|
||||
intentionalTerminalStops.delete(terminalKey)
|
||||
terminalProcesses.set(terminalKey, proc)
|
||||
proc.onData((data) => {
|
||||
if (!sender.isDestroyed()) sender.send('terminal-data', sessionId, data)
|
||||
})
|
||||
proc.onExit(({ exitCode }) => {
|
||||
if (terminalProcesses.get(terminalKey) !== proc) return
|
||||
terminalProcesses.delete(terminalKey)
|
||||
if (intentionalTerminalStops.delete(terminalKey)) return
|
||||
if (!sender.isDestroyed()) sender.send('terminal-data', sessionId, `\r\n[terminal exited: ${exitCode}]\r\n`)
|
||||
})
|
||||
}
|
||||
|
||||
async function containerTerminalCommand(moduleRoot: string, moduleName: string | undefined, cols: number, rows: number, target?: ReturnType<typeof normalizeRemoteTarget>): Promise<string> {
|
||||
const info = parseModuleDockerInfo(basename(moduleRoot))
|
||||
if (!info || !isAllowedContainerName(info.containerKey)) throw new Error('Invalid module container name')
|
||||
const output = target
|
||||
? await runSshOutput(target, `docker inspect ${quoteShellSingle(info.containerKey)}`, 10000)
|
||||
: await runCommandOutput('docker', ['inspect', info.containerKey], undefined, undefined, 10000)
|
||||
const container = resolveContainerTerminal(output, moduleRoot)
|
||||
const shell = buildWorkspaceShellCommand(moduleName, container.cwd, cols, rows, false)
|
||||
const init = 'if [ -n "$ROS_DISTRO" ] && [ -f "/opt/ros/$ROS_DISTRO/setup.bash" ]; then source "/opt/ros/$ROS_DISTRO/setup.bash"; fi; if [ -f ./install/setup.bash ]; then source ./install/setup.bash; fi; exec ' + shell
|
||||
return `exec docker exec -it -e TERM=xterm-256color -w ${quoteShellSingle(container.cwd)} ${quoteShellSingle(container.id)} /bin/bash --noprofile --norc -c ${quoteShellSingle(init)}`
|
||||
}
|
||||
|
||||
function registerTerminalHandlers(): void {
|
||||
ipcMain.handle('terminal-start', async (event, moduleName?: string, cols?: number, rows?: number, sessionId = 'default', workspaceRoot?: string | null, folderName?: string | null) => {
|
||||
const webContentsId = event.sender.id
|
||||
@@ -6006,7 +6021,7 @@ function registerTerminalHandlers(): void {
|
||||
event.sender.once('destroyed', () => stopTerminalsForWebContents(webContentsId))
|
||||
return true
|
||||
})
|
||||
ipcMain.handle('local-workspace:terminal:start', async (event, moduleName?: string, cols?: number, rows?: number, sessionId = 'default', workspaceRoot?: string | null, folderName?: string | null) => {
|
||||
ipcMain.handle('local-workspace:terminal:start', async (event, moduleName?: string, cols?: number, rows?: number, sessionId = 'default', workspaceRoot?: string | null, folderName?: string | null, mode: 'host' | 'container' = 'host') => {
|
||||
const webContentsId = event.sender.id
|
||||
const terminalKey = getTerminalKey(webContentsId, sessionId)
|
||||
cancelStopTerminal(terminalKey)
|
||||
@@ -6024,7 +6039,10 @@ function registerTerminalHandlers(): void {
|
||||
|
||||
const safeCols = Math.max(20, Math.min(300, Math.floor(cols ?? 80)))
|
||||
const safeRows = Math.max(5, Math.min(120, Math.floor(rows ?? 24)))
|
||||
const proc = pty.spawn('/bin/bash', ['--noprofile', '--norc', '-c', buildWorkspaceShellCommand(moduleName, terminalCwd, safeCols, safeRows, false)], {
|
||||
const command = mode === 'container'
|
||||
? await containerTerminalCommand(terminalCwd, moduleName, safeCols, safeRows)
|
||||
: buildWorkspaceShellCommand(moduleName, terminalCwd, safeCols, safeRows, false)
|
||||
const proc = pty.spawn('/bin/bash', ['--noprofile', '--norc', '-c', command], {
|
||||
cwd: terminalCwd,
|
||||
name: 'xterm-256color',
|
||||
cols: safeCols,
|
||||
@@ -6036,7 +6054,7 @@ function registerTerminalHandlers(): void {
|
||||
event.sender.once('destroyed', () => stopTerminalsForWebContents(webContentsId))
|
||||
return true
|
||||
})
|
||||
ipcMain.handle('remote-workspace:terminal:start', async (event, targetInput: RemoteWorkspaceTarget, moduleName?: string, cols?: number, rows?: number, sessionId = 'default', folderName?: string | null) => {
|
||||
ipcMain.handle('remote-workspace:terminal:start', async (event, targetInput: RemoteWorkspaceTarget, moduleName?: string, cols?: number, rows?: number, sessionId = 'default', folderName?: string | null, mode: 'host' | 'container' = 'host') => {
|
||||
const webContentsId = event.sender.id
|
||||
const terminalKey = getTerminalKey(webContentsId, sessionId)
|
||||
cancelStopTerminal(terminalKey)
|
||||
@@ -6068,7 +6086,9 @@ function registerTerminalHandlers(): void {
|
||||
'-i',
|
||||
target.identityFile,
|
||||
`${target.user}@${target.host}`,
|
||||
buildWorkspaceShellCommand(moduleName, resolved.moduleRoot, safeCols, safeRows, true)
|
||||
mode === 'container'
|
||||
? await containerTerminalCommand(resolved.moduleRoot, moduleName, safeCols, safeRows, target)
|
||||
: buildWorkspaceShellCommand(moduleName, resolved.moduleRoot, safeCols, safeRows, true)
|
||||
], {
|
||||
name: 'xterm-256color',
|
||||
cols: safeCols,
|
||||
|
||||
Reference in New Issue
Block a user