I'm looking to drop the user into a "prototyping" REPL where they can do a few things and then the result is saved in a script. I'd like to create a new temporary namespace for this and then drop back to old-Main later. From https://github.com/JuliaLang/julia/issues/2385#issuecomment-1261400850 I get the impression this might be possible, but I have no idea how.
In version 1.9, you can do
using REPL
REPL.activate(::Module)
But that won't help you with the "save input to a script" part
Well, I guess you could save the REPL history from the logs
Here's how I'd approach it:
using ReplMaker
function script_mode(filename, mod=(@eval module $(gensym(:Scratch)) end))
initrepl(;prompt_text="Script> ", start_key=')', mode_name="Script mode", valid_input_checker=complete_julia) do s
ex = Meta.parse(s)
if ex.head == :block
ex.head = :toplevel
end
open(filename, "a+") do io
write(io, s*"\n\n")
end
:(@eval $mod $ex)
end
nothing
end
Now at the REPL,
julia> script_mode("my_script.jl")
REPL mode Script mode initialized. Press ) to enter and backspace to exit.
Script> x = 1
1
Script> y = 2
2
Script> z = x + y
3
Script> function f(x)
x + 1
end
f (generic function with 1 method)
And here's the generated script:
shell> cat my_script.jl
x = 1
y = 2
z = x + y
function f(x)
x + 1
end
And everything is properly namespaced, so the Main
repl isn't polluted:
julia> x
ERROR: UndefVarError: x not defined
julia> y
ERROR: UndefVarError: y not defined
julia> z
ERROR: UndefVarError: z not defined
julia> f
ERROR: UndefVarError: f not defined
Mason Protter said:
Here's how I'd approach it:
using ReplMaker ...
There should be a github of just useful scripts like this
Thanks Mason, I'll have a look at REPL.activate
.
For reference, this is what I've got so far
subrepl = REPL.LineEditREPL(REPL.TerminalMenus.terminal,
get(stdout, :color, false))
if subrepl.hascolor
subrepl.prompt_color = Base.text_colors[:light_magenta]
end
subrepl.interface = REPL.setup_interface(subrepl)
(; hp) = first(subrepl.interface.modes[isa.(subrepl.interface.modes, REPL.LineEdit.HistoryPrompt)])
hp.mode_mapping[:julia].prompt = "(data) julia> "
hp.mode_mapping[:shell].prompt = "(data) shell> "
REPL.run_repl(subrepl)
lines = map(zip(hp.history, hp.modes)) do (line, mode)
if mode == :julia
line
# Identify data""-y commands and extract to variables
elseif mode == :shell
"run(`$line`)"
elseif mode == :help
nothing
else
@warn "Mode $mode unrecognised"
nothing
end
end
content = string("# Formed by wrangle command\n",
"function ()\n",
join(filter(!isnothing, lines), "\n "),
"\nend")
Syx Pek said:
Mason Protter said:
Here's how I'd approach it:
using ReplMaker ...
There should be a github of just useful scripts like this
There's the ReplMaker.jl readme :grinning_face_with_smiling_eyes:
@Timothy does the ReplMaker.jl thing I posted not work for you?
I won't be ably to try until later, I just thought I'd share what I started with.
That said, I do generally try to avoid dependencies so if I can make do without it I'll probably try that, but I expect it will be useful regardless :)
I find the REPL code in base really hard to use, and easy to get broken with version changes, so that's why I try to push ReplMaker.
But yeah, if you can figure out how to do it without ReplMaker then that's nice. Be warned though, it might mean you know enough about the REPL internals that I'll come ask for you help next time a julia update breaks ReplMaker.jl :laughing:
That sounds reasonable. If I wasn't "almost there" with my non-ReplMaker approach I'd probably jump on that (I've already got a bunch of other stuff sorted).
At this point I've got enough "nice things" on top of REPL
(e.g. sub-commands with automatic help and completion), that the little bit of extra here is very likely less effort than rewriting to use ReplMaker
.
Throwaway comment: I wish the Julia REPL
module came with a nice little library of interactive terminal utils (e.g. a better version of print("Question? "); readline(stdin)
for prompting the user, y/n confirmation, etc.)
Since the REPL experience with Julia is rather important, making it easy for packages to make nice REPL experiences seems like it would be well worth the ~1-200 lines for a few nice extras.
yeah
I want ReplMaker.jl to try and provide a good toolkit since REPL.jl itself refuses to, but it's hard.
As it is I have some hacky hand-rolled stand-ins, e.g.
"""
prompt(question::AbstractString, default::AbstractString="",
allowempty::Bool=false, cleardefault::Bool=true)
Interactively ask `question` and return the response string, optionally
with a `default` value.
Unless `allowempty` is set an empty response is not accepted.
If `cleardefault` is set, then an initial backspace will clear the default value.
The prompt supports the following line-edit-y keys:
- left arrow
- right arrow
- home
- end
- delete forwards
- delete backwards
### Example
```julia-repl
julia> prompt("What colour is the sky? ")
What colour is the sky? Blue
"Blue"
```
"""
function prompt(question::AbstractString, default::AbstractString="";
allowempty::Bool=false, cleardefault::Bool=true)
printstyled(question, color=REPL_QUESTION_COLOR)
get(stdout, :color, false) && print(Base.text_colors[REPL_USER_INPUT_COLOUR])
REPL.Terminals.raw!(REPL.TerminalMenus.terminal, true)
response = let response = collect(default)
point = length(response)
firstinput = true
print("\e[s")
while true
print("\e[u\e[J")
if String(response) == default
print("\e[90m")
end
print(String(response))
if point < length(response)
print("\e[$(length(response) - point)D")
end
next = Char(REPL.TerminalMenus.readkey(REPL.TerminalMenus.terminal.in_stream))
if next == '\r' # RET
if (!isempty(response) || allowempty)
print('\n')
break
end
elseif next == 'Ϭ' # DEL-forward
if point < length(response)
deleteat!(response, point + 1)
end
elseif next == '\x03' # ^C
print("\e[m^C\n")
throw(InterruptException())
elseif next == '\x7f' # DEL
if firstinput && cleardefault
response = Char[]
point = 0
elseif point > 0
deleteat!(response, point)
point -= 1
end
elseif next == 'Ϩ' # <left>
point = max(0, point - 1)
elseif next == 'ϩ' # <right>
point = min(length(response), point + 1)
elseif next == 'ϭ' # HOME
point = 0
elseif next == 'Ϯ' # END
point = length(response)
else
point += 1
insert!(response, point, next)
end
firstinput = false
end
String(response)
end
REPL.Terminals.raw!(REPL.TerminalMenus.terminal, false)
get(stdout, :color, false) && print("\e[m")
response
end
"""
prompt_char(question::AbstractString, options::Vector{Char},
default::Union{Char, Nothing}=nothing)
Interatively ask `question`, only accepting `options` keys as answers.
All keys are converted to lower case on input. If `default` is not nothing and
'RET' is hit, then `default` will be returned.
Should '^C' be pressed, an InterruptException will be thrown.
"""
function prompt_char(question::AbstractString, options::Vector{Char},
default::Union{Char, Nothing}=nothing)
printstyled(question, color=REPL_QUESTION_COLOR)
REPL.Terminals.raw!(REPL.TerminalMenus.terminal, true)
char = '\x01'
while char ∉ options
char = lowercase(Char(REPL.TerminalMenus.readkey(REPL.TerminalMenus.terminal.in_stream)))
if char == '\r' && !isnothing(default)
char = default
elseif char == '\x03' # ^C
print("\e[m^C\n")
throw(InterruptException())
end
end
REPL.Terminals.raw!(REPL.TerminalMenus.terminal, false)
get(stdout, :color, false) && print(Base.text_colors[REPL_USER_INPUT_COLOUR])
print(stdout, char, '\n')
get(stdout, :color, false) && print("\e[m")
char
end
"""
confirm_yn(question::AbstractString, default::Bool=false)
Interactively ask `question` and accept y/Y/n/N as the response.
If any other key is pressed, then `default` will be taken as the response.
A " [y/n]: " string will be appended to the question, with y/n capitalised
to indicate the default value.
### Example
```julia-repl
julia> confirm_yn("Do you like chocolate?", true)
Do you like chocolate? [Y/n]: y
true
```
"""
function confirm_yn(question::AbstractString, default::Bool=false)
char = prompt_char(question * (" [y/N]: ", " [Y/n]: ")[1+ default],
['y', 'n'], ('n', 'y')[1+default])
char == 'y'
end
So if during your development, you find any customizations and stuff you think might make a nice contribution to ReplMaker.jl, I'd be very glad to receive PRs
If I do end up going that way, I'll be sure to do so :+1:
Hmm, maybe it would be worth having something like TermInteractionUtils.jl
which provides the sort of things you see in the gifs of the readme of https://github.com/charmbracelet/gum
Actually, I'd be potentially up for collaborating on something like that.
Mason Protter said:
Here's how I'd approach it: [snip]
@eval module $(gensym(:Scratch)) end)
[snip]
This seems to work well, thanks.
Bonus complication: how hard would it be to make packages available in the current Main
available in the created module too?
You can take a look at the Infiltrator.jl source code. I'm doing a lot of stuff with anon modules and binding-imports/exports in there
Timothy said:
Bonus complication: how hard would it be to make packages available in the current
Main
available in the created module too?
You could do something like
let mod = (@eval module $(gensym(:Scratch)) end)
for s ∈ names(Main, all=true)
if getproperty(Main, s) isa Module
@eval mod using $s
end
end
end
But I guess that might cause a problem because there might be a discrepancy if the user did somehting like
using Foo: bar
instead of plain using Foo
@Timothy you might find https://github.com/NHDaly/DeepcopyModules.jl helpful
Thanks Mason, you've been wonderfully helpful to me as of late :hug:
Last updated: Dec 28 2024 at 04:38 UTC