Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolves #123, resolves #119 - extension pattern 🕺 💯 #127

Merged
merged 4 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
@@ -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)

30 changes: 30 additions & 0 deletions src/main/framework/execute-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,6 +64,34 @@ export class ExecuteContext {
return context
}

async runTask(env, folder: string, spawnTask: string, hide: boolean = true): Promise<void> {
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<string> {
return await process(this, text)
}
Expand Down
97 changes: 97 additions & 0 deletions src/main/framework/extensions.ts
Original file line number Diff line number Diff line change
@@ -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<ExtensionHook, Array<ExtensionHookResolution>> = new Map<
ExtensionHook,
Array<ExtensionHookResolution>
>()
public extensionList: string[] = []
constructor(private workspace: Workspace) {}

async run(hook: ExtensionHook): Promise<string> {
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<void> {
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<void> {
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')
}
}
5 changes: 4 additions & 1 deletion src/main/framework/runtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] => {
Expand Down Expand Up @@ -204,7 +205,9 @@ export function attach({ app, workspace }: BootstrapContext): void {
})

ipcMain.handle('runner.theme', async (_, profile): Promise<string> => {
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<boolean> => {
Expand Down
4 changes: 3 additions & 1 deletion src/main/framework/runtime-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,7 +41,8 @@ export const systemCommands: Array<SystemCommand> = [
Reset,
Commands,
Restart,
Theme
Theme,
Ext
]
export async function execute(context: ExecuteContext): Promise<void | boolean> {
// check for system commands
Expand Down
64 changes: 64 additions & 0 deletions src/main/framework/system-commands/ext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ExecuteContext } from '../execute-context'

async function ext(context: ExecuteContext, task?: string): Promise<void> {
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
}
4 changes: 4 additions & 0 deletions src/main/framework/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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[] = []
Expand All @@ -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)
}

Expand Down Expand Up @@ -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
Expand Down
Loading