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: add file editing 💯 #99

Merged
merged 15 commits into from
May 7, 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
42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mterm-io_mterm&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mterm-io_mterm)
[![release](https://github.com/mterm-io/mterm/actions/workflows/release.yml/badge.svg)](https://github.com/mterm-io/mterm/actions/workflows/release.yml)

![image](https://github.com/mterm-io/mterm/assets/7341502/6eb47f43-1ab5-41c5-9c0e-5eb61ce575bf)
![image](https://github.com/mterm-io/mterm/assets/7341502/27bcad62-6891-4b49-80b5-e5a17e0562ab)

**mterm** is a cross-platform command-line terminal that proxies the underlying command-line interpreters, such as [powershell](https://learn.microsoft.com/en-us/powershell/), [sh](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html) or [wsl](https://ubuntu.com/desktop/wsl). commands are executed in the background and results streamed to the foreground.
Expand Down Expand Up @@ -128,21 +129,23 @@ 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 |
| `: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` |
| `:settings` | | Open the mterm settings gui to manage `~/mterm/settings.json` |
| `: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` |
| `: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 Expand Up @@ -212,6 +215,15 @@ export function who() {

![image](https://github.com/mterm-io/mterm/assets/7341502/76b26a62-33ea-4883-b07c-677f99ab3355)

### Editor

mterm provides an editor with `:edit <FILE>` or `edit <FILE>` commands -

![image](https://github.com/mterm-io/mterm/assets/7341502/25db8038-7a86-419c-a5d7-777b97025ec7)

hit `control + s` within the file editor to save this


### Other Notes

When you change the tab name to include `$idx` - this will be replaced with the current tab index
Expand Down
2 changes: 0 additions & 2 deletions src/main/bootstrap/create-tray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,4 @@ export async function createTray(context: BootstrapContext): Promise<void> {

tray.setToolTip('MTERM')
tray.setContextMenu(menu)

console.log(tray)
}
5 changes: 1 addition & 4 deletions src/main/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ export async function boostrap(context: BootstrapContext): Promise<void> {
}
})

autoUpdater
.checkForUpdatesAndNotify()
.then((r) => console.log(r))
.catch(console.error)
autoUpdater.checkForUpdatesAndNotify().catch(console.error)

attach(context)

Expand Down
2 changes: 2 additions & 0 deletions src/main/framework/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface HistoricalExecution {
error: boolean
aborted: boolean
profile: string
edit?: string
when: {
start: number
finish: number
Expand Down Expand Up @@ -40,6 +41,7 @@ export class History {
result: saveResult ? command.result.stream.map((o) => o.raw) : undefined,
error: command.error,
profile,
edit: command?.result?.edit?.path,
when: {
start,
finish: Date.now()
Expand Down
103 changes: 94 additions & 9 deletions src/main/framework/runtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '../../constants'
import Convert from 'ansi-to-html'
import { HistoricalExecution } from './history'
import { readFile, writeFile } from 'fs-extra'

const convert = new Convert()
const DOMPurify = createDOMPurify(new JSDOM('').window)
Expand All @@ -35,7 +36,15 @@ export function attach({ app, workspace }: BootstrapContext): void {
const focus = runtime.history.find((cmd) => cmd.id === runtime.commandFocus)

const result = focus
? focus.result
? {
...focus.result,
edit: focus.result.edit
? {
...focus.result.edit,
callback: undefined
}
: undefined
}
: {
code: 0,
stream: []
Expand All @@ -44,7 +53,17 @@ export function attach({ app, workspace }: BootstrapContext): void {
const history: CommandViewModel[] = runtime.history.map((historyItem) => {
return {
...historyItem,
process: undefined
process: undefined,
result: {
...historyItem.result,
edit: historyItem.result.edit
? {
content: historyItem.result.edit.content,
path: historyItem.result.edit.path,
modified: historyItem.result.edit.modified
}
: undefined
}
}
})

Expand Down Expand Up @@ -73,6 +92,57 @@ export function attach({ app, workspace }: BootstrapContext): void {
})
}

ipcMain.handle(
'runtime.set-edit',
async (_, runtimeId: string, commandId: string, result: string): Promise<boolean> => {
const runtime = workspace.runtimes.find((r) => r.id === runtimeId)
if (!runtime) {
return false
}

if (!commandId) {
commandId = runtime.commandFocus
}

const command = runtime.history.find((c) => c.id === commandId)
if (!command || !command.result.edit) {
return false
}

command.result.edit.content = result
command.result.edit.modified = true

return true
}
)

ipcMain.handle(
'runtime.save-edit',
async (_, runtimeId: string, commandId: string): Promise<boolean> => {
const runtime = workspace.runtimes.find((r) => r.id === runtimeId)
if (!runtime) {
return false
}

if (!commandId) {
commandId = runtime.commandFocus
}

const command = runtime.history.find((c) => c.id === commandId)
if (!command || !command.result.edit) {
return false
}

command.result.edit.modified = false

await writeFile(command.result.edit.path, command.result.edit.content)

await command.result.edit.callback(command.result.edit.content)

return true
}
)

ipcMain.handle(
'runtime.set-result',
async (_, runtimeId: string, commandId: string, result: string): Promise<boolean> => {
Expand Down Expand Up @@ -101,7 +171,7 @@ export function attach({ app, workspace }: BootstrapContext): void {
}
}

return false
return true
}
)

Expand Down Expand Up @@ -332,7 +402,8 @@ export function attach({ app, workspace }: BootstrapContext): void {
aborted: false,
result: {
code: 0,
stream: []
stream: [],
edit: undefined
},
runtime: runtime.id
}
Expand All @@ -345,7 +416,7 @@ export function attach({ app, workspace }: BootstrapContext): void {
return command
})

ipcMain.handle('runtime.execute', async (_, { id, runtime }: Command): Promise<Command> => {
ipcMain.handle('runtime.execute', async (_, { id, runtime }: Command): Promise<boolean> => {
const runtimeTarget = workspace.runtimes.find((r) => r.id === runtime)
if (!runtimeTarget) {
throw `Runtime '${runtime}' does not exist`
Expand Down Expand Up @@ -392,10 +463,12 @@ export function attach({ app, workspace }: BootstrapContext): void {

try {
const out = (text: string, error: boolean = false): void => {
if (command.aborted || command.complete) {
return
const isFinished = command.aborted || command.complete
if (isFinished) {
if (!command.result.edit) {
return
}
}

const raw = text.toString()

text = DOMPurify.sanitize(raw)
Expand Down Expand Up @@ -430,6 +503,18 @@ export function attach({ app, workspace }: BootstrapContext): void {
workspace,
runtime: runtimeTarget,
command,
async edit(path: string, callback: (text: string) => void) {
const file = await readFile(path)

this.command.result.edit = {
path,
modified: false,
content: file.toString(),
callback
}

_.sender.send('runtime.commandEvent')
},
out,
finish
})
Expand All @@ -452,7 +537,7 @@ export function attach({ app, workspace }: BootstrapContext): void {
finish(result.code)
}

return command
return true
})

ipcMain.handle('runtimes', async (): Promise<RuntimeModel[]> => {
Expand Down
3 changes: 2 additions & 1 deletion src/main/framework/runtime-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import Version from './system-commands/version'
import Vault from './system-commands/vault'
import Workspace from './system-commands/workspace'
import Settings from './system-commands/settings'
import Edit from './system-commands/edit'

const systemCommands: Array<{
command: string
alias?: string[]
task: (context: ExecuteContext, ...args: string[]) => Promise<void> | void
}> = [Reload, Exit, History, Cd, Tab, Test, Clear, Version, Vault, Workspace, Settings]
}> = [Reload, Exit, History, Cd, Tab, Test, Clear, Version, Vault, Workspace, Settings, Edit]
export async function execute(context: ExecuteContext): Promise<void | boolean> {
const { platform, workspace, runtime, command, out, finish } = context
const [cmd, ...args] = command.prompt.split(' ')
Expand Down
32 changes: 30 additions & 2 deletions src/main/framework/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { resolveFolderPathForMTERM, Workspace } from './workspace'
import short from 'short-uuid'
import { ChildProcessWithoutNullStreams } from 'node:child_process'
import { resolve } from 'path'

export interface ResultStream {
error: boolean
Expand All @@ -11,13 +12,30 @@ export interface ResultStream {
export interface Result {
code: number
stream: ResultStream[]
edit?: EditFile
}

export interface ResultViewModel {
code: number
stream: ResultStream[]
edit?: EditFileViewModel
}

export interface ResultStreamEvent {
runtime: string
command: string
entry: ResultStream
}

export interface EditFileViewModel {
path: string
modified: boolean
content: string
}

export interface EditFile extends EditFileViewModel {
callback: (text: string) => Promise<void> | void
}
export interface Command {
prompt: string
result: Result
Expand All @@ -31,7 +49,7 @@ export interface Command {

export interface CommandViewModel {
prompt: string
result: Result
result: ResultViewModel
runtime: string
aborted: boolean
complete: boolean
Expand All @@ -54,7 +72,7 @@ export interface RuntimeModel {
id: string
prompt: string
profile: string
result: Result
result: ResultViewModel
target: boolean
folder: string
history: CommandViewModel[]
Expand All @@ -81,13 +99,23 @@ export class Runtime {
icon: '',
title: 'mterm [$idx]'
}

resolve(path: string): string {
let location = resolve(this.folder, path)
if (path.startsWith('~')) {
location = resolveFolderPathForMTERM(path)
}

return location
}
}

export interface ExecuteContext {
platform: string
workspace: Workspace
runtime: Runtime
command: Command
edit: (path: string, callback: (content: string) => void) => Promise<void>
out: (text: string, error?: boolean) => void
finish: (code: number) => void
}
Loading
Loading