gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.55k stars 681 forks source link

Colors: `ColorSchemes` refactor - Should honor terminal color settings etc... #2381

Open tig opened 1 year ago

tig commented 1 year ago

Developers (like me) love tweaking their terminal colors/themes.

Terminal.Gui, today, overrides all this, defining it's own themes.

This means that when I run a TUI app, the effect is glaring and doesn't honor all the hard work I put into tweaking my terminal theme. E.g.:

JiQYZBw 1

In addition we always clear the Toplevel background vs, letting any bitmap the user has set show through.

For v2, I'd like to see Terminal.Gui pick up the colors theme of the Terminal and use it, by default.

Of course, this implies we nail #48 too.

I think this means a Tenet for v2:

Other things that should be addressed as part of this:

Nutzzz commented 1 year ago

I was thinking about how a default theme could be dynamically created based only on an inherited foreground and background. This is quite difficult. FWIW, I thought I'd lay down my ideas.

.Normal would just use the current colors, of course:

fg = defFore;  bg = defBack;

.Focus would just swap background and foreground [EDIT: though this collides with selection]:

fg = defBack;  bg = defFore;

.Disabled, I think, should compute a grayscale foreground color based on the background, adjusted for Luma. See Luma() below. We could use the following for RGB colors, or round to the nearest grayscale ConsoleColor (though see NB below):

fg = Luma(defBack) > 0.5 ? Gray(255 * Luma(defBack) - 63) : Gray(255 * Luma(defBack) + 63);
bg = defBack;

.HotNormal is where things get tough:

fg = AccentColor(defFore);  bg = defBack;

To get an accent color, my first thought was to use an inverse of the RGB of the foreground color. However, this wouldn't work for middle-grayish colors, would often result in ugly combinations, and it wouldn't take Luma into account. It's somewhat easy to handpick a color for each of the 16 ConsoleColors like my AccentColor() implementation below, though this is obviously a matter of taste, and taking both foreground and background into consideration does complicate things quite a bit.

AccentColor() would be somewhat more complicated using RGB (TrueColor or not), but I suppose the suggestions I offer below could still be used if we approximate numerically 16 zones of "reddish" or "dark-blueish" or "blackish" colors [something like the existing Color.ToConsoleColor()], but then we could adjust the accent result based on the calculated saturation and brightness.

.HotFocus is similar, but swapping background and foreground:

fg = AccentColorAdjusted(defFore);  bg = defFore;

However, we probably don't want to introduce another color which might clash with HotNormal, so we wouldn't just use fg = AccentColor(defBack); So my thought is to either re-use the AccentColor(defFore) when there is sufficient difference in Luma between foreground and background, otherwise flip the accent ConsoleColor from dark to bright or bright to dark (or a similar approximation for RGB values).

NB: On the other hand, since each of the ConsoleColors can be set by the user and might even have little bearing on the actual color, it's dangerous to make assumptions. Even assuming the defaults were kept, there's variance between platforms and terminal emulations, e.g., whether bright colors are allowed as backgrounds. Under Windows, depending on the upgrade path the user might still be using legacy colors or the newer Campbell colors.

However, is it even possible to programmatically determine the RGB values of the currently used colors? E.g., using Windows Terminal, we'd have to find and read the appropriate .json file; using Windows conhost, in some circumstances these values would be in the registry, and in some circumstances they'd be in the .lnk file used to launch it; with a multitude of implementations for other shells and platforms and terminal emulations.

EDIT: Also, how is this supposed to work with the bitmap background? How do we ensure a transparent color is used so the bitmap isn't hidden when we've drawn spaces after, e.g., a window is dragged around? Does it just draw the bitmap anywhere the current background color exists? And I assume these implementation details could differ between shells... EDIT 2: It appears that my assumption was correct, for WT at least, so no need to worry about transparency there.

float Luma(Color c) {
   return 0.2126 * c.ScR + 0.7152 * c.ScG + 0.0722 * c.ScB; }

Color Gray(int x) {
   return Color(x, x, x); }

static ConsoleColor AccentColor(ConsoleColor fg, ConsoleColor bg) {
   ConsoleColor newc = new();
   switch (fg) {
      case ConsoleColor.Gray:
      case ConsoleColor.White:
         newc = bg == ConsoleColor.DarkGreen ? ConsoleColor.Black : ConsoleColor.DarkGreen; break;
      case ConsoleColor.DarkGreen:
         newc = bg == ConsoleColor.White ? ConsoleColor.Green : ConsoleColor.White; break;
      case ConsoleColor.DarkGray:
      case ConsoleColor.Black:
         newc = bg == ConsoleColor.Green ? ConsoleColor.White : ConsoleColor.Green; break;
      case ConsoleColor.Green:
         newc = bg == ConsoleColor.Black ? ConsoleColor.DarkCyan : ConsoleColor.Black; break;
      case ConsoleColor.DarkBlue:
         newc = bg == ConsoleColor.Cyan ? ConsoleColor.Yellow : ConsoleColor.Cyan; break;
      case ConsoleColor.Cyan:
         newc = bg == ConsoleColor.DarkBlue ? ConsoleColor.DarkGray : ConsoleColor.DarkBlue; break;
      case ConsoleColor.DarkRed:
      case ConsoleColor.DarkMagenta:
         newc = bg == ConsoleColor.White ? ConsoleColor.Yellow : ConsoleColor.White; break;
      case ConsoleColor.Magenta:
      case ConsoleColor.Red:
         newc = bg == ConsoleColor.Black ? ConsoleColor.DarkYellow : ConsoleColor.Black; break;
      case ConsoleColor.Yellow:
         newc = bg == ConsoleColor.DarkBlue ? ConsoleColor.Black : ConsoleColor.DarkBlue; break;
      case ConsoleColor.DarkCyan:
      case ConsoleColor.Blue:
         newc = bg == ConsoleColor.Yellow ? ConsoleColor.White : ConsoleColor.Yellow; break; }
   return newc; }
BDisp commented 1 year ago

.Focus would just swap background and foreground:

Normally inverted colors is used for selection, like used in TextField and TextView. Usually it can be fg = other color; bg = Normal Fore;

Nutzzz commented 1 year ago

Normally inverted colors is used for selection, like used in TextField and TextView. Usually it can be fg = other color; bg = Normal Fore;

I was hoping to limit the number of accent colors to compute, but that's true.

tig commented 1 year ago

See https://github.com/PowerShell/GraphicalTools/issues/211

This request argues for being able to use PowerShell's $PSSytyle as a source for settings.

tig commented 8 months ago

Proposal for expanded ColorScheme members

(Just notes for now - please ignore)

tig commented 7 months ago

Relevant / informational:

https://chadaustin.me/2024/01/truecolor-terminal-emacs/

dodexahedron commented 7 months ago

I think I mentioned it in the color discussion I started but in the spirit of that link, I'll link this project here, as well, as a potential resource for whatever we end up with:

https://github.com/koszeggy/KGySoft.Drawing