Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use StyledStrings for REPL prompt styling #51887

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
f4e49cee0253a6d00f1d057cad2d373a
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
487c8c9f0138710165e207dc198376417b312a4f5d91a8616c5093938a2b314afd2a4fb72ea687a9cdc7ddfac246dde793a5c2e60ce465851f7c83194ae6f7d8
3 changes: 3 additions & 0 deletions doc/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
version = "1.10.0"

[[deps.StyledStrings]]
uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b"

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
1 change: 1 addition & 0 deletions pkgimage.mk
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@
clean:
rm -rf $(JULIA_DEPOT_PATH)/compiled
rm -f $(BUILDDIR)/stdlib/*.image

Check warning on line 38 in pkgimage.mk

View workflow job for this annotation

GitHub Actions / Check whitespace

Whitespace check

no trailing newline

Check warning on line 38 in pkgimage.mk

View workflow job for this annotation

GitHub Actions / Check whitespace

Whitespace check

trailing blank lines

Check warning on line 38 in pkgimage.mk

View workflow job for this annotation

GitHub Actions / Check whitespace

Whitespace check

trailing whitespace
128 changes: 51 additions & 77 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ module LineEdit
import ..REPL
using ..REPL: AbstractREPL, Options

import StyledStrings: Face, loadface!, @styled_str

using ..Terminals
import ..Terminals: raw!, width, height, clear_line, beep

import Base: ensureroom, show, AnyDict, position
import Base: ensureroom, show, AnyDict, position,
AnnotatedString, annotations
using Base: something

using InteractiveUtils: InteractiveUtils
Expand Down Expand Up @@ -42,15 +45,9 @@ end

mutable struct Prompt <: TextInterface
# A string or function to be printed as the prompt.
prompt::Union{String,Function}
# A string or function to be printed before the prompt. May not change the length of the prompt.
# This may be used for changing the color, issuing other terminal escape codes, etc.
prompt_prefix::Union{String,Function}
# Same as prefix except after the prompt
prompt_suffix::Union{String,Function}
output_prefix::Union{String,Function}
output_prefix_prefix::Union{String,Function}
output_prefix_suffix::Union{String,Function}
prompt::Union{AnnotatedString{String},String,Function}
# used for things like IPython mode
output_prefix::Union{AnnotatedString{String},String,Function}
keymap_dict::Dict{Char,Any}
repl::Union{AbstractREPL,Nothing}
complete::CompletionProvider
Expand Down Expand Up @@ -190,36 +187,31 @@ complete_line(c::CompletionProvider, s, ::Module; hint::Bool=false) = complete_l
terminal(s::IO) = s
terminal(s::PromptState) = s.terminal


function beep(s::PromptState, duration::Real=options(s).beep_duration,
blink::Real=options(s).beep_blink,
maxduration::Real=options(s).beep_maxduration;
colors=options(s).beep_colors,
blink::Real=options(s).beep_blink,
maxduration::Real=options(s).beep_maxduration;
beep_face=options(s).beep_face,
use_current::Bool=options(s).beep_use_current)
isinteractive() || return # some tests fail on some platforms
s.beeping = min(s.beeping + duration, maxduration)
let colors = Base.copymutable(colors)
errormonitor(@async begin
trylock(s.refresh_lock) || return
try
orig_prefix = s.p.prompt_prefix
use_current && push!(colors, prompt_string(orig_prefix))
i = 0
while s.beeping > 0.0
prefix = colors[mod1(i+=1, end)]
s.p.prompt_prefix = prefix
refresh_multi_line(s, beeping=true)
sleep(blink)
s.beeping -= blink
end
s.p.prompt_prefix = orig_prefix
refresh_multi_line(s, beeping=true)
s.beeping = 0.0
finally
unlock(s.refresh_lock)
errormonitor(@async begin
trylock(s.refresh_lock) || return
try
og_prompt = s.p.prompt
beep_prompt = styled"{$beep_face:$(prompt_string(og_prompt))}"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tecosaur : do you have feedback on this approach, or guidance for a better one? For context, the problem here is that the prompt string needs to be styled with the repl_prompt_beep face for a short period (to indicate a non-action to the user, like when pressing backspace at the beginning of a prompt)

I initially tried to use withfaces, but realized that doing so would require overriding all prompt faces (:repl_prompt_*), and doing so would:

  1. make future modifications slightly more fragile and
  2. exclude user-defined REPL modes.

Thus, my solution here is to create a new styled string which wraps the prompt string in a new style, $beep_face, and then restores the original prompt afterward.

Copy link
Contributor

@tecosaur tecosaur Feb 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faces applied later take priority, so applying repl_prompt_face around should do the trick nicely I think 🙂

image

I'm curious though, is there any particular reason why beep_face is a variable and you don't just use repl_prompt_beep?

Copy link
Author

@caleb-allen caleb-allen Feb 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thank you for the direction 👍

is there any particular reason why beep_face is a variable and you don't just use repl_prompt_beep

I think my original intent was to maintain the pre-existing logic for the case when color was disabled. Essentially what was removed here:

https://github.com/JuliaLang/julia/pull/51887/files#diff-730f90e0bf9ca018477a1d4a52e0d7398af2bb8cf9e401568fd86690eec81bb7L499

Perhaps a variable like this isn't needed at all, now that StyledStrings is used? In other words, the original variable existed so that the terminal's color state could be carried through to the point of printing the beep control sequences (or not), but is no longer necessary (I think?) because it is precisely the problem which StyledStrings solves. Does that sound right @tecosaur?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so 😀.

If we're interpreting this correctly, a chunk of this code was originally needed because of the lack of composable styling, but that's a feature that StyledStrings has OOTB.

That said, there's also the complication that it seems like beep_colors is designed so that it could hypothetically cycle through a rainbow of colours? This seems rather strange though. Similarly, there are other aspects that just don't make sense to me, like beep_use_current. I don't understand when you'd want it to be false, and I went through all 104 results for that symbol in GitHub's site-wide search, and couldn't come across any code that set it to anything but true 😕.

The first (of two) commit I could find with it was 1767963 (6 years ago), and that's not introducing it but replacing a global const BEEP_USE_CURRENT = Ref(true). That const itself came from 8c2fc37 just a few months earlier, and the PR that introduces it has this for explanation:

I like it sober, so it blinks once by default, alternating with :light_black color (can be customized).

I think with the benefit of hindsight I'm inclined to view the addition of five variables to control the behaviour of the prompt "beep" as an enthusiastic addition which isn't retrospectively worth the complication it adds, given I haven't heard of anyone customising it previously, nor have I been able to find any examples by searching GitHub.

@rfourquet perhaps you might have something to add here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're able to reduce the number of beep customisations, we could potentially reduce the main logic to just something like this (untested):

try
    orig_prompt = s.p.prompt
    prompt_str = prompt_string(orig_prompt)
    isbeep = false
    while s.beeping > 0.0
        s.p.prompt = if (isbeep = !isbeep)
            styled"{repl_beep:$prompt_str}"
        else prompt_str end
        refresh_multi_line(s, beeping=true)
        sleep(blink)
        s.beeping -= blink
    end
    s.p.prompt = orig_prompt
    refresh_multi_line(s, beeping=true)
    s.beeping = 0.0
finally
    unlock(s.refresh_lock)
end

s.p.prompt = beep_prompt
refresh_multi_line(s, beeping=true)
while s.beeping > 0.0
sleep(blink)
s.beeping -= blink
end
end)
end
s.p.prompt = og_prompt
refresh_multi_line(s, beeping=true)
s.beeping = 0.0
finally
unlock(s.refresh_lock)
end
end)
nothing
end

Expand Down Expand Up @@ -530,7 +522,7 @@ function refresh_multi_line(termbuf::TerminalBuffer, terminal::UnixTerminal, buf
regstart, regstop = region(buf)
written = 0
# Write out the prompt string
lindent = write_prompt(termbuf, prompt, hascolor(terminal))::Int
lindent = write_prompt(termbuf, prompt)::Int
# Count the '\n' at the end of the line if the terminal emulator does (specific to DOS cmd prompt)
miscountnl = @static Sys.iswindows() ? (isa(Terminals.pipe_reader(terminal), Base.TTY) && !(Base.ispty(Terminals.pipe_reader(terminal)))::Bool) : false

Expand Down Expand Up @@ -628,11 +620,10 @@ function highlight_region(lwrite::Union{String,SubString{String}}, regstart::Int
end

function refresh_multi_line(terminal::UnixTerminal, args...; kwargs...)
outbuf = IOBuffer()
termbuf = TerminalBuffer(outbuf)
termbuf = TerminalBuffer(terminal)
ret = refresh_multi_line(termbuf, terminal, args...;kwargs...)
# Output the entire refresh at once
write(terminal, take!(outbuf))
write(terminal, take!(termbuf))
flush(terminal)
return ret
end
Expand Down Expand Up @@ -918,7 +909,7 @@ function edit_insert(s::PromptState, c::StringLike)
termbuf = terminal(s)
w = width(termbuf)
offset = s.ias.curs_row == 1 || s.indent < 0 ?
sizeof(prompt_string(s.p.prompt)::String) : s.indent
sizeof(prompt_string(s.p.prompt)::Union{String,AnnotatedString}) : s.indent
offset += position(buf) - beginofline(buf) # size of current line
spinner = '\0'
delayup = !eof(buf) || old_wait
Expand Down Expand Up @@ -1582,27 +1573,21 @@ refresh_line(s::BufferLike, termbuf::AbstractTerminal) = refresh_multi_line(term
default_completion_cb(::IOBuffer) = []
default_enter_cb(_) = true

write_prompt(terminal::AbstractTerminal, s::PromptState, color::Bool) = write_prompt(terminal, s.p, color)
function write_prompt(terminal::AbstractTerminal, p::Prompt, color::Bool)
prefix = prompt_string(p.prompt_prefix)
suffix = prompt_string(p.prompt_suffix)
write(terminal, prefix)
color && write(terminal, Base.text_colors[:bold])
width = write_prompt(terminal, p.prompt, color)
color && write(terminal, Base.text_colors[:normal])
write(terminal, suffix)
return width
end

function write_output_prefix(io::IO, p::Prompt, color::Bool)
prefix = prompt_string(p.output_prefix_prefix)
suffix = prompt_string(p.output_prefix_suffix)
print(io, prefix)
color && write(io, Base.text_colors[:bold])
width = write_prompt(io, p.output_prefix, color)
color && write(io, Base.text_colors[:normal])
print(io, suffix)
return width
write_prompt(terminal::AbstractTerminal, s::PromptState) = write_prompt(terminal, s.p)

# returns the width of the written prompt
function write_prompt(io::IO, p::Union{AbstractString,Function,Prompt})
@static Sys.iswindows() && _reset_console_mode()
promptstr = prompt_string(p)::AbstractString
write(io, promptstr)
return textwidth(promptstr)
end

function write_output_prefix(io::IO, p::Prompt)
@static Sys.iswindows() && _reset_console_mode()
promptstr = prompt_string(p.output_prefix)::String
write(io, promptstr)
return textwidth(promptstr)
end

# On Windows, when launching external processes, we cannot control what assumption they make on the
Expand Down Expand Up @@ -1635,13 +1620,6 @@ function _reset_console_mode()
end
end

# returns the width of the written prompt
function write_prompt(terminal::Union{IO, AbstractTerminal}, s::Union{AbstractString,Function}, color::Bool)
@static Sys.iswindows() && _reset_console_mode()
promptstr = prompt_string(s)::String
write(terminal, promptstr)
return textwidth(promptstr)
end

### Keymap Support

Expand Down Expand Up @@ -2108,7 +2086,7 @@ end

input_string(s::PrefixSearchState) = String(take!(copy(s.response_buffer)))

write_prompt(terminal, s::PrefixSearchState, color::Bool) = write_prompt(terminal, s.histprompt.parent_prompt, color)
write_prompt(terminal, s::PrefixSearchState) = write_prompt(terminal, s.histprompt.parent_prompt)
prompt_string(s::PrefixSearchState) = prompt_string(s.histprompt.parent_prompt.prompt)

terminal(s::PrefixSearchState) = s.terminal
Expand Down Expand Up @@ -2682,7 +2660,7 @@ end
activate(m::ModalInterface, s::MIState, termbuf::AbstractTerminal, term::TextTerminal) =
activate(mode(s), s, termbuf, term)

commit_changes(t::UnixTerminal, termbuf::TerminalBuffer) = (write(t, take!(termbuf.out_stream)); nothing)
commit_changes(t::UnixTerminal, termbuf::TerminalBuffer) = (write(t, take!(termbuf)); nothing)

function transition(f::Function, s::MIState, newmode::Union{TextInterface,Symbol})
cancel_beep(s)
Expand All @@ -2697,8 +2675,8 @@ function transition(f::Function, s::MIState, newmode::Union{TextInterface,Symbol
if !haskey(s.mode_state, newmode)
s.mode_state[newmode] = init_state(terminal(s), newmode)
end
termbuf = TerminalBuffer(IOBuffer())
t = terminal(s)
termbuf = TerminalBuffer(t)
s.mode_state[mode(s)] = deactivate(mode(s), state(s), termbuf, t)
s.current_mode = newmode
f()
Expand Down Expand Up @@ -2730,11 +2708,7 @@ const default_keymap_dict = keymap([default_keymap, escape_defaults])

function Prompt(prompt
;
prompt_prefix = "",
prompt_suffix = "",
output_prefix = "",
output_prefix_prefix = "",
output_prefix_suffix = "",
keymap_dict = default_keymap_dict,
repl = nothing,
complete = EmptyCompletionProvider(),
Expand All @@ -2743,8 +2717,8 @@ function Prompt(prompt
hist = EmptyHistoryProvider(),
sticky = false)

return Prompt(prompt, prompt_prefix, prompt_suffix, output_prefix, output_prefix_prefix, output_prefix_suffix,
keymap_dict, repl, complete, on_enter, on_done, hist, sticky)
return Prompt(prompt, output_prefix, keymap_dict, repl, complete, on_enter,
on_done, hist, sticky)
end

run_interface(::Prompt) = nothing
Expand Down
63 changes: 15 additions & 48 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ import Base:
display,
show,
AnyDict,
==
==,
AnnotatedString

_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}

Expand Down Expand Up @@ -154,10 +155,10 @@ include("Pkg_beforeload.jl")

answer_color(::AbstractREPL) = ""

const JULIA_PROMPT = "julia> "
const PKG_PROMPT = "pkg> "
const SHELL_PROMPT = "shell> "
const HELP_PROMPT = "help?> "
const JULIA_PROMPT = styled"{repl_prompt_julia:julia> }"
const PKG_PROMPT = styled"{repl_prompt_pkg:pkg> }"
const SHELL_PROMPT = styled"{repl_prompt_shell:shell> }"
const HELP_PROMPT = styled"{repl_prompt_help:help?> }"

mutable struct REPLBackend
"channel for AST"
Expand Down Expand Up @@ -556,7 +557,7 @@ function display(d::REPLDisplay, mime::MIME"text/plain", x)
mistate = d.repl.mistate
mode = LineEdit.mode(mistate)
if mode isa LineEdit.Prompt
LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
LineEdit.write_output_prefix(io, mode)
end
end
get(io, :color, false)::Bool && write(io, answer_color(d.repl))
Expand Down Expand Up @@ -761,12 +762,6 @@ end
mutable struct LineEditREPL <: AbstractREPL
t::TextTerminal
hascolor::Bool
prompt_color::String
input_color::String
answer_color::String
shell_color::String
help_color::String
pkg_color::String
history_file::Bool
in_shell::Bool
in_help::Bool
Expand All @@ -779,13 +774,12 @@ mutable struct LineEditREPL <: AbstractREPL
interface::ModalInterface
backendref::REPLBackendRef
frontend_task::Task
function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
function LineEditREPL(t,hascolor,history_file,in_shell,in_help,envcolors)
opts = Options()
opts.hascolor = hascolor
if !hascolor
opts.beep_colors = [""]
end
new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
new(t,hascolor,history_file,in_shell,in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
# updated invocation from master:
# new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
end
end
Expand All @@ -796,15 +790,8 @@ terminal(r::LineEditREPL) = r.t
hascolor(r::LineEditREPL) = r.hascolor

LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
LineEditREPL(t, hascolor,
hascolor ? Base.text_colors[:green] : "",
hascolor ? Base.input_color() : "",
hascolor ? Base.answer_color() : "",
hascolor ? Base.text_colors[:red] : "",
hascolor ? Base.text_colors[:yellow] : "",
hascolor ? Base.text_colors[:blue] : "",
false, false, false, envcolors
)
LineEditREPL(t, hascolor, false, false, false, envcolors)
# TODO caleb-allen 10/21/2024 validate this call post-rebase

mutable struct REPLCompletionProvider <: CompletionProvider
modifiers::LineEdit.Modifiers
Expand Down Expand Up @@ -1253,11 +1240,11 @@ repl_filename(repl, hp) = "REPL"
const JL_PROMPT_PASTE = Ref(true)
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v

function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
function contextual_prompt(repl::LineEditREPL, prompt::Union{AnnotatedString,Function})
function ()
mod = Base.active_module(repl)
prefix = mod == Main ? "" : string('(', mod, ") ")
pr = prompt isa String ? prompt : prompt()
pr = prompt isa AnnotatedString ? prompt : prompt()
prefix * pr
end
end
Expand Down Expand Up @@ -1308,19 +1295,12 @@ function setup_interface(

# Set up the main Julia prompt
julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
# Copy colors from the prompt object
prompt_prefix = hascolor ? repl.prompt_color : "",
prompt_suffix = hascolor ?
(repl.envcolors ? Base.input_color : repl.input_color) : "",
repl = repl,
complete = replc,
on_enter = return_callback)

# Setup help mode
help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
prompt_prefix = hascolor ? repl.help_color : "",
prompt_suffix = hascolor ?
(repl.envcolors ? Base.input_color : repl.input_color) : "",
repl = repl,
complete = replc,
# When we're done transform the entered line into a call to helpmode function
Expand All @@ -1330,9 +1310,6 @@ function setup_interface(

# Set up shell mode
shell_mode = Prompt(SHELL_PROMPT;
prompt_prefix = hascolor ? repl.shell_color : "",
prompt_suffix = hascolor ?
(repl.envcolors ? Base.input_color : repl.input_color) : "",
repl = repl,
complete = ShellCompletionProvider(),
# Transform "foo bar baz" into `foo bar baz` (shell quoting)
Expand Down Expand Up @@ -1829,19 +1806,9 @@ function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
dopushdisplay = !in(d,Base.Multimedia.displays)
dopushdisplay && pushdisplay(d)
while !eof(repl.stream)::Bool
if have_color
print(repl.stream,repl.prompt_color)
end
print(repl.stream, JULIA_PROMPT)
if have_color
print(repl.stream, input_color(repl))
end
line = readline(repl.stream, keep=true)
if !isempty(line)
ast = Base.parse_input_line(line)
if have_color
print(repl.stream, Base.color_normal)
end
response = eval_with_backend(ast, backend)
print_response(repl, response, !ends_with_semicolon(line), have_color)
end
Expand Down
Loading
Loading