rivo / tview

Terminal UI library with rich, interactive widgets — written in Golang
MIT License
11.11k stars 576 forks source link

respect terminal colors #859

Closed boyter closed 1 year ago

boyter commented 1 year ago

To start, this is a fantastic library and I love it to bits. Thank you so much for having created it.

I had a look through issues, the documentation and a few other places but was unable to find an answer to this.

Is it possible to have tview respect someones existing terminal colors and use those in the display?

I am running into the issue where its in "dark" mode where the persons terminal has a white background. Its a similar question for colors for display and such.

I had a look at https://github.com/gdamore/tcell and it looks like that might support it, but I don't have enough knowledge over it to say one way or the other.

digitallyserviced commented 1 year ago

@boyter the first 16 colors in the tcell colornames and color constants are the terminal's color palette for those colors. Here are the color constants, and their associated hex value. Using any of these will cause the terminal to use the palette defined color for it. The hex value, AND the constant are usable, in case you want to use strings.

should... if it is xterm compatible

    ColorBlack:                0x000000,
    ColorMaroon:               0x800000,
    ColorGreen:                0x008000,
    ColorOlive:                0x808000,
    ColorNavy:                 0x000080,
    ColorPurple:               0x800080,
    ColorTeal:                 0x008080,
    ColorSilver:               0xC0C0C0,
    ColorGray:                 0x808080,
    ColorRed:                  0xFF0000,
    ColorLime:                 0x00FF00,
    ColorYellow:               0xFFFF00,
    ColorBlue:                 0x0000FF,
    ColorFuchsia:              0xFF00FF,
    ColorAqua:                 0x00FFFF,
    ColorWhite:                0xFFFFFF,

This can be a bit confusing because you are using a constant or string that is NOT the actual color from their palette. To be able to find their actual colors used in the palette you will need to do some OSC escape sequences to query their term emulator.

https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands

Specifically Ps=4 and ? for the indexed colors.

boyter commented 1 year ago

Yes confusing. Actually I must confess I still am.

I guess what I would need to know, is given a tview application how do I set it up to respect the user's preferences?

I create the tview application like so https://github.com/boyter/cs/blob/master/tui.go#L250 and set colours based on the above (other than one I am going to replace), but I am not sure what else I need to do here. Also how does one refer to the colours correctly inside coloured content where you use [red] or [white] for example?

rivo commented 1 year ago

This is discussed in one of two questions in the FAQ: https://github.com/rivo/tview/wiki/FAQ In short, it's not possible to determine a user's color settings.

You can offer your users a choice of a few predefined themes but it will not be automatic.

digitallyserviced commented 1 year ago

@rivo @boyter

In short, it's not possible to determine a user's color settings.

I will say this is mostly incorrect.

Rather it is not possible to CONSISTENTLY and RELIABLY determine a user's color settings, because of terminal emulators supporting the xterm OSC 4 queries and tmux settings concerning allow passthrough.

See this file where I query the OSC color palette for the first 16 colors and then the OSC teens for background/select/cursor color palettes. I will not explain but you can get a gist as to what is going on. THere are issues where tmux can passthrough the esc sequences and the emulator can respond but sometimes there is issues reading stdin reliably without user input as well as using stderr to output the escape sequences while also having tview use stderr to print the TUI.

Here is a short gif of the color palette bar test, then running my app which queries the colors via OSC and outputting each with the associated ansi name.

Peek 2023-06-26 18-25

package coolor

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/signal"
    "syscall"

    // "time"

    "golang.org/x/sys/unix"

    "github.com/creack/pty"
    "github.com/gookit/color"
    "golang.org/x/term"

    "github.com/digitallyserviced/coolors/coolor/colorinfo"
)

const (
    tesc  = "\033Ptmux;\033\033]"
    tcesc = "\007\033\\"
    esc   = "\033]"
    cesc  = "\007"
)

var (
    oe = ""
    ce = ""
)

func EscReqColor(idx int) (string, string) {
    oe = esc
    ce = cesc
    if os.Getenv("TMUX") != "" {
        color.Yellowf("Within tmux environment... TMUX=%q\n", os.Getenv("TMUX"))
        oe = tesc
        ce = tcesc
        return oe, ce
    }
    color.Yellowf("tmux not detected... SHLVL=%s\n ", os.Getenv("SHLVL"))
    return oe, ce
}

const (
    ioctlReadTermios  = syscall.TCGETS
    ioctlWriteTermios = syscall.TCSETS
)

var (
    fd          int
    termios     *unix.Termios
    inputBuffer = make([]byte, 32)
)

func write(c byte) {
    doLog("%c", c)
}

func get(ptmx *os.File) string {
    n, _ := ptmx.Read(inputBuffer)
    return string(inputBuffer[:n])
}

func size() (int, int) {
    ws, _ := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
    return int(ws.Col), int(ws.Row)
}

func hideCursor() {
    doLog("\x1b[?25l")
}

func showCursor() {
    doLog("\x1b[?25h")
}

func clear() {
    fmt.Print("\x1b[2J")
}

func setCursor(x, y int) {
    doLog("\x1b[%d;%dH", y, x)
}

func reset() {
    showCursor()
    setCursor(0, 0)
    _ = unix.IoctlSetTermios(fd, ioctlWriteTermios, termios)
}

func initVT100(fd int) {
    termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
    if err != nil {
        panic(err)
    }

    newState := *termios
    newState.Lflag &^= unix.ECHO   // Disable echo
    newState.Lflag &^= unix.ICANON // Disable buffering
    if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil {
        panic(err)
    }
}

func queryOscColor(ptmx *os.File, idx int) {
    s, e := oe, ce
    fmt.Fprintf(ptmx, "%s%d;?%s\n", s, idx, e)
  // color.Fprintf(w io.Writer, format string, a ...interface{})
  // color.Bluef("Press ENTER %s to continue if necessary...", "")
    // fmt.Fprintf(ptmx, "\n")
}

func queryIndexColor(ptmx *os.File, idx int) {
    s, e := oe, ce
    fmt.Fprintf(ptmx, "%s4;%d;?%s\n", s, idx, e)
    // fmt.Fprintf(ptmx, "\n")
}

func readOscColor(ptmx *os.File, idx int) (r, g, b uint) {
    var i, rr, gg, bb uint = 0, 0, 0, 0
    txt := get(ptmx)
    n, err := fmt.Sscanf(txt, "\x1b]%d;rgb:%02x%02x/%02x%02x/%02x%02x\x1b", &i, &r, &rr, &g, &gg, &b, &bb)
    if err != nil || n != 7 {
    }
    return
}

func readIndexColor(ptmx *os.File, idx int) (r, g, b uint) {
    var i, rr, gg, bb uint = 0, 0, 0, 0
    txt := get(ptmx)
    n, err := fmt.Sscanf(txt, "\x1b]4;%d;rgb:%02x%02x/%02x%02x/%02x%02x\x1b", &i, &r, &rr, &g, &gg, &b, &bb)
    if err != nil || n != 7 {
    }
    return
}

func QueryColorScheme(n int) []Color {
    initVT100(int(os.Stderr.Fd()))
    clear()
    EscReqColor(0)
  color.SetOutput(os.Stderr)
    cols := make([]Color, n+7)
    ps := []int{10, 11, 12, 13, 14, 17, 19}
  color.Bluef("Keep pressing ENTER until the UI launches...", "")

  for i, v := range ps {
    done := make(chan struct{})
    go func() {
      r, g, b := readOscColor(os.Stdin, v)
            x := Color{float64(r) / 255.0, float64(g) / 255.0, float64(b) / 255.0}
      cols[n+i] = x
      Base16KeyScheme.Editables[i].SetOriginal(x.Hex())
      close(done)
    }()
    queryOscColor(os.Stderr, v)
    // go func() {
    //  tickk := time.NewTicker(time.Millisecond * 100)
    //  for {
    //      select {
    //      case <-done:
    //          return
    //      case <-tickk.C:
    //          fmt.Fprintf(os.Stdin, "\n")
    //      }
    //  }
    // }()
    <-done
  }
    for i := 0; i < n; i++ {
    // color.Println(a ...interface{})
    // color.Infof("Querying xterm indexed color #%d (%s)\n", i, colorinfo.BaseXtermAnsiColorNames[i])
        done := make(chan struct{})
        go func() {
            r, g, b := readIndexColor(os.Stdin, i)
            x := Color{float64(r) / 255.0, float64(g) / 255.0, float64(b) / 255.0}
            cols[i] = x
      color.Printf("Color #%d resulted with (<bg=%[2]s>%[2]s</>)\n", i, x.Hex()[1:])
      Base16KeyScheme.Editables[len(ps)+i].SetOriginal(x.Hex())
            close(done)
        }()
    color.Printf("Querying ansi color #%d (<%[2]s>%[2]s</>)\n", i, colorinfo.BaseXtermAnsiColorNames[i])
        queryIndexColor(os.Stderr, i)
        // go func() {
        //  tickk := time.NewTicker(time.Millisecond * 100)
        //  for {
        //      select {
        //      case <-done:
        //          return
        //      case <-tickk.C:
        //          fmt.Fprintf(os.Stderr, "\n")
        //      }
        //  }
        // }()
        <-done
    }

    reset()
    return cols
}

func test() error {
    c := exec.Command("zsh")

    ptmx, err := pty.Start(c)
    if err != nil {
        return err
    }
    defer func() { _ = ptmx.Close() }() // Best effort.

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGWINCH)
    go func() {
        for range ch {
            if err := pty.InheritSize(os.Stdin, ptmx); err != nil {
                log.Printf("error resizing pty: %s", err)
            }
        }
    }()
    ch <- syscall.SIGWINCH                        // Initial resize.
    defer func() { signal.Stop(ch); close(ch) }() // Cleanup signals when done.

    oldState, err := term.MakeRaw(int(ptmx.Fd()))
    if err != nil {
        panic(err)
    }
    defer func() { _ = term.Restore(int(ptmx.Fd()), oldState) }() // Best effort.

    return nil
}
digitallyserviced commented 1 year ago

Yes confusing. Actually I must confess I still am.

I am going to try and explain the terminal xterm/ansi coloring schemes/palettes here to hopefully clear things up.

Xterm began to allow color schemes to be set for user terminal emulators. However how do you rewrite the colors for things when they are already outputting a specific color index/name/HEX value?

Well that is why those simple FF0000 (red) colors are rewritten to the user's defined palette.

The xterm/ansi 16 indexed colors (base16) are the indexed colors 0-15, 8 regular, 8 bright/dim. from black to white.

The base16 color schemes you see usually only change these first 16 colors, because again, they are the most commonly set/used colors when coloring items in the terminal because back in the day you did not define an rgb/css hex value for colors... You specified the color index from the palette that was available.

The color derived from indexed palettes were originally 2, then 8, then 16/8 (dims), then 16 anything, then some 220 color gradient and then another 30 or so greys (256). Now all of these colors can be set specifically to any rgb value, as well as the OSC escapes allowing you to set even specific HSL/RGB values.

Basically you are working within a system that has had to work from legacy/older ways of doing things while also adding features for newer systems while not fucking up the old stuff.

image

Below you can see how I separated the colors hex strings, and then the actual names.

// strict/hard base ansi colors
var BaseXterm = []string{
    "#000000",
    "#800000",
    "#008000",
    "#808000",
    "#000080",
    "#800080",
    "#008080",
    "#707070",
}

var BaseBrightXterm = []string{
    "#A9A9A9",
    "#FF0000",
    "#00FF00",
    "#FFFF00",
    "#0000FF",
    "#FF00FF",
    "#00FFFF",
    "#FFFFFF",
}

// base16 xterm color names
var BaseXtermAnsiColorNames = []string{
    "black",     // 0
    "maroon",
    "green",
    "olive",
    "navy",
    "purple",
    "teal",
    "silver",  // 7
    "gray",    // 0 bright
    "red",
    "lime",
    "yellow",
    "blue",
    "fuchsia",
    "aqua",
    "white", // 7 bright
}
rivo commented 1 year ago

I'll get @gdamore from tcell in the loop here as this is really a tcell topic and not tview. If this is ever supported at some point by tcell, it will be supported by tview as well.

boyter commented 1 year ago

Cool. I don't mind putting in the work to support this myself, or indeed adding themes to have it mostly work across the board but ideally something that is applied across everything would be better, even if its just an example to follow from that perhaps we can setup for people to follow should they want to do this.

rivo commented 1 year ago

I'm closing this issue now. If there is anything left to discuss regarding this topic, please open an issue with tcell. As soon as tcell supports this, I can implement it in tview, too. In short, tview does not interact with the terminal directly so any solution to this tcell's responsibility to implement.

gdamore commented 1 year ago

tcell does respect colors, if you ask it to. However, if you use the low order color names, or convert the color to a palette color (via the PaletteColor function), then it will respect user preferences set up in the terminal.

So the application developer has complete control to decide whether to strictly enforce and control color (via RGB colors) or to use palette colors.

The default for the first 16 colors (actually the first 256 colors) is to respect the palette of the user.

gdamore commented 1 year ago

The first 16 colors in tcell are:

        ColorBlack = ColorValid + iota
        ColorMaroon
        ColorGreen
        ColorOlive
        ColorNavy
        ColorPurple
        ColorTeal
        ColorSilver
        ColorGray
        ColorRed
        ColorLime
        ColorYellow
        ColorBlue
        ColorFuchsia
        ColorAqua
        ColorWhite

And they have values:

        ColorBlack:                0x000000,
        ColorMaroon:               0x800000,
        ColorGreen:                0x008000,
        ColorOlive:                0x808000,
        ColorNavy:                 0x000080,
        ColorPurple:               0x800080,
        ColorTeal:                 0x008080,
        ColorSilver:               0xC0C0C0,
        ColorGray:                 0x808080,
        ColorRed:                  0xFF0000,
        ColorLime:                 0x00FF00,
        ColorYellow:               0xFFFF00,
        ColorBlue:                 0x0000FF,
        ColorFuchsia:              0xFF00FF,
        ColorAqua:                 0x00FFFF,
        ColorWhite:                0xFFFFFF,

Note that these values may be different than what you've chosen, as some actual terminals represent these somewhat differently. But if you want to use the palette/theme of the user, then stick to these color values.

rivo commented 1 year ago

I just had a look at this in iTerm on macOS. The colour preferences have two sections:

image

If I change the "ANSI Colors" on the right hand side, the tview application will automatically change accordingly. On the other hand, if I make changes on the left hand side for "Basic Colors", it has no effect. (I don't know what these "basic colours" refer to in the context of a terminal.)

The "Color Presets..." dialog opens up a number of predefined themes from which I can choose one. Some of them change the ANSI colour palette, others don't.

So I guess it's all really a matter of how a terminal colour theme is defined. If "Light Mode" also changes the ANSI palette, tview should look as expected.

2minchul commented 11 months ago

I have found a way.

I've discovered a neat little trick. By inserting the following code before tview.NewApplication(), you can ensure that all the fields in tview are set to your terminal's default color scheme.

theme := tview.Theme{
    PrimitiveBackgroundColor:    tcell.ColorDefault,
    ContrastBackgroundColor:     tcell.ColorDefault,
    MoreContrastBackgroundColor: tcell.ColorDefault,
    BorderColor:                 tcell.ColorDefault,
    TitleColor:                  tcell.ColorDefault,
    GraphicsColor:               tcell.ColorDefault,
    PrimaryTextColor:            tcell.ColorDefault,
    SecondaryTextColor:          tcell.ColorDefault,
    TertiaryTextColor:           tcell.ColorDefault,
    InverseTextColor:            tcell.ColorDefault,
    ContrastSecondaryTextColor:  tcell.ColorDefault,
}
tview.Styles = theme