Implement edit and exit
This commit is contained in:
@@ -23,7 +23,8 @@ const output: OutputManager = {
|
|||||||
clipboard: new WlClipboard,
|
clipboard: new WlClipboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
const exec = new Executor(output, parser)
|
const exec = new Executor(output, parser, input)
|
||||||
|
exec.adoptLifecycle(lifecycle)
|
||||||
exec.subscribe(state => state.outputSubject())
|
exec.subscribe(state => state.outputSubject())
|
||||||
|
|
||||||
input.setupPrompt()
|
input.setupPrompt()
|
||||||
|
|||||||
3
src/util/fs.ts
Normal file
3
src/util/fs.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
export const tempFile = () => `/tmp/str-${crypto.randomBytes(4).readUInt32LE(0)}.txt`
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import {Command, ParseContext} from "./command.js";
|
import {Command, ParseContext, wrapString} from "./command.js";
|
||||||
import {LexInput} from "../lexer.js";
|
import {LexInput} from "../lexer.js";
|
||||||
import {StrVM} from "../vm.js";
|
import {StrVM} from "../vm.js";
|
||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import childProcess from "node:child_process";
|
||||||
|
import {tempFile} from "../../util/fs.js";
|
||||||
|
|
||||||
export class Edit extends Command<{}> {
|
export class Edit extends Command<{}> {
|
||||||
isParseCandidate(token: LexInput): boolean {
|
isParseCandidate(token: LexInput): boolean {
|
||||||
@@ -19,8 +22,17 @@ export class Edit extends Command<{}> {
|
|||||||
execute(vm: StrVM, data: {}): Awaitable<StrVM> {
|
execute(vm: StrVM, data: {}): Awaitable<StrVM> {
|
||||||
return vm.replaceContextMatchingTerm({
|
return vm.replaceContextMatchingTerm({
|
||||||
string: async sub => {
|
string: async sub => {
|
||||||
|
await vm.control$.next({ cmd: 'close-prompt' })
|
||||||
|
|
||||||
return sub
|
const tmp = tempFile()
|
||||||
|
await fs.writeFile(tmp, sub, 'utf8')
|
||||||
|
const proc = childProcess.spawn(process.env.EDITOR || 'vim', [tmp], { stdio: 'inherit' })
|
||||||
|
await new Promise<void>(res => {
|
||||||
|
proc.on('close', () => res())
|
||||||
|
})
|
||||||
|
|
||||||
|
await vm.control$.next({ cmd: 'restore-prompt' })
|
||||||
|
return (await fs.readFile(tmp, 'utf8')).toString()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
import * as readline from 'node:readline'
|
import * as readline from 'node:readline'
|
||||||
import {BehaviorSubject} from "../util/subject.js";
|
import {BehaviorSubject} from "../util/subject.js";
|
||||||
import {Lifecycle, LifecycleAware} from "../util/lifecycle.js";
|
import {Lifecycle, LifecycleAware} from "../util/lifecycle.js";
|
||||||
|
import {StreamLogger} from "../util/log.js";
|
||||||
|
import {log} from "../log.js";
|
||||||
|
|
||||||
export class Input extends BehaviorSubject<string> implements LifecycleAware {
|
export class Input extends BehaviorSubject<string> implements LifecycleAware {
|
||||||
private rl?: readline.Interface
|
private rl?: readline.Interface
|
||||||
|
private log: StreamLogger = log.getStreamLogger('input')
|
||||||
|
|
||||||
|
public hasPrompt(): boolean {
|
||||||
|
return !!this.rl
|
||||||
|
}
|
||||||
|
|
||||||
public setupPrompt(): void {
|
public setupPrompt(): void {
|
||||||
|
this.log.verbose({
|
||||||
|
setupPrompt: { hasExistingPrompt: !!this.rl },
|
||||||
|
})
|
||||||
|
|
||||||
if ( this.rl ) {
|
if ( this.rl ) {
|
||||||
this.closePrompt()
|
this.closePrompt()
|
||||||
}
|
}
|
||||||
@@ -25,6 +36,10 @@ export class Input extends BehaviorSubject<string> implements LifecycleAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closePrompt(): void {
|
public closePrompt(): void {
|
||||||
|
this.log.verbose({
|
||||||
|
closePrompt: { hasExistingPrompt: !!this.rl },
|
||||||
|
})
|
||||||
|
|
||||||
this.rl?.close()
|
this.rl?.close()
|
||||||
this.rl = undefined
|
this.rl = undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {Awaitable} from "../util/types.js";
|
|||||||
import childProcess from "node:child_process";
|
import childProcess from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import {tempFile} from "../util/fs.js";
|
||||||
|
|
||||||
export const getSubjectDisplay = (sub: StrRVal): string => {
|
export const getSubjectDisplay = (sub: StrRVal): string => {
|
||||||
if ( sub.term === 'string' ) {
|
if ( sub.term === 'string' ) {
|
||||||
@@ -53,7 +54,6 @@ export class FakeClipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempFile = () => `/tmp/str-${crypto.randomBytes(4).readUInt32LE(0)}.txt`
|
|
||||||
export class WlClipboard {
|
export class WlClipboard {
|
||||||
async read(): Promise<string> {
|
async read(): Promise<string> {
|
||||||
const tmp = tempFile()
|
const tmp = tempFile()
|
||||||
|
|||||||
57
src/vm/vm.ts
57
src/vm/vm.ts
@@ -15,6 +15,8 @@ import {Parser} from "./parser.js";
|
|||||||
import {log} from "../log.js";
|
import {log} from "../log.js";
|
||||||
import {Executable} from "./parse.js";
|
import {Executable} from "./parse.js";
|
||||||
import {getSubjectDisplay, OutputManager} from "./output.js";
|
import {getSubjectDisplay, OutputManager} from "./output.js";
|
||||||
|
import {Input} from "./input.js";
|
||||||
|
import {Lifecycle, LifecycleAware} from "../util/lifecycle.js";
|
||||||
|
|
||||||
export class Scope {
|
export class Scope {
|
||||||
private entries: Record<string, StrRVal> = {}
|
private entries: Record<string, StrRVal> = {}
|
||||||
@@ -250,29 +252,41 @@ export class ExecutionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Control =
|
export type Control =
|
||||||
{ cmd: 'undo' | 'redo' | 'exit' | 'no-show' | 'preserve-history' }
|
{ cmd: 'undo' | 'redo' | 'exit' | 'no-show' | 'preserve-history' | 'close-prompt' | 'restore-prompt' }
|
||||||
|
|
||||||
export class StrVM {
|
export class StrVM implements LifecycleAware {
|
||||||
public static make(output: OutputManager): StrVM {
|
public static make(output: OutputManager, input?: Input): StrVM {
|
||||||
return new StrVM(
|
return new StrVM(
|
||||||
new ExecutionContext(wrapString(''), new Scope()),
|
new ExecutionContext(wrapString(''), new Scope()),
|
||||||
output,
|
output,
|
||||||
|
input,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private noShowNext: boolean = false
|
private noShowNext: boolean = false
|
||||||
private preserveHistoryNext: boolean = false
|
private preserveHistoryNext: boolean = false
|
||||||
|
private wasPromptWhenClosed: boolean = false
|
||||||
public readonly control$: BehaviorSubject<Control> = new BehaviorSubject()
|
public readonly control$: BehaviorSubject<Control> = new BehaviorSubject()
|
||||||
|
private log: StreamLogger
|
||||||
|
private lifecycle?: Lifecycle
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private context: ExecutionContext,
|
private context: ExecutionContext,
|
||||||
private output: OutputManager,
|
private output: OutputManager,
|
||||||
|
private input?: Input, // if provided, commands will properly handle prompt control
|
||||||
) {
|
) {
|
||||||
|
this.log = log.getStreamLogger('vm')
|
||||||
this.control$.subscribe((control: Control) =>
|
this.control$.subscribe((control: Control) =>
|
||||||
this.handleControl(control))
|
this.handleControl(control))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adoptLifecycle(lifecycle: Lifecycle): void {
|
||||||
|
this.lifecycle = lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
private async handleControl(control: Control) {
|
private async handleControl(control: Control) {
|
||||||
|
this.log.debug({ handlingControl: control })
|
||||||
|
|
||||||
if ( control.cmd === 'no-show' ) {
|
if ( control.cmd === 'no-show' ) {
|
||||||
this.noShowNext = true
|
this.noShowNext = true
|
||||||
} else if ( control.cmd === 'preserve-history' ) {
|
} else if ( control.cmd === 'preserve-history' ) {
|
||||||
@@ -281,6 +295,18 @@ export class StrVM {
|
|||||||
this.context.restoreHistory()
|
this.context.restoreHistory()
|
||||||
} else if ( control.cmd === 'redo' ) {
|
} else if ( control.cmd === 'redo' ) {
|
||||||
this.context.restoreForwardHistory()
|
this.context.restoreForwardHistory()
|
||||||
|
} else if ( control.cmd === 'close-prompt' ) {
|
||||||
|
this.wasPromptWhenClosed = !!this.input?.hasPrompt()
|
||||||
|
if ( this.wasPromptWhenClosed ) {
|
||||||
|
this.input?.closePrompt()
|
||||||
|
}
|
||||||
|
} else if ( control.cmd === 'restore-prompt' ) {
|
||||||
|
if ( this.wasPromptWhenClosed && !this.input?.hasPrompt() ) {
|
||||||
|
this.input?.setupPrompt()
|
||||||
|
}
|
||||||
|
this.wasPromptWhenClosed = false
|
||||||
|
} else if ( control.cmd === 'exit' ) {
|
||||||
|
this.lifecycle?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +358,11 @@ export class StrVM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeChild(): StrVM {
|
makeChild(): StrVM {
|
||||||
return new StrVM(this.context.makeChild(), this.output)
|
const child = new StrVM(this.context.makeChild(), this.output, this.input)
|
||||||
|
if ( this.lifecycle ) {
|
||||||
|
child.adoptLifecycle(this.lifecycle)
|
||||||
|
}
|
||||||
|
return child
|
||||||
}
|
}
|
||||||
|
|
||||||
async outputSubject(): Promise<void> {
|
async outputSubject(): Promise<void> {
|
||||||
@@ -345,17 +375,30 @@ export class StrVM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Executor extends BehaviorSubject<StrVM> {
|
export class Executor extends BehaviorSubject<StrVM> implements LifecycleAware{
|
||||||
private logger: StreamLogger
|
private logger: StreamLogger
|
||||||
|
private lifecycle?: Lifecycle
|
||||||
|
|
||||||
constructor(private output: OutputManager, parser?: Parser) {
|
constructor(private output: OutputManager, parser?: Parser, private input?: Input) {
|
||||||
super()
|
super()
|
||||||
this.logger = log.getStreamLogger('executor')
|
this.logger = log.getStreamLogger('executor')
|
||||||
parser?.subscribe(exec => this.handleExecutable(exec))
|
parser?.subscribe(exec => this.handleExecutable(exec))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adoptLifecycle(lifecycle: Lifecycle): void {
|
||||||
|
this.lifecycle = lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
async handleExecutable(exec: Executable<CommandData>) {
|
async handleExecutable(exec: Executable<CommandData>) {
|
||||||
const vm = this.currentValue || StrVM.make(this.output)
|
const vm = this.currentValue || this.makeVM()
|
||||||
await this.next(await exec.command.execute(vm, exec.data))
|
await this.next(await exec.command.execute(vm, exec.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private makeVM(): StrVM {
|
||||||
|
const vm = StrVM.make(this.output, this.input)
|
||||||
|
if ( this.lifecycle ) {
|
||||||
|
vm.adoptLifecycle(this.lifecycle)
|
||||||
|
}
|
||||||
|
return vm
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user