Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to improve argument visibility in terminal light themes? #466

Open
arshcaria opened this issue Jun 13, 2023 · 10 comments
Open

How to improve argument visibility in terminal light themes? #466

arshcaria opened this issue Jun 13, 2023 · 10 comments
Labels
external The issue is due to external causes outside of Clink

Comments

@arshcaria
Copy link

arshcaria commented Jun 13, 2023

The visibility is somewhat poor in light themes of Windows Terminal and JetBrain IDEs.

I know the text colors can be changed for each element in clink config file. But since I use both dark and light themes in different software, is there a way to make clink have good visibility for both light and dark terminal themes?

image

image

@arshcaria arshcaria changed the title Poor argument visibility in terminal light themes How to improve argument visibility in terminal light themes? Jun 13, 2023
@chrisant996
Copy link
Owner

Do you mean a single clink color theme that works with both light and dark backgrounds?

It's only possible by completely avoiding the terminal theme colors for anything at all (CSI SGR params 0,30-37,39,40-47,49,90-97,100-107) and instead using fixed 8-bit or 24-bit color codes.

Because everyone who is making light themes is making them in such a way that "bright/bold" is still bright in light themes, which produces opposite of the intended effect. Similar problems in how they do the 4 gray scale colors, and the default fore/back colors.

I'm thinking about what can potentially be done in Clink to compensate. But terminal usage in general isn't really seamlessly compatible flipping between light/dark backgrounds without reconfiguring all customized colors individually. Even if Clink completely overrides the terminal theme, that will only compensate in Clink. Any other console mode programs that use CSI 1 m for example will still produce wrong output.

The problem is broader than just Clink.

@chrisant996
Copy link
Owner

Same as #256.

@chrisant996
Copy link
Owner

I'm going to change the default colors in the "use enhanced defaults" case, so that all colors have sufficient contrast to be readable in both Light and Dark terminal color themes.

However, there's no way for programs to reliably detect whether a terminal is using a Light or Dark color theme. In fact, when a program uses the console APIs to ask Windows Terminal about the color theme, Windows Terminal always reports that the colors are the original default OS black background color theme. There's no way to find out whether Windows Terminal is using a Light theme. There's also no way to find out whether it's using Acrylic background, or what background opacity is used, or what the "effective" background color is at any particular location in the terminal window.

So, there's no way for Clink to automatically update its colors on the fly for Light or Dark themes.
So, the closest I can do is adjust the default colors to have sufficient contrast to be readable in both Light and Dark themes.

However, there are two caveats:

  • In Dark themes, the contrast change will make color.input darker and more ugly.
  • In Light themes, color.suggestion will be hard to tell apart from other dark-colored text.

Sorry, this is just a fact of life when using terminals. For output to be fully readable in both Light and Dark themes then it's necessary to either not use colors, or to customize the colors specifically to fit the terminal theme you're using. All shells have similar problems with color customizations, including PowerShell and fish and bash and zsh and etc.

P.S. I recognize that it would be handy to be able to do something simple to select different Clink color settings for different terminal windows. I'm exploring options for that. I have a bunch of half-baked ideas, but I'm not sure yet what will really be possible or feasible (especially since it's impossible for it to be automatic).

@chrisant996 chrisant996 added the external The issue is due to external causes outside of Clink label Jul 13, 2023
@chrisant996 chrisant996 reopened this Jul 13, 2023
@chrisant996
Copy link
Owner

I cannot fix this properly in Clink until microsoft/terminal#10639 is fixed.

However, once that's fixed then it becomes possible to make the color.suggestion setting work properly in both Light and Dark terminal themes (and also color.input).

@bruceoberg
Copy link

One way to alleviate this problem would be a command to show me all the current color settings in their own colors. I'm using a light theme and sometimes the input color makes things illegible. It depends on which color is being used for the stuff I'm typing at the moment.

To fix this right now, I have to figure out which setting I need to change and then fix it. The trouble is, there are many color settings at play and I keep running into "bad" ones as I'm trying to get work done.

I'd like to be able to see all the color settings in their own colors so that I can fix any problem ones while I still have their goofy syntax in my head. As it stands, I never know if I have fixed all the settings that are causing me trouble.

@chrisant996
Copy link
Owner

chrisant996 commented Dec 15, 2023

One way to alleviate this problem would be a command to show me all the current color settings in their own colors.

@bruceoberg I think you mean something like the sample script below, which creates a pseudo-command "CLINK_COLOR_TEST" which can be typed at the command line.

I have some thoughts about some kind of "color themes" kind of thing for Clink. It's complicated because some colors are specified in the Readline init file (.inputrc file), some are specified in the %LS_COLORS%, and some are specified in Clink settings. There isn't a good way to have a single fancy wizard tool to aid in configuring colors. And it's hard to have a "theme file" format that could be compatible with all three (but it might be able to supersede them ... but then clink set gets super problematic).

It would be nice to have a wizard that shows each color setting, plus in-context samples for common ones, a way to modify/pick colors, and a way to save/load themes.

goofy syntax

Do you mean that "bright write on blue" feels like goofy syntax?
Do you mean that terminal escape codes are goofy?
Do you mean the %LS_COLORS% syntax from Unix/Linux is goofy?
Do you mean something else?

Sample script

-- This installs a pseudo-command "CLINK_COLOR_TEST", which prints a color
-- sample for each Clink color setting.  Each sample is shown with the
-- background color initially set to default, and initially set to white.

if not clink.onfilterinput then
    print("color_test.lua requires a newer version of Clink; please upgrade.")
    return
end

local function get_clink()
    local clink_alias = os.getalias('clink')
    if not clink_alias or clink_alias == '' then
        return ''
    end
    return clink_alias:gsub(' $[*]', '')
end

local function maybe_prefix(color)
    color = ";"..(color or "")..";"
    color = color:gsub("^;0;", ";")
    if not color:find(";[34]8;") then
        color = color:gsub(";[34]9;", ";")
    end
    color = color:match("^;*(.-);*$")
    if color ~= "" then
        color = ";"..color
    end
    return color
end

local function show_color_sample(name)
    local color = settings.get(name)
    local sgrBlack = "\x1b[0;48;5;0" .. maybe_prefix(color) .. "m"
    local sgrWhite = "\x1b[0;48;5;15" .. maybe_prefix(color) .. "m"
    local sgrDefault = "\x1b[0" .. maybe_prefix(color) .. "m"
    local sample = string.format("%s  Abc  %s  xyZ  \x1b[m  %s  gTi  \x1b[m", sgrBlack, sgrWhite, sgrDefault)
    clink.print(string.format("%32s    %s    %s", name, sample, color))
end

clink.onfilterinput(function (line)
    if line == "CLINK_COLOR_TEST" then
        print("\nCLINK COLOR SAMPLES:")
        print(string.rep(" ", 32) .. "     \x1b[4mBlack\x1b[m  \x1b[4mWhite\x1b[m   \x1b[4mDefault\x1b[m")
        local alias = get_clink()
        local r = io.popen(alias.." set --list")
        for name in r:lines() do
            if name:match("^color.") then
                show_color_sample(name)
            end
        end
        r:close()
        print()
        return ""
    end
end)

image

@chrisant996
Copy link
Owner

@bruceoberg For the next update, I've added color samples into the clink set completions. E.g. typing clink set colorAlt-= will list matching completions, and the description for each color setting includes a sample of its current color value. See the second screen shot further below.

Also, you might like the following script.

Run it using the undocumented clink lua command: clink lua ansi_colors.lua

For usage info and available flags, run: clink lua ansi_colors.lua --help

When run, the script prints a table of ANSI codes; see the first screen shot further below. The color codes are all 256-color codes, i.e. for use with 38;5;number or 48;5;number. ANSI escape code is a good article with information about escape codes.

ansi_colors.lua

--  Shows ANSI 256-color codes and styles.
--
--  Usage:
--    clink lua  ansi_colors.lua  [options]
--
--  Options:
--    --bg          Show background colors with contrasting foreground colors.
--    --fg          Show foreground colors with default background (the default).
--    --black       Show foreground colors with black background.
--    --white       Show foreground colors with white background.
--    --dark        Show foreground colors with black background.
--    --light       Show foreground colors with white background.
--    --pretty      Show pretty layout for 6x6x6 color cube.
--    --simple      Show simple layout for 6x6x6 color cube (the default).
--    --help, -?    Show help text.

if not clink then
    print("This ansi_colors.lua script requires the standalone Clink Lua engine.")
    return
elseif clink.argmatcher then
    log.info("This ansi_colors.lua script cannot be loaded into an injected Clink instance; it belongs in a separate directory that doesn't get loaded when Clink is injected.")
    return
end

local fg = true
local forcing_background
local pretty_cube = false
local contrast_strategy = 1

local norm = "\x1b[m"
local bold = "\x1b[1m"
local italic = "\x1b[3m"
local underline = "\x1b[4m"
local overline = "\x1b[53m"
local emphasis = "\x1b[36m"
local CSI = "\\x1b["
local param = "\x1b[33m"

for _, a in ipairs(arg) do
    if a == "--bg" then
        fg = false
    elseif a == "--fg" then
        fg = true
    elseif a == "--black" or a == "--dark" then
        fg = true
        forcing_background = 0
    elseif a == "--white" or a == "--light" then
        fg = true
        forcing_background = 15
    elseif a == "--pretty" then
        pretty_cube = true
    elseif a == "--simple" then
        pretty_cube = false
    elseif a == "--help" or a == "-?" or a == "-h" or a == "help" then
        print("Shows ANSI 256-color codes.")
        print()
        print("Usage:")
        print("  clink lua  ansi_colors.lua  [options]")
        print()
        print("Options:")
        print("  --bg          Show background colors with contrasting foreground colors.")
        print("  --fg          Show foreground colors with default background (the default).")
        print("  --black       Show foreground colors with black background.")
        print("  --white       Show foreground colors with white background.")
        print("  --dark        Show foreground colors with black background.")
        print("  --light       Show foreground colors with white background.")
        print("  --pretty      Show pretty layout for 6x6x6 color cube.")
        print("  --simple      Show simple layout for 6x6x6 color cube (the default).")
        print("  --help, -?    Show help text.")
        return
    else
        print("Unrecognized option '"..tostring(a).."'.")
        return
    end
end

local function in_range(num, lo, hi)
    return num >= lo and num <= hi
end

local hi_ranges = {}
table.insert(hi_ranges, { 0, 7 })
if contrast_strategy == 1 then
    for i = 0, 5 do
        table.insert(hi_ranges, { 16 + i*36, 16 + i*36 + 17 })
    end
elseif contrast_strategy == 2 then
    table.insert(hi_ranges, { 16, 123 })
else
    -- TODO: calculate luminance to choose contrast color.
end
table.insert(hi_ranges, { 232, 243 })

local contrast_table = {}
for _, r in ipairs(hi_ranges) do
    for i = r[1], r[2] do
        contrast_table[i] = true
    end
end

local function contrast(num)
    assert(num >= 0)
    assert(num <= 255)
    return contrast_table[num] and 15 or 0
end

local function make_sgr(num, addl)
    local color
    if fg then
        color = string.format("%s;38;5;%d", addl or "", num)
        if forcing_background then
            color = color..string.format(";48;5;%d", forcing_background)
        end
    else
        color = string.format("%s;48;5;%d;38;5;%d", addl or "", num, contrast(num))
    end
    return color
end

local function make_color(num, addl)
    return "\x1b[0;"..make_sgr(num, addl).."m"
end

local function print_sample(num)
    local sample = string.format("%s%3d %s", make_color(num), num, norm)
    clink.print(sample, NONL)
end

local function center(text)
    local width = 3*4*6 + 2
    local len = console.cellcount(text)
    clink.print(string.rep(" ", (width - len) / 2)..italic..text..norm)
end

print()

center("System colors:")
clink.print("    ", NONL)
for i = 0, 7 do
    print_sample(i)
end
clink.print("  ", NONL)
for i = 8, 15 do
    print_sample(i)
end
print("\n")

center("Color cube:")
if pretty_cube then
    for i = 0, 5 do
        for j = 0, 2 do
            for k = 0, 5 do
                print_sample(16 + i*6 + j*72 + k)
            end
            clink.print(" ", NONL)
        end
        print()
    end
    for i = 5, 0, -1 do
        for j = 0, 2 do
            for k = 0, 5 do
                print_sample(52 + i*6 + j*72 + k)
            end
            clink.print(" ", NONL)
        end
        print()
    end
else
    for i = 0, 5 do
        for j = 0, 2 do
            for k = 0, 5 do
                print_sample(16 + i*36 + j*6 + k)
            end
            clink.print(" ", NONL)
        end
        print()
    end
    for i = 0, 5 do
        for j = 0, 2 do
            for k = 0, 5 do
                print_sample(34 + i*36 + j*6 + k)
            end
            clink.print(" ", NONL)
        end
        print()
    end
end
print()

center("Grayscale ramp:")
clink.print("             ", NONL)
for i = 232, 243 do
    print_sample(i)
end
print()
clink.print("             ", NONL)
for i = 244, 255 do
    print_sample(i)
end
print()

print()
center("Styles:")
local styles = {
    { "1",  "22",   "Boldface" },
    { "3",  "23",   "Italics" },
    { "7",  "27",   "Reverse" },
    { "4",  "24",   "Underline" },
    { "53", "55",   "Overline" },
}
local text = ""
local on = ""
local off = ""
for _, s in ipairs(styles) do
    if text ~= "" then
        text = text.."   "
        on = on.."   "
        off = off.."   "
    end
    text = text.."\x1b[0;"..s[1].."m"..s[3]..norm
    local fmt = string.format("%%%ds", #s[3]--[[ - 4]])
    on = on..string.format(--[["on: "..]]fmt, s[1])
    off = off..string.format(--[["off:"..]]fmt, s[2])
end
center(norm..text)
center(norm..on)
center(norm..off)

print()

The script output

image

The upcoming change to include color samples when displaying completions

image

@bruceoberg
Copy link

Thanks for all this @chrisant996. Your lua scripts look like what I want. I'll check them out tonight. I know clink doesn't have a help command, but the samples you show above are what I would expect from clink help colors or equivalent.

And sorry about the goofy syntax snark. I was referring to the sgr codes. They're sorta like morse code to me... they make sense and do what they say on the tin. But whenever I have to deal with them directly I get a huge headache.

@chrisant996
Copy link
Owner

Thanks for all this @chrisant996. Your lua scripts look like what I want. I'll check them out tonight. I know clink doesn't have a help command, but the samples you show above are what I would expect from clink help colors or equivalent.

A help command would print documentation, and the documentation web page covers that. Documentation is static i.e. it doesn't dynamically report current settings. A help command isn't where something like current color samples would go.

Maybe more like a status command to report various kinds of settings. clink settings is the closest to that right now.

But "current color samples" is just scratching the surface. I don't want to make a bunch of piecemeal separate enhancements that don't fit together well. That's why I'm taking a slower and more methodical approach to designing a solution for the broader area of "color themes".

And sorry about the goofy syntax snark. I was referring to the sgr codes. They're sorta like morse code to me... they make sense and do what they say on the tin. But whenever I have to deal with them directly I get a huge headache.

I think you mean the gobbledygook that goes after the "sgr", right?

The gobbledygook is just how ANSI escape codes work. That's just part of how terminals work.

I didn't define that, and it's not actually part of Clink. 🙃 I agree they're not friendly, which is why Clink tries to allow simpler friendly color strings, unless you want to use fancy/sophisticated escape codes. To use fancy codes, Clink expects awareness of escape codes.

There is a rough edge, though: the "enhanced defaults" set some fancy codes. And so when clink set reports them, the escape code values are shown regardless whether the user is familiar with escape codes.

I think the best solution there is just education, but I'm open to other ideas.

@chrisant996
Copy link
Owner

Update

Based on the conversation in microsoft/terminal#16493, it seems the current view is "Light themes are always going to be broken [in terminals]".

Why

VT terminal specifications were designed for CRT monitors, which had black backgrounds. The fairly recent interest in trying to make Light themes for terminals isn't compatible with the existing VT escape codes for colors and styles. While it's possible to change the default background color in a theme to be white, that breaks the VT color specifications such that it's impossible for apps to present text with readable contrast using the colors in the theme. And currently in Windows Terminal it's impossible for an app to detect whether a Light theme is being used (microsoft/terminal#10639), so an app can't even automatically adjust its colors based on the current theme.

Even though Windows Terminal itself includes some Light themes, the current state of things means Light themes are really faking it, and apps cannot automatically coexist with a Light theme unless the app doesn't use any colors.

Options

So, in order to support Light themes, there are really only two options:

  1. Apps can allow (force) the user to configure every color, and the user must manually choose which colors to use. The most an app can do to help with that is to have its own color theme system, to make it convenient to swap a collection of colors in a single operation (e.g. "load theme 'Foo'").
  2. Apps can define their default colors to force a black background for the app text, to ensure readability, instead of allowing the terminal's default background color to be used. That's what apps have done for decades. Many apps are moving to
    Option 1 instead, to try to support Light themes, but that requires extra work from users if they want to use Light themes.

This has always been an issue in Unix and Linux terminals. In Windows Terminal and ConPTY-based terminals it's a problem only until microsoft/terminal#10639 is fixed so that the GetConsoleScreenBufferEx() API again returns the current color table (like it does in legacy ConHost terminal windows).

Clink

In the meantime, I'm working on a way to define / save / load colors themes for Clink.

The main challenge is that colors are stored in the profile, and the same profile is shared by different terminal windows which may be using different background colors.

  1. It's possible to use set CLINK_PROFILE=path_to_profile to override the profile. That seems heavy-handed, and makes it difficult/impossible to share command history between terminals using different color themes.
  2. Clink could have a way to load a color theme into memory and supersede the clink_settings from the profile. But that interferes with the ability for clink set to report and/or modify the current settings.

I'm exploring other options, and variations of the above options.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
external The issue is due to external causes outside of Clink
Projects
None yet
Development

No branches or pull requests

3 participants