From 06ff1b396fcd82fb76decea9025a3f83d4b7d160 Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Tue, 10 Feb 2026 00:06:16 -0600 Subject: [PATCH] Implement output/clipboard interfaces, stub implementations, and implement execute()s for Copy and Paste --- src/index.ts | 10 ++++++++-- src/vm/commands/copy.ts | 10 +++++++++- src/vm/commands/paste.ts | 12 +++++++++++- src/vm/output.ts | 37 +++++++++++++++++++++++++++++++++++++ src/vm/vm.ts | 25 +++++++++++++++---------- 5 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index c1a3a7c..fccbe9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import {Lexer} from "./vm/lexer.js"; import {Parser} from "./vm/parser.js"; import {commands} from "./vm/commands/index.js"; import {Executor} from "./vm/vm.js"; +import {ConsoleDisplay, FakeClipboard, OutputManager} from "./vm/output.js"; const lifecycle = new Lifecycle() const input = new Input() @@ -17,8 +18,13 @@ lexer.subscribe(token => log.verbose('token', token)) const parser = new Parser(commands, lexer) parser.subscribe(exec => log.verbose('exec', exec)) -const exec = new Executor(parser) -exec.subscribe(state => state.output()) +const output: OutputManager = { + display: new ConsoleDisplay, + clipboard: new FakeClipboard, +} + +const exec = new Executor(output, parser) +exec.subscribe(state => state.outputSubject()) input.setupPrompt() diff --git a/src/vm/commands/copy.ts b/src/vm/commands/copy.ts index 7a766f7..f27ad38 100644 --- a/src/vm/commands/copy.ts +++ b/src/vm/commands/copy.ts @@ -1,5 +1,7 @@ -import {Command, ParseContext} from "./command.js"; +import {Command, ParseContext, unwrapString} from "./command.js"; import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; +import {Awaitable} from "../../util/types.js"; export class Copy extends Command<{}> { isParseCandidate(token: LexInput): boolean { @@ -13,4 +15,10 @@ export class Copy extends Command<{}> { getDisplayName(): string { return 'copy' } + + execute(vm: StrVM): Awaitable { + return vm.tapInPlace(ctx => + vm.withOutput(output => + output.clipboard.overwrite(unwrapString(ctx.getSubject())))) + } } diff --git a/src/vm/commands/paste.ts b/src/vm/commands/paste.ts index 2de9824..d620b3e 100644 --- a/src/vm/commands/paste.ts +++ b/src/vm/commands/paste.ts @@ -1,5 +1,7 @@ -import {Command, ParseContext} from "./command.js"; +import {Command, ParseContext, wrapString} from "./command.js"; import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; +import {Awaitable} from "../../util/types.js"; export class Paste extends Command<{}> { isParseCandidate(token: LexInput): boolean { @@ -13,4 +15,12 @@ export class Paste extends Command<{}> { getDisplayName(): string { return 'paste' } + + execute(vm: StrVM): Awaitable { + return vm.replaceContextMatchingTerm({ + override: () => + vm.withOutput(async output => + wrapString(await output.clipboard.read())) + }) + } } diff --git a/src/vm/output.ts b/src/vm/output.ts index 0fbafc1..824fe68 100644 --- a/src/vm/output.ts +++ b/src/vm/output.ts @@ -1,4 +1,5 @@ import {StrRVal} from "./commands/command.js"; +import {Awaitable} from "../util/types.js"; export const getSubjectDisplay = (sub: StrRVal): string => { if ( sub.term === 'string' ) { @@ -11,3 +12,39 @@ export const getSubjectDisplay = (sub: StrRVal): string => { return JSON.stringify(sub.value, null, '\t') // fixme } + +export type Display = { + showSubject(sub: StrRVal): Awaitable +} + +export class ConsoleDisplay implements Display { + showSubject(sub: StrRVal) { + console.log(`\n---------------\n${getSubjectDisplay(sub)}\n---------------\n`) + } +} + +export class NullDisplay implements Display { + showSubject() {} +} + +export type Clipboard = { + read(): Awaitable + overwrite(sub: string): Awaitable +} + +export class FakeClipboard { + private val = '' + + read() { + return this.val + } + + overwrite(sub: string) { + this.val = sub + } +} + +export type OutputManager = { + display: Display, + clipboard: Clipboard, +} diff --git a/src/vm/vm.ts b/src/vm/vm.ts index 73e4007..532acec 100644 --- a/src/vm/vm.ts +++ b/src/vm/vm.ts @@ -14,7 +14,7 @@ import {StreamLogger} from "../util/log.js"; import {Parser} from "./parser.js"; import {log} from "../log.js"; import {Executable} from "./parse.js"; -import {getSubjectDisplay} from "./output.js"; +import {getSubjectDisplay, OutputManager} from "./output.js"; export class Scope { private entries: Record = {} @@ -188,13 +188,16 @@ export class ExecutionContext { } export class StrVM { - public static make(): StrVM { + public static make(output: OutputManager): StrVM { return new StrVM( - new ExecutionContext(wrapString(''), new Scope())) + new ExecutionContext(wrapString(''), new Scope()), + output, + ) } constructor( private context: ExecutionContext, + private output: OutputManager, ) {} public async runInPlace(operator: (ctx: ExecutionContext) => Awaitable): Promise { @@ -230,28 +233,30 @@ export class StrVM { }))) } - output() { - console.log('---------------') - console.log(getSubjectDisplay(this.context.getSubject())) - console.log('---------------') + public async withOutput(operator: (output: OutputManager) => Awaitable): Promise { + return operator(this.output) } makeChild(): StrVM { - return new StrVM(this.context.makeChild()) + return new StrVM(this.context.makeChild(), this.output) + } + + async outputSubject(): Promise { + await this.output.display.showSubject(this.context.getSubject()) } } export class Executor extends BehaviorSubject { private logger: StreamLogger - constructor(parser?: Parser) { + constructor(private output: OutputManager, parser?: Parser) { super() this.logger = log.getStreamLogger('executor') parser?.subscribe(exec => this.handleExecutable(exec)) } async handleExecutable(exec: Executable) { - const vm = this.currentValue || StrVM.make() + const vm = this.currentValue || StrVM.make(this.output) await this.next(await exec.command.execute(vm, exec.data)) } }