Skip to content

Commit

Permalink
fix: resolves #118 - add command add, remove and scafolding (#125)
Browse files Browse the repository at this point in the history
* chore: add package manager line to pkg json

* chore: save all commands in snake case

* chore: auto complete to consider snake case

* chore: consider snake case on command execution

* chore: add command flow

* chore: add remove command flow

* chore: cmd <cmd_name> now edits or creates a command

* chore: improve readme for command scafolding and more

* fix: lint fix
  • Loading branch information
daretodave authored May 27, 2024
1 parent f2a9f76 commit a9cecb0
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 25 deletions.
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": "yarn@1.22.22+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)
}
}
}
}

0 comments on commit a9cecb0

Please sign in to comment.