Skip to content

Commit

Permalink
* fix: add right click menu to tabs
Browse files Browse the repository at this point in the history
* fix: add context menu base

* fix: add runtime rename logic to backend

* fix: rename tab support

* fix: duplicate and close right functionality

* fix: close others and close functions for tabs

* fix: remove runner console.logs

* fix: when app closes last tab, close app
  • Loading branch information
daretodave authored May 4, 2024
1 parent 391dac8 commit 9491744
Show file tree
Hide file tree
Showing 9 changed files with 968 additions and 49 deletions.
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

0 comments on commit 9491744

Please sign in to comment.