Skip to content
Open
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
16 changes: 16 additions & 0 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,20 @@ type Screen interface {
// the results may vary. Use of unicode characters may not be supported.
SetTitle(string)

// SetForegroundColor sets the terminal's default foreground color, which
// text without a color of its own is drawn in. An invalid color, such as
// color.Default, restores the terminal's own. Tcell restores it when the
// screen is finalized or suspended, and sets it again on resume.
// Terminals may ignore this.
SetForegroundColor(color.Color)

// SetBackgroundColor sets the terminal's default background color, which
// cells without a color of their own are drawn in. An invalid color, such as
// color.Default, restores the terminal's own. Tcell restores it when the
// screen is finalized or suspended, and sets it again on resume.
// Terminals may ignore this.
SetBackgroundColor(color.Color)

// SetClipboard is used to post arbitrary data to the system clipboard.
// This need not be UTF-8 string data. It's up to the recipient to decode the
// data meaningfully. Terminals may prevent this for security reasons.
Expand Down Expand Up @@ -366,6 +380,8 @@ type screenImpl interface {
Resume() error
Beep() error
SetSize(int, int)
SetForegroundColor(color.Color)
SetBackgroundColor(color.Color)
SetTitle(string)
Tty() (Tty, bool)
SetClipboard([]byte)
Expand Down
50 changes: 50 additions & 0 deletions tscreen.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ const (
exitKeypad = "\x1b[?1l\x1b>" // Also mode 1
requestWindowSize = "\x1b[18t" // For modern terminals
requestPixelSize = "\x1b[14t" // Text area in pixels, for when the tty does not know it
setForeground = "\x1b]10;#%02x%02x%02x\007" // Set the terminal's default foreground
setBackground = "\x1b]11;#%02x%02x%02x\007" // Set the terminal's default background
resetForeground = "\x1b]110\007" // Restore the terminal's default foreground
resetBackground = "\x1b]111\007" // Restore the terminal's default background
requestPrimaryDA = "\x1b[c" // Request primary device attributes
requestExtAttr = "\x1b[>q" // Request extended attribute (emulator name and version)
setClipboard = "\x1b]52;c;%s\x1b\\" // Clipboard content is base64
Expand Down Expand Up @@ -293,6 +297,8 @@ type tScreen struct {
saveTitle string
restoreTitle string
title string
foreground color.Color // the terminal's default foreground, if valid
background color.Color // the terminal's default background, if valid
setClipboard string
notifyDesktop string
termName string
Expand Down Expand Up @@ -1800,6 +1806,8 @@ func (t *tScreen) engageLocked() error {
if t.title != "" && t.setTitle != "" {
t.Printf(t.setTitle, t.title)
}
t.printColor(setForeground, t.foreground)
t.printColor(setBackground, t.background)
if t.negotiate && !t.legacy && useVTWindowSizeQuery(runtime.GOOS) {
t.Print(requestWindowSize)
}
Expand Down Expand Up @@ -1836,6 +1844,13 @@ func (t *tScreen) disengageStart() bool {
}

t.running = false
// Reset the colors now, as setters stop writing them once we are not running.
if t.foreground.Valid() {
t.Print(resetForeground)
}
if t.background.Valid() {
t.Print(resetBackground)
}
if t.inlineResize {
t.Print(vt.PmResizeReports.Disable())
} else {
Expand Down Expand Up @@ -1925,6 +1940,41 @@ func (t *tScreen) GetCells() *CellBuffer {
return &t.cells
}

func (t *tScreen) SetForegroundColor(c color.Color) {
t.setColor(&t.foreground, c, setForeground, resetForeground)
}

func (t *tScreen) SetBackgroundColor(c color.Color) {
t.setColor(&t.background, c, setBackground, resetBackground)
}

// setColor stores c in dst, and sends it with set, or reset if it is invalid, while running.
func (t *tScreen) setColor(dst *color.Color, c color.Color, set, reset string) {
t.Lock()
defer t.Unlock()
if t.fini {
return
}
old := *dst
*dst = c
if !t.running {
return
}
if c.Valid() {
t.printColor(set, c)
} else if old.Valid() {
t.Print(reset)
}
}

// printColor prints c with set, if it is valid.
func (t *tScreen) printColor(set string, c color.Color) {
if c.Valid() {
r, g, b := c.RGB()
t.Printf(set, r, g, b)
}
}

func (t *tScreen) SetTitle(title string) {
t.Lock()
if t.fini {
Expand Down
124 changes: 124 additions & 0 deletions tscreen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package tcell

import (
"bytes"
"fmt"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -1890,3 +1891,126 @@ func TestMouseDisabledPreventsNativeConsoleMouse(t *testing.T) {
t.Fatalf("native console mouse started despite TCELL_MOUSE=disable")
}
}

// TestDefaultColorSequences pins the xterm sequences the other default color tests use.
func TestDefaultColorSequences(t *testing.T) {
for _, c := range []struct{ got, want string }{
{fmt.Sprintf(setForeground, 0xc0, 0xca, 0xf5), "\x1b]10;#c0caf5\x07"},
{fmt.Sprintf(setBackground, 0x1a, 0x1b, 0x26), "\x1b]11;#1a1b26\x07"},
{resetForeground, "\x1b]110\x07"},
{resetBackground, "\x1b]111\x07"},
} {
if c.got != c.want {
t.Errorf("got %q, want %q", c.got, c.want)
}
}
}

func TestDefaultColors(t *testing.T) {
tty := &spyTty{MockTerm: vt.NewMockTerm()}
scr, err := NewTerminfoScreenFromTty(tty)
if err != nil {
t.Fatalf("failed to get screen: %v", err)
}
if err := scr.Init(); err != nil {
t.Fatalf("failed to initialize screen: %v", err)
}

setFg := fmt.Sprintf(setForeground, 0xc0, 0xca, 0xf5)
setBg := fmt.Sprintf(setBackground, 0x1a, 0x1b, 0x26)
// written returns what step wrote to the terminal.
written := func(step func()) string {
before := len(tty.Output())
step()
return tty.Output()[before:]
}
expect := func(t *testing.T, got string, want ...string) {
t.Helper()
for _, w := range want {
if !strings.Contains(got, w) {
t.Fatalf("%q not written, got %q", w, got)
}
}
}

t.Run("set", func(t *testing.T) {
expect(t, written(func() {
scr.SetForegroundColor(NewHexColor(0xc0caf5))
scr.SetBackgroundColor(NewHexColor(0x1a1b26))
}), setFg, setBg)
})
t.Run("restored while suspended", func(t *testing.T) {
expect(t, written(func() { _ = scr.Suspend() }), resetForeground, resetBackground)
expect(t, written(func() { _ = scr.Resume() }), setFg, setBg)
})
t.Run("reset", func(t *testing.T) {
expect(t, written(func() { scr.SetForegroundColor(ColorDefault) }), resetForeground)
if got := written(scr.Fini); strings.Contains(got, resetForeground) || !strings.Contains(got, resetBackground) {
t.Fatalf("Fini wrote %q, want only the background restored", got)
}
})
}

// pausingTty blocks the first write that contains pause, once armed, until released.
type pausingTty struct {
spyTty
mu sync.Mutex
pause string
paused chan struct{}
release chan struct{}
}

func (t *pausingTty) Write(b []byte) (int, error) {
t.mu.Lock()
hit := t.pause != "" && strings.Contains(string(b), t.pause)
if hit {
t.pause = ""
}
t.mu.Unlock()
if hit {
close(t.paused)
<-t.release
}
return t.spyTty.Write(b)
}

func TestDefaultColorResetDuringSuspend(t *testing.T) {
tty := &pausingTty{
spyTty: spyTty{MockTerm: vt.NewMockTerm()},
paused: make(chan struct{}),
release: make(chan struct{}),
}
scr, err := NewTerminfoScreenFromTty(tty)
if err != nil {
t.Fatalf("failed to get screen: %v", err)
}
if err := scr.Init(); err != nil {
t.Fatalf("failed to initialize screen: %v", err)
}
defer scr.Fini()

scr.SetForegroundColor(NewHexColor(0xc0caf5))
before := len(tty.Output())

// Pause Suspend after it has stopped running, but before it restores the terminal.
tty.mu.Lock()
tty.pause = vt.PmShowCursor.Enable()
tty.mu.Unlock()
done := make(chan struct{})
go func() {
_ = scr.Suspend()
close(done)
}()
select {
case <-tty.paused:
case <-time.After(5 * time.Second):
t.Fatal("Suspend did not reach the terminal restore")
}
scr.SetForegroundColor(ColorDefault)
close(tty.release)
<-done

if got := tty.Output()[before:]; !strings.Contains(got, resetForeground) {
t.Fatalf("foreground was never restored, got %q", got)
}
}
Loading