swh:1:snp:a72e953ecd624a7df6e6196bbdd05851996c5e40
Tip revision: 594e6ef6a6043b312338f45e13d266f7ce0c0855 authored by d-netto on 19 October 2023, 15:57:59 UTC
Introduce an interface for external GC backends
Introduce an interface for external GC backends
Tip revision: 594e6ef
terminfo.jl
# This file is a part of Julia. License is MIT: https://julialang.org/license
# Since this code is in the startup-path, we go to some effort to
# be easier on the compiler, such as using `map` over broadcasting.
include("terminfo_data.jl")
"""
struct TermInfoRaw
A structured representation of a terminfo file, without any knowledge of
particular capabilities, solely based on `term(5)`.
!!! warning
This is not part of the public API, and thus subject to change without notice.
# Fields
- `names::Vector{String}`: The names this terminal is known by.
- `flags::BitVector`: A list of 0–$(length(TERM_FLAGS)) flag values.
- `numbers::Union{Vector{Int16}, Vector{Int32}}`: A list of 0–$(length(TERM_NUMBERS))
number values. A value of `typemax(eltype(numbers))` is used to skip over
unspecified capabilities while ensuring value indices are correct.
- `strings::Vector{Union{String, Nothing}}`: A list of 0–$(length(TERM_STRINGS))
string values. A value of `nothing` is used to skip over unspecified
capabilities while ensuring value indices are correct.
- `extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}}`: Should an
extended info section exist, this gives the entire extended info as a
dictionary. Otherwise `nothing`.
See also: `TermInfo` and `TermCapability`.
"""
struct TermInfoRaw
names::Vector{String}
flags::BitVector
numbers::Vector{Int}
strings::Vector{Union{String, Nothing}}
extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String, Nothing}}}
end
"""
struct TermInfo
A parsed terminfo paired with capability information.
!!! warning
This is not part of the public API, and thus subject to change without notice.
# Fields
- `names::Vector{String}`: The names this terminal is known by.
- `flags::Int`: The number of flags specified.
- `numbers::BitVector`: A mask indicating which of `TERM_NUMBERS` have been
specified.
- `strings::BitVector`: A mask indicating which of `TERM_STRINGS` have been
specified.
- `extensions::Vector{Symbol}`: A list of extended capability variable names.
- `capabilities::Dict{Symbol, Union{Bool, Int, String}}`: The capability values
themselves.
See also: `TermInfoRaw` and `TermCapability`.
"""
struct TermInfo
names::Vector{String}
flags::Dict{Symbol, Bool}
numbers::Dict{Symbol, Int}
strings::Dict{Symbol, String}
extensions::Union{Nothing, Set{Symbol}}
end
TermInfo() = TermInfo([], Dict(), Dict(), Dict(), nothing)
function read(data::IO, ::Type{TermInfoRaw})
# Parse according to `term(5)`
# Header
magic = read(data, UInt16) |> ltoh
NumInt = if magic == 0o0432
Int16
elseif magic == 0o01036
Int32
else
throw(ArgumentError("Terminfo data did not start with the magic number 0o0432 or 0o01036"))
end
name_bytes, flag_bytes, numbers_count, string_count, table_bytes =
@ntuple 5 _->read(data, Int16) |> ltoh
# Terminal Names
term_names = map(String, split(String(read(data, name_bytes - 1)), '|'))
0x00 == read(data, UInt8) ||
throw(ArgumentError("Terminfo data did not contain a null byte after the terminal names section"))
# Boolean Flags
flags = map(==(0x01), read(data, flag_bytes))
if position(data) % 2 != 0
0x00 == read(data, UInt8) ||
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"))
end
# Numbers, Strings, Table
numbers = map(Int ∘ ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
string_indices = map(ltoh, reinterpret(Int16, read(data, string_count * sizeof(Int16))))
strings_table = read(data, table_bytes)
strings = _terminfo_read_strings(strings_table, string_indices)
TermInfoRaw(term_names, flags, numbers, strings,
if !eof(data) extendedterminfo(data, NumInt) end)
end
"""
extendedterminfo(data::IO; NumInt::Union{Type{Int16}, Type{Int32}})
Read an extended terminfo section from `data`, with `NumInt` as the numbers type.
This will accept any terminfo content that conforms with `term(5)`.
See also: `read(::IO, ::Type{TermInfoRaw})`
"""
function extendedterminfo(data::IO, NumInt::Union{Type{Int16}, Type{Int32}})
# Extended info
if position(data) % 2 != 0
0x00 == read(data, UInt8) ||
throw(ArgumentError("Terminfo did not contain a null byte before the extended section; expected to position the start on an even byte"))
end
# Extended header
flag_bytes, numbers_count, string_count, table_count, table_bytes =
@ntuple 5 _->read(data, Int16) |> ltoh
# Extended flags/numbers/strings
flags = map(==(0x01), read(data, flag_bytes))
if flag_bytes % 2 != 0
0x00 == read(data, UInt8) ||
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"))
end
numbers = map(Int ∘ ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
table_indices = map(ltoh, reinterpret(Int16, read(data, table_count * sizeof(Int16))))
table_data = read(data, table_bytes)
strings = _terminfo_read_strings(table_data, table_indices[1:string_count])
table_halfoffset = Int16(get(table_indices, string_count, 0) +
ncodeunits(something(get(strings, length(strings), ""), "")) + 1)
for index in string_count+1:lastindex(table_indices)
table_indices[index] += table_halfoffset
end
labels = map(Symbol, _terminfo_read_strings(table_data, table_indices[string_count+1:end]))
Dict{Symbol, Union{Bool, Int, String, Nothing}}(
zip(labels, Iterators.flatten((flags, numbers, strings))))
end
"""
_terminfo_read_strings(table::Vector{UInt8}, indices::Vector{Int16})
From `table`, read a string starting at each position in `indices`. Each string
must be null-terminated. Should an index be -1 or -2, `nothing` is given instead
of a string.
"""
function _terminfo_read_strings(table::Vector{UInt8}, indices::Vector{Int16})
strings = Vector{Union{Nothing, String}}(undef, length(indices))
map!(strings, indices) do idx
if idx >= 0
len = findfirst(==(0x00), view(table, 1+idx:length(table)))
!isnothing(len) ||
throw(ArgumentError("Terminfo table entry @$idx does not terminate with a null byte"))
String(table[1+idx:idx+len-1])
elseif idx ∈ (-1, -2)
else
throw(ArgumentError("Terminfo table index is invalid: -2 ≰ $idx"))
end
end
strings
end
"""
TermInfo(raw::TermInfoRaw)
Construct a `TermInfo` from `raw`, using known terminal capabilities (as of
NCurses 6.3, see `TERM_FLAGS`, `TERM_NUMBERS`, and `TERM_STRINGS`).
"""
function TermInfo(raw::TermInfoRaw)
capabilities = Dict{Symbol, Union{Bool, Int, String}}()
sizehint!(capabilities, 2 * (length(raw.flags) + length(raw.numbers) + length(raw.strings)))
flags = Dict{Symbol, Bool}()
numbers = Dict{Symbol, Int}()
strings = Dict{Symbol, String}()
extensions = nothing
for (flag, value) in zip(TERM_FLAGS, raw.flags)
flags[flag.short] = value
flags[flag.long] = value
end
for (num, value) in zip(TERM_NUMBERS, raw.numbers)
numbers[num.short] = Int(value)
numbers[num.long] = Int(value)
end
for (str, value) in zip(TERM_STRINGS, raw.strings)
if !isnothing(value)
strings[str.short] = value
strings[str.long] = value
end
end
if !isnothing(raw.extended)
extensions = Set{Symbol}()
for (key, value) in raw.extended
push!(extensions, key)
if value isa Bool
flags[key] = value
elseif value isa Int
numbers[key] = value
elseif value isa String
strings[key] = value
end
end
end
TermInfo(raw.names, flags, numbers, strings, extensions)
end
get(ti::TermInfo, key::Symbol, default::Bool) = get(ti.flags, key, default)
get(ti::TermInfo, key::Symbol, default::Int) = get(ti.numbers, key, default)
get(ti::TermInfo, key::Symbol, default::String) = get(ti.strings, key, default)
haskey(ti::TermInfo, key::Symbol) =
haskey(ti.flags, key) || haskey(ti.numbers, key) || haskey(ti.strings, key)
function getindex(ti::TermInfo, key::Symbol)
haskey(ti.flags, key) && return ti.flags[key]
haskey(ti.numbers, key) && return ti.numbers[key]
haskey(ti.strings, key) && return ti.strings[key]
throw(KeyError(key))
end
keys(ti::TermInfo) = keys(ti.flags) ∪ keys(ti.numbers) ∪ keys(ti.strings)
function show(io::IO, ::MIME"text/plain", ti::TermInfo)
print(io, "TermInfo(", ti.names, "; ", length(ti.flags), " flags, ",
length(ti.numbers), " numbers, ", length(ti.strings), " strings")
!isnothing(ti.extensions) &&
print(io, ", ", length(ti.extensions), " extended capabilities")
print(io, ')')
end
"""
find_terminfo_file(term::String)
Locate the terminfo file for `term`, return `nothing` if none could be found.
The lookup policy is described in `terminfo(5)` "Fetching Compiled
Descriptions".
"""
function find_terminfo_file(term::String)
isempty(term) && return
chr, chrcode = string(first(term)), string(Int(first(term)), base=16)
terminfo_dirs = if haskey(ENV, "TERMINFO")
[ENV["TERMINFO"]]
elseif isdir(joinpath(homedir(), ".terminfo"))
[joinpath(homedir(), ".terminfo")]
else
String[]
end
haskey(ENV, "TERMINFO_DIRS") &&
append!(terminfo_dirs,
replace(split(ENV["TERMINFO_DIRS"], ':'),
"" => "/usr/share/terminfo"))
Sys.isunix() &&
push!(terminfo_dirs, "/etc/terminfo", "/lib/terminfo", "/usr/share/terminfo")
for dir in terminfo_dirs
if isfile(joinpath(dir, chr, term))
return joinpath(dir, chr, term)
elseif isfile(joinpath(dir, chrcode, term))
return joinpath(dir, chrcode, term)
end
end
end
"""
load_terminfo(term::String)
Load the `TermInfo` for `term`, falling back on a blank `TermInfo`.
"""
function load_terminfo(term::String)
file = find_terminfo_file(term)
isnothing(file) && return TermInfo()
try
TermInfo(read(file, TermInfoRaw))
catch err
if err isa ArgumentError || err isa IOError
TermInfo()
else
rethrow()
end
end
end
"""
The terminfo of the current terminal.
"""
current_terminfo::TermInfo = TermInfo()
# Legacy/TTY methods and the `:color` parameter
if Sys.iswindows()
ttyhascolor(term_type = nothing) = true
else
function ttyhascolor(term_type = get(ENV, "TERM", ""))
startswith(term_type, "xterm") ||
haskey(current_terminfo, :setaf)
end
end
"""
ttyhastruecolor()
Return a boolean signifying whether the current terminal supports 24-bit colors.
This uses the `COLORTERM` environment variable if possible, returning true if it
is set to either `"truecolor"` or `"24bit"`.
As a fallback, first on unix systems the `colors` terminal capability is checked
— should more than 256 colors be reported, this is taken to signify 24-bit
support.
"""
function ttyhastruecolor()
get(ENV, "COLORTERM", "") ∈ ("truecolor", "24bit") ||
@static if Sys.isunix()
get(current_terminfo, :colors, 0) > 256
else
false
end
end
function get_have_color()
global have_color
have_color === nothing && (have_color = ttyhascolor())
return have_color::Bool
end
function get_have_truecolor()
global have_truecolor
have_truecolor === nothing && (have_truecolor = ttyhastruecolor())
return have_truecolor::Bool
end
in(key_value::Pair{Symbol,Bool}, ::TTY) = key_value.first === :color && key_value.second === get_have_color()
haskey(::TTY, key::Symbol) = key === :color
getindex(::TTY, key::Symbol) = key === :color ? get_have_color() : throw(KeyError(key))
get(::TTY, key::Symbol, default) = key === :color ? get_have_color() : default