Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.11.4
version: v2.14.0
# Build golangci-lint with the Go version from go.mod. Release
# binaries lag behind new Go versions and refuse to lint newer code.
install-mode: goinstall
args: --timeout=3m

- name: Lint summary
Expand Down
89 changes: 89 additions & 0 deletions background.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package color

import (
"os"
"strconv"
"strings"
"time"
)

// Background returns the background color of the terminal, for example to
// choose between a dark and a light theme with Color.IsDark.
//
// It reads COLORFGBG first. When that is unset it asks the terminal with an
// OSC 11 query and waits at most timeout for the answer. The query needs a
// controlling terminal on Unix; on Windows only COLORFGBG is read. The bool
// is false when the background could not be found out.
//
// The query reads from the terminal, so call it before reading user input,
// not while another goroutine does. Keys pressed during the query are lost.
func Background(timeout time.Duration) (Color, bool) {
if c, ok := parseColorFgBg(os.Getenv("COLORFGBG")); ok {
return c, true
}

return queryBackground(timeout)
}

// parseColorFgBg reads the background from COLORFGBG, which rxvt, Konsole
// and others set to "fg;bg" or "fg;default;bg" with basic color indexes.
func parseColorFgBg(v string) (Color, bool) {
if v == "" {
return None, false
}

parts := strings.Split(v, ";")

n, err := strconv.ParseUint(parts[len(parts)-1], 10, 8)
if err != nil || n > 15 {
return None, false
}

return Basic(uint8(n)), true
}

// parseOSC11 extracts the color from a terminal's answer to an OSC 11 query,
// such as "\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\". Each channel has 1 to 4 hex
// digits.
func parseOSC11(resp string) (Color, bool) {
_, after, ok := strings.Cut(resp, "]11;")
if !ok {
return None, false
}

end := strings.IndexAny(after, "\x07\x1b")
if end < 0 {
return None, false
}

spec := after[:end]
if !strings.HasPrefix(spec, "rgb:") && !strings.HasPrefix(spec, "rgba:") {
return None, false
}

_, spec, _ = strings.Cut(spec, ":")

channels := strings.Split(spec, "/")
if len(channels) < 3 {
return None, false
}

var rgb [3]uint8

for i := range 3 {
ch := channels[i]
if ch == "" || len(ch) > 4 {
return None, false
}

v, err := strconv.ParseUint(ch, 16, 16)
if err != nil {
return None, false
}

maxValue := uint64(1)<<(4*len(ch)) - 1
rgb[i] = uint8(v * 255 / maxValue) //nolint:gosec // scaled into 0-255.
}

return RGB(rgb[0], rgb[1], rgb[2]), true
}
10 changes: 10 additions & 0 deletions background_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !unix

package color

import "time"

// queryBackground has no terminal query on this platform.
func queryBackground(time.Duration) (Color, bool) {
return None, false
}
74 changes: 74 additions & 0 deletions background_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:build unix

package color

import (
"os"
"strings"
"time"

"golang.org/x/sys/unix"
"golang.org/x/term"
)

// queryBackground asks the terminal for its background color with OSC 11.
// A primary device attributes query (DA1) follows it. Every terminal answers
// DA1, so when that answer arrives without an OSC 11 answer before it, the
// terminal does not support the query and there is no need to wait for the
// timeout.
func queryBackground(timeout time.Duration) (Color, bool) {
tty, err := os.OpenFile("/dev/tty", os.O_RDWR|unix.O_NOCTTY, 0)
if err != nil {
return None, false
}
defer tty.Close() //nolint:errcheck // read-only use, nothing to flush.

fd := int(tty.Fd())

state, err := term.MakeRaw(fd)
if err != nil {
return None, false
}
defer term.Restore(fd, state) //nolint:errcheck // nothing to do if restoring fails.

if _, err := tty.WriteString("\x1b]11;?\x1b\\\x1b[c"); err != nil {
return None, false
}

deadline := time.Now().Add(timeout)

var resp strings.Builder

buf := make([]byte, 256)

for {
left := time.Until(deadline)
if left <= 0 {
return None, false
}

fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} //nolint:gosec // fd fits in int32.

n, err := unix.Poll(fds, int(left.Milliseconds())+1)
if err != nil && err != unix.EINTR {
return None, false
}

if n <= 0 {
continue
}

read, err := unix.Read(fd, buf)
if err != nil || read <= 0 {
return None, false
}

resp.Write(buf[:read])

// The DA1 answer ends the exchange: "\x1b[?...c".
s := resp.String()
if i := strings.LastIndex(s, "\x1b[?"); i >= 0 && strings.HasSuffix(s, "c") {
return parseOSC11(s[:i])
}
}
}
35 changes: 0 additions & 35 deletions color-ansi.go

This file was deleted.

Loading
Loading