Skip to content

Commit 745ce82

Browse files
authored
feat(image protocol): support for the terminals (see description) (#95)
1 parent 09e942c commit 745ce82

2 files changed

Lines changed: 598 additions & 82 deletions

File tree

view/html.go

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,85 @@ func ghosttySupported() bool {
198198
return os.Getenv("GHOSTTY_RESOURCES_DIR") != ""
199199
}
200200

201+
func iterm2Supported() bool {
202+
termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
203+
if termProgram == "iterm.app" {
204+
return true
205+
}
206+
207+
// Check for iTerm2-specific environment variables
208+
if os.Getenv("ITERM_SESSION_ID") != "" || os.Getenv("ITERM_PROFILE") != "" {
209+
return true
210+
}
211+
212+
return false
213+
}
214+
215+
func weztermSupported() bool {
216+
// Check for WezTerm-specific environment variables
217+
if os.Getenv("WEZTERM_EXECUTABLE") != "" || os.Getenv("WEZTERM_CONFIG_FILE") != "" {
218+
return true
219+
}
220+
221+
termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
222+
if termProgram == "wezterm" {
223+
return true
224+
}
225+
226+
term := strings.ToLower(os.Getenv("TERM"))
227+
if strings.Contains(term, "wezterm") {
228+
return true
229+
}
230+
231+
return false
232+
}
233+
234+
func waystSupported() bool {
235+
term := strings.ToLower(os.Getenv("TERM"))
236+
if strings.Contains(term, "wayst") {
237+
return true
238+
}
239+
240+
termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
241+
if termProgram == "wayst" {
242+
return true
243+
}
244+
245+
return false
246+
}
247+
248+
func warpSupported() bool {
249+
termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
250+
if termProgram == "warp" {
251+
return true
252+
}
253+
254+
// Check for Warp-specific environment variables
255+
if os.Getenv("WARP_IS_LOCAL_SHELL_SESSION") != "" || os.Getenv("WARP_COMBINED_PROMPT_COMMAND_FINISHED") != "" {
256+
return true
257+
}
258+
259+
return false
260+
}
261+
262+
func konsoleSupported() bool {
263+
// Check for Konsole-specific environment variables
264+
if os.Getenv("KONSOLE_DBUS_SESSION") != "" || os.Getenv("KONSOLE_VERSION") != "" {
265+
return true
266+
}
267+
268+
termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
269+
if termProgram == "konsole" {
270+
return true
271+
}
272+
273+
return false
274+
}
275+
201276
// imageProtocolSupported checks if any supported image protocol terminal is detected.
202277
func imageProtocolSupported() bool {
203-
return kittySupported() || ghosttySupported()
278+
return kittySupported() || ghosttySupported() || iterm2Supported() ||
279+
weztermSupported() || waystSupported() || warpSupported() || konsoleSupported()
204280
}
205281

206282
func debugImageProtocol(format string, args ...interface{}) {
@@ -326,6 +402,52 @@ func kittyInlineImage(payload string) string {
326402
return b.String()
327403
}
328404

405+
// iterm2InlineImage renders an image using iTerm2's image protocol
406+
func iterm2InlineImage(payload string) string {
407+
if payload == "" {
408+
return ""
409+
}
410+
411+
// Calculate rows for cursor positioning
412+
rows := 1
413+
if data, err := base64.StdEncoding.DecodeString(payload); err == nil {
414+
if img, _, err := image.Decode(bytes.NewReader(data)); err == nil {
415+
cellHeight := getTerminalCellSize()
416+
h := img.Bounds().Dy()
417+
rows = (h + cellHeight - 1) / cellHeight
418+
if rows < 1 {
419+
rows = 1
420+
}
421+
debugImageProtocol("image height: %d pixels, cell height: %d pixels, rows needed: %d", h, cellHeight, rows)
422+
}
423+
}
424+
425+
// iTerm2 image protocol: ESC]1337;File=inline=1:<base64_data>BEL
426+
result := fmt.Sprintf("\x1b]1337;File=inline=1:%s\x07\n", payload)
427+
428+
// Add placeholder for row spacing
429+
result += fmt.Sprintf("%s%d%s\n", imageRowPlaceholderPrefix, rows, imageRowPlaceholderSuffix)
430+
431+
return result
432+
}
433+
434+
// renderInlineImage renders an image using the appropriate protocol for the detected terminal
435+
func renderInlineImage(payload string) string {
436+
if payload == "" {
437+
return ""
438+
}
439+
440+
if kittySupported() || ghosttySupported() || weztermSupported() || waystSupported() || konsoleSupported() {
441+
// These terminals use the Kitty graphics protocol
442+
return kittyInlineImage(payload)
443+
} else if iterm2Supported() || warpSupported() {
444+
// iTerm2 and Warp use the iTerm2 image protocol
445+
return iterm2InlineImage(payload)
446+
}
447+
448+
return ""
449+
}
450+
329451
// expandImageRowPlaceholders replaces image row placeholders with actual newlines.
330452
func expandImageRowPlaceholders(text string) string {
331453
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
441563
}
442564

443565
if payload != "" {
444-
if rendered := kittyInlineImage(payload); rendered != "" {
445-
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())
566+
if rendered := renderInlineImage(payload); rendered != "" {
567+
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())
446568
s.ReplaceWithHtml("\n" + rendered + "\n")
447569
return
448570
}
@@ -451,7 +573,7 @@ func processBody(rawBody string, inline map[string]string, h1Style, h2Style, bod
451573
debugImageProtocol("no payload for src=%s dataURI=%t cid=%t", src, strings.HasPrefix(src, "data:"), strings.HasPrefix(src, "cid:"))
452574
}
453575
} else {
454-
debugImageProtocol("image protocol not supported for src=%s (kitty=%t ghostty=%t)", src, kittySupported(), ghosttySupported())
576+
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())
455577
}
456578
if hyperlinkSupported() {
457579
s.ReplaceWithHtml(hyperlink(src, fmt.Sprintf("\n [Click here to view image: %s] \n", alt)))

0 commit comments

Comments
 (0)