diff --git a/detect/detect.go b/detect/detect.go index 56a76df..11ce347 100644 --- a/detect/detect.go +++ b/detect/detect.go @@ -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 } diff --git a/detect/detect_test.go b/detect/detect_test.go index 7d11c4e..04898e9 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -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, }, diff --git a/termimage.go b/termimage.go index 170ab66..be15871 100644 --- a/termimage.go +++ b/termimage.go @@ -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 @@ -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 } @@ -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) +} diff --git a/termimage_test.go b/termimage_test.go index ca019eb..d7935d0 100644 --- a/termimage_test.go +++ b/termimage_test.go @@ -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) } diff --git a/termsize_linux.go b/termsize_linux.go index 9ad68a3..dc82945 100644 --- a/termsize_linux.go +++ b/termsize_linux.go @@ -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) +} diff --git a/termsize_stub.go b/termsize_stub.go index 7d2cb3a..d1d4bd1 100644 --- a/termsize_stub.go +++ b/termsize_stub.go @@ -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 }