diff --git a/terminal_renderer.go b/terminal_renderer.go index af3d8ee..6fae375 100644 --- a/terminal_renderer.go +++ b/terminal_renderer.go @@ -5,6 +5,7 @@ import ( "errors" "hash/maphash" "io" + "os" "strings" "github.com/charmbracelet/colorprofile" @@ -1584,6 +1585,9 @@ func xtermCaps(termtype string) (v capabilities) { v &^= capHPA v &^= capCHT v &^= capREP + if isJediTerm() { + v &^= capCBT + } } case "alacritty": v = allCaps @@ -1599,3 +1603,28 @@ func xtermCaps(termtype string) (v capabilities) { return v } + +// isJediTerm returns true when running inside JetBrains' JediTerm terminal +// emulator (GoLand, IntelliJ, etc.). JediTerm mishandles CBT escape +// sequences that ultraviolet emits under normal TERM values. +// +// Detection layers (checked in order): +// 1. TERMINAL_EMULATOR == "JetBrains-JediTerm" — all platforms, Terminal tool window +// 2. __CFBundleIdentifier starts with "com.jetbrains." — macOS, "Emulate terminal" Run/Debug +// 3. XPC_SERVICE_NAME contains "com.jetbrains." — macOS, "Emulate terminal" Run/Debug +// 4. TOOLBOX_VERSION is non-empty — all platforms, all contexts (JetBrains Toolbox installs only) +func isJediTerm() bool { + if os.Getenv("TERMINAL_EMULATOR") == "JetBrains-JediTerm" { + return true + } + if strings.HasPrefix(os.Getenv("__CFBundleIdentifier"), "com.jetbrains.") { + return true + } + if strings.Contains(os.Getenv("XPC_SERVICE_NAME"), "com.jetbrains.") { + return true + } + if os.Getenv("TOOLBOX_VERSION") != "" { + return true + } + return false +} diff --git a/terminal_renderer_test.go b/terminal_renderer_test.go index 012a423..d93b156 100644 --- a/terminal_renderer_test.go +++ b/terminal_renderer_test.go @@ -1326,6 +1326,92 @@ func TestRendererEnterExitAltScreen(t *testing.T) { } } +func TestIsJediTerm(t *testing.T) { + tests := []struct { + name string + env map[string]string + want bool + }{ + { + name: "detected via TERMINAL_EMULATOR", + env: map[string]string{"TERMINAL_EMULATOR": "JetBrains-JediTerm"}, + want: true, + }, + { + name: "detected via __CFBundleIdentifier", + env: map[string]string{"__CFBundleIdentifier": "com.jetbrains.goland"}, + want: true, + }, + { + name: "detected via XPC_SERVICE_NAME", + env: map[string]string{"XPC_SERVICE_NAME": "application.com.jetbrains.goland.12345"}, + want: true, + }, + { + name: "not detected when env is empty", + env: map[string]string{}, + want: false, + }, + { + name: "detected via TOOLBOX_VERSION", + env: map[string]string{"TOOLBOX_VERSION": "2.6.2.40984"}, + want: true, + }, + { + name: "not detected for other terminals", + env: map[string]string{"TERMINAL_EMULATOR": "iTerm2"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear relevant env vars (t.Setenv handles save/restore) + keys := []string{"TERMINAL_EMULATOR", "__CFBundleIdentifier", "XPC_SERVICE_NAME", "TOOLBOX_VERSION"} + for _, k := range keys { + t.Setenv(k, "") + } + // Set test env vars + for k, v := range tt.env { + t.Setenv(k, v) + } + + if got := isJediTerm(); got != tt.want { + t.Errorf("isJediTerm() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestXtermCapsJediTermDisablesCBT(t *testing.T) { + // Save and clear env + keys := []string{"TERMINAL_EMULATOR", "__CFBundleIdentifier", "XPC_SERVICE_NAME", "TOOLBOX_VERSION"} + for _, k := range keys { + t.Setenv(k, "") + } + + // Without JediTerm, xterm-256color should have CBT + caps := xtermCaps("xterm-256color") + if !caps.Contains(capCBT) { + t.Error("xterm-256color should have capCBT when not in JediTerm") + } + + // With JediTerm, xterm-256color should NOT have CBT + t.Setenv("TERMINAL_EMULATOR", "JetBrains-JediTerm") + caps = xtermCaps("xterm-256color") + if caps.Contains(capCBT) { + t.Error("xterm-256color should NOT have capCBT when in JediTerm") + } + + // Other caps should still be present (VPA, CHA, etc.) + if !caps.Contains(capVPA) { + t.Error("xterm-256color in JediTerm should still have capVPA") + } + if !caps.Contains(capCHA) { + t.Error("xterm-256color in JediTerm should still have capCHA") + } +} + // Helper type for testing logger type testLogger struct { buf *bytes.Buffer