https://github.com/JuliaLang/julia
Raw File
Tip revision: b7498550cbabafab972bfcae3c662107d1f974de authored by K Pamnany on 29 November 2023, 21:07:14 UTC
Improve `ReentrantLock`
Tip revision: b749855
terminfo.jl
# This file is a part of Julia. License is MIT: https://julialang.org/license

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{UInt16}, Vector{UInt32}}`: 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::Union{Vector{UInt16}, Vector{UInt32}}
    strings::Vector{Union{String, Nothing}}
    extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}}
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::Int
    numbers::BitVector
    strings::BitVector
    extensions::Vector{Symbol}
    capabilities::Dict{Symbol, Union{Bool, Int, String}}
end

TermInfo() = TermInfo([], 0, [], [], [], Dict())

function read(data::IO, ::Type{TermInfoRaw})
    # Parse according to `term(5)`
    # Header
    magic = read(data, UInt16) |> ltoh
    NumInt = if magic == 0o0432
        UInt16
    elseif magic == 0o01036
        UInt32
    else
        throw(ArgumentError("Terminfo data did not start with the magic number 0o0432 or 0o01036"))
    end
    name_bytes = read(data, UInt16) |> ltoh
    flag_bytes = read(data, UInt16) |> ltoh
    numbers_count = read(data, UInt16) |> ltoh
    string_count = read(data, UInt16) |> ltoh
    table_bytes = read(data, UInt16) |> ltoh
    # Terminal Names
    term_names = split(String(read(data, name_bytes - 1)), '|') .|> String
    0x00 == read(data, UInt8) ||
        throw(ArgumentError("Terminfo data did not contain a null byte after the terminal names section"))
    # Boolean Flags
    flags = read(data, flag_bytes) .== 0x01
    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(ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
    string_indices = map(ltoh, reinterpret(UInt16, read(data, string_count * sizeof(UInt16))))
    strings_table = read(data, table_bytes)
    strings = map(string_indices) do idx
        if idx ∉ (0xffff, 0xfffe)
            len = findfirst(==(0x00), view(strings_table, 1+idx:length(strings_table)))
            !isnothing(len) ||
                throw(ArgumentError("Terminfo string table entry does not terminate with a null byte"))
            String(strings_table[1+idx:idx+len-1])
        end
    end
    TermInfoRaw(term_names, flags, numbers, strings,
                if !eof(data) extendedterminfo(data, NumInt) end)
end

"""
    extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}})

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{UInt16}, Type{UInt32}})
    # 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 = read(data, UInt16) |> ltoh
    numbers_count = read(data, UInt16) |> ltoh
    string_count = read(data, UInt16) |> ltoh
    table_count = read(data, UInt16) |> ltoh
    table_bytes = read(data, UInt16) |> ltoh
    # Extended flags/numbers/strings
    flags = read(data, flag_bytes) .== 0x01
    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(n -> Int(ltoh(n)), reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
    table_indices = map(ltoh, reinterpret(UInt16, read(data, table_count * sizeof(UInt16))))
    table_strings = [String(readuntil(data, 0x00)) for _ in 1:length(table_indices)]
    info = Dict{Symbol, Union{Bool, Int, String}}()
    strings = table_strings[1:string_count]
    labels = table_strings[string_count+1:end]
    for (label, val) in zip(labels, vcat(flags, numbers, strings))
        info[Symbol(label)] = val
    end
    return info
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)))
    for (flag, value) in zip(TERM_FLAGS, raw.flags)
        capabilities[flag.short] = value
        capabilities[flag.long] = value
    end
    for (num, value) in zip(TERM_NUMBERS, raw.numbers)
        if value != typemax(eltype(raw.numbers))
            capabilities[num.short] = Int(value)
            capabilities[num.long] = Int(value)
        end
    end
    for (str, value) in zip(TERM_STRINGS, raw.strings)
        if !isnothing(value)
            capabilities[str.short] = value
            capabilities[str.long] = value
        end
    end
    extensions = if !isnothing(raw.extended)
        capabilities = merge(capabilities, raw.extended)
        keys(raw.extended) |> collect
    else
        Symbol[]
    end
    TermInfo(raw.names, length(raw.flags),
             map(n-> n != typemax(typeof(n)), raw.numbers),
             map(!isnothing, raw.strings),
             extensions, capabilities)
end

getindex(ti::TermInfo, key::Symbol) = ti.capabilities[key]
get(ti::TermInfo, key::Symbol, default::D) where D<:Union{Bool, Int, String} =
    get(ti.capabilities, key, default)::D
get(ti::TermInfo, key::Symbol, default) = get(ti.capabilities, key, default)
keys(ti::TermInfo) = keys(ti.capabilities)
haskey(ti::TermInfo, key::Symbol) = haskey(ti.capabilities, key)

function show(io::IO, ::MIME"text/plain", ti::TermInfo)
    print(io, "TermInfo(", ti.names, "; ", ti.flags, " flags, ",
          sum(ti.numbers), " numbers, ", sum(ti.strings), " strings")
    !isempty(ti.extensions) > 0 &&
        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")]
    elseif haskey(ENV, "TERMINFO_DIRS")
        split(ENV["TERMINFO_DIRS"], ':')
    elseif Sys.isunix()
        ["/usr/share/terminfo"]
    else
        String[]
    end
    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
back to top