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 15 commits into
base: master
Choose a base branch
from
Draft

This file was deleted.

This file was deleted.

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
5 changes: 4 additions & 1 deletion doc/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[deps.REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[deps.Random]]
Expand All @@ -111,6 +111,9 @@ uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
[[deps.Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

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

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
2 changes: 1 addition & 1 deletion pkgimage.mk
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ $(eval $(call stdlib_builder,InteractiveUtils,Markdown))
# 3-depth packages
$(eval $(call stdlib_builder,LibGit2_jll,MbedTLS_jll LibSSH2_jll Artifacts Libdl))
$(eval $(call stdlib_builder,LibCURL_jll,LibSSH2_jll nghttp2_jll MbedTLS_jll Zlib_jll Artifacts Libdl))
$(eval $(call stdlib_builder,REPL,InteractiveUtils Markdown Sockets Unicode))
$(eval $(call stdlib_builder,REPL,InteractiveUtils Markdown Sockets StyledStrings Unicode))
$(eval $(call stdlib_builder,SharedArrays,Distributed Mmap Random Serialization))
$(eval $(call stdlib_builder,TOML,Dates))
$(eval $(call stdlib_builder,Test,Logging Random Serialization InteractiveUtils))
Expand Down
1 change: 1 addition & 0 deletions stdlib/REPL/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ version = "1.11.0"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[extras]
Expand Down
128 changes: 51 additions & 77 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ module LineEdit
import ..REPL
using REPL: AbstractREPL, Options

import StyledStrings: Face, loadface!, @styled_str

using ..Terminals
import ..Terminals: raw!, width, height, cmove, getX,
getY, 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 @@ -43,15 +46,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 @@ -189,36 +186,31 @@ complete_line(c::CompletionProvider, s, ::Module) = complete_line(c, s)
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

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 @@ -516,7 +508,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 @@ -614,11 +606,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 @@ -874,7 +865,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 @@ -1532,27 +1523,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 @@ -1585,13 +1570,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 @@ -2058,7 +2036,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 @@ -2632,7 +2610,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 @@ -2647,8 +2625,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 @@ -2680,11 +2658,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 @@ -2693,8 +2667,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