Skip to content

Commit

Permalink
feat(term): ansi: add passthrough sequences
Browse files Browse the repository at this point in the history
Screen and Tmux passthrough
  • Loading branch information
aymanbagabas committed Apr 8, 2024
1 parent cff6f7e commit 0100b8b
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 0 deletions.
63 changes: 63 additions & 0 deletions exp/term/ansi/passthrough.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ansi

import (
"bytes"
)

// ScreenPassthrough wraps the given ANSI sequence in a DCS passthrough
// sequence to be sent to the outer terminal. This is used to send raw escape
// sequences to the outer terminal when running inside GNU Screen.
//
// DCS <data> ST
//
// Note: Screen limits the length of string sequences to 768 bytes (since 2014).
// Use zero to indicate no limit, otherwise, this will chunk the returned
// string into limit sized chunks.
//
// See: https://www.gnu.org/software/screen/manual/screen.html#String-Escapes
// See: https://git.savannah.gnu.org/cgit/screen.git/tree/src/screen.h?id=c184c6ec27683ff1a860c45be5cf520d896fd2ef#n44
func ScreenPassthrough(seq string, limit int) string {
var b bytes.Buffer
b.WriteString("\x1bP")
if limit > 0 {
for i := 0; i < len(seq); i += limit {
end := i + limit
if end > len(seq) {
end = len(seq)
}
b.WriteString(seq[i:end])
if end < len(seq) {
b.WriteString("\x1b\\\x1bP")
}
}
} else {
b.WriteString(seq)
}
b.WriteString("\x1b\\")
return b.String()
}

// TmuxPassthrough wraps the given ANSI sequence in a special DCS passthrough
// sequence to be sent to the outer terminal. This is used to send raw escape
// sequences to the outer terminal when running inside Tmux.
//
// DCS tmux ; <escaped-data> ST
//
// Where <escaped-data> is the given sequence in which all occurrences of ESC
// (0x1b) are doubled i.e. replaced with ESC ESC (0x1b 0x1b).
//
// Note: this needs the `allow-passthrough` option to be set to `on`.
//
// See: https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
func TmuxPassthrough(seq string) string {
var b bytes.Buffer
b.WriteString("\x1bPtmux;")
for i := 0; i < len(seq); i++ {
if seq[i] == ESC {
b.WriteByte(ESC)
}
b.WriteByte(seq[i])
}
b.WriteString("\x1b\\")
return b.String()
}
63 changes: 63 additions & 0 deletions exp/term/ansi/passthrough_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ansi_test

import (
"testing"

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

var passthroughCases = []struct {
name string
seq string
limit int
screen string
tmux string
}{
{
name: "empty",
seq: "",
screen: "\x1bP\x1b\\",
tmux: "\x1bPtmux;\x1b\\",
},
{
name: "short",
seq: "hello",
screen: "\x1bPhello\x1b\\",
tmux: "\x1bPtmux;hello\x1b\\",
},
{
name: "limit",
seq: "foobarbaz",
limit: 3,
screen: "\x1bPfoo\x1b\\\x1bPbar\x1b\\\x1bPbaz\x1b\\",
tmux: "\x1bPtmux;foobarbaz\x1b\\",
},
{
name: "escaped",
seq: "\x1b]52;c;Zm9vYmFy\x07",
screen: "\x1bP\x1b]52;c;Zm9vYmFy\x07\x1b\\",
tmux: "\x1bPtmux;\x1b\x1b]52;c;Zm9vYmFy\x07\x1b\\",
},
}

func TestScreenPassthrough(t *testing.T) {
for i, tt := range passthroughCases {
t.Run(tt.name, func(t *testing.T) {
got := ansi.ScreenPassthrough(tt.seq, tt.limit)
if got != tt.screen {
t.Errorf("case: %d, ScreenPassthrough() = %q, want %q", i+1, got, tt.screen)
}
})
}
}

func TestTmuxPassthrough(t *testing.T) {
for i, tt := range passthroughCases {
t.Run(tt.name, func(t *testing.T) {
got := ansi.TmuxPassthrough(tt.seq)
if got != tt.tmux {
t.Errorf("case: %d, TmuxPassthrough() = %q, want %q", i+1, got, tt.tmux)
}
})
}
}

0 comments on commit 0100b8b

Please sign in to comment.