Skip to content

feat: built-in image preview via Kitty graphics protocol#2621

Open
RenovZ wants to merge 6 commits into
gokcehan:masterfrom
RenovZ:master
Open

feat: built-in image preview via Kitty graphics protocol#2621
RenovZ wants to merge 6 commits into
gokcehan:masterfrom
RenovZ:master

Conversation

@RenovZ

@RenovZ RenovZ commented Jun 14, 2026

Copy link
Copy Markdown

This PR adds native image preview support using the Kitty graphics protocol, allowing lf to display image previews (PNG, JPEG, GIF) directly in any terminal emulator that supports the protocol — no external dependencies required.

Previously, lf only supported Sixel-based image previews. Kitty protocol support broadens compatibility to modern terminals like Kitty, WezTerm, Ghostty...

use case:

chafa -f kitty -s "${w}x${h}" "$img" 2>/dev/null in preview scripts.

Besides I have spent too many time on configuring preview in terminal with lf. So it's time to support this feature.

One fix for sixel image rendering bug:

I found this bug when use chafa -s "${w}x${h}" "$img" 2>/dev/null in preview scripts.
This triggers a problem the text and image stick together, the situation seems like image has a background text or the text has a overlay image.

RenovZ added 3 commits June 14, 2026 02:44
Prevent old text from previous file previews lingering on screen
when switching to a new image.

**image preview (kitty.go)**
- Unlock the old region in clearKitty after deleting images so
  tcell can redraw the full pane for the new preview
- Fill preview pane cells with spaces in tcells buffer to erase
  old text before rendering the new image
- Emit clear-to-end-of-line ANSI codes for each row directly to
  the terminal for additional terminal-level cleanup
- Merge clear sequence and kitty image output into one
  synchronized update to avoid flickering
@valoq

valoq commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Just a few notes:

  • the image preview should run in external preview scripts only, like sixel does. This feature needs chafa anyway but the current PR implements it for internal preview as well.
  • the Kitty parser doesn't stop at the frame's end marker (ESC backslash). It reads to the end of the input, so any escape codes placed after the image get sent to the terminal raw. the code should have the same safeguards as the sixel code to ensure no dangerous terminal control sequences can reach the terminal and instead only allow valid data for the kitty image protocol.

@RenovZ

RenovZ commented Jun 14, 2026

Copy link
Copy Markdown
Author

@valoq
I have tried many ways to preview image with chafa -f kitty in ghostty or someother terminal simulator (does not support sixel), the result is that the image preview is no matter how you configure with the preview script, you just can't preview it with kitty format without this pr. Besides it costs me too much time to preview image with kitty protocol, and at last I found it's impossible without lf built-in support. So I made this PR.

@CatsDeservePets CatsDeservePets left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I am a huge fan of adding support for the kitty image protocol to lf. Thanks for your work so far.

I am not a big fan of the built-in preview generation. Just like with sixel, lf should only parse (and display) the kitty protocol when passed by the user inside his previewer script.
Always forcing a kitty preview for every supported image file is bad:

  • What if the terminal emulator doesn't support it (like the default macOS and Windows terminals)?
  • What if the user wants to have a different preview (like different scaling and placement, or no image preview at all)?

Comment thread eval.go Outdated
Comment thread kitty.go Outdated
Comment thread misc.go Outdated
RenovZ added 3 commits June 15, 2026 02:26
Extend the built-in Kitty graphics protocol preview to support additional
image formats (BMP, TIFF, WebP) using the golang.org/x/image package.
Remove the dead no-op "kitty" config option handler.

**Kitty image preview (kitty.go)**
- Register BMP, TIFF, and WebP decoders via blank imports from
  golang.org/x/image
- Add ".bmp", ".tiff", ".tif", ".webp" to the imageExtensions map
- Reorganize imports into standard-library, tcell, and x/image groups

**Config (eval.go)**
- Remove the "kitty"/"nokitty"/"kitty!" case in setExpr.eval() — this
  was a no-op (err = nil) that existed only to silently accept the
  option in config files; Kitty protocol is now always active

**Dependencies (go.mod, go.sum)**
- Add golang.org/x/image v0.42.0 (direct)
- Bump golang.org/x/text v0.37.0 → v0.38.0 (indirect)
Consolidate the separate `sixel` and `kitty` bool fields on the `reg`
struct into a single `previewKind` enum, eliminating the impossible
state where both could be true simultaneously.

**previewKind type (ui.go)**
- Introduce `previewKind` enum with `previewText`, `previewSixel`,
  and `previewKitty` constants
- Replace `reg.sixel bool` + `reg.kitty bool` with `reg.kind previewKind`
- Update `printReg()` conditions from field access to comparison
  (`reg.sixel` → `reg.kind == previewSixel`)

**readLines (misc.go)**
- Change return signature from `(lines, binary, sixel, kitty)` to
  `(lines, binary, kind previewKind)`
- Set `kind = previewSixel` / `kind = previewKitty` in place of
  separate bool assignments

**Tests (misc_test.go)**
- Update all 30+ test cases: replace `sixel`/`kitty` bool fields with
  single `kind` field using the new constants
- Update `TestReadLinesChafa` assertions accordingly

**Preview / nav (nav.go)**
- Update `readLines()` callsites to use single `kind` return value
- Replace `reg.sixel = true` / `reg.kitty = true` assignments with
  `reg.kind = previewKitty`
- Simplify resize cache eviction: `r.sixel || r.kitty` →
  `r.kind != previewText`
Old text from a previous file preview would linger around or behind
the sixel image because nothing explicitly cleared the pane beforehand.

**sixelScreen (sixel.go)**
- Clear the preview pane in tcell's buffer by writing spaces to every
  cell in `printSixel()` before rendering
- Emit `\033[Y;XH\033[0K` sequences directly to the terminal for each
  preview row to erase any leftover text outside tcell's control
- Flush tcell with `screen.Show()` before writing sixel data so the
  cleared buffer is visible
@CatsDeservePets CatsDeservePets added the new Pull requests that add new behavior label Jun 14, 2026
@valoq

valoq commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

I am not a big fan of the built-in preview generation. Just like with sixel, lf should only parse (and display) the kitty protocol when passed by the user inside his previewer script

Fully agree here. Lf was created as a minimal file manager with basic default features and great extensibility through the config. External previews is exactly for this kind of feature.

@RenovZ

RenovZ commented Jun 16, 2026

Copy link
Copy Markdown
Author

Well, if someone has a better solution, just told me, and I will close this PR. And thanks.

@SirZenith

SirZenith commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@RenovZ
If I understand you right, you just want to see image preview in terminal.

Then In my case, I'm using kitty as my terminal simulator, it's enough to do it by redirecting kitty protocol content to /dev/tty.

The problem is you have to position image content to the right place, so that it will not ruins content drawn by lf.

If you do nothing chafa will print thing like this:

\x1b[?25l<protocol-content>
\x1b[?25h

Workaround is simple, you prepend cursor positioning ANSI control sequence to the content, so that image content shows up in preview area. Then print it to /dev/tty.

Like this:

chafa -f kitty -s $"($context.width)x($context.height)" $context.file e> /dev/null
| $"\x1b[2;149H" + $in
| save -f /dev/tty

(This snippet is written in nushell script, but I think it's clear enough to understand).

Here, \x1b[2;149H moves cursor to row 2, column 149, don't forgot to adjust this in your script

@RenovZ

RenovZ commented Jun 17, 2026

Copy link
Copy Markdown
Author

@SirZenith I don't like your solution, it's just like a very hack way fully bypass lf, I have tried this with bash or fish before, but failed. The problem still stay there.

@SirZenith

SirZenith commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

it's just like a very hack way fully bypass lf

In my opinion, previewer script itself is an extension (or plugin) to lf, and its purpose is to draw thing in preview window. Since lf pass preview window's scale and position to previewer script, it makes no difference if the content is drawn by lf or not.

So I chose to tell lf "draw nothing there", then ask kitty to put an image there for me. Usage of ANSI control sequence is just what kitty does with its icat kitten.

The problem still stay there.

By the way, I think it has nothing to do with what shell you're using. May I ask what have you done and what do you see in your terminal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new Pull requests that add new behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants