Skip to content

Commit

Permalink
feat(term): query terminal features
Browse files Browse the repository at this point in the history
This adds a few methods that query the terminal for features.

- `BackgroundColor` the terminal background color
- `ForegroundColor` the terminal foreground color
- `CursorColor`
- `SupportsKittyKeyboard` whether Kitty keyboard protocol is supported
  • Loading branch information
aymanbagabas committed Mar 14, 2024
1 parent 1028789 commit 7631c37
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 2 deletions.
38 changes: 38 additions & 0 deletions exp/term/examples/query/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"
"image/color"
"log"
"os"

"github.com/charmbracelet/x/exp/term"
)

func main() {
in, out := os.Stdin, os.Stdout
hasKitty := term.SupportsKittyKeyboard(in, out)
log.Printf("Kitty keyboard support: %v", hasKitty)
bg := term.BackgroundColor(in, out)
log.Printf("Background color: %s", colorToHexString(bg))
fg := term.ForegroundColor(in, out)
log.Printf("Foreground color: %s", colorToHexString(fg))
cursor := term.CursorColor(in, out)
log.Printf("Cursor color: %s", colorToHexString(cursor))
}

// colorToHexString returns a hex string representation of a color.
func colorToHexString(c color.Color) string {
if c == nil {
return ""
}
shift := func(v uint32) uint32 {
if v > 0xff {
return v >> 8
}
return v
}
r, g, b, _ := c.RGBA()
r, g, b = shift(r), shift(g), shift(b)
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
5 changes: 3 additions & 2 deletions exp/term/input/cancelreader_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"sync"
"time"

"github.com/charmbracelet/x/exp/term"
"github.com/erikgeiser/coninput"
"github.com/muesli/cancelreader"
"golang.org/x/sys/windows"
Expand All @@ -34,11 +33,13 @@ func newCancelreader(r io.Reader) (cancelreader.CancelReader, error) {
fallback := func(io.Reader) (cancelreader.CancelReader, error) {
return cancelreader.NewReader(r)
}

var dummy uint32
if f, ok := r.(cancelreader.File); !ok || f.Fd() != os.Stdin.Fd() ||
// If data was piped to the standard input, it does not emit events
// anymore. We can detect this if the console mode cannot be set anymore,
// in this case, we fallback to the default cancelreader implementation.
!term.IsTerminal(f.Fd()) {
windows.GetConsoleMode(windows.Handle(f.Fd()), &dummy) != nil {
return fallback(r)
}

Expand Down
173 changes: 173 additions & 0 deletions exp/term/terminal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package term

import (
"image/color"
"io"
"os"
"time"

"github.com/charmbracelet/x/exp/term/ansi"
"github.com/charmbracelet/x/exp/term/input"
)

// Environ represents the terminal environment.
type Environ interface {
Getenv(string) string
LookupEnv(string) (string, bool)
Environ() []string
}

// OsEnviron is an implementation of Environ that uses os.Environ.
type OsEnviron struct{}

var _ Environ = OsEnviron{}

// Environ implements Environ.
func (OsEnviron) Environ() []string {
return os.Environ()
}

// Getenv implements Environ.
func (OsEnviron) Getenv(key string) string {
return os.Getenv(key)
}

// LookupEnv implements Environ.
func (OsEnviron) LookupEnv(key string) (string, bool) {
return os.LookupEnv(key)
}

// BackgroundColor queries the terminal for the background color.
// If the terminal does not support querying the background color, nil is
// returned.
func BackgroundColor(in, out *os.File) (c color.Color) {
// nolint: errcheck
queryTerminal(in, out, func(events []input.Event) bool {
for _, e := range events {
switch e := e.(type) {
case input.BackgroundColorEvent:
c = e.Color
continue // we need to consume the next DA1 event
case input.PrimaryDeviceAttributesEvent:
return false
}
}
return true
}, ansi.RequestBackgroundColor+ansi.RequestPrimaryDeviceAttributes)
return
}

// ForegroundColor queries the terminal for the foreground color.
// If the terminal does not support querying the foreground color, nil is
// returned.
func ForegroundColor(in, out *os.File) (c color.Color) {
// nolint: errcheck
queryTerminal(in, out, func(events []input.Event) bool {
for _, e := range events {
switch e := e.(type) {
case input.ForegroundColorEvent:
c = e.Color
continue // we need to consume the next DA1 event
case input.PrimaryDeviceAttributesEvent:
return false
}
}
return true
}, ansi.RequestForegroundColor+ansi.RequestPrimaryDeviceAttributes)
return
}

// CursorColor queries the terminal for the cursor color.
// If the terminal does not support querying the cursor color, nil is returned.
func CursorColor(in, out *os.File) (c color.Color) {
// nolint: errcheck
queryTerminal(in, out, func(events []input.Event) bool {
for _, e := range events {
switch e := e.(type) {
case input.CursorColorEvent:
c = e.Color
continue // we need to consume the next DA1 event
case input.PrimaryDeviceAttributesEvent:
return false
}
}
return true
}, ansi.RequestCursorColor+ansi.RequestPrimaryDeviceAttributes)
return
}

// SupportsKittyKeyboard returns true if the terminal supports the Kitty
// keyboard protocol.
func SupportsKittyKeyboard(in, out *os.File) (supported bool) {
// nolint: errcheck
queryTerminal(in, out, func(events []input.Event) bool {
for _, e := range events {
switch e.(type) {
case input.KittyKeyboardEvent:
supported = true
continue // we need to consume the next DA1 event
case input.PrimaryDeviceAttributesEvent:
return false
}
}
return true
}, ansi.RequestKittyKeyboard+ansi.RequestPrimaryDeviceAttributes)
return
}

// queryTerminalFunc is a function that filters input events using a type
// switch. If false is returned, the queryTerminal function will stop reading
// input.
type queryTerminalFunc func(events []input.Event) bool

// queryTerminal queries the terminal for support of various features and
// returns a list of response events.
func queryTerminal(
in *os.File,
out *os.File,
filter queryTerminalFunc,
query string,
) error {
state, err := MakeRaw(in.Fd())
if err != nil {
return err
}

defer Restore(in.Fd(), state) // nolint: errcheck

rd, err := input.NewDriver(in, "", 0)
if err != nil {
return err
}

defer rd.Close() // nolint: errcheck

done := make(chan struct{}, 1)
defer close(done)
go func() {
select {
case <-done:
case <-time.After(2 * time.Second):
rd.Cancel()
}
}()

if _, err := io.WriteString(out, query); err != nil {
return err
}

events := make([]input.Event, 2)

for {
n, err := rd.ReadInput(events)
if err != nil {
return err
}

if !filter(events[:n]) {
break
}
}

return nil
}
16 changes: 16 additions & 0 deletions exp/term/terminal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package term_test

import (
"os"
"testing"

"github.com/charmbracelet/x/exp/term"
)

func TestTerminalQueries(t *testing.T) {
in, out := os.Stdin, os.Stdout
_ = term.BackgroundColor(in, out)
_ = term.ForegroundColor(in, out)
_ = term.CursorColor(in, out)
_ = term.SupportsKittyKeyboard(in, out)
}

0 comments on commit 7631c37

Please sign in to comment.