|
| 1 | +# This file is a part of Julia. License is MIT: https://julialang.org/license |
| 2 | + |
| 3 | +include("terminfo_data.jl") |
| 4 | + |
| 5 | +""" |
| 6 | + struct TermInfoRaw |
| 7 | +
|
| 8 | +A structured representation of a terminfo file, without any knowledge of |
| 9 | +particular capabilities, solely based on `term(5)`. |
| 10 | +
|
| 11 | +!!! warning |
| 12 | + This is not part of the public API, and thus subject to change without notice. |
| 13 | +
|
| 14 | +# Fields |
| 15 | +
|
| 16 | +- `names::Vector{String}`: The names this terminal is known by. |
| 17 | +- `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)) |
| 19 | + number values. A value of `typemax(eltype(numbers))` is used to skip over |
| 20 | + unspecified capabilities while ensuring value indices are correct. |
| 21 | +- `strings::Vector{Union{String, Nothing}}`: A list of 0–$(length(TERM_STRINGS)) |
| 22 | + string values. A value of `nothing` is used to skip over unspecified |
| 23 | + capabilities while ensuring value indices are correct. |
| 24 | +- `extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}}`: Should an |
| 25 | + extended info section exist, this gives the entire extended info as a |
| 26 | + dictionary. Otherwise `nothing`. |
| 27 | +
|
| 28 | +See also: `TermInfo` and `TermCapability`. |
| 29 | +""" |
| 30 | +struct TermInfoRaw |
| 31 | + names::Vector{String} |
| 32 | + flags::BitVector |
| 33 | + numbers::Union{Vector{UInt16}, Vector{UInt32}} |
| 34 | + strings::Vector{Union{String, Nothing}} |
| 35 | + extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}} |
| 36 | +end |
| 37 | + |
| 38 | +""" |
| 39 | + struct TermInfo |
| 40 | +
|
| 41 | +A parsed terminfo paired with capability information. |
| 42 | +
|
| 43 | +!!! warning |
| 44 | + This is not part of the public API, and thus subject to change without notice. |
| 45 | +
|
| 46 | +# Fields |
| 47 | +
|
| 48 | +- `names::Vector{String}`: The names this terminal is known by. |
| 49 | +- `flags::Int`: The number of flags specified. |
| 50 | +- `numbers::BitVector`: A mask indicating which of `TERM_NUMBERS` have been |
| 51 | + specified. |
| 52 | +- `strings::BitVector`: A mask indicating which of `TERM_STRINGS` have been |
| 53 | + specified. |
| 54 | +- `extensions::Vector{Symbol}`: A list of extended capability variable names. |
| 55 | +- `capabilities::Dict{Symbol, Union{Bool, Int, String}}`: The capability values |
| 56 | + themselves. |
| 57 | +
|
| 58 | +See also: `TermInfoRaw` and `TermCapability`. |
| 59 | +""" |
| 60 | +struct TermInfo |
| 61 | + names::Vector{String} |
| 62 | + flags::Int |
| 63 | + numbers::BitVector |
| 64 | + strings::BitVector |
| 65 | + extensions::Vector{Symbol} |
| 66 | + capabilities::Dict{Symbol, Union{Bool, Int, String}} |
| 67 | +end |
| 68 | + |
| 69 | +TermInfo() = TermInfo([], 0, [], [], [], Dict()) |
| 70 | + |
| 71 | +function read(data::IO, ::Type{TermInfoRaw}) |
| 72 | + # Parse according to `term(5)` |
| 73 | + # Header |
| 74 | + magic = read(data, UInt16) |> ltoh |
| 75 | + NumInt = if magic == 0o0432 |
| 76 | + UInt16 |
| 77 | + elseif magic == 0o01036 |
| 78 | + UInt32 |
| 79 | + else |
| 80 | + throw(ArgumentError("Terminfo data did not start with the magic number 0o0432 or 0o01036")) |
| 81 | + 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 |
| 87 | + # Terminal Names |
| 88 | + term_names = split(String(read(data, name_bytes - 1)), '|') .|> String |
| 89 | + 0x00 == read(data, UInt8) || |
| 90 | + throw(ArgumentError("Terminfo data did not contain a null byte after the terminal names section")) |
| 91 | + # Boolean Flags |
| 92 | + flags = read(data, flag_bytes) .== 0x01 |
| 93 | + if position(data) % 2 != 0 |
| 94 | + 0x00 == read(data, UInt8) || |
| 95 | + 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")) |
| 96 | + end |
| 97 | + # 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 |
| 100 | + 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 |
| 109 | + TermInfoRaw(term_names, flags, numbers, strings, |
| 110 | + if !eof(data) extendedterminfo(data; NumInt) end) |
| 111 | +end |
| 112 | + |
| 113 | +""" |
| 114 | + extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}}) |
| 115 | +
|
| 116 | +Read an extended terminfo section from `data`, with `NumInt` as the numbers type. |
| 117 | +
|
| 118 | +This will accept any terminfo content that conforms with `term(5)`. |
| 119 | +
|
| 120 | +See also: `read(::IO, ::Type{TermInfoRaw})` |
| 121 | +""" |
| 122 | +function extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}}) |
| 123 | + # Extended info |
| 124 | + if position(data) % 2 != 0 |
| 125 | + 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")) |
| 127 | + end |
| 128 | + # 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 |
| 134 | + # Extended flags/numbers/strings |
| 135 | + flags = read(data, flag_bytes) .== 0x01 |
| 136 | + if flag_bytes % 2 != 0 |
| 137 | + 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")) |
| 139 | + 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}}( |
| 146 | + labels .=> vcat(flags, numbers, strings)) |
| 147 | +end |
| 148 | + |
| 149 | +""" |
| 150 | + TermInfo(raw::TermInfoRaw) |
| 151 | +
|
| 152 | +Construct a `TermInfo` from `raw`, using known terminal capabilities (as of |
| 153 | +NCurses 6.3, see `TERM_FLAGS`, `TERM_NUMBERS`, and `TERM_STRINGS`). |
| 154 | +""" |
| 155 | +function TermInfo(raw::TermInfoRaw) |
| 156 | + capabilities = Dict{Symbol, Union{Bool, Int, String}}() |
| 157 | + sizehint!(capabilities, 2 * (length(raw.flags) + length(raw.numbers) + length(raw.strings))) |
| 158 | + for (flag, value) in zip(TERM_FLAGS, raw.flags) |
| 159 | + capabilities[flag.short] = value |
| 160 | + capabilities[flag.long] = value |
| 161 | + end |
| 162 | + 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 |
| 167 | + end |
| 168 | + for (str, value) in zip(TERM_STRINGS, raw.strings) |
| 169 | + if !isnothing(value) |
| 170 | + capabilities[str.short] = value |
| 171 | + capabilities[str.long] = value |
| 172 | + end |
| 173 | + end |
| 174 | + extensions = if !isnothing(raw.extended) |
| 175 | + capabilities = merge(capabilities, raw.extended) |
| 176 | + keys(raw.extended) |> collect |
| 177 | + else |
| 178 | + Symbol[] |
| 179 | + end |
| 180 | + TermInfo(raw.names, length(raw.flags), |
| 181 | + raw.numbers .!= typemax(eltype(raw.numbers)), |
| 182 | + map(!isnothing, raw.strings), |
| 183 | + extensions, capabilities) |
| 184 | +end |
| 185 | + |
| 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) |
| 192 | + |
| 193 | +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 && |
| 197 | + print(io, ", ", length(ti.extensions), " extended capabilities") |
| 198 | + print(io, ')') |
| 199 | +end |
| 200 | + |
| 201 | +""" |
| 202 | + find_terminfo_file(term::String) |
| 203 | +
|
| 204 | +Locate the terminfo file for `term`, return `nothing` if none could be found. |
| 205 | +
|
| 206 | +The lookup policy is described in `terminfo(5)` "Fetching Compiled |
| 207 | +Descriptions". |
| 208 | +""" |
| 209 | +function find_terminfo_file(term::String) |
| 210 | + isempty(term) && return |
| 211 | + chr, chrcode = string(first(term)), string(Int(first(term)), base=16) |
| 212 | + terminfo_dirs = if haskey(ENV, "TERMINFO") |
| 213 | + [ENV["TERMINFO"]] |
| 214 | + elseif isdir(joinpath(homedir(), ".terminfo")) |
| 215 | + [joinpath(homedir(), ".terminfo")] |
| 216 | + elseif haskey(ENV, "TERMINFO_DIRS") |
| 217 | + split(ENV["TERMINFO_DIRS"], ':') |
| 218 | + elseif Sys.isunix() |
| 219 | + ["/usr/share/terminfo"] |
| 220 | + else |
| 221 | + String[] |
| 222 | + end |
| 223 | + for dir in terminfo_dirs |
| 224 | + if isfile(joinpath(dir, chr, term)) |
| 225 | + return joinpath(dir, chr, term) |
| 226 | + elseif isfile(joinpath(dir, chrcode, term)) |
| 227 | + return joinpath(dir, chrcode, term) |
| 228 | + end |
| 229 | + end |
| 230 | +end |
| 231 | + |
| 232 | +""" |
| 233 | + load_terminfo(term::String) |
| 234 | +
|
| 235 | +Load the `TermInfo` for `term`, falling back on a blank `TermInfo`. |
| 236 | +""" |
| 237 | +function load_terminfo(term::String) |
| 238 | + file = find_terminfo_file(term) |
| 239 | + isnothing(file) && return TermInfo() |
| 240 | + try |
| 241 | + TermInfo(read(file, TermInfoRaw)) |
| 242 | + catch err |
| 243 | + if err isa ArgumentError || err isa IOError |
| 244 | + TermInfo() |
| 245 | + else |
| 246 | + rethrow() |
| 247 | + end |
| 248 | + end |
| 249 | +end |
| 250 | + |
| 251 | +""" |
| 252 | +The terminfo of the current terminal. |
| 253 | +""" |
| 254 | +current_terminfo::TermInfo = TermInfo() |
| 255 | + |
| 256 | +# Legacy/TTY methods and the `:color` parameter |
| 257 | + |
| 258 | +if Sys.iswindows() |
| 259 | + ttyhascolor(term_type = nothing) = true |
| 260 | +else |
| 261 | + function ttyhascolor(term_type = get(ENV, "TERM", "")) |
| 262 | + startswith(term_type, "xterm") || |
| 263 | + haskey(current_terminfo, :setaf) |
| 264 | + end |
| 265 | +end |
| 266 | + |
| 267 | +""" |
| 268 | + ttyhastruecolor() |
| 269 | +
|
| 270 | +Return a boolean signifying whether the current terminal supports 24-bit colors. |
| 271 | +
|
| 272 | +This uses the `COLORTERM` environment variable if possible, returning true if it |
| 273 | +is set to either `"truecolor"` or `"24bit"`. |
| 274 | +
|
| 275 | +As a fallback, first on unix systems the `colors` terminal capability is checked |
| 276 | +— should more than 256 colors be reported, this is taken to signify 24-bit |
| 277 | +support. |
| 278 | +""" |
| 279 | +function ttyhastruecolor() |
| 280 | + get(ENV, "COLORTERM", "") ∈ ("truecolor", "24bit") || |
| 281 | + @static if Sys.isunix() |
| 282 | + get(current_terminfo, :colors, 0) > 256 |
| 283 | + else |
| 284 | + false |
| 285 | + end |
| 286 | +end |
| 287 | + |
| 288 | +function get_have_color() |
| 289 | + global have_color |
| 290 | + have_color === nothing && (have_color = ttyhascolor()) |
| 291 | + return have_color::Bool |
| 292 | +end |
| 293 | + |
| 294 | +function get_have_truecolor() |
| 295 | + global have_truecolor |
| 296 | + have_truecolor === nothing && (have_truecolor = ttyhastruecolor()) |
| 297 | + return have_truecolor::Bool |
| 298 | +end |
| 299 | + |
| 300 | +in(key_value::Pair{Symbol,Bool}, ::TTY) = key_value.first === :color && key_value.second === get_have_color() |
| 301 | +haskey(::TTY, key::Symbol) = key === :color |
| 302 | +getindex(::TTY, key::Symbol) = key === :color ? get_have_color() : throw(KeyError(key)) |
| 303 | +get(::TTY, key::Symbol, default) = key === :color ? get_have_color() : default |
0 commit comments