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 #118 - add command add, remove and scafolding #125

Merged
merged 9 commits into from
May 27, 2024
74 changes: 56 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ this means commands such as `ls`, `cd` or program commands such as `node -v` or

Head over to the [release page](https://github.com/mterm-io/mterm/releases/latest) to find the binary for your system type. mterm is evergreen and updates are automatically installed on your system as hey get released. Run `:v` to see your current mterm version.

### Customize

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
- 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)

### Autocomplete

Start typing, mterm will pick up your available: programs, [system commands](#system), [custom commands](#commands) and history
Expand Down Expand Up @@ -143,24 +155,26 @@ 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 |
| `: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 |
| `: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 All @@ -182,6 +196,30 @@ Now run `hello X` from mterm -

> In this case, no argument was provided so the `os.userInfo().username` was the fallback. `Hello, DR` is the result!

Note: commands are in snake case always when running. `helloWorld` will be `hello_world` when running in mterm. however, mterm will still resolve `helloWorld` as `hello_world` when running - it's just important to note this when dealing with name collisions. every command is stored in snake case.

#### add a command

add a command to and edit this command -

```bash
:cmd command_name
```
![image](https://github.com/mterm-io/mterm/assets/7341502/619706da-fe9e-4a97-8786-ce294492d8af)

`command_name` is now available to mterm (or `commandName` or `COMMAND_NAME` - note all names are resolved to `snake_case`)

#### editing a command

in the same fasion you add a command, edit a command with the same syntax -
```bash
:cmd command_name
```

this editor will save with `Ctrl+S` and reload the command every time you save.


#### other notes on commands
Try fetching data!

```typescript
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@
"ts-jest": "^29.1.2",
"typescript": "^5.4.5",
"vite": "^5.0.12"
}
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
5 changes: 3 additions & 2 deletions src/main/framework/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isAbsolute, join, parse } from 'path'
import { Runtime } from './runtime'
import { HistoricalExecution } from './history'
import { SystemCommand, systemCommands } from './runtime-executor'
import { Commands } from './commands'

export interface PathParts {
path: string
Expand Down Expand Up @@ -291,8 +292,8 @@ export class Autocomplete {
(o) => o.command.startsWith(prompt) || o.alias?.find((o) => o.startsWith(prompt))
)

const userCommandMatches = Object.keys(this.workspace.commands.lib).filter((o) =>
o.startsWith(prompt)
const userCommandMatches = Object.keys(this.workspace.commands.lib).filter(
(o) => o.startsWith(prompt) || o.startsWith(Commands.toCommandName(prompt))
)

const programMatches = this.programList.filter(
Expand Down
64 changes: 61 additions & 3 deletions src/main/framework/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirs, pathExists, readFile, readJson, writeFile, writeJson } from 'fs-extra'
import { mkdirs, pathExists, readFile, readJson, writeFile, writeJson, remove } from 'fs-extra'
import { tmpdir } from 'node:os'
import { join } from 'path'
import short from 'short-uuid'
Expand All @@ -9,6 +9,7 @@ import { Settings } from './settings'
import { ExecuteContext } from './execute-context'
import { CommandUtils } from './command-utils'
import { shell } from 'electron'
import { snakeCase } from 'lodash'
export class Commands {
public lib: object = {}
public commandFileLocation: string = ''
Expand All @@ -27,12 +28,16 @@ export class Commands {
fetch = global.fetch

has(key: string): boolean {
return !!this.lib[key]
return !!this.lib[key] || !!this.lib[Commands.toCommandName(key)]
}

static toCommandName(command: string = ''): string {
return snakeCase(command).toLowerCase().trim()
}

async run(context: ExecuteContext, key: string, ...args: string[]): Promise<unknown> {
let state = this.state[key]
const cmd = this.lib[key]
const cmd = this.lib[key] || this.lib[Commands.toCommandName(key)]

if (!state) {
state = {}
Expand Down Expand Up @@ -115,5 +120,58 @@ export class Commands {
this.lib = {}

runInNewContext(`${jsFile}`, this)

const libTranslated = {}

Object.keys(this.lib).forEach((key) => {
libTranslated[Commands.toCommandName(key)] = this.lib[key]
})

this.lib = libTranslated
}

getCommandFileLocation(cmd: string): string {
return join(this.workingDirectory, 'commands', `${Commands.toCommandName(cmd)}.ts`)
}

async addCommand(cmd: string, cmdScript: string = ''): Promise<void> {
cmd = Commands.toCommandName(cmd)

const commandFolder = join(this.workingDirectory, 'commands')
const commandFolderExists = await pathExists(commandFolder)
if (!commandFolderExists) {
await mkdirs(commandFolder)
}

const scriptFileBuffer = await readFile(this.commandFileLocation)
const scriptFile = scriptFileBuffer.toString()
const exportText = `export { ${cmd} } from './commands/${cmd}'`
const script = `${scriptFile}\n${exportText}`

const commandFile = join(commandFolder, `${cmd}.ts`)
const commandFileContents = `export function ${cmd}() {\n\t// your code here\n\t${cmdScript}\n}\n`

await writeFile(commandFile, commandFileContents)
await writeFile(this.commandFileLocation, script)
}

async removeCommand(cmd: string): Promise<void> {
cmd = Commands.toCommandName(cmd)

const commandFolder = join(this.workingDirectory, 'commands')
const commandFolderExists = await pathExists(commandFolder)
if (commandFolderExists) {
const commandFile = join(commandFolder, `${cmd}.ts`)
const commandFileExists = await pathExists(commandFile)
if (commandFileExists) {
await remove(cmd)
}
}

const scriptFileBuffer = await readFile(this.commandFileLocation)
const scriptFile = scriptFileBuffer.toString()
const script = scriptFile.replaceAll(`\nexport { ${cmd} } from './commands/${cmd}'`, '')

await writeFile(this.commandFileLocation, script)
}
}
55 changes: 54 additions & 1 deletion src/main/framework/system-commands/commands.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import { ExecuteContext } from '../execute-context'
import { errorModal } from '../../index'
import { Commands } from '../commands'

export async function addCommand(context: ExecuteContext, cmd: string): Promise<void> {
if (!cmd) {
context.out('Provide a command name to add\n\nExample: :cmd add restart_explorer\n\n', true)
context.finish(1)
return
}

if (context.workspace.commands.has(cmd)) {
context.out('Command already exists\n', true)
context.finish(1)
return
}

context.out(`Adding command: ${cmd}\n`)

console.log('Adding command:', cmd)

await context.workspace.commands.addCommand(cmd, `return 'Running ${cmd}!'`)

context.out('Command added\n')

await context.workspace.commands.load(context.workspace.settings)

context.out('Command reloaded\n')

await context.edit(context.workspace.commands.getCommandFileLocation(cmd), async () => {
context.out(`${cmd} reloaded\n`)
await context.workspace.commands.load(context.workspace.settings)
})
}

export default {
command: ':commands',
alias: [':commands', ':cmd'],
alias: [':commands', ':cmd', ':c'],
async task(context: ExecuteContext, task?: string): Promise<void> {
context.out('')
if (!task) {
Expand All @@ -25,6 +57,27 @@ export default {

context.out('Commands reloaded\n')
})
} else if (task === 'add') {
const cmd = Commands.toCommandName(context.prompt.args[1] || '')

await addCommand(context, cmd)
} else if (task === 'remove') {
const cmd = Commands.toCommandName(context.prompt.args[1] || '')

await context.workspace.commands.removeCommand(cmd)

context.out('Command removed\n')
} else if (task.trim()) {
const cmd = Commands.toCommandName(task || '')
if (context.workspace.commands.has(cmd)) {
context.out(`Editing command: ${cmd}\n`)
await context.edit(context.workspace.commands.getCommandFileLocation(cmd), async () => {
context.out(`${cmd} reloaded\n`)
await context.workspace.commands.load(context.workspace.settings)
})
} else {
await addCommand(context, cmd)
}
}
}
}
Loading