-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathStickyMessages.jl
148 lines (137 loc) · 5.58 KB
/
StickyMessages.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""
StickyMessages(io::IO; ansi_codes=io isa Base.TTY && !Sys.iswindows())
A `StickyMessages` type manages the display of a set of persistent "sticky"
messages in a terminal. That is, messages which are not part of the normal
scrolling output. Each message is identified by a label and may may be added to
the set using `push!(messages, label=>msg)`, and removed using
`pop!(messages, label)`, or `empty!()`.
Only a single StickyMessages object should be associated with a given TTY, as
the object manipulates the terminal scrolling region.
"""
mutable struct StickyMessages
io::IO
# Bool for controlling TTY escape codes.
ansi_codes::Bool
# Messages is just used as a (short) OrderedDict here
messages::Vector{Pair{Any,String}}
end
function StickyMessages(io::IO; ansi_codes=io isa Base.TTY &&
# Give up on Windows for now, as libuv doesn't recognize the scroll region code.
# Will need to be fixed in libuv and thence julia, but first libuv PR 1884 should merge. See
# https://github.com/libuv/libuv/pull/1884
# https://github.com/JuliaLang/libuv/commit/ed3700c849289ed01fe04273a7bf865340b2bd7e
!Sys.iswindows())
sticky = StickyMessages(io, ansi_codes, Vector{Pair{Any,String}}())
# Ensure we clean up the terminal
finalizer(sticky) do sticky
# See also empty!()
if !sticky.ansi_codes
return
end
prev_nlines = _countlines(sticky.messages)
if prev_nlines > 0
# Clean up sticky lines. Hack: must be async to do the IO outside
# of finalizer. Proper fix would be an uninstall event triggered by
# with_logger.
@async showsticky(sticky.io, prev_nlines, [])
end
end
sticky
end
function firstline!(sticky::StickyMessages, label)
idx = findfirst(m -> m[1] == label, sticky.messages)
idx === nothing && throw(KeyError(label))
idx == 1 && return
sticky.messages[idx], sticky.messages[1] = sticky.messages[1], sticky.messages[idx]
return
end
# Count newlines in a message or sequence of messages
_countlines(msg::String) = sum(c->c=='\n', msg)
_countlines(messages) = length(messages) > 0 ? sum(_countlines, messages) : 0
_countlines(messages::Vector{<:Pair}) = _countlines(m[2] for m in messages)
# Selected TTY cursor and screen control via ANSI codes
# * https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
# * See man terminfo on linux, eg `tput csr $row1 $row2` and `tput cup $row $col`
# * For windows see https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
change_scroll_region!(io, rows::Pair) = write(io, "\e[$(rows[1]);$(rows[2])r")
change_cursor_line!(io, line::Integer) = write(io, "\e[$line;1H")
clear_to_end!(io) = write(io, "\e[J")
function showsticky(io, prev_nlines, messages)
height,_ = displaysize(io)
iob = IOBuffer()
if prev_nlines > 0
change_cursor_line!(iob, height + 1 - prev_nlines)
clear_to_end!(iob)
end
# Set scroll region to the first N lines.
#
# Terminal scrollback buffers seem to be populated with a heuristic which
# relies on the scrollable region starting at the first row, so to have
# normal scrollback work we have to position sticky messages at the bottom
# of the screen.
linesrequired = _countlines(messages)
if prev_nlines < linesrequired
# Scroll screen up to preserve the lines which we will overwrite
change_cursor_line!(iob, height - prev_nlines)
write(iob, "\n"^(linesrequired-prev_nlines))
end
if prev_nlines != linesrequired
change_scroll_region!(iob, 1=>height-linesrequired)
end
# Write messages. Avoid writing \n of last message to kill extra scrolling
if !isempty(messages)
change_cursor_line!(iob, height + 1 - linesrequired)
for i = 1:length(messages)-1
write(iob, messages[i][2])
end
write(iob, chop(messages[end][2]))
end
# TODO: Ideally we'd query the terminal for the line it was on before doing
# all this and restore it if it's not in the new non-scrollable region.
change_cursor_line!(iob, height - max(prev_nlines, linesrequired))
# Write in one block to make the whole operation as atomic as possible.
write(io, take!(iob))
nothing
end
function Base.push!(sticky::StickyMessages, message::Pair; first=true)
if !sticky.ansi_codes
write(sticky.io, message[2])
return
end
label,text = message
endswith(text, '\n') || (text *= '\n';)
prev_nlines = _countlines(sticky.messages)
idx = findfirst(m->m[1] == label, sticky.messages)
if idx === nothing
if first
insert!(sticky.messages, 1, label=>text)
else
push!(sticky.messages, label=>text)
end
else
if first
sticky.messages[idx] = sticky.messages[1]
sticky.messages[1] = label=>text
else
sticky.messages[idx] = label=>text
end
end
showsticky(sticky.io, prev_nlines, sticky.messages)
end
function Base.pop!(sticky::StickyMessages, label)
sticky.ansi_codes || return
idx = findfirst(m->m[1] == label, sticky.messages)
if idx !== nothing
prev_nlines = _countlines(sticky.messages)
deleteat!(sticky.messages, idx)
showsticky(sticky.io, prev_nlines, sticky.messages)
end
nothing
end
function Base.empty!(sticky::StickyMessages)
sticky.ansi_codes || return
prev_nlines = _countlines(sticky.messages)
empty!(sticky.messages)
showsticky(sticky.io, prev_nlines, sticky.messages) # Resets scroll region
nothing
end