Skip to content

Commit b5e6ea3

Browse files
committed
Improved terminfo parser
This bundles up the following changes: - Rejiged TermInfo struct - Read the extended terminfo table using the same method as the non-extended table - Use signed integer types for most numeric values, as per term(5) - More robust get(::TermInfo, ...) methods - Better match the terminfo(5) "Fetching Compiled Descriptions" behaviour
1 parent 00ca93c commit b5e6ea3

File tree

2 files changed

+108
-79
lines changed

2 files changed

+108
-79
lines changed

base/terminfo.jl

Lines changed: 100 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ particular capabilities, solely based on `term(5)`.
1515
1616
- `names::Vector{String}`: The names this terminal is known by.
1717
- `flags::BitVector`: A list of 0–$(length(TERM_FLAGS)) flag values.
18-
- `numbers::Union{Vector{UInt16}, Vector{UInt32}}`: A list of 0–$(length(TERM_NUMBERS))
18+
- `numbers::Union{Vector{Int16}, Vector{Int32}}`: A list of 0–$(length(TERM_NUMBERS))
1919
number values. A value of `typemax(eltype(numbers))` is used to skip over
2020
unspecified capabilities while ensuring value indices are correct.
2121
- `strings::Vector{Union{String, Nothing}}`: A list of 0–$(length(TERM_STRINGS))
@@ -30,9 +30,9 @@ See also: `TermInfo` and `TermCapability`.
3030
struct TermInfoRaw
3131
names::Vector{String}
3232
flags::BitVector
33-
numbers::Union{Vector{UInt16}, Vector{UInt32}}
33+
numbers::Vector{Int}
3434
strings::Vector{Union{String, Nothing}}
35-
extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}}
35+
extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String, Nothing}}}
3636
end
3737

3838
"""
@@ -59,31 +59,27 @@ See also: `TermInfoRaw` and `TermCapability`.
5959
"""
6060
struct TermInfo
6161
names::Vector{String}
62-
flags::Int
63-
numbers::BitVector
64-
strings::BitVector
65-
extensions::Vector{Symbol}
66-
capabilities::Dict{Symbol, Union{Bool, Int, String}}
62+
flags::Dict{Symbol, Bool}
63+
numbers::Dict{Symbol, Int}
64+
strings::Dict{Symbol, String}
65+
extensions::Union{Nothing, Set{Symbol}}
6766
end
6867

69-
TermInfo() = TermInfo([], 0, [], [], [], Dict())
68+
TermInfo() = TermInfo([], Dict(), Dict(), Dict(), nothing)
7069

7170
function read(data::IO, ::Type{TermInfoRaw})
7271
# Parse according to `term(5)`
7372
# Header
7473
magic = read(data, UInt16) |> ltoh
7574
NumInt = if magic == 0o0432
76-
UInt16
75+
Int16
7776
elseif magic == 0o01036
78-
UInt32
77+
Int32
7978
else
8079
throw(ArgumentError("Terminfo data did not start with the magic number 0o0432 or 0o01036"))
8180
end
82-
name_bytes = read(data, UInt16) |> ltoh
83-
flag_bytes = read(data, UInt16) |> ltoh
84-
numbers_count = read(data, UInt16) |> ltoh
85-
string_count = read(data, UInt16) |> ltoh
86-
table_bytes = read(data, UInt16) |> ltoh
81+
name_bytes, flag_bytes, numbers_count, string_count, table_bytes =
82+
@ntuple 5 _->read(data, Int16) |> ltoh
8783
# Terminal Names
8884
term_names = split(String(read(data, name_bytes - 1)), '|') .|> String
8985
0x00 == read(data, UInt8) ||
@@ -95,57 +91,73 @@ function read(data::IO, ::Type{TermInfoRaw})
9591
throw(ArgumentError("Terminfo did not contain a null byte after the flag section, expected to position the start of the numbers section on an even byte"))
9692
end
9793
# Numbers, Strings, Table
98-
numbers = reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))) .|> ltoh
99-
string_indices = reinterpret(UInt16, read(data, string_count * sizeof(UInt16))) .|> ltoh
94+
numbers = reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))) .|> ltoh .|> Int
95+
string_indices = reinterpret(Int16, read(data, string_count * sizeof(Int16))) .|> ltoh
10096
strings_table = read(data, table_bytes)
101-
strings = map(string_indices) do idx
102-
if idx (0xffff, 0xfffe)
103-
len = findfirst(==(0x00), view(strings_table, 1+idx:length(strings_table)))
104-
!isnothing(len) ||
105-
throw(ArgumentError("Terminfo string table entry does not terminate with a null byte"))
106-
String(strings_table[1+idx:idx+len-1])
107-
end
108-
end
97+
strings = _terminfo_read_strings(strings_table, string_indices)
10998
TermInfoRaw(term_names, flags, numbers, strings,
11099
if !eof(data) extendedterminfo(data; NumInt) end)
111100
end
112101

113102
"""
114-
extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}})
103+
extendedterminfo(data::IO; NumInt::Union{Type{Int16}, Type{Int32}})
115104
116105
Read an extended terminfo section from `data`, with `NumInt` as the numbers type.
117106
118107
This will accept any terminfo content that conforms with `term(5)`.
119108
120109
See also: `read(::IO, ::Type{TermInfoRaw})`
121110
"""
122-
function extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}})
111+
function extendedterminfo(data::IO; NumInt::Union{Type{Int16}, Type{Int32}})
123112
# Extended info
124113
if position(data) % 2 != 0
125114
0x00 == read(data, UInt8) ||
126-
throw(ArgumentError("Terminfo did not contain a null byte before the extended section, expected to position the start on an even byte"))
115+
throw(ArgumentError("Terminfo did not contain a null byte before the extended section; expected to position the start on an even byte"))
127116
end
128117
# Extended header
129-
flag_bytes = read(data, UInt16) |> ltoh
130-
numbers_count = read(data, UInt16) |> ltoh
131-
string_count = read(data, UInt16) |> ltoh
132-
table_count = read(data, UInt16) |> ltoh
133-
table_bytes = read(data, UInt16) |> ltoh
118+
flag_bytes, numbers_count, string_count, table_count, table_bytes =
119+
@ntuple 5 _->read(data, Int16) |> ltoh
134120
# Extended flags/numbers/strings
135121
flags = read(data, flag_bytes) .== 0x01
136122
if flag_bytes % 2 != 0
137123
0x00 == read(data, UInt8) ||
138-
throw(ArgumentError("Terminfo did not contain a null byte after the extended flag section, expected to position the start of the numbers section on an even byte"))
124+
throw(ArgumentError("Terminfo did not contain a null byte after the extended flag section; expected to position the start of the numbers section on an even byte"))
139125
end
140-
numbers = reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))) .|> ltoh
141-
table_indices = reinterpret(UInt16, read(data, table_count * sizeof(UInt16))) .|> ltoh
142-
table_strings = [String(readuntil(data, 0x00)) for _ in 1:length(table_indices)]
143-
strings = table_strings[1:string_count]
144-
labels = Symbol.(table_strings[string_count+1:end])
145-
Dict{Symbol, Union{Bool, Int, String}}(
126+
numbers = reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))) .|> ltoh .|> Int
127+
table_indices = reinterpret(Int16, read(data, table_count * sizeof(Int16))) .|> ltoh
128+
table_data = read(data, table_bytes)
129+
strings = _terminfo_read_strings(table_data, table_indices[1:string_count])
130+
table_halfoffset = Int16(get(table_indices, string_count, 0) +
131+
ncodeunits(something(get(strings, length(strings), ""), "")) + 1)
132+
labels = _terminfo_read_strings(table_data, table_halfoffset .+ table_indices[string_count+1:end]) |>
133+
Vector{String} .|> Symbol
134+
Dict{Symbol, Union{Bool, Int, String, Nothing}}(
146135
labels .=> vcat(flags, numbers, strings))
147136
end
148137

138+
"""
139+
_terminfo_read_strings(table::Vector{UInt8}, indices::Vector{Int16})
140+
141+
From `table`, read a string starting at each position in `indices`. Each string
142+
must be null-terminated. Should an index be -1 or -2, `nothing` is given instead
143+
of a string.
144+
"""
145+
function _terminfo_read_strings(table::Vector{UInt8}, indices::Vector{Int16})
146+
strings = Vector{Union{Nothing, String}}(undef, length(indices))
147+
map!(strings, indices) do idx
148+
if idx >= 0
149+
len = findfirst(==(0x00), view(table, 1+idx:length(table)))
150+
!isnothing(len) ||
151+
throw(ArgumentError("Terminfo table entry @$idx does not terminate with a null byte"))
152+
String(table[1+idx:idx+len-1])
153+
elseif idx (-1, -2)
154+
else
155+
throw(ArgumentError("Terminfo table index is invalid: -2 ≰ $idx"))
156+
end
157+
end
158+
strings
159+
end
160+
149161
"""
150162
TermInfo(raw::TermInfoRaw)
151163
@@ -155,45 +167,60 @@ NCurses 6.3, see `TERM_FLAGS`, `TERM_NUMBERS`, and `TERM_STRINGS`).
155167
function TermInfo(raw::TermInfoRaw)
156168
capabilities = Dict{Symbol, Union{Bool, Int, String}}()
157169
sizehint!(capabilities, 2 * (length(raw.flags) + length(raw.numbers) + length(raw.strings)))
170+
flags = Dict{Symbol, Bool}()
171+
numbers = Dict{Symbol, Int}()
172+
strings = Dict{Symbol, String}()
173+
extensions = nothing
158174
for (flag, value) in zip(TERM_FLAGS, raw.flags)
159-
capabilities[flag.short] = value
160-
capabilities[flag.long] = value
175+
flags[flag.short] = value
176+
flags[flag.long] = value
161177
end
162178
for (num, value) in zip(TERM_NUMBERS, raw.numbers)
163-
if value != typemax(eltype(raw.numbers))
164-
capabilities[num.short] = Int(value)
165-
capabilities[num.long] = Int(value)
166-
end
179+
numbers[num.short] = Int(value)
180+
numbers[num.long] = Int(value)
167181
end
168182
for (str, value) in zip(TERM_STRINGS, raw.strings)
169183
if !isnothing(value)
170-
capabilities[str.short] = value
171-
capabilities[str.long] = value
184+
strings[str.short] = value
185+
strings[str.long] = value
172186
end
173187
end
174-
extensions = if !isnothing(raw.extended)
175-
capabilities = merge(capabilities, raw.extended)
176-
keys(raw.extended) |> collect
177-
else
178-
Symbol[]
188+
if !isnothing(raw.extended)
189+
extensions = Set{Symbol}()
190+
for (key, value) in raw.extended
191+
push!(extensions, key)
192+
if value isa Bool
193+
flags[key] = value
194+
elseif value isa Int
195+
numbers[key] = value
196+
elseif value isa String
197+
strings[key] = value
198+
end
199+
end
179200
end
180-
TermInfo(raw.names, length(raw.flags),
181-
raw.numbers .!= typemax(eltype(raw.numbers)),
182-
map(!isnothing, raw.strings),
183-
extensions, capabilities)
201+
TermInfo(raw.names, flags, numbers, strings, extensions)
202+
end
203+
204+
get(ti::TermInfo, key::Symbol, default::Bool) = get(ti.flags, key, default)
205+
get(ti::TermInfo, key::Symbol, default::Int) = get(ti.numbers, key, default)
206+
get(ti::TermInfo, key::Symbol, default::String) = get(ti.strings, key, default)
207+
208+
haskey(ti::TermInfo, key::Symbol) =
209+
haskey(ti.flags, key) || haskey(ti.numbers, key) || haskey(ti.strings, key)
210+
211+
function getindex(ti::TermInfo, key::Symbol)
212+
haskey(ti.flags, key) && return ti.flags[key]
213+
haskey(ti.numbers, key) && return ti.numbers[key]
214+
haskey(ti.strings, key) && return ti.strings[key]
215+
throw(KeyError(key))
184216
end
185217

186-
getindex(ti::TermInfo, key::Symbol) = ti.capabilities[key]
187-
get(ti::TermInfo, key::Symbol, default::D) where D<:Union{Bool, Int, String} =
188-
get(ti.capabilities, key, default)::D
189-
get(ti::TermInfo, key::Symbol, default) = get(ti.capabilities, key, default)
190-
keys(ti::TermInfo) = keys(ti.capabilities)
191-
haskey(ti::TermInfo, key::Symbol) = haskey(ti.capabilities, key)
218+
keys(ti::TermInfo) = keys(ti.flags) keys(ti.numbers) keys(ti.strings)
192219

193220
function show(io::IO, ::MIME"text/plain", ti::TermInfo)
194-
print(io, "TermInfo(", ti.names, "; ", ti.flags, " flags, ",
195-
sum(ti.numbers), " numbers, ", sum(ti.strings), " strings")
196-
!isempty(ti.extensions) > 0 &&
221+
print(io, "TermInfo(", ti.names, "; ", length(ti.flags), " flags, ",
222+
length(ti.numbers), " numbers, ", length(ti.strings), " strings")
223+
!isnothing(ti.extensions) &&
197224
print(io, ", ", length(ti.extensions), " extended capabilities")
198225
print(io, ')')
199226
end
@@ -213,13 +240,15 @@ function find_terminfo_file(term::String)
213240
[ENV["TERMINFO"]]
214241
elseif isdir(joinpath(homedir(), ".terminfo"))
215242
[joinpath(homedir(), ".terminfo")]
216-
elseif haskey(ENV, "TERMINFO_DIRS")
217-
split(ENV["TERMINFO_DIRS"], ':')
218-
elseif Sys.isunix()
219-
["/usr/share/terminfo"]
220243
else
221244
String[]
222245
end
246+
haskey(ENV, "TERMINFO_DIRS") &&
247+
append!(terminfo_dirs,
248+
replace(split(ENV["TERMINFO_DIRS"], ':'),
249+
"" => "/usr/share/terminfo"))
250+
Sys.isunix() &&
251+
push!(terminfo_dirs, "/etc/terminfo", "/usr/share/terminfo")
223252
for dir in terminfo_dirs
224253
if isfile(joinpath(dir, chr, term))
225254
return joinpath(dir, chr, term)

test/terminfo.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -889,20 +889,20 @@ let
889889
@testset "terminfo" begin
890890
dumb = Base.TermInfo(read(IOBuffer(dumb_terminfo), Base.TermInfoRaw))
891891
@test dumb.names == ["dumb", "80-column dumb tty"]
892-
@test dumb.flags == 2
893-
@test dumb.numbers == [true]
894-
@test dumb.extensions == Symbol[]
895-
@test length(dumb.capabilities) == 14
892+
@test length(dumb.flags) == 4
893+
@test length(dumb.numbers) == 2
894+
@test length(dumb.strings) == 8
895+
@test isnothing(dumb.extensions)
896896
for (key, value) in dumb_capabilities
897897
@test dumb[key] == value
898898
end
899899

900900
xterm = Base.TermInfo(read(IOBuffer(xterm_terminfo), Base.TermInfoRaw))
901901
@test xterm.names == ["xterm", "xterm terminal emulator (X Window System)"]
902-
@test xterm.flags == 38
903-
@test xterm.numbers == Bool[1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
904-
@test sort(xterm.extensions) == sort(xterm_extensions)
905-
@test length(xterm.capabilities) == 519
902+
@test length(xterm.flags) == 78
903+
@test length(xterm.numbers) == 29
904+
@test length(xterm.strings) == 432
905+
@test sort(xterm.extensions |> collect) == sort(xterm_extensions)
906906
for (key, value) in xterm_capabilities
907907
@test xterm[key] == value
908908
end

0 commit comments

Comments
 (0)