diff --git a/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl b/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl index f283a9c097934..ac35ca710f603 100644 --- a/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl +++ b/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl @@ -26,7 +26,7 @@ Details can be found in # Subtypes -All subtypes must contain the fields `pagesize::Int` and +All subtypes must be mutable, and must contain the fields `pagesize::Int` and `pageoffset::Int`. They must also implement the following functions. ## Necessary Functions @@ -36,7 +36,9 @@ These functions must be implemented for all subtypes of AbstractMenu. - `pick(m::AbstractMenu, cursor::Int)` - `cancel(m::AbstractMenu)` - `options(m::AbstractMenu)` - - `writeline(buf::IOBuffer, m::AbstractMenu, idx::Int, showcursor::Bool)` + - `writeline(buf::IOBuffer, m::AbstractMenu, idx::Int, cursor)` + +If `m` does not have a field called `selected`, then you must also implement `selected(m)`. ## Optional Functions @@ -45,7 +47,8 @@ subtypes. - `header(m::AbstractMenu)` - `keypress(m::AbstractMenu, i::UInt32)` - - `noptions(m::AbstractMenu)` + - `numoptions(m::AbstractMenu)` + - `selected(m::AbstractMenu)` """ abstract type AbstractMenu end @@ -78,28 +81,35 @@ cancel(m::AbstractMenu) = error("unimplemented") Return a list of strings to be displayed as options in the current page. -Alternatively, implement `noptions`, in which case `options` is not needed. +Alternatively, implement `numoptions`, in which case `options` is not needed. """ options(m::AbstractMenu) = error("unimplemented") """ - writeline(buf::IOBuffer, m::AbstractMenu, idx::Int, cursor::Union{Char,Nothing}) + writeline(buf::IOBuffer, m::AbstractMenu, idx::Int, cursor::Bool, indicators::Indicators) -Write the option at index `idx` to the buffer. If `isa(cursor, Char)`, it should be printed. +Write the option at index `idx` to the buffer. If `indicators !== nothing`, the current line +corresponds to the cursor position. The method is responsible for displaying visual indicator(s) +about the state of this menu item; the configured characters are returned in `indicators` +in fields with the following names: + `cursor::Char`: the character used to indicate the cursor position + `checked::String`: a string used to indicate this option has been marked + `unchecked::String`: a string used to indicate this option has not been marked +The latter two are relevant only for menus that support multiple selection. !!! compat "Julia 1.6" `writeline` requires Julia 1.6 or higher. On older versions of Julia, this was `writeLine(buf::IOBuffer, m::AbstractMenu, idx, cursor::Bool)` - and if `cursor` is `true`, the cursor obtained from `TerminalMenus.CONFIG[:cursor]` - should be printed. + and the indicators can be obtained from `TerminalMenus.CONFIG`, a Dict indexed by `Symbol` + keys with the same names as the fields of `indicators`. This older function is supported on all Julia 1.x versions but will be dropped in Julia 2.0. """ -function writeline(buf::IOBuffer, m::AbstractMenu, idx::Int, cursor::Union{Char,Nothing}) +function writeline(buf::IOBuffer, m::AbstractMenu, idx::Int, cursor::Bool, indicators) # error("unimplemented") # TODO: use this in Julia 2.0 - writeLine(buf, m, idx, isa(cursor, Char)) + writeLine(buf, m, idx, cursor) end @@ -111,6 +121,7 @@ end header(m::AbstractMenu) Displays the header above the menu when it is rendered to the screen. +Defaults to "". """ header(m::AbstractMenu) = "" @@ -119,22 +130,31 @@ header(m::AbstractMenu) = "" Send any non-standard keypress event to this function. If `true` is returned, `request()` will exit. +Defaults to `false`. """ keypress(m::AbstractMenu, i::UInt32) = false """ - noptions(m::AbstractMenu) + numoptions(m::AbstractMenu) Return the number of options in menu `m`. Defaults to `length(options(m))`. """ -noptions(m::AbstractMenu) = length(options(m)) +numoptions(m::AbstractMenu) = length(options(m)) +""" + selected(m::AbstractMenu) + +Return information about the user-selected option. Defaults to `m.selected`. +""" +selected(m::AbstractMenu) = m.selected """ request(m::AbstractMenu; cursor=1) -Display the menu and enter interactive mode. Returns `m.selected` which -varies based on menu type. +Display the menu and enter interactive mode. `cursor` indicates the initial item +for the cursor. + +Returns `selected(m)` which varies based on menu type. """ request(m::AbstractMenu; kwargs...) = request(terminal, m; kwargs...) @@ -147,7 +167,7 @@ function request(term::REPL.Terminals.TTYTerminal, m::AbstractMenu; cursor::Int= raw_mode_enabled = REPL.Terminals.raw!(term, true) raw_mode_enabled && print(term.out_stream, "\x1b[?25l") # hide the cursor - lastoption = noptions(m) + lastoption = numoptions(m) try while true c = readkey(term.in_stream) @@ -223,7 +243,7 @@ function request(term::REPL.Terminals.TTYTerminal, m::AbstractMenu; cursor::Int= end println(term.out_stream) - return m.selected + return selected(m) end @@ -255,6 +275,7 @@ function printmenu(out, m::AbstractMenu, cursor::Int; init::Bool=false) CONFIG[:suppress_output] && return buf = IOBuffer() + indicators = Indicators() lines = m.pagesize-1 @@ -274,13 +295,13 @@ function printmenu(out, m::AbstractMenu, cursor::Int; init::Bool=false) if i == firstline && m.pageoffset > 0 print(buf, CONFIG[:up_arrow]) - elseif i == lastline && i != noptions(m) + elseif i == lastline && i != numoptions(m) print(buf, CONFIG[:down_arrow]) else print(buf, " ") end - writeline(buf, m, i, i == cursor ? CONFIG[:cursor] : nothing) + writeline(buf, m, i, i == cursor, indicators) i != lastline && print(buf, "\r\n") end diff --git a/stdlib/REPL/src/TerminalMenus/MultiSelectMenu.jl b/stdlib/REPL/src/TerminalMenus/MultiSelectMenu.jl index 5cf23cd73c16b..b0f02b63df003 100644 --- a/stdlib/REPL/src/TerminalMenus/MultiSelectMenu.jl +++ b/stdlib/REPL/src/TerminalMenus/MultiSelectMenu.jl @@ -87,13 +87,13 @@ function pick(menu::MultiSelectMenu, cursor::Int) return false #break out of the menu end -function writeline(buf::IOBuffer, menu::MultiSelectMenu, idx::Int, cursor::Union{Char,Nothing}) +function writeline(buf::IOBuffer, menu::MultiSelectMenu, idx::Int, cursor::Bool, indicators) # print a ">" on the selected entry - isa(cursor, Char) ? print(buf, cursor ," ") : print(buf, " ") + cursor ? print(buf, indicators.cursor ," ") : print(buf, " ") if idx in menu.selected - print(buf, CONFIG[:checked], " ") + print(buf, indicators.checked, " ") else - print(buf, CONFIG[:unchecked], " ") + print(buf, indicators.unchecked, " ") end print(buf, replace(menu.options[idx], "\n" => "\\n")) diff --git a/stdlib/REPL/src/TerminalMenus/RadioMenu.jl b/stdlib/REPL/src/TerminalMenus/RadioMenu.jl index 8a8f8c857323d..4783f00e78b43 100644 --- a/stdlib/REPL/src/TerminalMenus/RadioMenu.jl +++ b/stdlib/REPL/src/TerminalMenus/RadioMenu.jl @@ -71,9 +71,9 @@ function pick(menu::RadioMenu, cursor::Int) return true #break out of the menu end -function writeline(buf::IOBuffer, menu::RadioMenu, idx::Int, cursor::Union{Char,Nothing}) +function writeline(buf::IOBuffer, menu::RadioMenu, idx::Int, cursor::Bool, indicators) # print a ">" on the selected entry - isa(cursor, Char) ? print(buf, cursor ," ") : print(buf, " ") + cursor ? print(buf, indicators.cursor ," ") : print(buf, " ") print(buf, replace(menu.options[idx], "\n" => "\\n")) end diff --git a/stdlib/REPL/src/TerminalMenus/config.jl b/stdlib/REPL/src/TerminalMenus/config.jl index efd6395faf7c8..9a12226da9ced 100644 --- a/stdlib/REPL/src/TerminalMenus/config.jl +++ b/stdlib/REPL/src/TerminalMenus/config.jl @@ -3,6 +3,14 @@ """global menu configuration parameters""" const CONFIG = Dict{Symbol,Union{Char,String,Bool}}() +struct Indicators + cursor::Char + checked::String + unchecked::String +end +Indicators() = Indicators(CONFIG) +Indicators(settings) = Indicators(settings[:cursor], settings[:checked], settings[:unchecked]) + """ config( ) diff --git a/stdlib/REPL/test/TerminalMenus/multiselect_menu.jl b/stdlib/REPL/test/TerminalMenus/multiselect_menu.jl index 47b867de4dbb8..04a18a81bb1d3 100644 --- a/stdlib/REPL/test/TerminalMenus/multiselect_menu.jl +++ b/stdlib/REPL/test/TerminalMenus/multiselect_menu.jl @@ -23,15 +23,18 @@ CONFIG = TerminalMenus.CONFIG multi_menu = MultiSelectMenu(string.(1:10)) buf = IOBuffer() -TerminalMenus.writeline(buf, multi_menu, 1, true) -@test String(take!(buf)) == string(CONFIG[:cursor], " ", CONFIG[:unchecked], " 1") +TerminalMenus.writeline(buf, multi_menu, 1, true, TerminalMenus.Indicators('@',"c","u")) +@test String(take!(buf)) == "@ u 1" TerminalMenus.config(cursor='+') -TerminalMenus.writeline(buf, multi_menu, 1, true) -@test String(take!(buf)) == string("+ ", CONFIG[:unchecked], " 1") +TerminalMenus.printmenu(buf, multi_menu, 1; init=true) +@test startswith(String(take!(buf)), string("\e[2K + ", CONFIG[:unchecked], " 1")) TerminalMenus.config(charset=:unicode) -TerminalMenus.writeline(buf, multi_menu, 1, true) -@test String(take!(buf)) == string(CONFIG[:cursor], " ", CONFIG[:unchecked], " 1") +push!(multi_menu.selected, 1) +TerminalMenus.printmenu(buf, multi_menu, 2; init=true) +@test startswith(String(take!(buf)), string("\e[2K ", CONFIG[:checked], " 1\r\n\e[2K ", CONFIG[:cursor], " ", CONFIG[:unchecked], " 2")) # Test SDTIN multi_menu = MultiSelectMenu(string.(1:10)) +CONFIG[:suppress_output] = true @test simulate_input(Set([1,2]), multi_menu, :enter, :down, :enter, 'd') +CONFIG[:suppress_output] = false diff --git a/stdlib/REPL/test/TerminalMenus/radio_menu.jl b/stdlib/REPL/test/TerminalMenus/radio_menu.jl index 1c43ebbdb5afa..67d81b89b04e2 100644 --- a/stdlib/REPL/test/TerminalMenus/radio_menu.jl +++ b/stdlib/REPL/test/TerminalMenus/radio_menu.jl @@ -26,15 +26,17 @@ CONFIG = TerminalMenus.CONFIG radio_menu = RadioMenu(string.(1:10)) buf = IOBuffer() -TerminalMenus.writeline(buf, radio_menu, 1, true) -@test String(take!(buf)) == string(CONFIG[:cursor], " 1") +TerminalMenus.writeline(buf, radio_menu, 1, true, TerminalMenus.Indicators('@',"","")) +@test String(take!(buf)) == "@ 1" TerminalMenus.config(cursor='+') -TerminalMenus.writeline(buf, radio_menu, 1, true) -@test String(take!(buf)) == "+ 1" +TerminalMenus.printmenu(buf, radio_menu, 1; init=true) +@test startswith(String(take!(buf)), "\e[2K + 1") TerminalMenus.config(charset=:unicode) -TerminalMenus.writeline(buf, radio_menu, 1, true) -@test String(take!(buf)) == string(CONFIG[:cursor], " 1") +TerminalMenus.printmenu(buf, radio_menu, 2; init=true) +@test startswith(String(take!(buf)), string("\e[2K 1\r\n\e[2K ", CONFIG[:cursor], " 2")) # Test using stdin radio_menu = RadioMenu(string.(1:10)) +CONFIG[:suppress_output] = true @test simulate_input(3, radio_menu, :down, :down, :enter) +CONFIG[:suppress_output] = false diff --git a/stdlib/REPL/test/TerminalMenus/runtests.jl b/stdlib/REPL/test/TerminalMenus/runtests.jl index c747f7874f69b..64bdead8e2ead 100644 --- a/stdlib/REPL/test/TerminalMenus/runtests.jl +++ b/stdlib/REPL/test/TerminalMenus/runtests.jl @@ -4,8 +4,6 @@ import REPL using REPL.TerminalMenus using Test -TerminalMenus.config(suppress_output=true) - function simulate_input(expected, menu::TerminalMenus.AbstractMenu, keys...) keydict = Dict(:up => "\e[A", :down => "\e[B", @@ -24,6 +22,7 @@ end include("radio_menu.jl") include("multiselect_menu.jl") +println("done") # Other test