• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37527

pending completion
#37527

push

local

web-flow
make `IRShow.method_name` inferrable (#49607)

18 of 18 new or added lines in 3 files covered. (100.0%)

68710 of 81829 relevant lines covered (83.97%)

33068903.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/stdlib/REPL/src/TerminalMenus/AbstractMenu.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
"""
4

5
    AbstractMenu
6

7
The supertype for all Menu types.
8

9

10
# Functions
11

12
The following functions can be called on all <:AbstractMenu types.
13
Details can be found in
14

15
## Exported
16

17
  - `request(m::AbstractMenu)`
18
  - `request(msg::AbstractString, m::AbstractMenu)`
19

20
## Hidden
21

22
  - `printmenu(m::AbstractMenu, cursor::Int; init::Bool=false, oldstate=nothing)`
23

24

25
# Subtypes
26

27
All subtypes must be mutable, and must contain the fields `pagesize::Int` and
28
`pageoffset::Int`. They must also implement the following functions.
29

30
## Necessary Functions
31

32
These functions must be implemented for all subtypes of AbstractMenu.
33

34
  - `pick(m::AbstractMenu, cursor::Int)`
35
  - `cancel(m::AbstractMenu)`
36
  - `options(m::AbstractMenu)`   # `numoptions` is an alternative
37
  - `writeline(buf::IO, m::AbstractMenu, idx::Int, iscursor::Bool)`
38

39
If `m` does not have a field called `selected`, then you must also implement `selected(m)`.
40

41
## Optional Functions
42

43
These functions do not need to be implemented for all AbstractMenu
44
subtypes.
45

46
  - `header(m::AbstractMenu)`
47
  - `keypress(m::AbstractMenu, i::UInt32)`
48
  - `numoptions(m::AbstractMenu)`
49
  - `selected(m::AbstractMenu)`
50

51
"""
52
abstract type AbstractMenu end
53

54
function getproperty(m::AbstractMenu, name::Symbol)
×
55
    if name === :pagesize
×
56
        return getfield(m, :pagesize)::Int
×
57
    elseif name === :pageoffset
×
58
        return getfield(m, :pageoffset)::Int
×
59
    end
60
    return getfield(m, name)
×
61
end
62

63

64
# TODO Julia2.0: get rid of parametric intermediate, making it just
65
#   abstract type ConfiguredMenu <: AbstractMenu end
66
# Or perhaps just make all menus ConfiguredMenus
67
# Also consider making `cursor` a mandatory field in the Menu structs
68
# instead of going via the RefValue in `request`.
69
abstract type _ConfiguredMenu{C} <: AbstractMenu end
70
const ConfiguredMenu = _ConfiguredMenu{<:AbstractConfig}
71

72
# NECESSARY FUNCTIONS
73
# These functions must be implemented for all subtypes of AbstractMenu
74
######################################################################
75

76
"""
77
    pick(m::AbstractMenu, cursor::Int)
78

79
Defines what happens when a user presses the Enter key while the menu is open.
80
If `true` is returned, `request()` will exit.
81
`cursor` indexes the position of the selection.
82
"""
83
pick(m::AbstractMenu, cursor::Int) = error("unimplemented")
×
84

85
"""
86
    cancel(m::AbstractMenu)
87

88
Define what happens when a user cancels ('q' or ctrl-c) a menu.
89
`request()` will always exit after calling this function.
90
"""
91
cancel(m::AbstractMenu) = error("unimplemented")
×
92

93
"""
94
    options(m::AbstractMenu)
95

96
Return a list of strings to be displayed as options in the current page.
97

98
Alternatively, implement `numoptions`, in which case `options` is not needed.
99
"""
100
options(m::AbstractMenu) = error("unimplemented")
×
101

102
"""
103
    writeline(buf::IO, m::AbstractMenu, idx::Int, iscursor::Bool)
104

105
Write the option at index `idx` to `buf`. `iscursor`, if `true`, indicates that this
106
item is at the current cursor position (the one that will be selected by hitting "Enter").
107

108
If `m` is a `ConfiguredMenu`, `TerminalMenus` will print the cursor indicator.
109
Otherwise the callee is expected to handle such printing.
110

111
!!! compat "Julia 1.6"
112
    `writeline` requires Julia 1.6 or higher.
113

114
    On older versions of Julia, this was
115
        `writeLine(buf::IO, m::AbstractMenu, idx, iscursor::Bool)`
116
    and `m` is assumed to be unconfigured. The selection and cursor indicators can be
117
    obtained from `TerminalMenus.CONFIG`.
118

119
    This older function is supported on all Julia 1.x versions but will be dropped in Julia 2.0.
120
"""
121
function writeline(buf::IO, m::AbstractMenu, idx::Int, iscursor::Bool)
×
122
    # error("unimplemented")    # TODO: use this in Julia 2.0
123
    writeLine(buf, m, idx, iscursor)
×
124
end
125

126

127
# OPTIONAL FUNCTIONS
128
# These functions do not need to be implemented for all menu types
129
##################################################################
130

131
"""
132
    header(m::AbstractMenu) -> String
133

134
Return a header string to be printed above the menu.
135
Defaults to "".
136
"""
137
header(m::AbstractMenu) = ""
×
138

139
"""
140
    keypress(m::AbstractMenu, i::UInt32) -> Bool
141

142
Handle any non-standard keypress event.
143
If `true` is returned, [`TerminalMenus.request`](@ref) will exit.
144
Defaults to `false`.
145
"""
146
keypress(m::AbstractMenu, i::UInt32) = false
×
147

148
"""
149
    numoptions(m::AbstractMenu) -> Int
150

151
Return the number of options in menu `m`. Defaults to `length(options(m))`.
152

153
!!! compat "Julia 1.6"
154
    This function requires Julia 1.6 or later.
155
"""
156
numoptions(m::AbstractMenu) = length(options(m))
×
157

158
"""
159
    selected(m::AbstractMenu)
160

161
Return information about the user-selected option.
162
By default it returns `m.selected`.
163
"""
164
selected(m::AbstractMenu) = m.selected
×
165

166
"""
167
    request(m::AbstractMenu; cursor=1)
168

169
Display the menu and enter interactive mode. `cursor` indicates the item
170
number used for the initial cursor position. `cursor` can be either an
171
`Int` or a `RefValue{Int}`. The latter is useful for observation and
172
control of the cursor position from the outside.
173

174
Returns `selected(m)`.
175

176
!!! compat "Julia 1.6"
177
    The `cursor` argument requires Julia 1.6 or later.
178
"""
179
request(m::AbstractMenu; kwargs...) = request(terminal, m; kwargs...)
×
180

181
function request(term::REPL.Terminals.TTYTerminal, m::AbstractMenu; cursor::Union{Int, Base.RefValue{Int}}=1, suppress_output=false)
×
182
    if cursor isa Int
×
183
        cursor = Ref(cursor)
×
184
    end
185

186
    state = nothing
×
187
    if !suppress_output
×
188
        state = printmenu(term.out_stream, m, cursor[], init=true)
×
189
    end
190

191
    raw_mode_enabled = try
×
192
        REPL.Terminals.raw!(term, true)
×
193
        true
×
194
    catch err
195
        suppress_output || @warn "TerminalMenus: Unable to enter raw mode: " exception=(err, catch_backtrace())
×
196
        false
×
197
    end
198
    # hide the cursor
199
    raw_mode_enabled && !suppress_output && print(term.out_stream, "\x1b[?25l")
×
200

201
    try
×
202
        while true
×
203
            lastoption = numoptions(m)
×
204
            c = readkey(term.in_stream)
×
205

206
            if c == Int(ARROW_UP)
×
207
                cursor[] = move_up!(m, cursor[], lastoption)
×
208
            elseif c == Int(ARROW_DOWN)
×
209
                cursor[] = move_down!(m, cursor[], lastoption)
×
210
            elseif c == Int(PAGE_UP)
×
211
                cursor[] = page_up!(m, cursor[], lastoption)
×
212
            elseif c == Int(PAGE_DOWN)
×
213
                cursor[] = page_down!(m, cursor[], lastoption)
×
214
            elseif c == Int(HOME_KEY)
×
215
                cursor[] = 1
×
216
                m.pageoffset = 0
×
217
            elseif c == Int(END_KEY)
×
218
                cursor[] = lastoption
×
219
                m.pageoffset = max(0, lastoption - m.pagesize)
×
220
            elseif c == 13 # <enter>
×
221
                # will break if pick returns true
222
                pick(m, cursor[]) && break
×
223
            elseif c == UInt32('q')
×
224
                cancel(m)
×
225
                break
×
226
            elseif c == 3 # ctrl-c
×
227
                cancel(m)
×
228
                ctrl_c_interrupt(m) ? throw(InterruptException()) : break
×
229
            else
230
                # will break if keypress returns true
231
                keypress(m, c) && break
×
232
            end
233

234
            if !suppress_output
×
235
                state = printmenu(term.out_stream, m, cursor[], oldstate=state)
×
236
            end
237
        end
×
238
    finally # always disable raw mode
239
        if raw_mode_enabled
×
240
            !suppress_output && print(term.out_stream, "\x1b[?25h") # unhide cursor
×
241
            REPL.Terminals.raw!(term, false)
×
242
        end
243
    end
244
    !suppress_output && println(term.out_stream)
×
245

246
    return selected(m)
×
247
end
248

249

250
"""
251
    request([term,] msg::AbstractString, m::AbstractMenu)
252

253
Shorthand for `println(msg); request(m)`.
254
"""
255
request(msg::AbstractString, m::AbstractMenu; kwargs...) = request(terminal, msg, m; kwargs...)
×
256

257
function request(term::REPL.Terminals.TTYTerminal, msg::AbstractString, m::AbstractMenu; kwargs...)
×
258
    println(term.out_stream, msg)
×
259
    request(term, m; kwargs...)
×
260
end
261

262

263
function move_up!(m::AbstractMenu, cursor::Int, lastoption::Int=numoptions(m))
×
264
    if cursor > 1
×
265
        cursor -= 1 # move selection up
×
266
        if cursor < (2+m.pageoffset) && m.pageoffset > 0
×
267
            m.pageoffset -= 1 # scroll page up
×
268
        end
269
    elseif scroll_wrap(m)
×
270
        # wrap to bottom
271
        cursor = lastoption
×
272
        m.pageoffset = max(0, lastoption - m.pagesize)
×
273
    end
274
    return cursor
×
275
end
276

277
function move_down!(m::AbstractMenu, cursor::Int, lastoption::Int=numoptions(m))
×
278
    if cursor < lastoption
×
279
        cursor += 1 # move selection down
×
280
        pagepos = m.pagesize + m.pageoffset
×
281
        if pagepos <= cursor && pagepos < lastoption
×
282
            m.pageoffset += 1 # scroll page down
×
283
        end
284
    elseif scroll_wrap(m)
×
285
        # wrap to top
286
        cursor = 1
×
287
        m.pageoffset = 0
×
288
    end
289
    return cursor
×
290
end
291

292
function page_up!(m::AbstractMenu, cursor::Int, lastoption::Int=numoptions(m))
×
293
    # If we're at the bottom, move the page 1 less to move the cursor up from
294
    # the bottom entry, since we try to avoid putting the cursor at bounds.
295
    m.pageoffset -= m.pagesize - (cursor == lastoption ? 1 : 0)
×
296
    m.pageoffset = max(m.pageoffset, 0)
×
297
    return max(cursor - m.pagesize, 1)
×
298
end
299

300
function page_down!(m::AbstractMenu, cursor::Int, lastoption::Int=numoptions(m))
×
301
    m.pageoffset += m.pagesize - (cursor == 1 ? 1 : 0)
×
302
    m.pageoffset = max(0, min(m.pageoffset, lastoption - m.pagesize))
×
303
    return min(cursor + m.pagesize, lastoption)
×
304
end
305

306
"""
307
    printmenu(out, m::AbstractMenu, cursoridx::Int; init::Bool=false, oldstate=nothing) -> newstate
308

309
Display the state of a menu. `init=true` causes `m.pageoffset` to be initialized to start printing at
310
or just above the current cursor location; when `init` is false, the terminal will
311
preserve the current setting of `m.pageoffset` and overwrite the previous display.
312
Returns `newstate`, which can be passed in as `oldstate` on the next call to allow accurate
313
overwriting of the previous display.
314

315
!!! compat "Julia 1.6"
316
    `printmenu` requires Julia 1.6 or higher.
317

318
    On older versions of Julia, this was called `printMenu` and it lacked the `state` argument/return value.
319
    This older function is supported on all Julia 1.x versions but will be dropped in Julia 2.0.
320
"""
321
function printmenu(out::IO, m::AbstractMenu, cursoridx::Int; oldstate=nothing, init::Bool=false)
×
322
    # TODO Julia 2.0?: get rid of `init` and just use `oldstate`
323
    buf = IOBuffer()
×
324
    lastoption = numoptions(m)::Int
×
325
    ncleared = oldstate === nothing ? m.pagesize-1 : oldstate
×
326

327
    if init
×
328
        # like clamp, except this takes the min if max < min
329
        m.pageoffset = max(0, min(cursoridx - m.pagesize รท 2, lastoption - m.pagesize))
×
330
    else
331
        print(buf, "\r")
×
332
        if ncleared > 0
×
333
            # Move up `ncleared` lines. However, moving up zero lines
334
            # is interpreted as one line, so need to do this
335
            # conditionally. (More specifically, the `0` value means
336
            # to use the default, and for move up this is one.)
337
            print(buf, "\x1b[$(ncleared)A")
×
338
        end
339
    end
340

341
    nheaderlines = 0
×
342
    for headerline in split(header(m), "\n", keepempty=false)
×
343
        print(buf, "\x1b[2K", headerline, "\r\n")
×
344
        nheaderlines += 1
×
345
    end
×
346

347
    firstline = m.pageoffset+1
×
348
    lastline = min(m.pagesize+m.pageoffset, lastoption)
×
349

350
    for i in firstline:lastline
×
351
        # clearline
352
        print(buf, "\x1b[2K")
×
353

354
        upscrollable = i == firstline && m.pageoffset > 0
×
355
        downscrollable = i == lastline && i != lastoption
×
356

357
        if upscrollable && downscrollable
×
358
            print(buf, updown_arrow(m)::Union{Char,String})
×
359
        elseif upscrollable
×
360
            print(buf, up_arrow(m)::Union{Char,String})
×
361
        elseif downscrollable
×
362
            print(buf, down_arrow(m)::Union{Char,String})
×
363
        else
364
            print(buf, ' ')
×
365
        end
366

367
        printcursor(buf, m, i == cursoridx)
×
368
        writeline(buf, m, i, i == cursoridx)
×
369

370
        (i != lastline) && print(buf, "\r\n")
×
371
    end
×
372

373
    newstate = nheaderlines + lastline - firstline  # final line doesn't have `\n`
×
374

375
    if newstate < ncleared && oldstate !== nothing
×
376
        # we printed fewer lines than last time. Erase the leftovers.
377
        for i = newstate+1:ncleared
×
378
            print(buf, "\r\n\x1b[2K")
×
379
        end
×
380
        print(buf, "\x1b[$(ncleared-newstate)A")
×
381
    end
382

383
    print(out, String(take!(buf)))
×
384

385
    return newstate
×
386
end
387

388
scroll_wrap(m::ConfiguredMenu) = scroll_wrap(m.config)
×
389
scroll_wrap(c::AbstractConfig) = scroll_wrap(c.config)
×
390
scroll_wrap(c::Config) = c.scroll_wrap
×
391
scroll_wrap(::AbstractMenu) = CONFIG[:scroll_wrap]::Bool
×
392

393
ctrl_c_interrupt(m::ConfiguredMenu) = ctrl_c_interrupt(m.config)
×
394
ctrl_c_interrupt(c::AbstractConfig) = ctrl_c_interrupt(c.config)
×
395
ctrl_c_interrupt(c::Config) = c.ctrl_c_interrupt
×
396
ctrl_c_interrupt(::AbstractMenu) = CONFIG[:ctrl_c_interrupt]::Bool
×
397

398
up_arrow(m::ConfiguredMenu) = up_arrow(m.config)
×
399
up_arrow(c::AbstractConfig) = up_arrow(c.config)
×
400
up_arrow(c::Config) = c.up_arrow
×
401
up_arrow(::AbstractMenu) = CONFIG[:up_arrow]::Char
×
402

403
down_arrow(m::ConfiguredMenu) = down_arrow(m.config)
×
404
down_arrow(c::AbstractConfig) = down_arrow(c.config)
×
405
down_arrow(c::Config) = c.down_arrow
×
406
down_arrow(::AbstractMenu) = CONFIG[:down_arrow]::Char
×
407

408
updown_arrow(m::ConfiguredMenu) = updown_arrow(m.config)
×
409
updown_arrow(c::AbstractConfig) = updown_arrow(c.config)
×
410
updown_arrow(c::Config) = c.updown_arrow
×
411
updown_arrow(::AbstractMenu) = CONFIG[:updown_arrow]::Char
×
412

413
printcursor(buf, m::ConfiguredMenu, iscursor::Bool) = print(buf, iscursor ? cursor(m.config) : ' ', ' ')
×
414
cursor(c::AbstractConfig) = cursor(c.config)
×
415
cursor(c::Config) = c.cursor
×
416
printcursor(buf, ::AbstractMenu, ::Bool) = nothing   # `writeLine` is expected to do the printing (get from CONFIG[:cursor])
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc