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
4 changes: 2 additions & 2 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ func Best() Protocol {
return Kitty
}

// Ghostty sets TERM=xterm-ghostty and supports Kitty graphics.
// Ghostty sets TERM=ghostty or TERM=xterm-ghostty, and TERM_PROGRAM=ghostty.
term := os.Getenv("TERM")
termProg := strings.ToLower(os.Getenv("TERM_PROGRAM"))

if strings.HasPrefix(term, "xterm-ghostty") {
if term == "ghostty" || strings.HasPrefix(term, "xterm-ghostty") || termProg == "ghostty" {
return Kitty
}

Expand Down
12 changes: 11 additions & 1 deletion detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ func TestBest(t *testing.T) {
want: Kitty,
},
{
name: "kitty via ghostty TERM",
name: "kitty via ghostty TERM=ghostty",
env: map[string]string{"TERM": "ghostty"},
want: Kitty,
},
{
name: "kitty via ghostty TERM_PROGRAM=ghostty",
env: map[string]string{"TERM_PROGRAM": "ghostty"},
want: Kitty,
},
{
name: "kitty via ghostty TERM=xterm-ghostty",
env: map[string]string{"TERM": "xterm-ghostty"},
want: Kitty,
},
Expand Down
29 changes: 25 additions & 4 deletions termimage.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ func Display(w io.Writer, src string, opts Options) error {
// DisplayContext is Display with caller-supplied context for cancellation of
// remote fetches and sandboxed decoding.
func DisplayContext(ctx context.Context, w io.Writer, src string, opts Options) error {
maxW, maxH := effectiveDimensions(opts)

proto := opts.Protocol
if proto == Auto {
proto = detect.Best()
}

maxW, maxH := effectiveDimensions(opts, proto)

resolved, err := source.Resolve(ctx, src)
if err != nil {
return err
Expand Down Expand Up @@ -108,12 +108,24 @@ func renderWith(w io.Writer, img *image.NRGBA, proto Protocol) error {
}
}

func effectiveDimensions(opts Options) (int, int) {
// effectiveDimensions returns the pixel bounds for image scaling.
// For HalfBlock, terminal character dimensions drive the limit (1 char = 1px
// wide, 2px tall) because the renderer emits one character per pixel column.
// For Kitty/Sixel, the terminal's actual pixel viewport is used.
func effectiveDimensions(opts Options, proto Protocol) (int, int) {
w, h := opts.MaxWidth, opts.MaxHeight
if w > 0 && h > 0 {
return w, h
}
tw, th := detectTermPixels()

var tw, th int
if proto == HalfBlock {
cols, rows := detectTermChars()
tw, th = cols, rows*2
} else {
tw, th = detectTermPixels()
}

if w <= 0 {
w = tw
}
Expand All @@ -131,3 +143,12 @@ func detectTermPixels() (int, int) {
defer func() { _ = f.Close() }()
return termPixels(f)
}

func detectTermChars() (int, int) {
f, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return 220, 50
}
defer func() { _ = f.Close() }()
return termChars(f)
}
9 changes: 4 additions & 5 deletions termimage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,22 @@ func TestProtocolConstants(t *testing.T) {
}

func TestEffectiveDimensions_ExplicitWidthAndHeight(t *testing.T) {
w, h := effectiveDimensions(Options{MaxWidth: 800, MaxHeight: 600})
w, h := effectiveDimensions(Options{MaxWidth: 800, MaxHeight: 600}, HalfBlock)
if w != 800 || h != 600 {
t.Errorf("got %dx%d, want 800x600", w, h)
}
}

func TestEffectiveDimensions_ZerosFallBackToDetected(t *testing.T) {
// With both 0, both come from detection. detectTermPixels returns 1920x1080
// when /dev/tty isn't available — accept any positive values.
w, h := effectiveDimensions(Options{})
// With both 0, both come from detection — accept any positive values.
w, h := effectiveDimensions(Options{}, HalfBlock)
if w <= 0 || h <= 0 {
t.Errorf("expected positive dimensions, got %dx%d", w, h)
}
}

func TestEffectiveDimensions_PartialOverride(t *testing.T) {
w, h := effectiveDimensions(Options{MaxWidth: 500})
w, h := effectiveDimensions(Options{MaxWidth: 500}, HalfBlock)
if w != 500 {
t.Errorf("MaxWidth=500 not honoured: got %d", w)
}
Expand Down
9 changes: 9 additions & 0 deletions termsize_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ func termPixels(f *os.File) (int, int) {
// Fallback: estimate from cell count (8×16 px per cell is a safe default).
return int(ws.Col) * 8, int(ws.Row) * 16
}

// termChars returns the terminal dimensions as (cols, rows).
func termChars(f *os.File) (int, int) {
ws, err := unix.IoctlGetWinsize(int(f.Fd()), unix.TIOCGWINSZ)
if err != nil || ws.Col == 0 || ws.Row == 0 {
return 220, 50
}
return int(ws.Col), int(ws.Row)
}
1 change: 1 addition & 0 deletions termsize_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ package termimage
import "os"

func termPixels(_ *os.File) (int, int) { return 1920, 1080 }
func termChars(_ *os.File) (int, int) { return 220, 50 }
Loading