From d8fbce56a8cff22e9e163eb282e8313dda350b63 Mon Sep 17 00:00:00 2001 From: David Rehmat Date: Wed, 8 May 2024 11:37:29 -0700 Subject: [PATCH] fix: content api for frontend events from backend, ui updates and reset command (#101) * fix: move context to it's own class * fix: result stream moved to it's own model * fix: set raw result string to a buffer output when provided * fix: attach context to the event list to runtime query * fix: add context event handling * fix: use dynamic event name list based on events * fix: prevent events after abort * fix: safe hook runtime usage * fix: reset command --- src/main/framework/commands.ts | 5 +- src/main/framework/execute-context.ts | 154 +++++++++++++++ src/main/framework/result-stream.ts | 18 ++ src/main/framework/runtime-events.ts | 177 ++++++------------ src/main/framework/runtime-executor.ts | 31 ++- src/main/framework/runtime.ts | 30 ++- src/main/framework/system-commands/cd.ts | 2 +- src/main/framework/system-commands/clear.ts | 2 +- src/main/framework/system-commands/edit.ts | 6 +- src/main/framework/system-commands/exit.ts | 2 +- src/main/framework/system-commands/history.ts | 2 +- src/main/framework/system-commands/reload.ts | 2 +- src/main/framework/system-commands/reset.ts | 44 +++++ .../framework/system-commands/settings.ts | 2 +- src/main/framework/system-commands/tab.ts | 2 +- src/main/framework/system-commands/test.ts | 3 +- src/main/framework/system-commands/vault.ts | 2 +- src/main/framework/system-commands/version.ts | 2 +- .../framework/system-commands/workspace.ts | 2 +- src/main/util.ts | 12 ++ src/renderer/src/runner/runner.tsx | 77 ++++---- src/renderer/src/runner/runtime.ts | 10 + 22 files changed, 386 insertions(+), 201 deletions(-) create mode 100644 src/main/framework/execute-context.ts create mode 100644 src/main/framework/result-stream.ts create mode 100644 src/main/framework/system-commands/reset.ts diff --git a/src/main/framework/commands.ts b/src/main/framework/commands.ts index 5f5f7cb..61378f1 100644 --- a/src/main/framework/commands.ts +++ b/src/main/framework/commands.ts @@ -5,13 +5,14 @@ import short from 'short-uuid' import { runInNewContext } from 'node:vm' import * as tryRequire from 'try-require' import { compile } from '../vendor/webpack' -import { ExecuteContext } from './runtime' import { Settings } from './settings' +import { ExecuteContext } from './execute-context' +export type FrontendHook = () => Promise | void export class Commands { public lib: object = {} public state: Map = new Map() - + public handlers: Map = new Map() constructor( private workingDirectory: string, private templateDirectory: string diff --git a/src/main/framework/execute-context.ts b/src/main/framework/execute-context.ts new file mode 100644 index 0000000..811d599 --- /dev/null +++ b/src/main/framework/execute-context.ts @@ -0,0 +1,154 @@ +import { Workspace } from './workspace' +import { Command, ResultContentEvent, ResultStreamEvent, Runtime } from './runtime' +import { WebContents } from 'electron' +import { readFile } from 'fs-extra' +import short from 'short-uuid' +import { ResultStream } from './result-stream' + +export interface RuntimeContentHandle { + id: string + update(html: string): void + on(event: string, handler: RuntimeContentEventCallback): void +} + +export interface RuntimeContentEvent { + event: string +} + +export type RuntimeContentEventCallback = (event: RuntimeContentEvent) => Promise | void + +export class ExecuteContext { + public readonly start: number = Date.now() + public readonly id: string = short.generate() + + public readonly events: ResultContentEvent[] = [] + private readonly eventHandlers = new Map() + + constructor( + public readonly platform: string, + public readonly sender: WebContents, + public readonly workspace: Workspace, + public readonly runtime: Runtime, + public readonly command: Command, + public readonly profile: string, + public readonly history: { + enabled: boolean + results: boolean + max: number + } + ) {} + + out(text: string, error: boolean = false): ResultStream | null { + const isFinished = this.command.aborted || this.command.complete + if (isFinished) { + if (!this.command.result.edit) { + return null + } + } + const streamEntry = new ResultStream(text, error) + + this.command.result.stream.push(streamEntry) + + const streamEvent: ResultStreamEvent = { + entry: streamEntry, + runtime: this.runtime.id, + command: this.command.id + } + + if (!this.sender.isDestroyed()) { + this.sender.send('runtime.commandEvent', streamEvent) + } + + return streamEntry + } + + content(html: string): RuntimeContentHandle { + const contextId = this.id + const runtimeId = this.runtime.id + const commandId = this.command.id + + const command = this.command + + const id = short.generate() + const events = this.events + const container = (html: string): string => `${html}` + + const R = this.out(container(html), false) + const sender = this.sender + const eventHandlers = this.eventHandlers + return { + id, + update(newHTML: string): void { + if (R !== null && !command.aborted && !command.complete) { + R.setText(container(newHTML)) + + sender.send('runtime.commandEvent') + } + }, + on(event: string, callback: RuntimeContentEventCallback): void { + const eventHandlerId = short.generate() + + eventHandlers.set(eventHandlerId, callback) + + events.push({ + event, + commandId, + contextId, + runtimeId, + contentId: id, + handlerId: eventHandlerId + }) + + sender.send('runtime.observe', { + contextId, + contentId: id, + eventHandlerId, + event + }) + + sender.send('runtime.commandEvent') + } + } + } + + async fireEvent(eventName: string, handlerId: string): Promise { + const event = this.eventHandlers.get(handlerId) + if (this.command.aborted || this.command.complete) { + return + } + if (!event) { + return + } + + await event({ + event: eventName + }) + } + + async edit(path: string, callback: (text: string) => void): Promise { + const file = await readFile(path) + + this.command.result.edit = { + path, + modified: false, + content: file.toString(), + callback + } + + this.sender.send('runtime.commandEvent') + } + + finish(code: number): void { + if (this.command.aborted || this.command.complete) { + return + } + + this.command.result.code = code + this.command.complete = true + this.command.error = this.command.result.code !== 0 + + if (this.history.enabled) { + this.workspace.history.append(this.command, this.start, this.profile, this.history.results) + } + } +} diff --git a/src/main/framework/result-stream.ts b/src/main/framework/result-stream.ts new file mode 100644 index 0000000..12b58b2 --- /dev/null +++ b/src/main/framework/result-stream.ts @@ -0,0 +1,18 @@ +import { sanitizeOutput } from '../util' + +export class ResultStream { + public text: string + constructor( + public raw: string, + public error: boolean = false + ) { + this.raw = this.raw.toString() + this.text = sanitizeOutput(raw) + } + + setText(raw: string, error: boolean = this.error): void { + this.raw = raw + this.text = sanitizeOutput(raw) + this.error = error + } +} diff --git a/src/main/framework/runtime-events.ts b/src/main/framework/runtime-events.ts index ee5d3a6..c1594ce 100644 --- a/src/main/framework/runtime-events.ts +++ b/src/main/framework/runtime-events.ts @@ -6,15 +6,13 @@ import { Profile, ProfileMap, Result, - ResultStream, - ResultStreamEvent, + ResultContentEvent, + ResultViewModel, Runtime, RuntimeModel } from './runtime' import short from 'short-uuid' import { execute } from './runtime-executor' -import createDOMPurify from 'dompurify' -import { JSDOM } from 'jsdom' import { DEFAULT_HISTORY_ENABLED, DEFAULT_HISTORY_MAX_ITEMS, @@ -23,13 +21,15 @@ import { DEFAULT_PROFILES, DEFAULT_SETTING_IS_COMMANDER_MODE } from '../../constants' -import Convert from 'ansi-to-html' import { HistoricalExecution } from './history' -import { readFile, writeFile } from 'fs-extra' +import { writeFile } from 'fs-extra' +import { ExecuteContext } from './execute-context' +import { ResultStream } from './result-stream' -const convert = new Convert() -const DOMPurify = createDOMPurify(new JSDOM('').window) export function attach({ app, workspace }: BootstrapContext): void { + const eventListForCommand = (command: Command): ResultContentEvent[] => { + return command?.context?.events || [] + } const runtimeList = (): RuntimeModel[] => { return workspace.runtimes.map((runtime, index) => { const isTarget = index === workspace.runtimeIndex @@ -38,6 +38,7 @@ export function attach({ app, workspace }: BootstrapContext): void { const result = focus ? { ...focus.result, + events: eventListForCommand(focus), edit: focus.result.edit ? { ...focus.result.edit, @@ -47,15 +48,18 @@ export function attach({ app, workspace }: BootstrapContext): void { } : { code: 0, - stream: [] + stream: [], + events: [] } const history: CommandViewModel[] = runtime.history.map((historyItem) => { return { ...historyItem, process: undefined, + context: undefined, result: { ...historyItem.result, + events: eventListForCommand(historyItem), edit: historyItem.result.edit ? { content: historyItem.result.edit.content, @@ -70,22 +74,16 @@ export function attach({ app, workspace }: BootstrapContext): void { return { target: isTarget, result: runtime.resultEdit - ? { + ? ({ code: 0, - stream: [ - { - text: runtime.resultEdit, - raw: runtime.resultEdit, - error: false - } - ] - } + stream: [new ResultStream(runtime.resultEdit)], + events: [] + } as ResultViewModel) : result, ...runtime, history, appearance: { ...runtime.appearance, - title: runtime.appearance.title.replace('$idx', `${index}`) } } @@ -116,6 +114,24 @@ export function attach({ app, workspace }: BootstrapContext): void { } ) + ipcMain.handle( + 'runtime.run-context-event', + async (_, event: ResultContentEvent): Promise => { + const runtime = workspace.runtimes.find((r) => r.id === event.runtimeId) + if (!runtime) { + return false + } + const command = runtime.history.find((c) => c.id === event.commandId) + if (!command) { + return false + } + + await command?.context?.fireEvent(event.event, event.handlerId) + + return true + } + ) + ipcMain.handle( 'runtime.save-edit', async (_, runtimeId: string, commandId: string): Promise => { @@ -161,13 +177,7 @@ export function attach({ app, workspace }: BootstrapContext): void { command.result = { code: command.result.code, - stream: [ - { - text: result, - raw: result, - error: command.error - } - ] + stream: [new ResultStream(result, command.error)] } } @@ -197,6 +207,7 @@ export function attach({ app, workspace }: BootstrapContext): void { if (!runtime) { return false } + const command = runtime.history.find((c) => c.id === commandId) if (!command) { return false @@ -247,16 +258,7 @@ export function attach({ app, workspace }: BootstrapContext): void { stream: !rewind.result ? [] : rewind.result.map((raw) => { - let text = raw.toString() - - text = DOMPurify.sanitize(raw) - text = convert.toHtml(text) - - return { - error: rewind.error, - raw, - text: text - } + return new ResultStream(raw, rewind.error) }) } } @@ -432,109 +434,46 @@ export function attach({ app, workspace }: BootstrapContext): void { profileKey = workspace.settings.get('defaultProfile', DEFAULT_PROFILE) } - const history = { - enabled: workspace.settings.get('history.enabled', DEFAULT_HISTORY_ENABLED), - results: workspace.settings.get('history.saveResult', DEFAULT_HISTORY_SAVE_RESULT), - max: workspace.settings.get('history.maxItems', DEFAULT_HISTORY_MAX_ITEMS) - } - const profiles = workspace.settings.get('profiles', DEFAULT_PROFILES) const profile: Profile = profiles[profileKey] const result: Result = command.result - const start = Date.now() let finalize: boolean = true - const finish = (code: number): void => { - if (command.aborted || command.complete) { - return + const context = new ExecuteContext( + profile.platform, + _.sender, + workspace, + runtimeTarget, + command, + profileKey, + { + enabled: workspace.settings.get('history.enabled', DEFAULT_HISTORY_ENABLED), + results: workspace.settings.get('history.saveResult', DEFAULT_HISTORY_SAVE_RESULT), + max: workspace.settings.get('history.maxItems', DEFAULT_HISTORY_MAX_ITEMS) } + ) - result.code = code - - command.complete = true - command.error = result.code !== 0 - - if (history.enabled) { - workspace.history.append(command, start, profileKey, history.results) - } - } + // attach context to command + command.context = context try { - const out = (text: string, error: boolean = false): void => { - const isFinished = command.aborted || command.complete - if (isFinished) { - if (!command.result.edit) { - return - } - } - const raw = text.toString() - - text = DOMPurify.sanitize(raw) - text = convert.toHtml(text) - - const streamEntry: ResultStream = { - text, - error, - raw - } - - result.stream.push(streamEntry) - - const streamEvent: ResultStreamEvent = { - entry: streamEntry, - runtime: runtime, - command: id - } - - if (!_.sender.isDestroyed()) { - _.sender.send('runtime.commandEvent', streamEvent) - } - } - - if (!profile) { - throw `Profile ${profileKey} does not exist, provided by runtime as = ${runtimeTarget.profile}` - } - - const platform = profile.platform - const finalizeConfirm = await execute({ - platform, - workspace, - runtime: runtimeTarget, - command, - async edit(path: string, callback: (text: string) => void) { - const file = await readFile(path) - - this.command.result.edit = { - path, - modified: false, - content: file.toString(), - callback - } - - _.sender.send('runtime.commandEvent') - }, - out, - finish - }) - if (finalizeConfirm !== undefined && finalizeConfirm === false) { + if ((await execute(context)) === false) { + // do we "finish"? unless told false by the executor we always finish finalize = false } } catch (e) { - result.stream.push({ - error: true, - text: `${e}`, - raw: `${e}` - }) - finish(1) + result.stream.push(new ResultStream(`${e}`, true)) + context.finish(1) } + // the "finish" if (finalize) { command.complete = true command.error = result.code !== 0 - finish(result.code) + context.finish(result.code) } return true diff --git a/src/main/framework/runtime-executor.ts b/src/main/framework/runtime-executor.ts index f4dccb3..cba27ea 100644 --- a/src/main/framework/runtime-executor.ts +++ b/src/main/framework/runtime-executor.ts @@ -1,4 +1,3 @@ -import { ExecuteContext } from './runtime' import { spawn } from 'node:child_process' import Reload from './system-commands/reload' @@ -13,29 +12,27 @@ import Vault from './system-commands/vault' import Workspace from './system-commands/workspace' import Settings from './system-commands/settings' import Edit from './system-commands/edit' +import Reset from './system-commands/reset' +import { ExecuteContext } from './execute-context' const systemCommands: Array<{ command: string alias?: string[] task: (context: ExecuteContext, ...args: string[]) => Promise | void -}> = [Reload, Exit, History, Cd, Tab, Test, Clear, Version, Vault, Workspace, Settings, Edit] +}> = [Reload, Exit, History, Cd, Tab, Test, Clear, Version, Vault, Workspace, Settings, Edit, Reset] export async function execute(context: ExecuteContext): Promise { - const { platform, workspace, runtime, command, out, finish } = context - const [cmd, ...args] = command.prompt.split(' ') + const [cmd, ...args] = context.command.prompt.split(' ') // check for system commands for (const systemCommand of systemCommands) { if (systemCommand.command === cmd || systemCommand?.alias?.includes(cmd)) { - await systemCommand.task(context, ...args) - - finish(0) - return + return systemCommand.task(context, ...args) } } // check for user commands - if (workspace.commands.has(cmd)) { - let result = await Promise.resolve(workspace.commands.run(context, cmd, ...args)) + if (context.workspace.commands.has(cmd)) { + let result = await Promise.resolve(context.workspace.commands.run(context, cmd, ...args)) if (!result) { // nothing was replied with, assume this is a run that will happen in time @@ -46,28 +43,28 @@ export async function execute(context: ExecuteContext): Promise result = JSON.stringify(result, null, 2) } - out(`${result}`) + context.out(`${result}`) return } // finally send to the system - const [platformProgram, ...platformProgramArgs] = platform.split(' ') + const [platformProgram, ...platformProgramArgs] = context.platform.split(' ') const argsClean = platformProgramArgs.map( - (arg: string) => `${arg.replace('$ARGS', command.prompt)}` + (arg: string) => `${arg.replace('$ARGS', context.command.prompt)}` ) const childSpawn = spawn(platformProgram, argsClean, { - cwd: runtime.folder, + cwd: context.runtime.folder, env: process.env }) - childSpawn.stdout.on('data', (data) => out(data)) - childSpawn.stderr.on('data', (data) => out(data, true)) + childSpawn.stdout.on('data', (data) => context.out(data)) + childSpawn.stderr.on('data', (data) => context.out(data, true)) return new Promise((resolve) => { childSpawn.on('exit', (code) => { - finish(code || 0) + context.finish(code || 0) resolve() }) diff --git a/src/main/framework/runtime.ts b/src/main/framework/runtime.ts index dece5af..8a9170e 100644 --- a/src/main/framework/runtime.ts +++ b/src/main/framework/runtime.ts @@ -1,24 +1,29 @@ -import { resolveFolderPathForMTERM, Workspace } from './workspace' +import { resolveFolderPathForMTERM } from './workspace' import short from 'short-uuid' import { ChildProcessWithoutNullStreams } from 'node:child_process' import { resolve } from 'path' - -export interface ResultStream { - error: boolean - text: string - raw: string -} +import { ResultStream } from './result-stream' +import { ExecuteContext } from './execute-context' export interface Result { code: number stream: ResultStream[] edit?: EditFile } +export interface ResultContentEvent { + event: string + runtimeId: string + commandId: string + contextId: string + contentId: string + handlerId: string +} export interface ResultViewModel { code: number stream: ResultStream[] edit?: EditFileViewModel + events: ResultContentEvent[] } export interface ResultStreamEvent { @@ -45,6 +50,7 @@ export interface Command { error: boolean id: string process?: ChildProcessWithoutNullStreams + context?: ExecuteContext } export interface CommandViewModel { @@ -109,13 +115,3 @@ export class Runtime { return location } } - -export interface ExecuteContext { - platform: string - workspace: Workspace - runtime: Runtime - command: Command - edit: (path: string, callback: (content: string) => void) => Promise - out: (text: string, error?: boolean) => void - finish: (code: number) => void -} diff --git a/src/main/framework/system-commands/cd.ts b/src/main/framework/system-commands/cd.ts index fb3be15..fcca304 100644 --- a/src/main/framework/system-commands/cd.ts +++ b/src/main/framework/system-commands/cd.ts @@ -1,5 +1,5 @@ -import { ExecuteContext } from '../runtime' import { pathExists, stat } from 'fs-extra' +import { ExecuteContext } from '../execute-context' export default { command: 'cd', diff --git a/src/main/framework/system-commands/clear.ts b/src/main/framework/system-commands/clear.ts index 311cbbd..07a6993 100644 --- a/src/main/framework/system-commands/clear.ts +++ b/src/main/framework/system-commands/clear.ts @@ -1,4 +1,4 @@ -import { ExecuteContext } from '../runtime' +import { ExecuteContext } from '../execute-context' export default { command: 'clear', diff --git a/src/main/framework/system-commands/edit.ts b/src/main/framework/system-commands/edit.ts index b67bd1c..b839b58 100644 --- a/src/main/framework/system-commands/edit.ts +++ b/src/main/framework/system-commands/edit.ts @@ -1,7 +1,7 @@ -import { ExecuteContext } from '../runtime' import { pathExists, stat } from 'fs-extra' +import { ExecuteContext } from '../execute-context' -async function exit(context: ExecuteContext): Promise { +async function edit(context: ExecuteContext): Promise { const [, ...args] = context.command.prompt.split(' ') const path: string = context.runtime.resolve(args[0] || '.') @@ -27,5 +27,5 @@ async function exit(context: ExecuteContext): Promise { export default { command: ':edit', alias: ['edit'], - task: exit + task: edit } diff --git a/src/main/framework/system-commands/exit.ts b/src/main/framework/system-commands/exit.ts index 5977dab..8006811 100644 --- a/src/main/framework/system-commands/exit.ts +++ b/src/main/framework/system-commands/exit.ts @@ -1,5 +1,5 @@ -import { ExecuteContext } from '../runtime' import { app } from 'electron' +import { ExecuteContext } from '../execute-context' async function exit(context: ExecuteContext): Promise { if (!context.workspace.removeRuntime(context.runtime)) { diff --git a/src/main/framework/system-commands/history.ts b/src/main/framework/system-commands/history.ts index 0943070..10179bb 100644 --- a/src/main/framework/system-commands/history.ts +++ b/src/main/framework/system-commands/history.ts @@ -1,4 +1,4 @@ -import { ExecuteContext } from '../runtime' +import { ExecuteContext } from '../execute-context' export default { command: ':history', diff --git a/src/main/framework/system-commands/reload.ts b/src/main/framework/system-commands/reload.ts index 4d3a816..8cdf762 100644 --- a/src/main/framework/system-commands/reload.ts +++ b/src/main/framework/system-commands/reload.ts @@ -1,5 +1,5 @@ -import { ExecuteContext } from '../runtime' import { RunnerWindow } from '../../window/windows/runner' +import { ExecuteContext } from '../execute-context' async function reload(context: ExecuteContext): Promise { await context.workspace.load() diff --git a/src/main/framework/system-commands/reset.ts b/src/main/framework/system-commands/reset.ts new file mode 100644 index 0000000..5835d96 --- /dev/null +++ b/src/main/framework/system-commands/reset.ts @@ -0,0 +1,44 @@ +import { ExecuteContext } from '../execute-context' +import { remove } from 'fs-extra' +import { RunnerWindow } from '../../window/windows/runner' +async function reset(context: ExecuteContext): Promise { + context.out('Reset all mterm settings, commands, modules?\n') + + const yes = context.content(``) + + context.out('|') + + const no = context.content(``) + + no.on('click', () => { + context.out('\n\nDeclined reset') + context.finish(0) + }) + + yes.on('click', async () => { + context.out('\n\nCleaning...') + + await remove(context.workspace.folder) + + context.out('\nReloading...') + + await context.workspace.load() + context.out('- settings reloaded \n') + + await context.workspace.commands.load(context.workspace.settings) + context.out('- commands reloaded \n') + + await context.workspace.reload(RunnerWindow) + context.out('- window reloaded \n') + + context.finish(0) + }) + + return false +} + +export default { + command: ':reset', + alias: ['reset'], + task: reset +} diff --git a/src/main/framework/system-commands/settings.ts b/src/main/framework/system-commands/settings.ts index c36a2e1..7a5de69 100644 --- a/src/main/framework/system-commands/settings.ts +++ b/src/main/framework/system-commands/settings.ts @@ -1,7 +1,7 @@ -import { ExecuteContext } from '../runtime' import { RunnerWindow } from '../../window/windows/runner' import { shell } from 'electron' import { parseInt } from 'lodash' +import { ExecuteContext } from '../execute-context' export default { command: ':settings', diff --git a/src/main/framework/system-commands/tab.ts b/src/main/framework/system-commands/tab.ts index b4c384e..1df54cb 100644 --- a/src/main/framework/system-commands/tab.ts +++ b/src/main/framework/system-commands/tab.ts @@ -1,4 +1,4 @@ -import { ExecuteContext } from '../runtime' +import { ExecuteContext } from '../execute-context' async function tab(context: ExecuteContext): Promise { context.workspace.addRuntime() diff --git a/src/main/framework/system-commands/test.ts b/src/main/framework/system-commands/test.ts index da6f87a..92fb45b 100644 --- a/src/main/framework/system-commands/test.ts +++ b/src/main/framework/system-commands/test.ts @@ -1,4 +1,5 @@ -import { ExecuteContext } from '../runtime' +import { ExecuteContext } from '../execute-context' + export default { command: ':test', async task(context: ExecuteContext): Promise { diff --git a/src/main/framework/system-commands/vault.ts b/src/main/framework/system-commands/vault.ts index 97e77fe..d8d8978 100644 --- a/src/main/framework/system-commands/vault.ts +++ b/src/main/framework/system-commands/vault.ts @@ -1,5 +1,5 @@ -import { ExecuteContext } from '../runtime' import { PlatformWindow } from '../../window/windows/platform' +import { ExecuteContext } from '../execute-context' async function vault(context: ExecuteContext): Promise { await context.workspace.showAndHideOthers(PlatformWindow, 'store') diff --git a/src/main/framework/system-commands/version.ts b/src/main/framework/system-commands/version.ts index bfb0ed3..cc9b434 100644 --- a/src/main/framework/system-commands/version.ts +++ b/src/main/framework/system-commands/version.ts @@ -1,5 +1,5 @@ -import { ExecuteContext } from '../runtime' import { app } from 'electron' +import { ExecuteContext } from '../execute-context' export default { command: ':version', diff --git a/src/main/framework/system-commands/workspace.ts b/src/main/framework/system-commands/workspace.ts index 05e5c39..3dd97aa 100644 --- a/src/main/framework/system-commands/workspace.ts +++ b/src/main/framework/system-commands/workspace.ts @@ -1,5 +1,5 @@ -import { ExecuteContext } from '../runtime' import { shell } from 'electron' +import { ExecuteContext } from '../execute-context' export default { command: ':workspace', diff --git a/src/main/util.ts b/src/main/util.ts index de73bb4..3c713e8 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -1,7 +1,12 @@ import { toNumber } from 'lodash' import { RunnerBounds } from './model' import { BrowserWindow, Display, screen } from 'electron' +import createDOMPurify from 'dompurify' +import { JSDOM } from 'jsdom' +import Convert from 'ansi-to-html' +const convert = new Convert() +const DOMPurify = createDOMPurify(new JSDOM('').window) export function getBoundaryValue( value: string | number, containerLength: number, @@ -70,3 +75,10 @@ export function setWindowValueFromPath(window: BrowserWindow, prop: string, valu break } } + +export function sanitizeOutput(text: string | Buffer): string { + text = DOMPurify.sanitize(text.toString()) + text = convert.toHtml(text) + + return text +} diff --git a/src/renderer/src/runner/runner.tsx b/src/renderer/src/runner/runner.tsx index f194a87..676e2ac 100644 --- a/src/renderer/src/runner/runner.tsx +++ b/src/renderer/src/runner/runner.tsx @@ -201,38 +201,51 @@ export default function Runner(): ReactElement { } } - // useEffect(() => { - // inputRef.current?.focus() - // - // if (Object.values(pendingTitles).filter((title) => title !== null).length > 0) { - // //editing session in progress - // return - // } - // - // // Conditionally handle keydown of letter or arrow to refocus input - // const handleGlobalKeyDown = (event): void => { - // // if pending a title? ignore this key event: user is probably editing the window title - // // and does not care for input - // - // if ( - // /^[a-zA-Z]$/.test(event.key) || - // event.key === 'ArrowUp' || - // event.key === 'ArrowDown' || - // (!event.shiftKey && !event.ctrlKey && !event.altKey) - // ) { - // if (document.activeElement !== inputRef.current) { - // inputRef.current?.focus() - // } - // handleKeyDown(event) - // } - // } - // - // document.addEventListener('keydown', handleGlobalKeyDown) - // - // return () => { - // document.removeEventListener('keydown', handleGlobalKeyDown) - // } - // }, []) + useEffect(() => { + const runtime = runtimeList.find((r) => r.target) + if (!runtime) { + return + } + const eventList = + (historyIndex === -1 + ? runtime.result.events + : runtime.history[historyIndex]?.result?.events) || [] + + const eventMap = eventList.map((o) => o.event) + const eventMapSet = new Set() + + for (const event of eventMap) { + eventMapSet.add(event) + } + + const checkAndSendEventForContent = (event): void => { + const element = event.target + + const matchingEvents = eventList.filter( + (backendEvent) => + [element.parentElement.id, element.id].includes(backendEvent.contentId) && + event.type === backendEvent.event + ) + + matchingEvents.forEach((eventToFire) => { + window.electron.ipcRenderer.invoke('runtime.run-context-event', eventToFire).then(() => { + return reloadRuntimesFromBackend() + }) + }) + } + + const eventListName = Array.from(eventMapSet.values()) + + eventListName.forEach((eventName) => + document.addEventListener(eventName, checkAndSendEventForContent) + ) + + return () => { + eventListName.forEach((eventName) => + document.removeEventListener(eventName, checkAndSendEventForContent) + ) + } + }, [historyIndex, runtimeList]) useEffect(() => { reloadRuntimesFromBackend().catch((error) => console.error(error)) diff --git a/src/renderer/src/runner/runtime.ts b/src/renderer/src/runner/runtime.ts index fed1cf6..e86666a 100644 --- a/src/renderer/src/runner/runtime.ts +++ b/src/renderer/src/runner/runtime.ts @@ -7,9 +7,19 @@ export interface ResultStream { export interface Result { code: number stream: ResultStream[] + events: ResultContentEvent[] edit?: EditFile } +export interface ResultContentEvent { + event: string + + runtimeId: string + commandId: string + contextId: string + contentId: string + handlerId: string +} export interface ResultStreamEvent { runtime: string command: string