From 7729e6e1aee1a0bfe15f240d6ab69ccbbeaee8c3 Mon Sep 17 00:00:00 2001 From: Petar Dobrev Date: Sat, 18 Apr 2026 18:26:30 +0300 Subject: [PATCH] kitten diff: add support for sticky header --- kittens/diff/main.py | 5 +++++ kittens/diff/render.go | 21 ++++++++++++++++++++- kittens/diff/ui.go | 26 +++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/kittens/diff/main.py b/kittens/diff/main.py index eb6b30577..a9dac8993 100644 --- a/kittens/diff/main.py +++ b/kittens/diff/main.py @@ -89,6 +89,11 @@ If an invalid expression is provided, diff will fail with an error. ''' ) +opt('sticky_header', 'no', option_type='to_bool', long_text=''' +When scrolled past the header of a file, keep the file's name and separator +pinned to the top of the screen. +''') + egr() # }}} # colors {{{ diff --git a/kittens/diff/render.go b/kittens/diff/render.go index 9dfd7046e..947582308 100644 --- a/kittens/diff/render.go +++ b/kittens/diff/render.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "os" + "sort" "strconv" "strings" @@ -318,11 +319,24 @@ func title_lines(left_path, right_path string, columns, margin_size int, ans []* type LogicalLines struct { lines []*LogicalLine + title_line_indices []int // sorted indices of TITLE_LINEs in lines -- used for sticky_header margin_size, columns int } func (self *LogicalLines) At(i int) *LogicalLine { return self.lines[i] } +func (self *LogicalLines) TitleLineIdxFor(line_idx int) int { + if len(self.title_line_indices) == 0 || line_idx < 0 { + return -1 + } + // first title index > line_idx, so the one before it is our answer + i := sort.SearchInts(self.title_line_indices, line_idx+1) + if i == 0 { + return -1 + } + return self.title_line_indices[i-1] +} + func (self *LogicalLines) ScreenLineAt(pos ScrollPos) *ScreenLine { if pos.logical_line < len(self.lines) && pos.logical_line >= 0 { line := self.lines[pos.logical_line] @@ -793,8 +807,13 @@ func rename_lines(path, other_path string, columns, margin_size int, ans []*Logi func render(collection *Collection, diff_map map[string]*Patch, screen_size screen_size, largest_line_number int, image_size graphics.Size) (result *LogicalLines, err error) { margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1) ans := make([]*LogicalLine, 0, 1024) + var title_line_indices []int + track_titles := conf != nil && conf.Sticky_header columns := screen_size.columns err = collection.Apply(func(path, item_type, changed_path string) error { + if track_titles { + title_line_indices = append(title_line_indices, len(ans)) + } ans = title_lines(path, changed_path, columns, margin_size, ans) defer func() { ans = append(ans, &LogicalLine{line_type: EMPTY_LINE, screen_lines: []*ScreenLine{{}}}) @@ -863,5 +882,5 @@ func render(collection *Collection, diff_map map[string]*Patch, screen_size scre // Having am empty list of lines causes panics later on ll = []*LogicalLine{{line_type: EMPTY_LINE, screen_lines: []*ScreenLine{{}}}} } - return &LogicalLines{lines: ll, margin_size: margin_size, columns: columns}, err + return &LogicalLines{lines: ll, title_line_indices: title_line_indices, margin_size: margin_size, columns: columns}, err } diff --git a/kittens/diff/ui.go b/kittens/diff/ui.go index 82456a3a4..0957f29e4 100644 --- a/kittens/diff/ui.go +++ b/kittens/diff/ui.go @@ -414,7 +414,11 @@ func (self *Handler) draw_screen() { } pos := self.scroll_pos seen_images := utils.NewSet[int]() - for num_written := 0; num_written < self.screen_size.num_lines; num_written++ { + num_written := 0 + if conf != nil && conf.Sticky_header { + num_written = self.draw_sticky_header(&pos) + } + for ; num_written < self.screen_size.num_lines; num_written++ { ll := self.logical_lines.At(pos.logical_line) if ll == nil || self.logical_lines.ScreenLineAt(pos) == nil { num_written-- @@ -443,6 +447,26 @@ func (self *Handler) draw_screen() { self.draw_status_line() } +func (self *Handler) draw_sticky_header(pos *ScrollPos) int { + title_idx := self.logical_lines.TitleLineIdxFor(pos.logical_line) + if title_idx < 0 { + return 0 + } + // file title + self.logical_lines.At(title_idx).render_screen_line(0, self.lp, self.logical_lines.margin_size, self.logical_lines.columns) + self.lp.MoveCursorVertically(1) + self.lp.QueueWriteString("\x1b[m\r") + self.logical_lines.IncrementScrollPosBy(pos, 1) + + // separator + self.logical_lines.At(title_idx+1).render_screen_line(0, self.lp, self.logical_lines.margin_size, self.logical_lines.columns) + self.lp.MoveCursorVertically(1) + self.lp.QueueWriteString("\x1b[m\r") + self.logical_lines.IncrementScrollPosBy(pos, 1) + + return 2 +} + func (self *Handler) draw_status_line() { if self.logical_lines == nil || self.diff_map == nil { return