Skip to content

Commit

Permalink
fix: add history scrolling when runner closes
Browse files Browse the repository at this point in the history
* fix: add history settings

* fix: history model

* fix: persist loop in workspace and confirm all flows go through finish

* fix: history execution save on command completion

* fix: update constant tests for new history constants

* fix: history scroll

* fix: make sure history can not exceed max
  • Loading branch information
daretodave authored May 5, 2024
1 parent 9491744 commit dc6647f
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 26 deletions.
48 changes: 34 additions & 14 deletions src/constants.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Profile } from './main/framework/runtime'
import {
DEFAULT_WORKSPACE,
DEFAULT_PROFILE,
DEFAULT_PROFILES,
DEFAULT_FOLDER,
DEFAULT_HISTORY_ENABLED,
DEFAULT_HISTORY_MAX_ITEMS,
DEFAULT_HISTORY_SAVE_RESULT,
DEFAULT_SETTING_RUNNER_SHORTCUT,
DEFAULT_SETTING_COMMANDER_MODE_TOGGLE_SHORTCUT,
DEFAULT_SETTING_IS_COMMANDER_MODE,
Expand All @@ -12,17 +16,16 @@ import {
} from './constants'

describe('constants', () => {
it('should have the correct default workspace', () => {
it('should have the correct DEFAULT_WORKSPACE', () => {
expect(DEFAULT_WORKSPACE).toBe('~/mterm')
})

it('should have the correct default profile based on platform', () => {
const expectedDefaultProfile = process.platform === 'win32' ? 'powershell' : 'sh'
expect(DEFAULT_PROFILE).toBe(expectedDefaultProfile)
it('should have the correct DEFAULT_PROFILE based on the platform', () => {
expect(DEFAULT_PROFILE).toBe(process.platform === 'win32' ? 'powershell' : 'sh')
})

it('should have the correct default profiles based on platform', () => {
const expectedDefaultProfiles =
it('should have the correct DEFAULT_PROFILES based on the platform', () => {
const expectedProfiles: Record<string, Profile> =
process.platform === 'win32'
? {
powershell: {
Expand All @@ -43,26 +46,38 @@ describe('constants', () => {
icon: 'default'
}
}
expect(DEFAULT_PROFILES).toEqual(expectedDefaultProfiles)
expect(DEFAULT_PROFILES).toEqual(expectedProfiles)
})

it('should have the correct default folder', () => {
it('should have the correct DEFAULT_FOLDER', () => {
expect(DEFAULT_FOLDER).toBe('$CWD')
})

it('should have the correct default setting runner shortcut', () => {
it('should have the correct DEFAULT_HISTORY_ENABLED', () => {
expect(DEFAULT_HISTORY_ENABLED).toBe(true)
})

it('should have the correct DEFAULT_HISTORY_MAX_ITEMS', () => {
expect(DEFAULT_HISTORY_MAX_ITEMS).toBe(100)
})

it('should have the correct DEFAULT_HISTORY_SAVE_RESULT', () => {
expect(DEFAULT_HISTORY_SAVE_RESULT).toBe(true)
})

it('should have the correct DEFAULT_SETTING_RUNNER_SHORTCUT', () => {
expect(DEFAULT_SETTING_RUNNER_SHORTCUT).toBe('`+CommandOrControl')
})

it('should have the correct default setting commander mode toggle shortcut', () => {
it('should have the correct DEFAULT_SETTING_COMMANDER_MODE_TOGGLE_SHORTCUT', () => {
expect(DEFAULT_SETTING_COMMANDER_MODE_TOGGLE_SHORTCUT).toBe('`+Shift+CommandOrControl')
})

it('should have the correct default setting for commander mode', () => {
it('should have the correct DEFAULT_SETTING_IS_COMMANDER_MODE', () => {
expect(DEFAULT_SETTING_IS_COMMANDER_MODE).toBe(true)
})

it('should have the correct default setting for commander mode bounds', () => {
it('should have the correct DEFAULT_SETTING_COMMANDER_MODE_BOUNDS', () => {
expect(DEFAULT_SETTING_COMMANDER_MODE_BOUNDS).toEqual({
screen: 0,
x: 0,
Expand All @@ -72,7 +87,7 @@ describe('constants', () => {
})
})

it('should have the correct default setting for runner bounds', () => {
it('should have the correct DEFAULT_SETTING_RUNNER_BOUNDS', () => {
expect(DEFAULT_SETTING_RUNNER_BOUNDS).toEqual({
screen: 'PRIMARY',
x: 'SCREEN:-.5',
Expand All @@ -82,10 +97,15 @@ describe('constants', () => {
})
})

it('should have the correct default settings', () => {
it('should have the correct DEFAULT_SETTINGS', () => {
expect(DEFAULT_SETTINGS).toEqual({
defaultProfile: DEFAULT_PROFILE,
profiles: DEFAULT_PROFILES,
history: {
enabled: DEFAULT_HISTORY_ENABLED,
maxItems: DEFAULT_HISTORY_MAX_ITEMS,
saveResult: DEFAULT_HISTORY_SAVE_RESULT
},
runner: {
shortcut: DEFAULT_SETTING_RUNNER_SHORTCUT,
bounds: DEFAULT_SETTING_RUNNER_BOUNDS,
Expand Down
10 changes: 10 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const DEFAULT_PROFILES: Record<string, Profile> =
}
}
export const DEFAULT_FOLDER = '$CWD'
export const DEFAULT_HISTORY_ENABLED = true

export const DEFAULT_HISTORY_MAX_ITEMS = 100
export const DEFAULT_HISTORY_SAVE_RESULT = true
export const DEFAULT_SETTING_RUNNER_SHORTCUT = '`+CommandOrControl'
export const DEFAULT_SETTING_COMMANDER_MODE_TOGGLE_SHORTCUT = '`+Shift+CommandOrControl'
export const DEFAULT_SETTING_IS_COMMANDER_MODE = true
Expand All @@ -43,9 +47,15 @@ export const DEFAULT_SETTING_RUNNER_BOUNDS = {
w: 720,
h: 500
}

export const DEFAULT_SETTINGS = {
defaultProfile: DEFAULT_PROFILE,
profiles: DEFAULT_PROFILES,
history: {
enabled: DEFAULT_HISTORY_ENABLED,
maxItems: DEFAULT_HISTORY_MAX_ITEMS,
saveResult: DEFAULT_HISTORY_SAVE_RESULT
},
runner: {
shortcut: DEFAULT_SETTING_RUNNER_SHORTCUT,
bounds: DEFAULT_SETTING_RUNNER_BOUNDS,
Expand Down
9 changes: 9 additions & 0 deletions src/main/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,16 @@ export async function boostrap(context: BootstrapContext): Promise<void> {
}
})

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

setInterval(
() =>
workspace.persist().catch((error) => {
console.error(error)
}),
workspace.settings.get<number>('workspace.persistInterval', 5000)
)
} catch (e) {
console.error(e)

Expand Down
72 changes: 72 additions & 0 deletions src/main/framework/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { pathExists, readJSON, writeFile } from 'fs-extra'
import { Command } from './runtime'

export interface HistoricalExecution {
prompt: string
result?: string[]
error: boolean
aborted: boolean
profile: string
when: {
start: number
finish: number
}
code: number
}

// so why save two seperate histories?
// new execution is the history that is still pending writing
// this gives us the ability to easily scroll without exposing all the data to the front all at once
export class History {
public priorExecution: HistoricalExecution[] = []
public newExecution: HistoricalExecution[] = []
public scrollIndex: number = 0
constructor(public location: string) {}

async load(): Promise<void> {
const isExist = await pathExists(this.location)
if (!isExist) {
const prettyJSON = JSON.stringify([], null, 2)

await writeFile(this.location, prettyJSON, 'utf-8')
} else {
this.priorExecution = await readJSON(this.location)
}
}
append(command: Command, start: number, profile: string, saveResult: boolean): void {
this.newExecution.push({
prompt: command.prompt,
aborted: command.aborted,
result: saveResult ? command.result.stream.map((o) => o.raw) : undefined,
error: command.error,
profile,
when: {
start,
finish: Date.now()
},
code: command.result.code
})
}

async write(max: number): Promise<void> {
const history: Array<HistoricalExecution> = [...this.priorExecution, ...this.newExecution]

history.sort((historyA, historyB) => historyA.when.start - historyB.when.start)

const historyFinalList =
max < history.length ? history.slice(history.length - max, history.length) : history

await writeFile(this.location, JSON.stringify(historyFinalList, null, 2))
}

rewind(): HistoricalExecution | undefined {
if (this.scrollIndex >= this.priorExecution.length) {
return
}
const historicalItem = this.priorExecution[this.priorExecution.length - 1 - this.scrollIndex]

this.scrollIndex++

return historicalItem
}
}
89 changes: 78 additions & 11 deletions src/main/framework/runtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ import { execute } from './runtime-executor'
import createDOMPurify from 'dompurify'
import { JSDOM } from 'jsdom'
import {
DEFAULT_HISTORY_ENABLED,
DEFAULT_HISTORY_MAX_ITEMS,
DEFAULT_HISTORY_SAVE_RESULT,
DEFAULT_PROFILE,
DEFAULT_PROFILES,
DEFAULT_SETTING_IS_COMMANDER_MODE
} from '../../constants'
import Convert from 'ansi-to-html'
import { HistoricalExecution } from './history'

const convert = new Convert()
const DOMPurify = createDOMPurify(new JSDOM('').window)
Expand Down Expand Up @@ -90,6 +94,55 @@ export function attach({ app, workspace }: BootstrapContext): void {
return true
})

ipcMain.handle('history.try-scroll-next', async (_, runtimeId): Promise<boolean> => {
const runtime = workspace.runtimes.find((r) => r.id === runtimeId)
if (!runtime) {
return false
}
const enabled: boolean = workspace.settings.get<boolean>(
'history.enabled',
DEFAULT_HISTORY_ENABLED
)
if (!enabled) {
return false
}

const rewind: HistoricalExecution | undefined = workspace.history.rewind()
if (rewind === undefined) {
return false
}

const command: Command = {
prompt: rewind.prompt,
error: rewind.error,
aborted: rewind.aborted,
runtime: runtimeId,
id: short.generate(),
complete: true,
result: {
code: rewind.code,
stream: !rewind.result
? []
: rewind.result.map((raw) => {
let text = raw.toString()

text = DOMPurify.sanitize(raw)
text = convert.toHtml(text)

return {
error: rewind.error,
raw,
text: text
}
})
}
}

runtime.history.push(command)

return true
})

ipcMain.handle('runtime.rename', async (_, runtimeId, name): Promise<boolean> => {
const runtime = workspace.runtimes.find((r) => r.id === runtimeId)
if (!runtime) {
Expand Down Expand Up @@ -253,13 +306,35 @@ export function attach({ app, workspace }: BootstrapContext): void {
profileKey = workspace.settings.get<string>('defaultProfile', DEFAULT_PROFILE)
}

const history = {
enabled: workspace.settings.get<boolean>('history.enabled', DEFAULT_HISTORY_ENABLED),
results: workspace.settings.get<boolean>('history.saveResult', DEFAULT_HISTORY_SAVE_RESULT),
max: workspace.settings.get<number>('history.maxItems', DEFAULT_HISTORY_MAX_ITEMS)
}

const profiles = workspace.settings.get<ProfileMap>('profiles', DEFAULT_PROFILES)
const profile: Profile = profiles[profileKey]

const result: Result = command.result
const start = Date.now()

let finalize: boolean = true

const finish = (code: number): void => {
if (command.aborted || command.complete) {
return
}

result.code = code

command.complete = true
command.error = result.code !== 0

if (history.enabled) {
workspace.history.append(command, start, profileKey, history.results)
}
}

try {
const out = (text: string, error: boolean = false): void => {
if (command.aborted || command.complete) {
Expand Down Expand Up @@ -289,16 +364,6 @@ export function attach({ app, workspace }: BootstrapContext): void {
_.sender.send('runtime.commandEvent', streamEvent)
}
}
const finish = (code: number): void => {
if (command.aborted || command.complete) {
return
}

result.code = code

command.complete = true
command.error = result.code !== 0
}

if (!profile) {
throw `Profile ${profileKey} does not exist, provided by runtime as = ${runtimeTarget.profile}`
Expand All @@ -322,12 +387,14 @@ export function attach({ app, workspace }: BootstrapContext): void {
text: `${e}`,
raw: `${e}`
})
result.code = 1
finish(1)
}

if (finalize) {
command.complete = true
command.error = result.code !== 0

finish(result.code)
}

return command
Expand Down
1 change: 1 addition & 0 deletions src/main/framework/runtime-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function execute(context: ExecuteContext): Promise<void | boolean>
if (systemCommand.command === cmd || systemCommand?.alias?.includes(cmd)) {
await systemCommand.task(context, ...args)

finish(0)
return
}
}
Expand Down
Loading

0 comments on commit dc6647f

Please sign in to comment.