Implement runfile and support for .str.rc

This commit is contained in:
2026-04-10 09:58:41 -05:00
parent a418f9a89f
commit 4dec54893c
5 changed files with 60 additions and 18 deletions

View File

@@ -358,3 +358,7 @@ str %> call $myFooReplacer bar
Replace the current subject with "Lorem ipsum..." placeholder text. Replace the current subject with "Lorem ipsum..." placeholder text.
Can generate individual words, lines, or paragraphs. Can generate individual words, lines, or paragraphs.
Example: `lipsum 4 word` -> `lorem ipsum dolor sit` 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.

View File

@@ -6,27 +6,39 @@ import {Parser} from "./vm/parser.js";
import {commands} from "./vm/commands/index.js"; import {commands} from "./vm/commands/index.js";
import {Executor} from "./vm/vm.js"; import {Executor} from "./vm/vm.js";
import {ConsoleDisplay, OutputManager, WlClipboard} from "./vm/output.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() ;(async () => {
const input = new Input() const lifecycle = new Lifecycle()
input.adoptLifecycle(lifecycle) const input = new Input()
input.subscribe(line => log.verbose('input', { line })) input.adoptLifecycle(lifecycle)
input.subscribe(line => log.verbose('input', { line }))
const lexer = new Lexer(input) const lexer = new Lexer(input)
lexer.subscribe(token => log.verbose('token', token)) lexer.subscribe(token => log.verbose('token', token))
const parser = new Parser(commands, lexer) const parser = new Parser(commands, lexer)
parser.subscribe(exec => log.verbose('exec', exec)) parser.subscribe(exec => log.verbose('exec', exec))
const output: OutputManager = { const output: OutputManager = {
display: new ConsoleDisplay, display: new ConsoleDisplay,
clipboard: new WlClipboard, clipboard: new WlClipboard,
} }
const exec = new Executor(output, parser, input) const exec = new Executor(output, parser, input)
exec.adoptLifecycle(lifecycle) exec.adoptLifecycle(lifecycle)
exec.subscribe(state => state.outputSubject()) 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())
})()

View File

@@ -1,5 +1,6 @@
import {Command, ParseContext, StrTerm} from "./command.js"; import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js"; import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
export class RunFile extends Command<{ path: StrTerm }> { export class RunFile extends Command<{ path: StrTerm }> {
isParseCandidate(token: LexInput): boolean { isParseCandidate(token: LexInput): boolean {
@@ -13,4 +14,12 @@ export class RunFile extends Command<{ path: StrTerm }> {
getDisplayName(): string { getDisplayName(): string {
return 'runfile' return 'runfile'
} }
execute(vm: StrVM, data: { path: StrTerm }): Promise<StrVM> {
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 })
})
}
} }

View File

@@ -10,6 +10,12 @@ export class Input extends BehaviorSubject<string> implements LifecycleAware {
public readonly errors$: BehaviorSubject<Error> = new BehaviorSubject() public readonly errors$: BehaviorSubject<Error> = new BehaviorSubject()
public async pushLines(str: string): Promise<void> {
for ( const line of str.split('\n') ) {
await this.next(line + '\n')
}
}
public hasPrompt(): boolean { public hasPrompt(): boolean {
return !!this.rl return !!this.rl
} }

View File

@@ -1,7 +1,7 @@
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js"; import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
import { import {
CommandData, destructureToLines, isStrLVal, CommandData, destructureToLines, isStrLVal,
isStrRVal, joinDestructured, StrDestructured, StrLamba, isStrRVal, joinDestructured, processPath, StrDestructured, StrLamba,
StrLVal, StrLVal,
StrRVal, StrRVal,
StrTerm, TypeError, unwrapDestructured, StrTerm, TypeError, unwrapDestructured,
@@ -17,6 +17,7 @@ import {Executable} from "./parse.js";
import {getSubjectDisplay, OutputManager} from "./output.js"; import {getSubjectDisplay, OutputManager} from "./output.js";
import {Input} from "./input.js"; import {Input} from "./input.js";
import {Lifecycle, LifecycleAware} from "../util/lifecycle.js"; import {Lifecycle, LifecycleAware} from "../util/lifecycle.js";
import * as fs from "node:fs/promises";
export class Scope { export class Scope {
private entries: Record<string, StrRVal> = {} private entries: Record<string, StrRVal> = {}
@@ -361,6 +362,7 @@ export class ExecutionContext {
export type Control = export type Control =
{ cmd: 'undo' | 'redo' | 'exit' | 'no-show' | 'preserve-history' | 'close-prompt' | 'restore-prompt' } { cmd: 'undo' | 'redo' | 'exit' | 'no-show' | 'preserve-history' | 'close-prompt' | 'restore-prompt' }
| { cmd: 'lex-file', path: string }
export class StrVM implements LifecycleAware { export class StrVM implements LifecycleAware {
public static make(output: OutputManager, input?: Input): StrVM { public static make(output: OutputManager, input?: Input): StrVM {
@@ -415,6 +417,15 @@ export class StrVM implements LifecycleAware {
this.wasPromptWhenClosed = false this.wasPromptWhenClosed = false
} else if ( control.cmd === 'exit' ) { } else if ( control.cmd === 'exit' ) {
this.lifecycle?.close() 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)
} }
} }