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

Feature/issue 4 #93

Merged
merged 7 commits into from
May 4, 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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,11 @@ export async function query(): Promise<{
}
```

You can install packages in the `~/mterm` folder and use them in `commands.ts`

<img src="https://github.com/mterm-io/mterm/assets/7341502/df8e74d4-896c-4964-861d-bad3ced17c80" alt="drawing" width="500"/>


> Note the return type is optional, just added above to highlight the typescript engine provided




### Secrets

Environment variables are a bit unsafe. You set these and leave the host machine all the ability to read and share these. Wonderful for services and backends, not the safest for personal usage.
Expand Down Expand Up @@ -217,6 +212,12 @@ export function who() {

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

### Other Notes

When you change the tab name to include `$idx` - this will be replaced with the current tab index

You can install packages in the `~/mterm` folder and use them in `commands.ts`

### contributing

see [local setup](#local-setup) for code setup info.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
"@electron-toolkit/utils": "^3.0.0",
"ansi-to-html": "^0.7.2",
"dompurify": "^3.1.0",
"electron-context-menu": "^3.6.1",
"electron-root-path": "^1.1.0",
"electron-updater": "^6.1.7",
"fs-extra": "^11.2.0",
"jsdom": "^24.0.0",
"lodash": "^4.17.21",
"rctx-contextmenu": "^1.4.1",
"short-uuid": "^4.2.2",
"stack-trace": "^1.0.0-pre2",
"try-require": "^1.2.1",
Expand Down
23 changes: 23 additions & 0 deletions src/main/bootstrap/create-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ContextMenuParams, shell } from 'electron'
import { BootstrapContext } from './index'
import contextMenu, { Actions } from 'electron-context-menu'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function createContext(_: BootstrapContext): Promise<void> {
contextMenu({
showSaveImageAs: true,
prepend: (_: Actions, parameters: ContextMenuParams) => {
return [
{
label: 'Search Google for “{selection}”',
visible: parameters.selectionText.trim().length > 0,
click: (): void => {
shell.openExternal(
`https://google.com/search?q=${encodeURIComponent(parameters.selectionText)}`
)
}
}
]
}
})
}
2 changes: 2 additions & 0 deletions src/main/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createShortcut } from './create-shortcut'
import { attach } from '../framework/runtime-events'
import { RunnerWindow } from '../window/windows/runner'
import { autoUpdater } from 'electron-updater'
import { createContext } from './create-context'

export interface BootstrapContext {
app: App
Expand Down Expand Up @@ -49,6 +50,7 @@ export async function boostrap(context: BootstrapContext): Promise<void> {
await createWindows(context)

await createTray(context)
await createContext(context)

createShortcut(context)

Expand Down
73 changes: 73 additions & 0 deletions src/main/framework/runtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Result,
ResultStream,
ResultStreamEvent,
Runtime,
RuntimeModel
} from './runtime'
import short from 'short-uuid'
Expand Down Expand Up @@ -89,6 +90,78 @@ export function attach({ app, workspace }: BootstrapContext): void {
return true
})

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

runtime.appearance.title = name

return true
})

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

const duplicatedRuntime = new Runtime(runtime.folder)

duplicatedRuntime.appearance.title = runtime.appearance.title
duplicatedRuntime.profile = runtime.profile
duplicatedRuntime.prompt = runtime.prompt

workspace.runtimes.push(duplicatedRuntime)

return true
})

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

const runtimeIndex = workspace.runtimes.indexOf(runtime)
const runtimesToDelete = workspace.runtimes.filter((_, index) => index > runtimeIndex)

runtimesToDelete.forEach((runtime) => workspace.removeRuntime(runtime))

workspace.runtimeIndex = runtimeIndex

return true
})

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

const runtimesToDelete = workspace.runtimes.filter((_) => _.id !== runtimeId)

runtimesToDelete.forEach((runtime) => workspace.removeRuntime(runtime))

workspace.runtimeIndex = 0

return true
})

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

if (!workspace.removeRuntime(runtime)) {
app.quit()
}

return true
})

ipcMain.on('open.workspace', async () => {
await shell.openPath(workspace.folder)
})
Expand Down
2 changes: 2 additions & 0 deletions src/main/framework/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export class Workspace {
this.runtimeIndex = 0
}
}
} else if (this.runtimes.length === 1) {
this.runtimeIndex = 0
}

return this.runtimes.length !== 0
Expand Down
14 changes: 14 additions & 0 deletions src/renderer/src/assets/runner.css
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,17 @@ body {
min-width: 32px;
min-height: 32px;
}
.contextmenu.tab-context-menu {
color: #f1f1f1;
width: 150px;
background-color: #464545;
padding: 5px 0;
}
.tab-context-menu > .contextmenu__item {
font-size: 11px;
text-transform: uppercase;
padding: 2px 9px;
}
.tab-context-menu > .contextmenu__item:hover {
color: #2248a9;
}
152 changes: 115 additions & 37 deletions src/renderer/src/runner/runner.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ChangeEvent, ReactElement, useEffect, useState, useRef } from 'react'
import { Command, ResultStreamEvent, Runtime } from './runtime'
import { ChangeEvent, ReactElement, useEffect, useRef, useState } from 'react'
import { Command, Runtime } from './runtime'
import { ContextMenu, ContextMenuItem, ContextMenuTrigger } from 'rctx-contextmenu'

export default function Runner(): ReactElement {
const [runtimeList, setRuntimes] = useState<Runtime[]>([])
const [pendingTitles, setPendingTitles] = useState<object>({})
const [historyIndex, setHistoryIndex] = useState<number>(-1)
const [commanderMode, setCommanderMode] = useState<boolean>(false)
const [rawMode, setRawMode] = useState<boolean>(false)
Expand All @@ -16,14 +18,9 @@ export default function Runner(): ReactElement {
}

window.electron.ipcRenderer.removeAllListeners('runtime.commandEvent')
window.electron.ipcRenderer.on(
'runtime.commandEvent',
async (_, streamEntry: ResultStreamEvent) => {
console.log(streamEntry)

await reloadRuntimesFromBackend()
}
)
window.electron.ipcRenderer.on('runtime.commandEvent', async () => {
await reloadRuntimesFromBackend()
})

const setPrompt = (prompt: string): void => {
setRuntimes((runtimes) => {
Expand Down Expand Up @@ -68,7 +65,6 @@ export default function Runner(): ReactElement {
}
const handlePromptChange = (event: ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value
console.log(event)
if (historyIndex !== -1) {
applyHistoryIndex(-1)
}
Expand All @@ -78,6 +74,15 @@ export default function Runner(): ReactElement {
window.electron.ipcRenderer.send('runtime.prompt', value)
}

const handleTitleChange = (id: string, event: ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value

setPendingTitles((titles) => ({
...titles,
[id]: value
}))
}

const selectRuntime = (runtimeIndex: number): void => {
window.electron.ipcRenderer.send('runtime.index', runtimeIndex)

Expand Down Expand Up @@ -125,30 +130,69 @@ export default function Runner(): ReactElement {
}
}

useEffect(() => {
inputRef.current?.focus()

// Conditionally handle keydown of letter or arrow to refocus input
const handleGlobalKeyDown = (event): void => {
if (
/^[a-zA-Z]$/.test(event.key) ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
(!event.shiftKey && !event.ctrlKey && !event.altKey)
) {
if (document.activeElement !== inputRef.current) {
inputRef.current?.focus()
const handleTabAction = (runtime: Runtime, action: string): void => {
switch (action) {
case 'rename':
{
setPendingTitles((titles) => ({
...titles,
[runtime.id]: runtime.appearance.title
}))
}
handleKeyDown(event)
break
default: {
window.electron.ipcRenderer.invoke(`runtime.${action}`, runtime.id).then(() => {
return reloadRuntimesFromBackend()
})
}
}
}

document.addEventListener('keydown', handleGlobalKeyDown)

return () => {
document.removeEventListener('keydown', handleGlobalKeyDown)
const handleTabTitleKeyDown = (id: string, e): void => {
if (e.key === 'Enter') {
const titleToSave = pendingTitles[id] || ''
if (titleToSave.trim().length > 0) {
// something to save
window.electron.ipcRenderer.invoke('runtime.rename', id, titleToSave).then(() => {
return reloadRuntimesFromBackend()
})
}
setPendingTitles((titles) => ({ ...titles, [id]: null }))
}
}, [])
}

// useEffect(() => {
// inputRef.current?.focus()
//
// if (Object.values(pendingTitles).filter((title) => title !== null).length > 0) {
// //editing session in progress
// return
// }
//
// // Conditionally handle keydown of letter or arrow to refocus input
// const handleGlobalKeyDown = (event): void => {
// // if pending a title? ignore this key event: user is probably editing the window title
// // and does not care for input
//
// if (
// /^[a-zA-Z]$/.test(event.key) ||
// event.key === 'ArrowUp' ||
// event.key === 'ArrowDown' ||
// (!event.shiftKey && !event.ctrlKey && !event.altKey)
// ) {
// if (document.activeElement !== inputRef.current) {
// inputRef.current?.focus()
// }
// handleKeyDown(event)
// }
// }
//
// document.addEventListener('keydown', handleGlobalKeyDown)
//
// return () => {
// document.removeEventListener('keydown', handleGlobalKeyDown)
// }
// }, [])

useEffect(() => {
reloadRuntimesFromBackend().catch((error) => console.error(error))
Expand Down Expand Up @@ -182,13 +226,47 @@ export default function Runner(): ReactElement {
>
<div className="runner-tabs">
{runtimeList.map((runtime, index: number) => (
<div
key={index}
onClick={() => selectRuntime(index)}
className={`runner-tabs-title ${runtime.target ? 'runner-tabs-title-active' : undefined}`}
>
<div>{runtime.appearance.title}</div>
</div>
<ContextMenuTrigger key={index} id={`tab-context-menu-${index}`}>
<div
onClick={() => selectRuntime(index)}
className={`runner-tabs-title ${runtime.target ? 'runner-tabs-title-active' : undefined}`}
>
<div>
{pendingTitles[runtime.id] !== null && pendingTitles[runtime.id] !== undefined ? (
<input
type="text"
onKeyDown={(e) => handleTabTitleKeyDown(runtime.id, e)}
onChange={(e) => handleTitleChange(runtime.id, e)}
value={pendingTitles[runtime.id]}
/>
) : (
runtime.appearance.title
)}
</div>

<ContextMenu
id={`tab-context-menu-${index}`}
hideOnLeave={false}
className="tab-context-menu"
>
<ContextMenuItem onClick={() => handleTabAction(runtime, 'close')}>
Close
</ContextMenuItem>
<ContextMenuItem onClick={() => handleTabAction(runtime, 'close-others')}>
Close Others
</ContextMenuItem>
<ContextMenuItem onClick={() => handleTabAction(runtime, 'close-right')}>
Close (right)
</ContextMenuItem>
<ContextMenuItem onClick={() => handleTabAction(runtime, 'duplicate')}>
Duplicate
</ContextMenuItem>
<ContextMenuItem onClick={() => handleTabAction(runtime, 'rename')}>
Rename
</ContextMenuItem>
</ContextMenu>
</div>
</ContextMenuTrigger>
))}
<div className="runner-spacer" onClick={onAddRuntimeClick}>
+
Expand Down
Loading
Loading