@@ -198,9 +198,85 @@ func ghosttySupported() bool {
return os.Getenv("GHOSTTY_RESOURCES_DIR") != ""
}
+func iterm2Supported() bool {
+ termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
+ if termProgram == "iterm.app" {
+ return true
+ }
+
+ // Check for iTerm2-specific environment variables
+ if os.Getenv("ITERM_SESSION_ID") != "" || os.Getenv("ITERM_PROFILE") != "" {
+ return true
+ }
+
+ return false
+}
+
+func weztermSupported() bool {
+ // Check for WezTerm-specific environment variables
+ if os.Getenv("WEZTERM_EXECUTABLE") != "" || os.Getenv("WEZTERM_CONFIG_FILE") != "" {
+ return true
+ }
+
+ termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
+ if termProgram == "wezterm" {
+ return true
+ }
+
+ term := strings.ToLower(os.Getenv("TERM"))
+ if strings.Contains(term, "wezterm") {
+ return true
+ }
+
+ return false
+}
+
+func waystSupported() bool {
+ term := strings.ToLower(os.Getenv("TERM"))
+ if strings.Contains(term, "wayst") {
+ return true
+ }
+
+ termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
+ if termProgram == "wayst" {
+ return true
+ }
+
+ return false
+}
+
+func warpSupported() bool {
+ termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
+ if termProgram == "warp" {
+ return true
+ }
+
+ // Check for Warp-specific environment variables
+ if os.Getenv("WARP_IS_LOCAL_SHELL_SESSION") != "" || os.Getenv("WARP_COMBINED_PROMPT_COMMAND_FINISHED") != "" {
+ return true
+ }
+
+ return false
+}
+
+func konsoleSupported() bool {
+ // Check for Konsole-specific environment variables
+ if os.Getenv("KONSOLE_DBUS_SESSION") != "" || os.Getenv("KONSOLE_VERSION") != "" {
+ return true
+ }
+
+ termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
+ if termProgram == "konsole" {
+ return true
+ }
+
+ return false
+}
+
// imageProtocolSupported checks if any supported image protocol terminal is detected.
func imageProtocolSupported() bool {
- return kittySupported() || ghosttySupported()
+ return kittySupported() || ghosttySupported() || iterm2Supported() ||
+ weztermSupported() || waystSupported() || warpSupported() || konsoleSupported()
}
func debugImageProtocol(format string, args ...interface{}) {
@@ -326,6 +402,52 @@ func kittyInlineImage(payload string) string {
return b.String()
}
+// iterm2InlineImage renders an image using iTerm2's image protocol
+func iterm2InlineImage(payload string) string {
+ if payload == "" {
+ return ""
+ }
+
+ // Calculate rows for cursor positioning
+ rows := 1
+ if data, err := base64.StdEncoding.DecodeString(payload); err == nil {
+ if img, _, err := image.Decode(bytes.NewReader(data)); err == nil {
+ cellHeight := getTerminalCellSize()
+ h := img.Bounds().Dy()
+ rows = (h + cellHeight - 1) / cellHeight
+ if rows < 1 {
+ rows = 1
+ }
+ debugImageProtocol("image height: %d pixels, cell height: %d pixels, rows needed: %d", h, cellHeight, rows)
+ }
+ }
+
+ // iTerm2 image protocol: ESC]1337;File=inline=1:<base64_data>BEL
+ result := fmt.Sprintf("\x1b]1337;File=inline=1:%s\x07\n", payload)
+
+ // Add placeholder for row spacing
+ result += fmt.Sprintf("%s%d%s\n", imageRowPlaceholderPrefix, rows, imageRowPlaceholderSuffix)
+
+ return result
+}
+
+// renderInlineImage renders an image using the appropriate protocol for the detected terminal
+func renderInlineImage(payload string) string {
+ if payload == "" {
+ return ""
+ }
+
+ if kittySupported() || ghosttySupported() || weztermSupported() || waystSupported() || konsoleSupported() {
+ // These terminals use the Kitty graphics protocol
+ return kittyInlineImage(payload)
+ } else if iterm2Supported() || warpSupported() {
+ // iTerm2 and Warp use the iTerm2 image protocol
+ return iterm2InlineImage(payload)
+ }
+
+ return ""
+}
+
// expandImageRowPlaceholders replaces image row placeholders with actual newlines.
func expandImageRowPlaceholders(text string) string {
re := regexp.MustCompile(regexp.QuoteMeta(imageRowPlaceholderPrefix) + `(\d+)` + regexp.QuoteMeta(imageRowPlaceholderSuffix))
@@ -441,8 +563,8 @@ func processBody(rawBody string, inline map[string]string, h1Style, h2Style, bod
}
if payload != "" {
- if rendered := kittyInlineImage(payload); rendered != "" {
- debugImageProtocol("rendered inline image src=%s len=%d dataURI=%t cid=%t (kitty=%t ghostty=%t)", src, len(payload), strings.HasPrefix(src, "data:"), strings.HasPrefix(src, "cid:"), kittySupported(), ghosttySupported())
+ if rendered := renderInlineImage(payload); rendered != "" {
+ debugImageProtocol("rendered inline image src=%s len=%d dataURI=%t cid=%t (kitty=%t ghostty=%t iterm2=%t wezterm=%t wayst=%t warp=%t konsole=%t)", src, len(payload), strings.HasPrefix(src, "data:"), strings.HasPrefix(src, "cid:"), kittySupported(), ghosttySupported(), iterm2Supported(), weztermSupported(), waystSupported(), warpSupported(), konsoleSupported())
s.ReplaceWithHtml("\n" + rendered + "\n")
return
}
@@ -451,7 +573,7 @@ func processBody(rawBody string, inline map[string]string, h1Style, h2Style, bod
debugImageProtocol("no payload for src=%s dataURI=%t cid=%t", src, strings.HasPrefix(src, "data:"), strings.HasPrefix(src, "cid:"))
}
} else {
- debugImageProtocol("image protocol not supported for src=%s (kitty=%t ghostty=%t)", src, kittySupported(), ghosttySupported())
+ debugImageProtocol("image protocol not supported for src=%s (kitty=%t ghostty=%t iterm2=%t wezterm=%t wayst=%t warp=%t konsole=%t)", src, kittySupported(), ghosttySupported(), iterm2Supported(), weztermSupported(), waystSupported(), warpSupported(), konsoleSupported())
}
if hyperlinkSupported() {
s.ReplaceWithHtml(hyperlink(src, fmt.Sprintf("\n [Click here to view image: %s] \n", alt)))
@@ -8,6 +8,26 @@ import (
"github.com/charmbracelet/lipgloss"
)
+// clearAllTerminalEnv clears all environment variables that could indicate terminal capabilities
+func clearAllTerminalEnv() {
+ // Clear hyperlink support indicators
+ os.Unsetenv("VTE_VERSION")
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WEZTERM_CONFIG_FILE")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("ITERM_PROFILE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("WARP_COMBINED_PROMPT_COMMAND_FINISHED")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ os.Unsetenv("KONSOLE_VERSION")
+
+ // Set basic terminal that doesn't support anything special
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "basic")
+}
+
func TestDecodeQuotedPrintable(t *testing.T) {
testCases := []struct {
name string
@@ -182,93 +202,482 @@ func TestHyperlinkSupported(t *testing.T) {
}
}
-func TestHyperlink(t *testing.T) {
+func TestImageProtocolSupported(t *testing.T) {
+ // Save original environment variables
+ origTerm := os.Getenv("TERM")
+ origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
+ origTermProgram := os.Getenv("TERM_PROGRAM")
+ origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+ origItermlSession := os.Getenv("ITERM_SESSION_ID")
+ origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
+ origWarpLocal := os.Getenv("WARP_IS_LOCAL_SHELL_SESSION")
+ origKonsoleDBus := os.Getenv("KONSOLE_DBUS_SESSION")
+
+ // Restore environment variables after test
+ defer func() {
+ os.Setenv("TERM", origTerm)
+ os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
+ os.Setenv("TERM_PROGRAM", origTermProgram)
+ os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+ os.Setenv("ITERM_SESSION_ID", origItermlSession)
+ os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
+ os.Setenv("WARP_IS_LOCAL_SHELL_SESSION", origWarpLocal)
+ os.Setenv("KONSOLE_DBUS_SESSION", origKonsoleDBus)
+ }()
+
testCases := []struct {
- name string
- url string
- text string
- hyperlinkSupported bool
- expected string
+ name string
+ setupEnv func()
+ clearAllEnv func()
+ expected bool
}{
{
- name: "Supported terminal with different text",
- url: "https://example.com",
- text: "Click here",
- hyperlinkSupported: true,
- expected: "\x1b]8;;https://example.com\x07Click here\x1b]8;;\x07",
+ name: "No supported terminals",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "basic")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: false,
},
{
- name: "Supported terminal with same text as URL",
- url: "https://example.com",
- text: "https://example.com",
- hyperlinkSupported: true,
- expected: "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
+ name: "Kitty supported via TERM",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm-kitty")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: true,
},
{
- name: "Supported terminal with empty text",
- url: "https://example.com",
- text: "",
- hyperlinkSupported: true,
- expected: "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
+ name: "Kitty supported via KITTY_WINDOW_ID",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("KITTY_WINDOW_ID", "1")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: true,
},
{
- name: "Unsupported terminal with different text",
- url: "https://example.com",
- text: "Click here",
- hyperlinkSupported: false,
- expected: "Click here <https://example.com>",
+ name: "Ghostty supported via TERM_PROGRAM",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "ghostty")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: true,
},
{
- name: "Unsupported terminal with same text as URL",
- url: "https://example.com",
- text: "https://example.com",
- hyperlinkSupported: false,
- expected: "<https://example.com>",
+ name: "iTerm2 supported via TERM_PROGRAM",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "iterm.app")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: true,
},
{
- name: "Unsupported terminal with empty text",
- url: "https://example.com",
- text: "",
- hyperlinkSupported: false,
- expected: "<https://example.com>",
+ name: "WezTerm supported via WEZTERM_EXECUTABLE",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: true,
+ },
+ {
+ name: "Warp supported via WARP_IS_LOCAL_SHELL_SESSION",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("WARP_IS_LOCAL_SHELL_SESSION", "1")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("KONSOLE_DBUS_SESSION")
+ },
+ expected: true,
+ },
+ {
+ name: "Konsole supported via KONSOLE_DBUS_SESSION",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("KONSOLE_DBUS_SESSION", "/Sessions/1")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ os.Unsetenv("WARP_IS_LOCAL_SHELL_SESSION")
+ },
+ expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- // Save original env vars
- origTerm := os.Getenv("TERM")
- origTermProgram := os.Getenv("TERM_PROGRAM")
- origVteVersion := os.Getenv("VTE_VERSION")
- origKittyWindowID := os.Getenv("KITTY_WINDOW_ID")
- origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
- origWeztermExecutable := os.Getenv("WEZTERM_EXECUTABLE")
+ tc.clearAllEnv()
+ tc.setupEnv()
- // Set env to control hyperlink support
- if tc.hyperlinkSupported {
+ if result != tc.expected {
+ t.Errorf("Expected %q, got %q", tc.expected, result)
+ }
+ })
+ }
+}
+
+func TestHyperlinkSupported(t *testing.T) {
+ // Save original environment variables
+ origTerm := os.Getenv("TERM")
+ origTermProgram := os.Getenv("TERM_PROGRAM")
+ origVTEVersion := os.Getenv("VTE_VERSION")
+ origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
+ origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+ origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
+
+ // Restore environment variables after test
+ defer func() {
+ os.Setenv("TERM", origTerm)
+ os.Setenv("TERM_PROGRAM", origTermProgram)
+ os.Setenv("VTE_VERSION", origVTEVersion)
+ os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
+ os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+ os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
+ }()
+
+ testCases := []struct {
+ name string
+ setupEnv func()
+ clearAllEnv func()
+ expected bool
+ }{
+ {
+ name: "No hyperlink support",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "basic")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("VTE_VERSION")
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ },
+ expected: false,
+ },
+ {
+ name: "Kitty hyperlink support via TERM",
+ setupEnv: func() {
os.Setenv("TERM", "xterm-kitty")
- } else {
- // Clear all environment variables that could indicate hyperlink support
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("VTE_VERSION")
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ },
+ expected: true,
+ },
+ {
+ name: "VTE-based terminal hyperlink support",
+ setupEnv: func() {
os.Setenv("TERM", "xterm")
- os.Unsetenv("TERM_PROGRAM")
+ os.Setenv("VTE_VERSION", "0.60.3")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ },
+ expected: true,
+ },
+ {
+ name: "iTerm2 hyperlink support",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "iterm.app")
+ },
+ clearAllEnv: func() {
os.Unsetenv("VTE_VERSION")
os.Unsetenv("KITTY_WINDOW_ID")
os.Unsetenv("GHOSTTY_RESOURCES_DIR")
os.Unsetenv("WEZTERM_EXECUTABLE")
+ },
+ expected: true,
+ },
+ {
+ name: "WezTerm hyperlink support",
+ setupEnv: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
+ },
+ clearAllEnv: func() {
+ os.Unsetenv("VTE_VERSION")
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ },
+ expected: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.clearAllEnv()
+ tc.setupEnv()
+
+ result := hyperlinkSupported()
+ if result != tc.expected {
+ t.Errorf("Expected %t, got %t", tc.expected, result)
}
+ })
+ }
+}
- result := hyperlink(tc.url, tc.text)
+func TestProcessBodyWithHyperlinkSupport(t *testing.T) {
+ // Save original environment variables
+ origTerm := os.Getenv("TERM")
+ origTermProgram := os.Getenv("TERM_PROGRAM")
+ origVTEVersion := os.Getenv("VTE_VERSION")
+ origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
+
+ // Restore environment variables after test
+ defer func() {
+ os.Setenv("TERM", origTerm)
+ os.Setenv("TERM_PROGRAM", origTermProgram)
+ os.Setenv("VTE_VERSION", origVTEVersion)
+ os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
+ }()
- // Restore original env vars
- os.Setenv("TERM", origTerm)
- os.Setenv("TERM_PROGRAM", origTermProgram)
- os.Setenv("VTE_VERSION", origVteVersion)
- os.Setenv("KITTY_WINDOW_ID", origKittyWindowID)
- os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
- os.Setenv("WEZTERM_EXECUTABLE", origWeztermExecutable)
+ h1Style := lipgloss.NewStyle().SetString("H1")
+ h2Style := lipgloss.NewStyle().SetString("H2")
+ bodyStyle := lipgloss.NewStyle().SetString("BODY")
- if result != tc.expected {
- t.Errorf("Expected %q, got %q", tc.expected, result)
+ testCases := []struct {
+ name string
+ setupHyperlinks func()
+ input string
+ expectedContains string
+ expectedNotContains string
+ }{
+ {
+ name: "Link with hyperlink support",
+ setupHyperlinks: func() {
+ os.Setenv("TERM", "xterm-kitty")
+ os.Unsetenv("VTE_VERSION")
+ os.Unsetenv("KITTY_WINDOW_ID")
+ },
+ input: `<a href="http://example.com">Click here</a>`,
+ expectedContains: "Click here",
+ expectedNotContains: "<http://example.com>",
+ },
+ {
+ name: "Link without hyperlink support",
+ setupHyperlinks: func() {
+ clearAllTerminalEnv()
+ },
+ input: `<a href="http://example.com">Click here</a>`,
+ expectedContains: "Click here <http://example.com>",
+ },
+ {
+ name: "Image link with hyperlink support",
+ setupHyperlinks: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("VTE_VERSION", "0.60.3")
+ os.Unsetenv("KITTY_WINDOW_ID")
+ },
+ input: `<img src="http://example.com/img.png" alt="alt text">`,
+ expectedContains: "[Click here to view image: alt text]",
+ expectedNotContains: "<http://example.com/img.png>",
+ },
+ {
+ name: "Image link without hyperlink support",
+ setupHyperlinks: func() {
+ clearAllTerminalEnv()
+ },
+ input: `<img src="http://example.com/img.png" alt="alt text">`,
+ expectedContains: "[Image: alt text, http://example.com/img.png]",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.setupHyperlinks()
+
+ processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
+ if err != nil {
+ t.Fatalf("ProcessBody() failed: %v", err)
+ }
+
+ if !strings.Contains(processed, tc.expectedContains) {
+ t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expectedContains)
+ }
+
+ if tc.expectedNotContains != "" && strings.Contains(processed, tc.expectedNotContains) {
+ t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", processed, tc.expectedNotContains)
+ }
+ })
+ }
+}
+
+func TestProcessBodyWithImageProtocol(t *testing.T) {
+ // Save original environment variables
+ origTerm := os.Getenv("TERM")
+ origTermProgram := os.Getenv("TERM_PROGRAM")
+ origKittyWindow := os.Getenv("KITTY_WINDOW_ID")
+ origGhosttyResources := os.Getenv("GHOSTTY_RESOURCES_DIR")
+ origItermlSession := os.Getenv("ITERM_SESSION_ID")
+ origWeztermExec := os.Getenv("WEZTERM_EXECUTABLE")
+
+ // Restore environment variables after test
+ defer func() {
+ os.Setenv("TERM", origTerm)
+ os.Setenv("TERM_PROGRAM", origTermProgram)
+ os.Setenv("KITTY_WINDOW_ID", origKittyWindow)
+ os.Setenv("GHOSTTY_RESOURCES_DIR", origGhosttyResources)
+ os.Setenv("ITERM_SESSION_ID", origItermlSession)
+ os.Setenv("WEZTERM_EXECUTABLE", origWeztermExec)
+ }()
+
+ h1Style := lipgloss.NewStyle().SetString("H1")
+ h2Style := lipgloss.NewStyle().SetString("H2")
+ bodyStyle := lipgloss.NewStyle().SetString("BODY")
+
+ // Create a simple base64 PNG image (1x1 pixel white PNG)
+ testBase64PNG := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
+
+ testCases := []struct {
+ name string
+ setupImageProtocol func()
+ clearAllImageEnv func()
+ input string
+ expectedContains string
+ expectedNotContains string
+ }{
+ {
+ name: "Data URI image with Kitty support",
+ setupImageProtocol: func() {
+ os.Setenv("TERM", "xterm-kitty")
+ },
+ clearAllImageEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ },
+ input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
+ expectedContains: "\x1b_Gf=100,a=T,q=2,C=1,m=0;",
+ expectedNotContains: "[Image: test image,",
+ },
+ {
+ name: "Data URI image with iTerm2 support",
+ setupImageProtocol: func() {
+ os.Setenv("TERM", "xterm")
+ os.Setenv("TERM_PROGRAM", "iterm.app")
+ },
+ clearAllImageEnv: func() {
+ os.Unsetenv("KITTY_WINDOW_ID")
+ os.Unsetenv("GHOSTTY_RESOURCES_DIR")
+ os.Unsetenv("ITERM_SESSION_ID")
+ os.Unsetenv("WEZTERM_EXECUTABLE")
+ },
+ input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
+ expectedContains: "\x1b]1337;File=inline=1:",
+ expectedNotContains: "[Image: test image,",
+ },
+ {
+ name: "Data URI image without protocol support",
+ setupImageProtocol: func() {
+ clearAllTerminalEnv()
+ },
+ clearAllImageEnv: func() {
+ // This is handled by clearAllTerminalEnv now
+ },
+ input: `<img src="data:image/png;base64,` + testBase64PNG + `" alt="test image">`,
+ expectedContains: "[Image: test image,",
+ },
+ {
+ name: "Remote image with WezTerm support (has hyperlink support)",
+ setupImageProtocol: func() {
+ clearAllTerminalEnv()
+ os.Setenv("WEZTERM_EXECUTABLE", "/usr/bin/wezterm")
+ },
+ clearAllImageEnv: func() {
+ // This is handled by clearAllTerminalEnv now
+ },
+ input: `<img src="http://example.com/img.png" alt="remote image">`,
+ expectedContains: "[Click here to view image: remote image]", // Remote images won't render without actual fetch, but hyperlinks work
+ },
+ {
+ name: "Remote image without protocol support",
+ setupImageProtocol: func() {
+ clearAllTerminalEnv()
+ },
+ clearAllImageEnv: func() {
+ // This is handled by clearAllTerminalEnv now
+ },
+ input: `<img src="http://example.com/img.png" alt="remote image">`,
+ expectedContains: "[Image: remote image,",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.clearAllImageEnv()
+ tc.setupImageProtocol()
+
+ processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle)
+ if err != nil {
+ t.Fatalf("ProcessBody() failed: %v", err)
+ }
+
+ if !strings.Contains(processed, tc.expectedContains) {
+ t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expectedContains)
+ }
+
+ if tc.expectedNotContains != "" && strings.Contains(processed, tc.expectedNotContains) {
+ t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", processed, tc.expectedNotContains)
}
})
}
@@ -289,36 +698,21 @@ func TestProcessBody(t *testing.T) {
input: "<p>Hello, world!</p>",
expected: "Hello, world!",
},
- {
- name: "With link HTML",
- input: `<a href="http://example.com">Click here</a>`,
- expected: "Click here",
- },
- {
- name: "With image HTML",
- input: `<img src="http://example.com/img.png" alt="alt text">`,
- expected: "[Image: alt text, http://example.com/img.png]",
- },
{
name: "With headers HTML",
input: "<h1>Header 1</h1>",
expected: "Header 1",
},
- {
- name: "With link Markdown",
- input: `[Click here](http://example.com)`,
- expected: "Click here",
- },
- {
- name: "With image Markdown",
- input: ``,
- expected: "[Image: alt text, http://example.com/img.png]",
- },
{
name: "With headers Markdown",
input: "# Header 1",
expected: "Header 1",
},
+ {
+ name: "Plain text",
+ input: "Just plain text without any markup",
+ expected: "Just plain text without any markup",
+ },
}
for _, tc := range testCases {