From 0100b8b9b531e16442f6084f7ee2cb038b6602bc Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 8 Apr 2024 12:26:57 +0300 Subject: [PATCH] feat(term): ansi: add passthrough sequences Screen and Tmux passthrough --- exp/term/ansi/passthrough.go | 63 +++++++++++++++++++++++++++++++ exp/term/ansi/passthrough_test.go | 63 +++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 exp/term/ansi/passthrough.go create mode 100644 exp/term/ansi/passthrough_test.go diff --git a/exp/term/ansi/passthrough.go b/exp/term/ansi/passthrough.go new file mode 100644 index 00000000..14a74522 --- /dev/null +++ b/exp/term/ansi/passthrough.go @@ -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 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 ; ST +// +// Where 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() +} diff --git a/exp/term/ansi/passthrough_test.go b/exp/term/ansi/passthrough_test.go new file mode 100644 index 00000000..0fc8f1e3 --- /dev/null +++ b/exp/term/ansi/passthrough_test.go @@ -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) + } + }) + } +}