Skip to content

Commit

Permalink
fix: ext patterns improvement
Browse files Browse the repository at this point in the history
* chore: ext smarter hook resolution

* chore: abstract ext hooks to a container object so package name is found

* chore: ext to have custom commands

* chore: better split args for commands

* chore: make split args reject the outer qoutes

* chore: handle undefined hooks

* chore: add google search ext to docs, relates to 126

* chore: add more context for ext

* chore: improve the ext docs

* chore: apply split args to newline nomalize

* chore: make AC disabled on multi line

* chore: add template to docs

* fix: set results col width in settings, solves #98

* add new default setting to constants test

---------

Co-authored-by: Daniel Hoffens <[email protected]>
Co-authored-by: Daniel Hoffens <[email protected]>
  • Loading branch information
3 people committed Jul 7, 2024
1 parent f54c0db commit 05057b5
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 59 deletions.
46 changes: 25 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ mterm is customizable in a few ways -
- 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
- make your own extensions, see [mterm-ext-template](https://github.com/mterm-io/mterm-ext-template) for a starting point
- change the [theme](#theme) of the terminal
- quick change the theme with `:theme` or `:css`
- change the [settings](#settings) of the terminal
- quick change the settings with `:settings edit`
- change the [profile](#configure) of the terminal to a desired interpreter
- quick change the profile with `:settings set defaultProfile wsl` (change wsl with the desired profile)


https://github.com/mterm-io/mterm/assets/7341502/c920853f-1f27-4ef9-ae72-945f1663e36d

Expand Down Expand Up @@ -160,26 +161,29 @@ here is an example `~/mterm/settings.json` -

mterm provided a few system commands to help control the terminal and settings. mterm settings will always start with `:` (a colon) unless the intention is to override a system command. for example, because `clear` needs to be handled in a special way for mterm windows + tabs, it is overriden in mterm.

| Command | Alias | Purpose |
|------------------------------|-------------|----------------------------------------------------------------------------------------|
| `clear` | `cls` | Clear the current terminal output |
| `cd` | | Navigate the file tree on the host machine |
| `:exit` | `exit` | Exit the current tab, or mterm if only 1 tab is open |
| `:edit` | `edit` | Open the in-terminal editor with the file provided. Hit `Ctrl+S` to save in the editor |
| `:history` | | Print out terminal history for debugging in a JSON format |
| `:reload` | | Reload settings, the ui, and commands without restarting |
| `:tab` | | Open a new tab |
| `:test` | | Sample command that executes after 10 seconds. Helpful for debugging |
| `:vault` | | Open the secret management tool, the mterm vault |
| `:version` | `:v` | Print the current mterm version |
| `:workspace` | | Open the mterm workspace folder on disk: `~/mterm` |
| `:theme` | `:css` | Edit the terminal theme real time |
| `:cmd` | `:commands` | Edit the command file |
| `:cmd {cmd_name}` | | Edit the command file for the cmd_name, creates if this doesn't exist |
| `:settings` | | Open the mterm settings gui to manage `~/mterm/settings.json` |
| `:settings edit` | | Open the `~/mterm/settings.json` in the terminal editor with hot reloading |
| `:settings reload` | | Reload `~/mterm/settings.json` and all ui etc associated with the settings |
| `:settings {get\|set} {key}` | | Set the setting key matching the path in `~/mterm/settings.json` and reload |
| Command | Alias | Purpose |
|------------------------------|--------------|----------------------------------------------------------------------------------------|
| `clear` | `cls` | Clear the current terminal output |
| `cd` | | Navigate the file tree on the host machine |
| `:exit` | `exit` | Exit the current tab, or mterm if only 1 tab is open |
| `:edit` | `edit` | Open the in-terminal editor with the file provided. Hit `Ctrl+S` to save in the editor |
| `:history` | | Print out terminal history for debugging in a JSON format |
| `:reload` | | Reload settings, the ui, and commands without restarting |
| `:tab` | | Open a new tab |
| `:test` | | Sample command that executes after 10 seconds. Helpful for debugging |
| `:vault` | | Open the secret management tool, the mterm vault |
| `:version` | `:v` | Print the current mterm version |
| `:workspace` | | Open the mterm workspace folder on disk: `~/mterm` |
| `:theme` | `:css` | Edit the terminal theme real time |
| `:cmd` | `:commands ` | Edit the command file |
| `:ext` | `ext` | List terminal extensions | Edit the command file |
| `:ext add {extenstion}` | | Add the extension, see [extension](./docs/extensions.md) | Edit the command file |
| `:ext rm {extenstion}` | | Remove the extension, see [extension](./docs/extensions.md) | Edit the command file |
| `:cmd {cmd_name}` | | Edit the command file for the cmd_name, creates if this doesn't exist |
| `:settings` | | Open the mterm settings gui to manage `~/mterm/settings.json` |
| `:settings edit` | | Open the `~/mterm/settings.json` in the terminal editor with hot reloading |
| `:settings reload` | | Reload `~/mterm/settings.json` and all ui etc associated with the settings |
| `:settings {get\|set} {key}` | | Set the setting key matching the path in `~/mterm/settings.json` and reload |

### Commands

Expand Down
26 changes: 23 additions & 3 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
> Please make a pull request to add your extension to this list
- [mterm-ext-red](#mterm-ext-red)
- more...

- poof, red terminal
- [mterm-ext-google](#mterm-ext-google)
- open the google search from the terminal
- want to make your own? check out the [mterm-ext-template](https://github.com/mterm-io/mterm-ext-template) and make a pull request to add it to this list
- note ANY extenstion that is published to npmjs.org is already available to be installed with `ext add <extension-name>`

### mterm-ext-red

Expand All @@ -14,5 +17,22 @@ 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)
https://github.com/mterm-io/mterm/assets/7341502/c920853f-1f27-4ef9-ae72-945f1663e36d

### mterm-ext-google

> source: https://github.com/mterm-io/mterm-ext-google
```bash
ext add mterm-google
```

```bash
google "search term"
google "search \" term"
google word1
```

Search away -

https://github.com/mterm-io/mterm/assets/7341502/f49c4133-e54a-4235-9cbf-e157490e9d97

64 changes: 63 additions & 1 deletion src/main/framework/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { ExecuteContext } from './execute-context'
import { CommandUtils } from './command-utils'
import { shell } from 'electron'
import { snakeCase } from 'lodash'

export class Commands {
public libManual: Map<string, boolean> = new Map<string, boolean>()
public lib: object = {}
public commandFileLocation: string = ''
public state: Map<string, object> = new Map<string, object>()
Expand Down Expand Up @@ -58,6 +60,7 @@ export class Commands {
if (!context.workspace.store.unlocked) {
throw 'Vault is locked, unlock before using secrets. Open with :vault'
}

return context.workspace.store.get(key, orElse)
}
}
Expand Down Expand Up @@ -117,7 +120,11 @@ export class Commands {

const jsFile: Buffer = await readFile(join(temp, 'commands.js'))

this.lib = {}
Object.keys(this.lib).forEach((key) => {
if (!this.libManual.get(key)) {
delete this.lib[key]
}
})

runInNewContext(`${jsFile}`, this)

Expand Down Expand Up @@ -174,4 +181,59 @@ export class Commands {

await writeFile(this.commandFileLocation, script)
}

add(command: string, exec: () => void): void {
const nameNormalized = Commands.toCommandName(command)

this.lib[nameNormalized] = exec

this.libManual.set(nameNormalized, true)
}

delete(command: string): void {
const nameNormalized = Commands.toCommandName(command)

delete this.lib[nameNormalized]

this.libManual.delete(nameNormalized)
this.state.delete(nameNormalized)
}
}

/**
* Splits a string into an array of substrings based on spaces, preserving spaces within quoted strings.
* Supports both single and double quotes, and handles escaped quotes within quoted text.
* The outer quotes, single or double, are removed from the returned substrings.
*
* @param {string} input - The input string to be split into arguments.
* @returns {string[]} An array of substrings representing the split arguments.
*
* @example
* const input = 'echo "Hello, world!" | grep "Hello \\"Hello"" \'Single Quoted\' \'Escaped\\\'Single\' "Double \\"Quoted\\""';
* const result = splitArgs(input);
* console.log(result);
* // Output: ["echo", "Hello, world!", "|", "grep", "Hello "Hello"", "Single Quoted", "Escaped'Single", "Double "Quoted""]
*
* @example
* const input = 'google "hello \\" world"';
* const result = splitArgs(input);
* console.log(result);
* // Output: ["google", "hello " world"]
*/
export function splitArgs(input: string): string[] {
const regex = /[^\s'"]+|'([^'\\]*(?:\\.[^'\\]*)*)'|"([^"\\]*(?:\\.[^"\\]*)*)"/gi
const matches: string[] = []
let match: RegExpExecArray | null

while ((match = regex.exec(input)) !== null) {
if (match[1] !== undefined) {
matches.push(match[1].replace(/\\(.)/g, '$1'))
} else if (match[2] !== undefined) {
matches.push(match[2].replace(/\\(.)/g, '$1'))
} else {
matches.push(match[0])
}
}

return matches
}
3 changes: 2 additions & 1 deletion src/main/framework/execute-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class ExecuteContext {
const R = this.out(container(html), false)
const sender = this.sender
const eventHandlers = this.eventHandlers

return {
id,
update(newHTML: string): void {
Expand Down Expand Up @@ -195,7 +196,7 @@ export class ExecuteContext {
this.sender.send('runtime.commandEvent')
}

finish(code: number): void {
finish(code: number = 0): void {
if (this.command.aborted || this.command.complete) {
return
}
Expand Down
117 changes: 99 additions & 18 deletions src/main/framework/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,88 @@ import { pathExists, readJson } from 'fs-extra'
import { log } from '../logger'

export enum ExtensionHook {
RUNNER_THEME_CSS = 'RUNNER_THEME_CSS'
RUNNER_THEME_CSS = 'RUNNER_THEME_CSS',
EXT_POST_INSTALL = 'EXT_POST_INSTALL',
COMMANDS = 'COMMANDS'
}

export type ExtensionHookCallback = (workspace?: Workspace) => string
export type ExtensionHookResolution = string | ExtensionHookCallback
export type ExtensionHookCallback<T> = (...args) => T
export type ExtensionHookResolution<T> = string | ExtensionHookCallback<T>

export interface ExtensionHookResolutionContainer<T> {
packageName: string
resolution: ExtensionHookResolution<T>
hook: ExtensionHook
}

export interface ExtensionCommandContainer {
packageName: string
command: string
}
export class Extensions {
public extensionHooks: Map<ExtensionHook, Array<ExtensionHookResolution>> = new Map<
ExtensionHook,
Array<ExtensionHookResolution>
>()
public extensionHooks: Map<ExtensionHook, Array<ExtensionHookResolutionContainer<object>>> =
new Map<ExtensionHook, Array<ExtensionHookResolutionContainer<object>>>()
public extensionList: string[] = []
public extensionCommands: ExtensionCommandContainer[] = []
constructor(private workspace: Workspace) {}

async run(hook: ExtensionHook): Promise<string> {
async run<T>(hook: ExtensionHook, ifNullThen: T, ...args): Promise<T | undefined> {
const resolutions = this.extensionHooks.get(hook) || []

let result = ''
for (const resolution of resolutions) {
let stringResult = ''
let result: T | undefined = undefined
for (const container of resolutions) {
const resolution = container.resolution

if (resolution === undefined) {
continue
}

if (typeof resolution === 'string') {
result += resolution
stringResult = stringResult || ''
stringResult += resolution

result = stringResult as unknown as T
} else {
result += resolution(this.workspace)
const maybeResult = resolution(...args) as T

if (typeof maybeResult === 'string') {
stringResult = stringResult || ''
stringResult += resolution

result = stringResult as unknown as T
}
}
}

return result
return result || ifNullThen
}

async execute(hook: ExtensionHook, ...args): Promise<void> {
await this.executeFor(hook, '*', ...args)
}

async executeFor(hook: ExtensionHook, extName: string, ...args): Promise<void> {
const resolutions = (this.extensionHooks.get(hook) || []).filter(
(r) => extName === '*' || r['packageName'] === extName
)

for (const container of resolutions) {
const resolution = container.resolution
if (typeof resolution === 'function') {
await resolution(...args)
}
}
}

async load(): Promise<void> {
this.extensionCommands.forEach((cmd) => {
// clean up prior command registrations
this.workspace.commands.delete(cmd.command)
})

this.extensionList = []
this.extensionCommands = []
this.extensionHooks.clear()

const start = Date.now()
Expand All @@ -53,13 +105,14 @@ export class Extensions {
packages.push(...Object.keys(packageJsonData.devDependencies))
}

const folder = this.workspace.folder
const workspace = this.workspace
const ext = this.extensionHooks
const list = this.extensionList
const cmdList = this.extensionCommands
async function scan(packageName: string): Promise<void> {
log(`Scanning package: ${packageName}..`)

const mtermExtPath = join(folder, 'node_modules', packageName, 'mterm.js')
const mtermExtPath = join(workspace.folder, 'node_modules', packageName, 'mterm.js')

const isMtermExtensionExists = await pathExists(mtermExtPath)
if (!isMtermExtensionExists) {
Expand All @@ -77,12 +130,40 @@ export class Extensions {

for (const hook of hooks) {
const hookKey = hook as ExtensionHook
let resolutions: ExtensionHookResolution[] = []
if (hookKey === ExtensionHook.COMMANDS) {
const commands = mtermExt[hookKey]
if (typeof commands === 'function') {
const commandsResult = commands()

Object.keys(commandsResult).forEach((command) => {
const { description, exec } = commandsResult[command]

log(`Registering command: ${command} for ${packageName} (${description})`)

workspace.commands.add(command, exec)

cmdList.push({
packageName,
command
})
})
}
continue
}
let resolutions: ExtensionHookResolutionContainer<object>[] = []
if (ext.has(ExtensionHook[hookKey])) {
resolutions = ext.get(ExtensionHook[hookKey]) as ExtensionHookResolution[]
resolutions = ext.get(
ExtensionHook[hookKey]
) as ExtensionHookResolutionContainer<object>[]
}

resolutions.push(mtermExt[hookKey])
const extHook = mtermExt[hookKey]

resolutions.push({
packageName,
resolution: extHook,
hook: hookKey
})

ext.set(hookKey, resolutions)
}
Expand Down
6 changes: 5 additions & 1 deletion src/main/framework/runtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,11 @@ export function attach({ app, workspace }: BootstrapContext): void {

ipcMain.handle('runner.theme', async (_, profile): Promise<string> => {
const theme = workspace.theme.get(profile)
const extensionTheme = await workspace.extensions.run(ExtensionHook.RUNNER_THEME_CSS)
const extensionTheme = await workspace.extensions.run<string>(
ExtensionHook.RUNNER_THEME_CSS,
'',
workspace
)
return `${extensionTheme}${theme}`
})

Expand Down
Loading

0 comments on commit 05057b5

Please sign in to comment.