From 22f85915797945159c661723c0935d5090e84073 Mon Sep 17 00:00:00 2001 From: David Rehmat Date: Mon, 27 May 2024 14:55:06 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20resolves=20#123,=20resolves=20#119=20-?= =?UTF-8?q?=20extension=20pattern=20=F0=9F=95=BA=20=F0=9F=92=AF=20=20(#127?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: extension pattern * chore: RUNNER_THEME_CSS extension hook * chore: ext add and ext rm * chore: some doc on extenstions --- README.md | 2 + docs/extensions.md | 17 ++++ src/main/framework/execute-context.ts | 30 +++++++ src/main/framework/extensions.ts | 97 +++++++++++++++++++++++ src/main/framework/runtime-events.ts | 5 +- src/main/framework/runtime-executor.ts | 4 +- src/main/framework/system-commands/ext.ts | 64 +++++++++++++++ src/main/framework/workspace.ts | 4 + 8 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 docs/extensions.md create mode 100644 src/main/framework/extensions.ts create mode 100644 src/main/framework/system-commands/ext.ts diff --git a/README.md b/README.md index bc36e56..303e56a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Head over to the [release page](https://github.com/mterm-io/mterm/releases/lates mterm is customizable in a few ways - - add your own [commands](#commands) to the terminal - quick add a command with `:cmd command_name` and edit this +- add extensions to the terminal + - quick add an extension with `ext add mterm-red`, see [extensions](./docs/extensions.md) for more info - change the [theme](#theme) of the terminal - quick change the theme with `:theme` or `:css` - change the [settings](#settings) of the terminal diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 0000000..8b734cf --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,17 @@ +## Known Extensions + +> Please make a pull request to add your extension to this list + +- [mterm-ext-red](#mterm-ext-red) +- more... + + +### mterm-ext-red + +```bash +ext add mterm-red +``` +Make your terminal (mterm) red 10 seconds - + +![red terminal](https://github.com/mterm-io/mterm-ext-red/blob/HEAD/info.png?raw=true) + diff --git a/src/main/framework/execute-context.ts b/src/main/framework/execute-context.ts index be35149..ff2ba31 100644 --- a/src/main/framework/execute-context.ts +++ b/src/main/framework/execute-context.ts @@ -4,7 +4,9 @@ import { WebContents } from 'electron' import { readFile } from 'fs-extra' import short from 'short-uuid' import { ResultStream } from './result-stream' + import { process } from './transformers' +import { spawn } from 'node:child_process' export interface RuntimeContentHandle { id: string update(html: string): void @@ -62,6 +64,34 @@ export class ExecuteContext { return context } + async runTask(env, folder: string, spawnTask: string, hide: boolean = true): Promise { + const [platformProgram, ...platformProgramArgs] = this.platform.split(' ') + + const argsClean = platformProgramArgs.map((arg: string) => `${arg.replace('$ARGS', spawnTask)}`) + + const childSpawn = spawn(platformProgram, argsClean, { + cwd: folder, + env + }) + + childSpawn.stdout.on('data', (data) => { + if (!hide) { + this.out(data.toString()) + } + }) + childSpawn.stderr.on('data', (data) => this.out(data, true)) + + return new Promise((resolve, reject) => { + childSpawn.on('exit', (code) => { + if (code !== 0) { + reject() + } else { + resolve() + } + }) + }) + } + async resolve(text: string): Promise { return await process(this, text) } diff --git a/src/main/framework/extensions.ts b/src/main/framework/extensions.ts new file mode 100644 index 0000000..22c7a6e --- /dev/null +++ b/src/main/framework/extensions.ts @@ -0,0 +1,97 @@ +import { Workspace } from './workspace' +import { join } from 'path' +import { pathExists, readJson } from 'fs-extra' +import { log } from '../logger' + +export enum ExtensionHook { + RUNNER_THEME_CSS = 'RUNNER_THEME_CSS' +} + +export type ExtensionHookCallback = (workspace?: Workspace) => string +export type ExtensionHookResolution = string | ExtensionHookCallback +export class Extensions { + public extensionHooks: Map> = new Map< + ExtensionHook, + Array + >() + public extensionList: string[] = [] + constructor(private workspace: Workspace) {} + + async run(hook: ExtensionHook): Promise { + const resolutions = this.extensionHooks.get(hook) || [] + + let result = '' + for (const resolution of resolutions) { + if (typeof resolution === 'string') { + result += resolution + } else { + result += resolution(this.workspace) + } + } + + return result + } + + async load(): Promise { + this.extensionList = [] + this.extensionHooks.clear() + + const start = Date.now() + const packageJson = join(this.workspace.folder, 'package.json') + const isPackageJsonExists = await pathExists(packageJson) + if (!isPackageJsonExists) { + log('No package.json found in workspace') + return + } + const packageJsonData = await readJson(packageJson) + const packages: string[] = [] + + if (packageJsonData.dependencies) { + packages.push(...Object.keys(packageJsonData.dependencies)) + } + if (packageJsonData.devDependencies) { + packages.push(...Object.keys(packageJsonData.devDependencies)) + } + + const folder = this.workspace.folder + const ext = this.extensionHooks + const list = this.extensionList + async function scan(packageName: string): Promise { + log(`Scanning package: ${packageName}..`) + + const mtermExtPath = join(folder, 'node_modules', packageName, 'mterm.js') + + const isMtermExtensionExists = await pathExists(mtermExtPath) + if (!isMtermExtensionExists) { + return + } + + log(`Loading package: ${packageName}`) + const mtermExt = require(mtermExtPath) + + const hooks = Object.keys(ExtensionHook) + + log(`Mapping hooks for ${packageName} = ${hooks}`) + + list.push(packageName) + + for (const hook of hooks) { + const hookKey = hook as ExtensionHook + let resolutions: ExtensionHookResolution[] = [] + if (ext.has(ExtensionHook[hookKey])) { + resolutions = ext.get(ExtensionHook[hookKey]) as ExtensionHookResolution[] + } + + resolutions.push(mtermExt[hookKey]) + + ext.set(hookKey, resolutions) + } + } + + const promiseList = packages.map(scan) + + await Promise.all(promiseList) + + log('Extensions loaded in ' + (Date.now() - start) + 'ms') + } +} diff --git a/src/main/framework/runtime-events.ts b/src/main/framework/runtime-events.ts index 2167c0c..d42186f 100644 --- a/src/main/framework/runtime-events.ts +++ b/src/main/framework/runtime-events.ts @@ -26,6 +26,7 @@ import { writeFile } from 'fs-extra' import { ExecuteContext } from './execute-context' import { ResultStream } from './result-stream' import { transform } from './transformers' +import { ExtensionHook } from './extensions' export function attach({ app, workspace }: BootstrapContext): void { const eventListForCommand = (command: Command): ResultContentEvent[] => { @@ -204,7 +205,9 @@ export function attach({ app, workspace }: BootstrapContext): void { }) ipcMain.handle('runner.theme', async (_, profile): Promise => { - return workspace.theme.get(profile) + const theme = workspace.theme.get(profile) + const extensionTheme = await workspace.extensions.run(ExtensionHook.RUNNER_THEME_CSS) + return `${extensionTheme}${theme}` }) ipcMain.handle('runtime.kill', async (_, commandId, runtimeId): Promise => { diff --git a/src/main/framework/runtime-executor.ts b/src/main/framework/runtime-executor.ts index 41ae0bf..b3c250a 100644 --- a/src/main/framework/runtime-executor.ts +++ b/src/main/framework/runtime-executor.ts @@ -18,6 +18,7 @@ import Reset from './system-commands/reset' import Commands from './system-commands/commands' import Restart from './system-commands/restart' import Theme from './system-commands/theme' +import Ext from './system-commands/ext' export interface SystemCommand { command: string @@ -40,7 +41,8 @@ export const systemCommands: Array = [ Reset, Commands, Restart, - Theme + Theme, + Ext ] export async function execute(context: ExecuteContext): Promise { // check for system commands diff --git a/src/main/framework/system-commands/ext.ts b/src/main/framework/system-commands/ext.ts new file mode 100644 index 0000000..ec6ed09 --- /dev/null +++ b/src/main/framework/system-commands/ext.ts @@ -0,0 +1,64 @@ +import { ExecuteContext } from '../execute-context' + +async function ext(context: ExecuteContext, task?: string): Promise { + if (!task) { + context.out( + `ext list: +${context.workspace.extensions.extensionList.length > 0 ? '-' : ''}` + + context.workspace.extensions.extensionList.join('\n-') + ) + context.finish(0) + return + } + if (task === 'load') { + await context.workspace.extensions.load() + context.out('Extensions loaded\n') + context.finish(0) + return + } + + if (task === 'add') { + const extName = (context.prompt.args[1] || '').trim() + if (!extName) { + context.out('No extension name provided\n\n:ext add EXT_NAME', true) + context.finish(1) + return + } + + context.out(`Installing ${extName}..\n`) + + await context.runTask(process.env, context.workspace.folder, `npm install ${extName}`) + + await context.workspace.extensions.load() + + context.out('Done\n') + } else if (task === 'remove' || task === 'rm' || task === 'delete') { + const extName = (context.prompt.args[1] || '').trim() + if (!extName) { + context.out('No extension name provided\n\n:ext rm EXT_NAME', true) + context.finish(1) + return + } + + context.out(`Removing ${extName}..\n`) + + await context.runTask(process.env, context.workspace.folder, `npm rm ${extName}`) + + await context.workspace.extensions.load() + + context.out('Done\n') + } else { + context.out( + 'Unknown command\n\nTry :ext {add,remove} EXT_NAME or :ext to list current extensions', + true + ) + context.finish(1) + return + } +} + +export default { + command: ':ext', + alias: [':extension', 'ext', 'extension'], + task: ext +} diff --git a/src/main/framework/workspace.ts b/src/main/framework/workspace.ts index c89e62e..607c819 100644 --- a/src/main/framework/workspace.ts +++ b/src/main/framework/workspace.ts @@ -13,6 +13,7 @@ import { Store } from './store' import { History } from './history' import { Theme } from './theme' import { Autocomplete } from './autocomplete' +import { Extensions } from './extensions' export function resolveFolderPathForMTERM(folder: string): string { folder = folder.replace('~', homedir()) @@ -28,6 +29,7 @@ export class Workspace { public settings: Settings public commands: Commands public autocomplete: Autocomplete + public extensions: Extensions public theme: Theme public isAppQuiting: boolean = false public windows: MTermWindow[] = [] @@ -47,6 +49,7 @@ export class Workspace { this.history = new History(join(this.folder, '.history')) this.store = new Store(join(this.folder, '.mterm-store')) this.theme = new Theme(this, join(app.getAppPath(), './resources/theme.css')) + this.extensions = new Extensions(this) this.autocomplete = new Autocomplete(this) } @@ -95,6 +98,7 @@ export class Workspace { await this.settings.load() await this.theme.load() + await this.extensions.load() // we ignore the result of this (catch error ofc) // let this run in the background