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

JuliaLang / julia / #37745

11 Apr 2024 03:54AM UTC coverage: 87.241% (+5.8%) from 81.402%
#37745

push

local

web-flow
Fix comparison base for line table compression (#54032)

I'm not entirely sure what the original intent of this statement was,
but the effect ends up being that some codeloc entries end up negative
in the compressed representation, but the code always assumes unsigned
integers, so things roundtripped badly, leading to badly corrupted stack
traces. I guess this might have been a rebase mistake,
since the same line exists (correctly) a few lines prior. Fixes #54031.

75950 of 87058 relevant lines covered (87.24%)

15852930.37 hits per line

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

70.06
/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) = ""
2✔
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))
69✔
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
13✔
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(default_terminal(), m; kwargs...)
×
180

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

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

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

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

206
            if c == Int(ARROW_UP)
51✔
207
                cursor[] = move_up!(m, cursor[], lastoption)
20✔
208
            elseif c == Int(ARROW_DOWN)
41✔
209
                cursor[] = move_down!(m, cursor[], lastoption)
18✔
210
            elseif c == Int(PAGE_UP)
23✔
211
                cursor[] = page_up!(m, cursor[], lastoption)
×
212
            elseif c == Int(PAGE_DOWN)
23✔
213
                cursor[] = page_down!(m, cursor[], lastoption)
×
214
            elseif c == Int(HOME_KEY)
23✔
215
                cursor[] = 1
×
216
                m.pageoffset = 0
×
217
            elseif c == Int(END_KEY)
23✔
218
                cursor[] = lastoption
×
219
                m.pageoffset = max(0, lastoption - m.pagesize)
×
220
            elseif c == 13 # <enter>
23✔
221
                # will break if pick returns true
222
                pick(m, cursor[]) && break
14✔
223
            elseif c == UInt32('q')
9✔
224
                cancel(m)
×
225
                break
×
226
            elseif c == 3 # ctrl-c
9✔
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
9✔
232
            end
233

234
            if !suppress_output
38✔
235
                state = printmenu(term.out_stream, m, cursor[], oldstate=state)
×
236
            end
237
        end
51✔
238
    finally # always disable raw mode
239
        if raw_mode_enabled
13✔
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)
13✔
245

246
    return selected(m)
13✔
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(default_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))
2✔
264
    if cursor > 1
14✔
265
        cursor -= 1 # move selection up
1✔
266
        if cursor < (2+m.pageoffset) && m.pageoffset > 0
1✔
267
            m.pageoffset -= 1 # scroll page up
×
268
        end
269
    elseif scroll_wrap(m)
11✔
270
        # wrap to bottom
271
        cursor = lastoption
1✔
272
        m.pageoffset = max(0, lastoption - m.pagesize)
1✔
273
    end
274
    return cursor
12✔
275
end
276

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

292
function page_up!(m::AbstractMenu, cursor::Int, lastoption::Int=numoptions(m))
2✔
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)
4✔
296
    m.pageoffset = max(m.pageoffset, 0)
2✔
297
    return max(cursor - m.pagesize, 1)
2✔
298
end
299

300
function page_down!(m::AbstractMenu, cursor::Int, lastoption::Int=numoptions(m))
2✔
301
    m.pageoffset += m.pagesize - (cursor == 1 ? 1 : 0)
4✔
302
    m.pageoffset = max(0, min(m.pageoffset, lastoption - m.pagesize))
2✔
303
    return min(cursor + m.pagesize, lastoption)
2✔
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)
62✔
322
    # TODO Julia 2.0?: get rid of `init` and just use `oldstate`
323
    buf = IOBuffer()
31✔
324
    lastoption = numoptions(m)::Int
31✔
325
    ncleared = oldstate === nothing ? m.pagesize-1 : oldstate
31✔
326

327
    if init
31✔
328
        # like clamp, except this takes the min if max < min
329
        m.pageoffset = max(0, min(cursoridx - m.pagesize รท 2, lastoption - m.pagesize))
19✔
330
    else
331
        print(buf, "\r")
12✔
332
        if ncleared > 0
12✔
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")
12✔
338
        end
339
    end
340

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

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

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

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

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

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

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

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

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

383
    print(out, String(take!(buf)))
62✔
384

385
    return newstate
31✔
386
end
387

388
scroll_wrap(m::ConfiguredMenu) = scroll_wrap(m.config)
10✔
389
scroll_wrap(c::AbstractConfig) = scroll_wrap(c.config)
3✔
390
scroll_wrap(c::Config) = c.scroll_wrap
10✔
391
scroll_wrap(::AbstractMenu) = CONFIG[:scroll_wrap]::Bool
9✔
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)
7✔
399
up_arrow(c::AbstractConfig) = up_arrow(c.config)
×
400
up_arrow(c::Config) = c.up_arrow
7✔
401
up_arrow(::AbstractMenu) = CONFIG[:up_arrow]::Char
×
402

403
down_arrow(m::ConfiguredMenu) = down_arrow(m.config)
8✔
404
down_arrow(c::AbstractConfig) = down_arrow(c.config)
1✔
405
down_arrow(c::Config) = c.down_arrow
8✔
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) : ' ', ' ')
211✔
414
cursor(c::AbstractConfig) = cursor(c.config)
9✔
415
cursor(c::Config) = c.cursor
31✔
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

© 2026 Coveralls, Inc