Skip to content

Commit

Permalink
feat(term): support XTGETTCAP sequence
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Mar 7, 2024
1 parent 8f64c9d commit ad8dadc
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 1 deletion.
31 changes: 31 additions & 0 deletions exp/term/ansi/termcap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ansi

import (
"encoding/hex"
"strings"
)

// RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings.
//
// DCS + q <Pt> ST
//
// Where <Pt> is a list of Termcap/Terminfo capabilities, encoded in 2-digit
// hexadecimals, separated by semicolons.
//
// See: https://man7.org/linux/man-pages/man5/terminfo.5.html
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
func RequestTermcap(caps ...string) string {
if len(caps) == 0 {
return ""
}

s := "\x1bP+q"
for i, c := range caps {
if i > 0 {
s += ";"
}
s += strings.ToUpper(hex.EncodeToString([]byte(c)))
}

return s + "\x1b\\"
}
62 changes: 62 additions & 0 deletions exp/term/examples/parserlog/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"fmt"
"log"
"os"

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

type dispatcher struct{}

func (p *dispatcher) Print(r rune) {
fmt.Printf("[Print] %c\n", r)
}

func (p *dispatcher) Execute(code byte) {
fmt.Printf("[Execute] 0x%02x\n", code)
}

func (p *dispatcher) DcsPut(code byte) {
fmt.Printf("[DcsPut] %02x\n", code)
}

func (p *dispatcher) DcsUnhook() {
fmt.Printf("[DcsUnhook]\n")
}

func (p *dispatcher) DcsHook(prefix string, params [][]uint16, intermediates []byte, r rune, ignore bool) {
fmt.Print("[DcsHook]")
if prefix != "" {
fmt.Printf(" prefix=%s", prefix)
}
fmt.Printf(" params=%v, intermediates=%v, final=%c, ignore=%v\n", params, intermediates, r, ignore)
}

func (p *dispatcher) OscDispatch(params [][]byte, bellTerminated bool) {
fmt.Printf("[OscDispatch]")
for _, param := range params {
fmt.Printf(" param=%q", param)
}
fmt.Printf(" bellTerminated=%v\n", bellTerminated)
}

func (p *dispatcher) CsiDispatch(prefix string, params [][]uint16, intermediates []byte, r rune, ignore bool) {
fmt.Print("[CsiDispatch]")
if prefix != "" {
fmt.Printf(" prefix=%s", prefix)
}
fmt.Printf(" params=%v, intermediates=%v, final=%c, ignore=%v\n", params, intermediates, r, ignore)
}

func (p *dispatcher) EscDispatch(intermediates []byte, r rune, ignore bool) {
fmt.Printf("[EscDispatch] intermediates=%v, final=%c, ignore=%v\n", intermediates, r, ignore)
}

func main() {
dispatcher := &dispatcher{}
if err := parser.New(dispatcher).Parse(os.Stdin); err != nil {
log.Fatal(err)
}
}
2 changes: 2 additions & 0 deletions exp/term/examples/readinput/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ OUT:
execute(ansi.RequestPrimaryDeviceAttributes)
case "x":
execute(ansi.RequestXTVersion)
case "t":
execute(ansi.RequestTermcap("kbs", "colors", "Tc", "cols"))
}
case prev == "m":
switch string(currKey.Rune) {
Expand Down
104 changes: 103 additions & 1 deletion exp/term/input/parse.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package input

import (
"log"
"unicode/utf8"

"github.com/charmbracelet/x/exp/term/ansi"
Expand Down Expand Up @@ -513,7 +514,108 @@ func parseCtrl(intro8, intro7 byte) func([]byte) (int, Event) {

func parseDcs(p []byte) (int, Event) {
// DCS sequences are introduced by DCS (0x90) or ESC P (0x1b 0x50)
return parseCtrl(ansi.DCS, 'P')(p)
var seq []byte
var i int
if p[i] == ansi.DCS || p[i] == ansi.ESC {
seq = append(seq, p[i])
i++
}
if i < len(p) && p[i-1] == ansi.ESC && p[i] == 'P' {
seq = append(seq, p[i])
i++
}

// Scan parameter bytes in the range 0x30-0x3F
var start, end int // start and end of the parameter bytes
for j := 0; i < len(p) && p[i] >= 0x30 && p[i] <= 0x3F; i, j = i+1, j+1 {
if j == 0 {
start = i
}
seq = append(seq, p[i])
}

end = i

// Scan intermediate bytes in the range 0x20-0x2F
var istart, iend int
for j := 0; i < len(p) && p[i] >= 0x20 && p[i] <= 0x2F; i, j = i+1, j+1 {
if j == 0 {
istart = i
}
seq = append(seq, p[i])
}

iend = i

// Final byte
var final byte

// Scan final byte in the range 0x40-0x7E
if i >= len(p) || p[i] < 0x40 || p[i] > 0x7E {
return len(seq), UnknownEvent(seq)
}
// Add the final byte
final = p[i]
seq = append(seq, p[i])

if i+1 >= len(p) {
return len(seq), UnknownEvent(seq)
}

// Collect data bytes until a ST character is found
// data bytes are in the range of 0x08-0x0D and 0x20-0x7F
// but we don't care about the actual values for now
var data []byte
for i++; i < len(p) && p[i] != ansi.ST && p[i] != ansi.ESC; i++ {
data = append(data, p[i])
seq = append(seq, p[i])
}

if i >= len(p) {
return len(seq), UnknownEvent(seq)
}

seq = append(seq, p[i])

// Check 7-bit ST (string terminator) character
if len(p) > i+1 && p[i] == ansi.ESC && p[i+1] == '\\' {
i++
seq = append(seq, p[i])
}

log.Printf("seq: %q\r\n", seq)

switch final {
case 'r':
inters := p[istart:iend] // intermediates
if len(inters) == 0 {
return len(seq), UnknownDcsEvent(seq)
}
switch inters[0] {
case '+':
// XTGETTCAP responses
params := ansi.Params(p[start:end])
if len(params) == 0 {
return len(seq), UnknownDcsEvent(seq)
}

switch params[0][0] {
case 0, 1:
tc := parseTermcap(data)
// XXX: some terminals like KiTTY report invalid responses with
// their queries i.e. sending a query for "Tc" using "\x1bP+q5463\x1b\\"
// returns "\x1bP0+r5463\x1b\\".
// The specs says that invalid responses should be in the form of
// DCS 0 + r ST "\x1bP0+r\x1b\\"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
tc.IsValid = params[0][0] == 1
return len(seq), tc
}
}
}

return len(seq), UnknownDcsEvent(seq)
}

func parseApc(p []byte) (int, Event) {
Expand Down
75 changes: 75 additions & 0 deletions exp/term/input/termcap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package input

import (
"bytes"
"encoding/hex"
"fmt"
"strings"
)

// TermcapEvent represents a Termcap response event. Termcap responses are
// generated by the terminal in response to RequestTermcap (XTGETTCAP)
// requests.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
type TermcapEvent struct {
Values map[string]string
IsValid bool
}

// String implements fmt.Stringer.
func (t TermcapEvent) String() string {
var s strings.Builder
var i int
if !t.IsValid {
s.WriteString("!")
if len(t.Values) > 0 {
s.WriteString(" ")
}
}
for k, v := range t.Values {
if i > 0 {
s.WriteString(",")
}
s.WriteString(k)
if v != "" {
s.WriteString("=")
s.WriteString(fmt.Sprintf("%q", v))
}
i++
}
return s.String()
}

func parseTermcap(data []byte) TermcapEvent {
// XTGETTCAP
if len(data) == 0 {
return TermcapEvent{}
}

tc := TermcapEvent{Values: make(map[string]string)}
split := bytes.Split(data, []byte{';'})
for _, s := range split {
parts := bytes.SplitN(s, []byte{'='}, 2)
if len(parts) == 0 {
return TermcapEvent{}
}

name, err := hex.DecodeString(string(parts[0]))
if err != nil || len(name) == 0 {
continue
}

var value []byte
if len(parts) > 1 {
value, err = hex.DecodeString(string(parts[1]))
if err != nil {
continue
}
}

tc.Values[string(name)] = string(value)
}

return tc
}

0 comments on commit ad8dadc

Please sign in to comment.