From f3db7e755479092e7f6f818198bd581f21048fd8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Jan 2025 06:00:24 +0530 Subject: [PATCH] diff kitten: Automatically change colors on terminal color scheme change --- kittens/diff/collect.go | 15 ++++++++-- kittens/diff/highlight.go | 21 ++++++++------ kittens/diff/main.go | 1 + kittens/diff/render.go | 4 +-- kittens/diff/ui.go | 30 +++++++++++++++++++- tools/tui/loop/api.go | 12 ++++++++ tools/tui/loop/run.go | 12 ++++++++ tools/tui/loop/terminal-state.go | 47 ++++++++++++++++++-------------- tools/utils/cache.go | 6 ++++ 9 files changed, 113 insertions(+), 35 deletions(-) diff --git a/kittens/diff/collect.go b/kittens/diff/collect.go index 42a6b4fc7..a29eac0df 100644 --- a/kittens/diff/collect.go +++ b/kittens/diff/collect.go @@ -20,7 +20,8 @@ var path_name_map, remote_dirs map[string]string var mimetypes_cache, data_cache, hash_cache *utils.LRUCache[string, string] var size_cache *utils.LRUCache[string, int64] var lines_cache *utils.LRUCache[string, []string] -var highlighted_lines_cache *utils.LRUCache[string, []string] +var light_highlighted_lines_cache *utils.LRUCache[string, []string] +var dark_highlighted_lines_cache *utils.LRUCache[string, []string] var is_text_cache *utils.LRUCache[string, bool] func init_caches() { @@ -32,7 +33,8 @@ func init_caches() { data_cache = utils.NewLRUCache[string, string](sz) is_text_cache = utils.NewLRUCache[string, bool](sz) lines_cache = utils.NewLRUCache[string, []string](sz) - highlighted_lines_cache = utils.NewLRUCache[string, []string](sz) + light_highlighted_lines_cache = utils.NewLRUCache[string, []string](sz) + dark_highlighted_lines_cache = utils.NewLRUCache[string, []string](sz) hash_cache = utils.NewLRUCache[string, string](sz) } @@ -150,7 +152,14 @@ func highlighted_lines_for_path(path string) ([]string, error) { if err != nil { return nil, err } - if ans, found := highlighted_lines_cache.Get(path); found && len(ans) == len(plain_lines) { + var ans []string + var found bool + if use_light_colors { + ans, found = light_highlighted_lines_cache.Get(path) + } else { + ans, found = dark_highlighted_lines_cache.Get(path) + } + if found && len(ans) == len(plain_lines) { return ans, nil } return plain_lines, nil diff --git a/kittens/diff/highlight.go b/kittens/diff/highlight.go index 28fa119a3..18971c7e3 100644 --- a/kittens/diff/highlight.go +++ b/kittens/diff/highlight.go @@ -158,8 +158,8 @@ func ansi_formatter(w io.Writer, style *chroma.Style, it chroma.Iterator) (err e return nil } -func resolved_chroma_style() *chroma.Style { - name := utils.IfElse(use_dark_colors, conf.Dark_pygments_style, conf.Pygments_style) +func resolved_chroma_style(use_light_colors bool) *chroma.Style { + name := utils.IfElse(use_light_colors, conf.Pygments_style, conf.Dark_pygments_style) var style *chroma.Style if name == "default" { style = DefaultStyle() @@ -185,7 +185,7 @@ func resolved_chroma_style() *chroma.Style { var tokens_map map[string][]chroma.Token var mu sync.Mutex -func highlight_file(path string) (highlighted string, err error) { +func highlight_file(path string, use_light_colors bool) (highlighted string, err error) { defer func() { if r := recover(); r != nil { e, ok := r.(error) @@ -235,19 +235,24 @@ func highlight_file(path string) (highlighted string, err error) { formatter := chroma.FormatterFunc(ansi_formatter) w := strings.Builder{} w.Grow(len(text) * 2) - err = formatter.Format(&w, resolved_chroma_style(), chroma.Literator(tokens...)) + err = formatter.Format(&w, resolved_chroma_style(use_light_colors), chroma.Literator(tokens...)) // os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600) return w.String(), err } -func highlight_all(paths []string) { +func highlight_all(paths []string, light bool) { ctx := images.Context{} ctx.Parallel(0, len(paths), func(nums <-chan int) { for i := range nums { path := paths[i] - raw, err := highlight_file(path) - if err == nil { - highlighted_lines_cache.Set(path, text_to_lines(raw)) + raw, err := highlight_file(path, light) + if err != nil { + continue + } + if light { + light_highlighted_lines_cache.Set(path, text_to_lines(raw)) + } else { + dark_highlighted_lines_cache.Set(path, text_to_lines(raw)) } } }) diff --git a/kittens/diff/main.go b/kittens/diff/main.go index 69de45616..0175b89da 100644 --- a/kittens/diff/main.go +++ b/kittens/diff/main.go @@ -139,6 +139,7 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) { if err != nil { return 1, err } + lp.ColorSchemeChangeNotifications() h := Handler{left: left, right: right, lp: lp} lp.OnInitialize = func() (string, error) { lp.SetCursorVisible(false) diff --git a/kittens/diff/render.go b/kittens/diff/render.go index 874bab8fb..a8e9185ed 100644 --- a/kittens/diff/render.go +++ b/kittens/diff/render.go @@ -160,7 +160,7 @@ var format_as_sgr struct { } var statusline_format, added_count_format, removed_count_format, message_format func(...any) string -var use_dark_colors bool = true +var use_light_colors bool = false type ResolvedColors struct { Added_bg style.RGBA @@ -189,7 +189,7 @@ var resolved_colors ResolvedColors func create_formatters() { rc := &resolved_colors - if use_dark_colors { + if !use_light_colors { rc.Added_bg = conf.Dark_added_bg rc.Added_margin_bg = conf.Dark_added_margin_bg rc.Background = conf.Dark_background diff --git a/kittens/diff/ui.go b/kittens/diff/ui.go index 5ee7886e8..5f5415522 100644 --- a/kittens/diff/ui.go +++ b/kittens/diff/ui.go @@ -124,6 +124,7 @@ func set_terminal_colors(lp *loop.Loop) { } func (self *Handler) on_capabilities_received(tc loop.TerminalCapabilities) { + var use_dark_colors bool switch conf.Color_scheme { case Color_scheme_auto: use_dark_colors = tc.ColorPreference != loop.LIGHT_COLOR_PREFERENCE @@ -132,14 +133,30 @@ func (self *Handler) on_capabilities_received(tc loop.TerminalCapabilities) { case Color_scheme_dark: use_dark_colors = true } + use_light_colors = !use_dark_colors set_terminal_colors(self.lp) self.terminal_capabilities_received = true self.draw_screen() } +func (self *Handler) on_color_scheme_change(cp loop.ColorPreference) error { + if conf.Color_scheme != Color_scheme_auto { + return nil + } + light := cp == loop.LIGHT_COLOR_PREFERENCE + if use_light_colors != light { + use_light_colors = light + set_terminal_colors(self.lp) + self.highlight_all() + self.draw_screen() + } + return nil +} + func (self *Handler) initialize() { self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"}) self.lp.OnEscapeCode = self.on_escape_code + self.lp.OnColorSchemeChange = self.on_color_scheme_change image_collection = graphics.NewImageCollection() self.current_context_count = opts.Context if self.current_context_count < 0 { @@ -195,11 +212,22 @@ func (self *Handler) on_wakeup() error { } } +var dark_highlight_started bool +var light_highlight_started bool + func (self *Handler) highlight_all() { + if (use_light_colors && light_highlight_started) || (!use_light_colors && dark_highlight_started) { + return + } + if use_light_colors { + light_highlight_started = true + } else { + dark_highlight_started = true + } text_files := utils.Filter(self.collection.paths_to_highlight.AsSlice(), is_path_text) go func() { r := AsyncResult{rtype: HIGHLIGHT} - highlight_all(text_files) + highlight_all(text_files, use_light_colors) self.async_results <- r self.lp.WakeupMainThread() }() diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 577dec3e0..7e4fab1d2 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -118,6 +118,9 @@ type Loop struct { // Called when capabilities response is received OnCapabilitiesReceived func(TerminalCapabilities) error + + // Called when the terminal's color scheme changes + OnColorSchemeChange func(ColorPreference) error } func New(options ...func(self *Loop)) (*Loop, error) { @@ -203,6 +206,15 @@ func NoRestoreColors(self *Loop) { self.terminal_options.restore_colors = false } +func (self *Loop) ColorSchemeChangeNotifications() *Loop { + self.terminal_options.color_scheme_change_notification = true + return self +} + +func ColorSchemeChangeNotifications(self *Loop) { + self.terminal_options.color_scheme_change_notification = true +} + func NoInBandResizeNotifications(self *Loop) { self.terminal_options.in_band_resize_notification = false } diff --git a/tools/tui/loop/run.go b/tools/tui/loop/run.go index ec1acdf3d..8567b5de9 100644 --- a/tools/tui/loop/run.go +++ b/tools/tui/loop/run.go @@ -27,6 +27,7 @@ func new_loop() *Loop { l.terminal_options.Alternate_screen = true l.terminal_options.restore_colors = true l.terminal_options.in_band_resize_notification = true + l.terminal_options.color_scheme_change_notification = false l.terminal_options.kitty_keyboard_mode = DISAMBIGUATE_KEYS | REPORT_ALTERNATE_KEYS | REPORT_ALL_KEYS_AS_ESCAPE_CODES | REPORT_TEXT_WITH_KEYS l.escape_code_parser.HandleCSI = l.handle_csi l.escape_code_parser.HandleOSC = l.handle_osc @@ -144,6 +145,17 @@ func (self *Loop) handle_csi(raw []byte) (err error) { self.TerminalCapabilities.KeyboardProtocol = true self.TerminalCapabilities.KeyboardProtocolResponseReceived = true } + } else if self.terminal_options.color_scheme_change_notification && strings.HasPrefix(csi, "?997;") && strings.HasSuffix(csi, "n") { + switch csi[len(csi)-2] { + case '1': + self.TerminalCapabilities.ColorPreference = DARK_COLOR_PREFERENCE + case '2': + self.TerminalCapabilities.ColorPreference = LIGHT_COLOR_PREFERENCE + } + self.TerminalCapabilities.ColorPreferenceResponseReceived = true + if self.OnColorSchemeChange != nil { + return self.OnColorSchemeChange(self.TerminalCapabilities.ColorPreference) + } } if self.OnEscapeCode != nil { return self.OnEscapeCode(CSI, raw) diff --git a/tools/tui/loop/terminal-state.go b/tools/tui/loop/terminal-state.go index 269ec43b4..93774e24c 100644 --- a/tools/tui/loop/terminal-state.go +++ b/tools/tui/loop/terminal-state.go @@ -46,27 +46,28 @@ type Mode uint32 const private Mode = 1 << 31 const ( - LNM Mode = 20 - IRM Mode = 4 - DECKM Mode = 1 | private - DECSCNM Mode = 5 | private - DECOM Mode = 6 | private - DECAWM Mode = 7 | private - DECARM Mode = 8 | private - DECTCEM Mode = 25 | private - MOUSE_BUTTON_TRACKING Mode = 1000 | private - MOUSE_MOTION_TRACKING Mode = 1002 | private - MOUSE_MOVE_TRACKING Mode = 1003 | private - FOCUS_TRACKING Mode = 1004 | private - MOUSE_UTF8_MODE Mode = 1005 | private - MOUSE_SGR_MODE Mode = 1006 | private - MOUSE_URXVT_MODE Mode = 1015 | private - MOUSE_SGR_PIXEL_MODE Mode = 1016 | private - ALTERNATE_SCREEN Mode = 1049 | private - BRACKETED_PASTE Mode = 2004 | private - PENDING_UPDATE Mode = 2026 | private - INBAND_RESIZE_NOTIFICATION Mode = 2048 | private - HANDLE_TERMIOS_SIGNALS Mode = kitty.HandleTermiosSignals | private + LNM Mode = 20 + IRM Mode = 4 + DECKM Mode = 1 | private + DECSCNM Mode = 5 | private + DECOM Mode = 6 | private + DECAWM Mode = 7 | private + DECARM Mode = 8 | private + DECTCEM Mode = 25 | private + MOUSE_BUTTON_TRACKING Mode = 1000 | private + MOUSE_MOTION_TRACKING Mode = 1002 | private + MOUSE_MOVE_TRACKING Mode = 1003 | private + FOCUS_TRACKING Mode = 1004 | private + MOUSE_UTF8_MODE Mode = 1005 | private + MOUSE_SGR_MODE Mode = 1006 | private + MOUSE_URXVT_MODE Mode = 1015 | private + MOUSE_SGR_PIXEL_MODE Mode = 1016 | private + ALTERNATE_SCREEN Mode = 1049 | private + BRACKETED_PASTE Mode = 2004 | private + PENDING_UPDATE Mode = 2026 | private + COLOR_SCHEME_CHANGE_NOTIFICATION Mode = 2031 | private + INBAND_RESIZE_NOTIFICATION Mode = 2048 | private + HANDLE_TERMIOS_SIGNALS Mode = kitty.HandleTermiosSignals | private ) func (self Mode) escape_code(which string) string { @@ -101,6 +102,7 @@ type TerminalStateOptions struct { mouse_tracking MouseTracking kitty_keyboard_mode KeyboardStateBits in_band_resize_notification bool + color_scheme_change_notification bool } func set_modes(sb *strings.Builder, modes ...Mode) { @@ -133,6 +135,9 @@ func (self *TerminalStateOptions) SetStateEscapeCodes() string { if self.in_band_resize_notification { set_modes(&sb, INBAND_RESIZE_NOTIFICATION) } + if self.color_scheme_change_notification { + set_modes(&sb, COLOR_SCHEME_CHANGE_NOTIFICATION) + } if self.Alternate_screen { set_modes(&sb, ALTERNATE_SCREEN) sb.WriteString(CLEAR_SCREEN) diff --git a/tools/utils/cache.go b/tools/utils/cache.go index 70fa7bf5d..3179ad77f 100644 --- a/tools/utils/cache.go +++ b/tools/utils/cache.go @@ -22,6 +22,12 @@ func NewLRUCache[K comparable, V any](max_size int) *LRUCache[K, V] { return &ans } +func (self *LRUCache[K, V]) Clear() { + self.lock.RLock() + clear(self.data) + self.lock.Unlock() +} + func (self *LRUCache[K, V]) Get(key K) (ans V, found bool) { self.lock.RLock() ans, found = self.data[key]