Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Fixed
- **Ghostty over SSH now shows the splash image, not the ASCII fallback.** The image
detection recognised Ghostty only by `TERM_PROGRAM=ghostty`, which SSH and tmux
strip — so a dashboard run over SSH from Ghostty fell back to ASCII even though the
terminal can render images. It now also treats `TERM=xterm-ghostty` (always
forwarded) as Kitty-graphics-capable.

### Added
- **Himinbjörg — one focus + scroll primitive for the TUI (`app/internal/tui/pane`).**
A `Pane` (scroll, selection-that-follows-the-cursor, `/` filter, sort) and a `Group`
Expand Down
15 changes: 14 additions & 1 deletion app/internal/tui/splash/splash.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ func resizeImage(img image.Image, w int) image.Image {

func forceASCII() bool { return os.Getenv("HEIMDALL_ASCII") != "" }

// kittyCapable reports whether the terminal speaks the Kitty graphics protocol.
// rasterm v1.1.2 only recognises Ghostty via TERM_PROGRAM=ghostty, which is dropped
// over SSH and tmux (and not exported by every Linux Ghostty build) — but TERM is
// always forwarded, and Ghostty's terminfo sets TERM=xterm-ghostty. Ghostty speaks
// Kitty graphics, so treat that TERM as capable too. Fixes the ASCII-splash fallback
// when running over SSH from Ghostty.
func kittyCapable() bool {
if strings.EqualFold(strings.TrimSpace(os.Getenv("TERM")), "xterm-ghostty") {
return true
}
return rasterm.IsKittyCapable()
}

// inlineImage renders LOGO_NO_BG.png with an inline-image protocol at a known
// cell footprint, centred in width x height. ok=false if the terminal can't
// display images. The logo already contains the wordmark + tagline, so nothing
Expand Down Expand Up @@ -143,7 +156,7 @@ func inlineImage(width, height int) (string, bool) {
buf.WriteString(strings.Repeat("\n", top)) // vertical centre
buf.WriteString(strings.Repeat(" ", left)) // horizontal centre
switch {
case rasterm.IsKittyCapable():
case kittyCapable():
if rasterm.KittyWriteImage(&buf, img, rasterm.KittyImgOpts{DstCols: uint32(cols), DstRows: uint32(rows)}) != nil {
return "", false
}
Expand Down
42 changes: 42 additions & 0 deletions app/internal/tui/splash/splash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Kinn Coelho Juliao <kinncj@gmail.com>

package splash

import "testing"

// Ghostty over SSH forwards TERM but not TERM_PROGRAM, so rasterm alone reports the
// terminal as image-incapable and the splash falls back to ASCII. kittyCapable must
// still recognise it from TERM=xterm-ghostty.
func TestKittyCapableDetectsGhosttyByTERM(t *testing.T) {
// Clear the identifiers rasterm keys off, to isolate the TERM fallback.
t.Setenv("TERM_PROGRAM", "")
t.Setenv("KITTY_WINDOW_ID", "")

t.Setenv("TERM", "xterm-ghostty")
if !kittyCapable() {
t.Error("TERM=xterm-ghostty (e.g. Ghostty over SSH) should be Kitty-capable")
}

// Case-insensitive / whitespace tolerant.
t.Setenv("TERM", " XTERM-GHOSTTY ")
if !kittyCapable() {
t.Error("TERM match should tolerate case and surrounding whitespace")
}

// A plain terminal with none of the identifiers must not be treated as capable.
t.Setenv("TERM", "xterm-256color")
if kittyCapable() {
t.Error("xterm-256color with no image identifiers should not be Kitty-capable")
}
}

// The existing TERM_PROGRAM path (macOS Ghostty, local) must keep working.
func TestKittyCapableKeepsTermProgramPath(t *testing.T) {
t.Setenv("KITTY_WINDOW_ID", "")
t.Setenv("TERM", "xterm-256color")
t.Setenv("TERM_PROGRAM", "ghostty")
if !kittyCapable() {
t.Error("TERM_PROGRAM=ghostty (local macOS Ghostty) should stay Kitty-capable")
}
}