diff --git a/HELP.md b/HELP.md index ec19090..6cfd00a 100644 --- a/HELP.md +++ b/HELP.md @@ -358,3 +358,7 @@ str %> call $myFooReplacer bar Replace the current subject with "Lorem ipsum..." placeholder text. Can generate individual words, lines, or paragraphs. Example: `lipsum 4 word` -> `lorem ipsum dolor sit` + +#### User-specific setup files +The file `~/.str.rc` is automatically executed when `str` starts up. +You can use this to define a user-specific environment. diff --git a/src/index.ts b/src/index.ts index 4a2efc9..88e14cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,27 +6,39 @@ import {Parser} from "./vm/parser.js"; import {commands} from "./vm/commands/index.js"; import {Executor} from "./vm/vm.js"; import {ConsoleDisplay, OutputManager, WlClipboard} from "./vm/output.js"; +import {processPath} from "./vm/commands/command.js"; +import * as fs from "node:fs"; -const lifecycle = new Lifecycle() -const input = new Input() -input.adoptLifecycle(lifecycle) -input.subscribe(line => log.verbose('input', { line })) +;(async () => { + const lifecycle = new Lifecycle() + const input = new Input() + input.adoptLifecycle(lifecycle) + input.subscribe(line => log.verbose('input', { line })) -const lexer = new Lexer(input) -lexer.subscribe(token => log.verbose('token', token)) + const lexer = new Lexer(input) + lexer.subscribe(token => log.verbose('token', token)) -const parser = new Parser(commands, lexer) -parser.subscribe(exec => log.verbose('exec', exec)) + const parser = new Parser(commands, lexer) + parser.subscribe(exec => log.verbose('exec', exec)) -const output: OutputManager = { - display: new ConsoleDisplay, - clipboard: new WlClipboard, -} + const output: OutputManager = { + display: new ConsoleDisplay, + clipboard: new WlClipboard, + } -const exec = new Executor(output, parser, input) -exec.adoptLifecycle(lifecycle) -exec.subscribe(state => state.outputSubject()) + const exec = new Executor(output, parser, input) + exec.adoptLifecycle(lifecycle) + exec.subscribe(state => state.outputSubject()) -input.setupPrompt() + const rcFile = processPath('~/.str.rc') + if ( fs.existsSync(rcFile) ) { + log.verbose('rc', { rcFile }) -process.on('SIGINT', () => lifecycle.close()) + const rcFileContent = fs.readFileSync(rcFile).toString() + await input.pushLines('\n' + rcFileContent) + } + + input.setupPrompt() + + process.on('SIGINT', () => lifecycle.close()) +})() diff --git a/src/vm/commands/runfile.ts b/src/vm/commands/runfile.ts index 52a5365..bd05a4c 100644 --- a/src/vm/commands/runfile.ts +++ b/src/vm/commands/runfile.ts @@ -1,5 +1,6 @@ import {Command, ParseContext, StrTerm} from "./command.js"; import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; export class RunFile extends Command<{ path: StrTerm }> { isParseCandidate(token: LexInput): boolean { @@ -13,4 +14,12 @@ export class RunFile extends Command<{ path: StrTerm }> { getDisplayName(): string { return 'runfile' } + + execute(vm: StrVM, data: { path: StrTerm }): Promise { + return vm.tapInPlace(async ctx => { + await vm.control$.next({ cmd: 'preserve-history' }) + const path = ctx.resolveString(data.path) + await vm.control$.next({ cmd: 'lex-file', path }) + }) + } } diff --git a/src/vm/input.ts b/src/vm/input.ts index be2c452..161a7e6 100644 --- a/src/vm/input.ts +++ b/src/vm/input.ts @@ -10,6 +10,12 @@ export class Input extends BehaviorSubject implements LifecycleAware { public readonly errors$: BehaviorSubject = new BehaviorSubject() + public async pushLines(str: string): Promise { + for ( const line of str.split('\n') ) { + await this.next(line + '\n') + } + } + public hasPrompt(): boolean { return !!this.rl } diff --git a/src/vm/vm.ts b/src/vm/vm.ts index 786d872..0ad0abd 100644 --- a/src/vm/vm.ts +++ b/src/vm/vm.ts @@ -1,7 +1,7 @@ import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js"; import { CommandData, destructureToLines, isStrLVal, - isStrRVal, joinDestructured, StrDestructured, StrLamba, + isStrRVal, joinDestructured, processPath, StrDestructured, StrLamba, StrLVal, StrRVal, StrTerm, TypeError, unwrapDestructured, @@ -17,6 +17,7 @@ import {Executable} from "./parse.js"; import {getSubjectDisplay, OutputManager} from "./output.js"; import {Input} from "./input.js"; import {Lifecycle, LifecycleAware} from "../util/lifecycle.js"; +import * as fs from "node:fs/promises"; export class Scope { private entries: Record = {} @@ -361,6 +362,7 @@ export class ExecutionContext { export type Control = { cmd: 'undo' | 'redo' | 'exit' | 'no-show' | 'preserve-history' | 'close-prompt' | 'restore-prompt' } + | { cmd: 'lex-file', path: string } export class StrVM implements LifecycleAware { public static make(output: OutputManager, input?: Input): StrVM { @@ -415,6 +417,15 @@ export class StrVM implements LifecycleAware { this.wasPromptWhenClosed = false } else if ( control.cmd === 'exit' ) { this.lifecycle?.close() + } else if ( control.cmd === 'lex-file' ) { + const path = processPath(control.path) + const content = (await fs.readFile(path)).toString() + + // Since control commands happen WITHIN the current promise chain, + // the parser stage in the pipeline may still think the current parse + // operation is in-progress. So, include an initial TERMINATOR to ensure + // the file parses starting as a new statement. + await this.input?.pushLines('\n' + content) } }