Created
May 28, 2025 18:36
-
-
Save MilesCranmer/0b530cf4602905d548acdfb3bb54ded0 to your computer and use it in GitHub Desktop.
Julia startup file with expanded shell mode compatibility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ~/.julia/config/startup.jl | |
### REPL CUSTOMIZATION | |
isinteractive() && VERSION < v"1.13.0-" && @eval begin | |
""" | |
ShellSpecification{is_windows, shell} | |
A type used for dispatch to select the appropriate shell command preparation logic. | |
It is parameterized by `is_windows::Bool` indicating the operating system, | |
and `shell::Symbol` representing the basename of the shell executable. | |
""" | |
struct ShellSpecification{is_windows,shell} end | |
""" | |
prepare_shell_command(spec::ShellSpecification, cmd::Cmd) -> Cmd | |
prepare_shell_command(spec::ShellSpecification, raw_string::String) -> Cmd | |
Returns a `Cmd` object configured for execution according to `spec`, | |
using the provided `cmd` (parsed command) or `raw_string` (original input). | |
Specialized methods for `ShellSpecification` define shell- and OS-specific behavior. | |
Define `Base.needs_cmd(::ShellSpecification)` to `false` for shells that do not require a `Cmd` as input. | |
They will then be passed the raw string instead. | |
""" | |
function prepare_shell_command(::ShellSpecification{true,shell}, cmd) where {shell} | |
return cmd | |
end | |
function prepare_shell_command(::ShellSpecification{false,shell}, cmd) where {shell} | |
shell_escape_cmd = "$(Base.shell_escape_posixly(cmd)) && true" | |
return `$shell -c $shell_escape_cmd` | |
end | |
function prepare_shell_command(::ShellSpecification{false,:fish}, cmd) | |
shell_escape_cmd = "begin; $(Base.shell_escape_posixly(cmd)); and true; end" | |
return `fish -c $shell_escape_cmd` | |
end | |
function prepare_shell_command(::ShellSpecification{false,:nu}, raw_string) | |
return `nu -c $raw_string` | |
end | |
function prepare_shell_command(::ShellSpecification{true,:cmd}, cmd) | |
return Cmd(`cmd /s /c $(string('"', cmd, '"'))`; windows_verbatim=true) | |
end | |
function prepare_shell_command(::ShellSpecification{true,:powershell}, cmd) | |
return `powershell -Command $cmd` | |
end | |
function prepare_shell_command(::ShellSpecification{true,:pwsh}, cmd) | |
return `pwsh -Command $cmd` | |
end | |
function prepare_shell_command(::ShellSpecification{true,:busybox}, cmd) | |
return `busybox sh -c $(shell_escape_posixly(cmd))` | |
end | |
""" | |
needs_cmd(::ShellSpecification) -> Bool | |
This trait is used to determine if the shell specification requires `Cmd` as input. | |
Setting this to `false` for a shell can help avoid specific parsing errors. | |
""" | |
needs_cmd(::ShellSpecification) = true | |
needs_cmd(::ShellSpecification{false,:nu}) = false | |
""" | |
is_cd_cmd(::ShellSpecification, cmd::Cmd) -> Bool | |
is_cd_cmd(::ShellSpecification, cmd::String) -> Bool | |
Determines if a command is a `cd` command. Overload this for | |
shells that have a different syntax for `cd`. | |
""" | |
is_cd_cmd(::ShellSpecification, cmd::Cmd) = cmd.exec[1] == "cd" | |
is_cd_cmd(::ShellSpecification{true,:cmd}, cmd::Cmd) = cmd.exec[1] in ("cd", "chdir") | |
is_cd_cmd(::ShellSpecification{true,:busybox}, cmd::Cmd) = cmd.exec[1] == "cd" | |
is_cd_cmd(::ShellSpecification{true,:powershell}, cmd::Cmd) = cmd.exec[1] ∈ ("Set-Location", "cd", "sl", "chdir") | |
is_cd_cmd(::ShellSpecification{true,:pwsh}, cmd::Cmd) = cmd.exec[1] ∈ ("Set-Location", "cd", "sl", "chdir") | |
is_cd_cmd(::ShellSpecification, cmd::String) = false # Safe default | |
function is_cd_cmd(::ShellSpecification{false,:nu}, raw_string::String) | |
startswith(strip(raw_string), "cd") | |
end | |
# function is_cd_cmd(::ShellSpecification{true,:powershell}, raw_string::String) | |
# any(prefix -> startswith(raw_string, prefix), ("Set-Location", "cd", "sl", "chdir")) | |
# end | |
# function is_cd_cmd(::ShellSpecification{true,:pwsh}, raw_string::String) | |
# any(prefix -> startswith(raw_string, prefix), ("Set-Location", "cd", "sl", "chdir")) | |
# end | |
function pre_repl_cmd(raw_string, parsed, out) | |
is_windows = Sys.iswindows() | |
shell_path = Base.shell_split(get(ENV, "JULIA_SHELL", is_windows ? "cmd" : get(ENV, "SHELL", "/bin/sh"))) | |
shell_name = Base.basename(shell_path[1]) | |
normalized_shell_name = is_windows ? lowercase(splitext(shell_name)[1]) : shell_name | |
shell_spec = ShellSpecification{is_windows,Symbol(normalized_shell_name)}() | |
if needs_cmd(shell_spec) | |
cmd = Base.cmd_gen(parsed) | |
return repl_cmd(shell_spec, cmd, parsed, out) | |
else | |
return repl_cmd(shell_spec, raw_string, parsed, out) | |
end | |
end | |
function repl_cmd(shell_spec, cmd::Cmd, parsed, out) | |
cmd.exec .= expanduser.(cmd.exec) | |
if isempty(cmd.exec) | |
throw(ArgumentError("no cmd to execute")) | |
end | |
if is_cd_cmd(shell_spec, cmd) | |
return repl_cd_cmd(shell_spec, cmd, parsed, out) | |
end | |
return repl_cmd_execute(shell_spec, cmd, out) | |
end | |
function repl_cmd(shell_spec, raw_string::String, parsed, out) | |
if is_cd_cmd(shell_spec, raw_string) | |
return repl_cd_cmd(shell_spec, raw_string, parsed, out) | |
end | |
return repl_cmd_execute(shell_spec, raw_string, out) | |
end | |
function repl_cmd_execute(shell_spec, cmd_or_string, out) | |
prepared_cmd = prepare_shell_command(shell_spec, cmd_or_string) | |
try | |
run(ignorestatus(prepared_cmd)) | |
catch | |
# Windows doesn't shell out right now (complex issue), so Julia tries to run the program itself | |
# Julia throws an exception if it can't find the program, but the stack trace isn't useful | |
lasterr = current_exceptions() | |
lasterr = ExceptionStack([(exception=e[1], backtrace=[]) for e in lasterr]) | |
invokelatest(display_error, lasterr) | |
end | |
return nothing | |
end | |
""" | |
repl_cd_cmd(shell_spec::ShellSpecification, cmd, parsed, out) | |
Parses a `cd` command and executes it. Overload this for | |
shells that have a different syntax for `cd`. | |
""" | |
function repl_cd_cmd(::ShellSpecification, cmd, _, out) | |
if length(cmd.exec) > 2 | |
throw(ArgumentError("cd method only takes one argument")) | |
elseif length(cmd.exec) == 2 | |
dir = cmd.exec[2] | |
if dir == "-" | |
if !haskey(ENV, "OLDPWD") | |
error("cd: OLDPWD not set") | |
end | |
dir = ENV["OLDPWD"] | |
end | |
else | |
dir = homedir() | |
end | |
try | |
ENV["OLDPWD"] = pwd() | |
catch ex | |
ex isa IOError || rethrow() | |
# if current dir has been deleted, then pwd() will throw an IOError: pwd(): no such file or directory (ENOENT) | |
delete!(ENV, "OLDPWD") | |
end | |
cd(dir) | |
return println(out, pwd()) | |
end | |
function repl_cd_cmd(spec::ShellSpecification{false,:nu}, _, parsed, out) | |
return repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out) | |
end | |
# function repl_cd_cmd(spec::ShellSpecification{true,:powershell}, _, parsed, out) | |
# return repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out) | |
# end | |
# function repl_cd_cmd(spec::ShellSpecification{true,:pwsh}, _, parsed, out) | |
# return repl_cd_cmd(spec, Base.cmd_gen(parsed), parsed, out) | |
# end | |
using REPL: | |
Prompt, SHELL_PROMPT, ShellCompletionProvider, respond, | |
outstream, REPL, position, edit_insert, LineEdit | |
@async begin | |
# This needs to be `@async`, because it has to run | |
# AFTER the REPL is initialized, and can't do it | |
# during `atreplinit` (since the interface gets overwritten!) | |
while true | |
if ( | |
isdefined(Base, :active_repl) && | |
isdefined(Base.active_repl, :interface) && | |
isdefined(Base.active_repl.interface, :modes) && | |
!isempty(Base.active_repl.interface.modes) | |
) | |
break | |
end | |
sleep(0.001) | |
end | |
repl = Base.active_repl | |
julia_prompt = repl.interface.modes[1] | |
old_shell_prompt = repl.interface.modes[2] | |
hascolor = true | |
new_shell_prompt = Prompt( | |
SHELL_PROMPT; | |
prompt_prefix=hascolor ? repl.shell_color : "", | |
prompt_suffix=if hascolor | |
(repl.envcolors ? Base.input_color : repl.input_color) | |
else | |
"" | |
end, | |
repl=repl, | |
complete=ShellCompletionProvider(), | |
on_done=respond(repl, julia_prompt) do line | |
return Expr( | |
:call, | |
pre_repl_cmd, | |
line::String, | |
Base.shell_parse(line, false)[1], | |
outstream(repl), | |
) | |
end, | |
keymap_dict=old_shell_prompt.keymap_dict, | |
hist=old_shell_prompt.hist, | |
on_enter=old_shell_prompt.on_enter, | |
sticky=true, | |
) | |
repl.interface.modes[2] = new_shell_prompt | |
julia_on_enter = julia_prompt.on_done | |
julia_prompt.keymap_dict[';'] = function (s, rest...) | |
if isempty(s) || position(LineEdit.buffer(s)) == 0 | |
buf = copy(LineEdit.buffer(s)) | |
LineEdit.transition(s, new_shell_prompt) do | |
LineEdit.state(s, new_shell_prompt).input_buffer = buf | |
end | |
else | |
edit_insert(s, ';') | |
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s) | |
end | |
end | |
end | |
end ### END REPL CUSTOMIZATION |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment