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

respect terminal colors #859

Closed
boyter opened this issue Jun 21, 2023 · 12 comments
Closed

respect terminal colors #859

boyter opened this issue Jun 21, 2023 · 12 comments

Comments

@boyter
Copy link

boyter commented Jun 21, 2023

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
Copy link
Contributor

digitallyserviced commented Jun 25, 2023

@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
Copy link
Author

boyter commented Jun 26, 2023

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
Copy link
Owner

rivo commented Jun 26, 2023

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
Copy link
Contributor

digitallyserviced commented Jun 26, 2023

@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
Copy link
Contributor

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
Copy link
Owner

rivo commented Jun 27, 2023

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
Copy link
Author

boyter commented Jun 27, 2023

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
Copy link
Owner

rivo commented Aug 26, 2023

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.

@rivo rivo closed this as completed Aug 26, 2023
@gdamore
Copy link

gdamore commented Aug 26, 2023

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
Copy link

gdamore commented Aug 26, 2023

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
Copy link
Owner

rivo commented Aug 26, 2023

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
Copy link

2minchul commented Dec 10, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants