Implement runfile and support for .str.rc
This commit is contained in:
4
HELP.md
4
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.
|
||||
|
||||
46
src/index.ts
46
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())
|
||||
})()
|
||||
|
||||
@@ -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<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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ export class Input extends BehaviorSubject<string> implements LifecycleAware {
|
||||
|
||||
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 {
|
||||
return !!this.rl
|
||||
}
|
||||
|
||||
13
src/vm/vm.ts
13
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<string, StrRVal> = {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user