diff kitten: Automatically use dark/light color scheme based on the color scheme of the parent terminal

Fixes #8170
This commit is contained in:
Kovid Goyal
2025-01-04 12:57:33 +05:30
parent 134271be93
commit e34a899ea6
6 changed files with 208 additions and 128 deletions

View File

@@ -158,6 +158,30 @@ 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)
var style *chroma.Style
if name == "default" {
style = DefaultStyle()
} else {
style = styles.Get(name)
}
if style == nil {
if resolved_colors.Background.IsDark() && !resolved_colors.Foreground.IsDark() {
style = styles.Get("monokai")
if style == nil {
style = styles.Get("github-dark")
}
} else {
style = DefaultStyle()
}
if style == nil {
style = styles.Fallback
}
}
return style
}
func highlight_file(path string) (highlighted string, err error) {
filename_for_detection := filepath.Base(path)
ext := filepath.Ext(filename_for_detection)
@@ -174,34 +198,12 @@ func highlight_file(path string) (highlighted string, err error) {
}
lexer := lexers.Match(filename_for_detection)
if lexer == nil {
if err == nil {
lexer = lexers.Analyse(text)
}
lexer = lexers.Analyse(text)
}
if lexer == nil {
return "", fmt.Errorf("Cannot highlight %#v: %w", path, ErrNoLexer)
}
lexer = chroma.Coalesce(lexer)
name := conf.Pygments_style
var style *chroma.Style
if name == "default" {
style = DefaultStyle()
} else {
style = styles.Get(name)
}
if style == nil {
if conf.Background.IsDark() && !conf.Foreground.IsDark() {
style = styles.Get("monokai")
if style == nil {
style = styles.Get("github-dark")
}
} else {
style = DefaultStyle()
}
if style == nil {
style = styles.Fallback
}
}
iterator, err := lexer.Tokenise(nil, text)
if err != nil {
return "", err
@@ -209,7 +211,7 @@ 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, style, iterator)
err = formatter.Format(&w, resolved_chroma_style(), iterator)
// os.WriteFile(filepath.Base(path+".highlighted"), []byte(w.String()), 0o600)
return w.String(), err
}

View File

@@ -112,7 +112,6 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
return 1, err
}
init_caches()
create_formatters()
defer func() {
for tdir := range remote_dirs {
os.RemoveAll(tdir)
@@ -154,6 +153,7 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
if !tc.KeyboardProtocol {
return fmt.Errorf("This terminal does not support the kitty keyboard protocol, or you are running inside a terminal multiplexer that is blocking querying for kitty keyboard protocol support. The diff kitten cannot function without it.")
}
h.on_capabilities_received(tc)
return nil
}
lp.OnWakeup = h.on_wakeup

View File

@@ -71,103 +71,90 @@ egr() # }}}
# colors {{{
agr('colors', 'Colors')
opt('pygments_style', 'default',
long_text='''
opt('color_scheme', 'auto', choices=('auto', 'light', 'dark'), long_text='''
Whether to use the light or dark colors. The default of :code:`auto` means
to follow the parent terminal color scheme.
''')
opt('pygments_style', 'default', long_text='''
The pygments color scheme to use for syntax highlighting. See :link:`pygments
builtin styles <https://pygments.org/styles/>` for a list of schemes. Note that
this **does not** change the colors used for diffing,
only the colors used for syntax highlighting. To change the general colors use the settings below.
This sets the colors used for light color schemes, use :opt:`dark_pygments_style` to change the
colors for dark color schemes.
'''
)
opt('foreground', 'black',
option_type='to_color',
long_text='Basic colors'
)
opt('dark_pygments_style', 'github-dark', long_text='''
The pygments color scheme to use for syntax highlighting with dark colors. See :link:`pygments
builtin styles <https://pygments.org/styles/>` for a list of schemes. Note that
this **does not** change the colors used for diffing,
only the colors used for syntax highlighting. To change the general colors use the settings below.
This sets the colors used for dark color schemes, use :opt:`pygments_style` to change the
colors for light color schemes.''')
opt('background', 'white',
option_type='to_color',
)
opt('foreground', 'black', option_type='to_color', long_text='Basic colors')
opt('dark_foreground', '#f8f8f2', option_type='to_color', long_text='Basic colors')
opt('title_fg', 'black',
option_type='to_color',
long_text='Title colors'
)
dark_bg = '#212830'
opt('background', 'white', option_type='to_color',)
opt('dark_background', dark_bg, option_type='to_color',)
opt('title_bg', 'white',
option_type='to_color',
)
opt('title_fg', 'black', option_type='to_color', long_text='Title colors')
opt('dark_title_fg', 'white', option_type='to_color')
opt('margin_bg', '#fafbfc',
option_type='to_color',
long_text='Margin colors'
)
opt('title_bg', 'white', option_type='to_color',)
opt('dark_title_bg', dark_bg, option_type='to_color',)
opt('margin_fg', '#aaaaaa',
option_type='to_color',
)
opt('margin_bg', '#fafbfc', option_type='to_color', long_text='Margin colors')
opt('dark_margin_bg', dark_bg, option_type='to_color')
opt('removed_bg', '#ffeef0',
option_type='to_color',
long_text='Removed text backgrounds'
)
opt('margin_fg', '#aaaaaa', option_type='to_color')
opt('dark_margin_fg', '#aaaaaa', option_type='to_color')
opt('highlight_removed_bg', '#fdb8c0',
option_type='to_color',
)
opt('removed_bg', '#ffeef0', option_type='to_color', long_text='Removed text backgrounds')
opt('dark_removed_bg', '#352c33', option_type='to_color')
opt('removed_margin_bg', '#ffdce0',
option_type='to_color',
)
opt('highlight_removed_bg', '#fdb8c0', option_type='to_color')
opt('dark_highlight_removed_bg', '#5c3539', option_type='to_color')
opt('added_bg', '#e6ffed',
option_type='to_color',
long_text='Added text backgrounds'
)
opt('removed_margin_bg', '#ffdce0', option_type='to_color')
opt('dark_removed_margin_bg', '#5c3539', option_type='to_color')
opt('highlight_added_bg', '#acf2bd',
option_type='to_color',
)
opt('added_bg', '#e6ffed', option_type='to_color', long_text='Added text backgrounds')
opt('dark_added_bg', '#263834', option_type='to_color')
opt('added_margin_bg', '#cdffd8',
option_type='to_color',
)
opt('highlight_added_bg', '#acf2bd', option_type='to_color')
opt('dark_highlight_added_bg', '#31503d', option_type='to_color')
opt('filler_bg', '#fafbfc',
option_type='to_color',
long_text='Filler (empty) line background'
)
opt('added_margin_bg', '#cdffd8', option_type='to_color')
opt('dark_added_margin_bg', '#31503d', option_type='to_color')
opt('margin_filler_bg', 'none',
option_type='to_color_or_none',
long_text='Filler (empty) line background in margins, defaults to the filler background'
)
opt('filler_bg', '#fafbfc', option_type='to_color', long_text='Filler (empty) line background')
opt('dark_filler_bg', '#262c36', option_type='to_color')
opt('hunk_margin_bg', '#dbedff',
option_type='to_color',
long_text='Hunk header colors'
)
opt('margin_filler_bg', 'none', option_type='to_color_or_none', long_text='Filler (empty) line background in margins, defaults to the filler background')
opt('dark_margin_filler_bg', 'none', option_type='to_color_or_none')
opt('hunk_bg', '#f1f8ff',
option_type='to_color',
)
opt('search_bg', '#444',
option_type='to_color',
long_text='Highlighting'
)
opt('hunk_margin_bg', '#dbedff', option_type='to_color', long_text='Hunk header colors')
opt('dark_hunk_margin_bg', '#0c2d6b', option_type='to_color')
opt('search_fg', 'white',
option_type='to_color',
)
opt('hunk_bg', '#f1f8ff', option_type='to_color')
opt('dark_hunk_bg', '#253142', option_type='to_color')
opt('select_bg', '#b4d5fe',
option_type='to_color',
)
opt('search_bg', '#444', option_type='to_color', long_text='Highlighting')
opt('dark_search_bg', '#2c599c', option_type='to_color')
opt('select_fg', 'black',
option_type='to_color_or_none',
)
opt('search_fg', 'white', option_type='to_color')
opt('dark_search_fg', 'white', option_type='to_color')
opt('select_bg', '#b4d5fe', option_type='to_color')
opt('dark_select_bg', '#2c599c', option_type='to_color')
opt('select_fg', 'black', option_type='to_color_or_none')
opt('dark_select_fg', 'white', option_type='to_color_or_none')
egr() # }}}
# shortcuts {{{

View File

@@ -160,37 +160,107 @@ 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
type ResolvedColors struct {
Added_bg style.RGBA
Added_margin_bg style.RGBA
Background style.RGBA
Filler_bg style.RGBA
Foreground style.RGBA
Highlight_added_bg style.RGBA
Highlight_removed_bg style.RGBA
Hunk_bg style.RGBA
Hunk_margin_bg style.RGBA
Margin_bg style.RGBA
Margin_fg style.RGBA
Margin_filler_bg style.NullableColor
Removed_bg style.RGBA
Removed_margin_bg style.RGBA
Search_bg style.RGBA
Search_fg style.RGBA
Select_bg style.RGBA
Select_fg style.NullableColor
Title_bg style.RGBA
Title_fg style.RGBA
}
var resolved_colors ResolvedColors
func create_formatters() {
rc := &resolved_colors
if use_dark_colors {
rc.Added_bg = conf.Dark_added_bg
rc.Added_margin_bg = conf.Dark_added_margin_bg
rc.Background = conf.Dark_background
rc.Filler_bg = conf.Dark_filler_bg
rc.Foreground = conf.Dark_foreground
rc.Highlight_added_bg = conf.Dark_highlight_added_bg
rc.Highlight_removed_bg = conf.Dark_highlight_removed_bg
rc.Hunk_bg = conf.Dark_hunk_bg
rc.Hunk_margin_bg = conf.Dark_hunk_margin_bg
rc.Margin_bg = conf.Dark_margin_bg
rc.Margin_fg = conf.Dark_margin_fg
rc.Margin_filler_bg = conf.Dark_margin_filler_bg
rc.Removed_bg = conf.Dark_removed_bg
rc.Removed_margin_bg = conf.Dark_removed_margin_bg
rc.Search_bg = conf.Dark_search_bg
rc.Search_fg = conf.Dark_search_fg
rc.Select_bg = conf.Dark_select_bg
rc.Select_fg = conf.Dark_select_fg
rc.Title_bg = conf.Dark_title_bg
rc.Title_fg = conf.Dark_title_fg
} else {
rc.Added_bg = conf.Added_bg
rc.Added_margin_bg = conf.Added_margin_bg
rc.Background = conf.Background
rc.Filler_bg = conf.Filler_bg
rc.Foreground = conf.Foreground
rc.Highlight_added_bg = conf.Highlight_added_bg
rc.Highlight_removed_bg = conf.Highlight_removed_bg
rc.Hunk_bg = conf.Hunk_bg
rc.Hunk_margin_bg = conf.Hunk_margin_bg
rc.Margin_bg = conf.Margin_bg
rc.Margin_fg = conf.Margin_fg
rc.Margin_filler_bg = conf.Margin_filler_bg
rc.Removed_bg = conf.Removed_bg
rc.Removed_margin_bg = conf.Removed_margin_bg
rc.Search_bg = conf.Search_bg
rc.Search_fg = conf.Search_fg
rc.Select_bg = conf.Select_bg
rc.Select_fg = conf.Select_fg
rc.Title_bg = conf.Title_bg
rc.Title_fg = conf.Title_fg
}
ctx := style.Context{AllowEscapeCodes: true}
only_open := func(x string) string {
ans := ctx.SprintFunc(x)("|")
ans, _, _ = strings.Cut(ans, "|")
return ans
}
format_as_sgr.filler = only_open("bg=" + conf.Filler_bg.AsRGBSharp())
if conf.Margin_filler_bg.IsSet {
format_as_sgr.margin_filler = only_open("bg=" + conf.Margin_filler_bg.Color.AsRGBSharp())
format_as_sgr.filler = only_open("bg=" + rc.Filler_bg.AsRGBSharp())
if rc.Margin_filler_bg.IsSet {
format_as_sgr.margin_filler = only_open("bg=" + rc.Margin_filler_bg.Color.AsRGBSharp())
} else {
format_as_sgr.margin_filler = only_open("bg=" + conf.Filler_bg.AsRGBSharp())
format_as_sgr.margin_filler = only_open("bg=" + rc.Filler_bg.AsRGBSharp())
}
format_as_sgr.added = only_open("bg=" + conf.Added_bg.AsRGBSharp())
format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Added_margin_bg.AsRGBSharp()))
format_as_sgr.removed = only_open("bg=" + conf.Removed_bg.AsRGBSharp())
format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Removed_margin_bg.AsRGBSharp()))
format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", conf.Title_fg.AsRGBSharp(), conf.Title_bg.AsRGBSharp()))
format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Margin_bg.AsRGBSharp()))
format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_bg.AsRGBSharp()))
format_as_sgr.hunk_margin = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Margin_fg.AsRGBSharp(), conf.Hunk_margin_bg.AsRGBSharp()))
format_as_sgr.search = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Search_fg.AsRGBSharp(), conf.Search_bg.AsRGBSharp()))
statusline_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Margin_fg.AsRGBSharp()))
added_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_added_bg.AsRGBSharp()))
removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", conf.Highlight_removed_bg.AsRGBSharp()))
format_as_sgr.added = only_open("bg=" + rc.Added_bg.AsRGBSharp())
format_as_sgr.added_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Added_margin_bg.AsRGBSharp()))
format_as_sgr.removed = only_open("bg=" + rc.Removed_bg.AsRGBSharp())
format_as_sgr.removed_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Removed_margin_bg.AsRGBSharp()))
format_as_sgr.title = only_open(fmt.Sprintf("fg=%s bg=%s bold", rc.Title_fg.AsRGBSharp(), rc.Title_bg.AsRGBSharp()))
format_as_sgr.margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Margin_bg.AsRGBSharp()))
format_as_sgr.hunk = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Hunk_bg.AsRGBSharp()))
format_as_sgr.hunk_margin = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Margin_fg.AsRGBSharp(), rc.Hunk_margin_bg.AsRGBSharp()))
format_as_sgr.search = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Search_fg.AsRGBSharp(), rc.Search_bg.AsRGBSharp()))
statusline_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", rc.Margin_fg.AsRGBSharp()))
added_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", rc.Highlight_added_bg.AsRGBSharp()))
removed_count_format = ctx.SprintFunc(fmt.Sprintf("fg=%s", rc.Highlight_removed_bg.AsRGBSharp()))
message_format = ctx.SprintFunc("bold")
if conf.Select_fg.IsSet {
format_as_sgr.selection = only_open(fmt.Sprintf("fg=%s bg=%s", conf.Select_fg.Color.AsRGBSharp(), conf.Select_bg.AsRGBSharp()))
if rc.Select_fg.IsSet {
format_as_sgr.selection = only_open(fmt.Sprintf("fg=%s bg=%s", rc.Select_fg.Color.AsRGBSharp(), rc.Select_bg.AsRGBSharp()))
} else {
format_as_sgr.selection = only_open("bg=" + conf.Select_bg.AsRGBSharp())
format_as_sgr.selection = only_open("bg=" + rc.Select_bg.AsRGBSharp())
}
}
@@ -198,9 +268,9 @@ func center_span(ltype string, offset, size int) *sgr.Span {
ans := sgr.NewSpan(offset, size)
switch ltype {
case "add":
ans.SetBackground(conf.Highlight_added_bg).SetClosingBackground(conf.Added_bg)
ans.SetBackground(resolved_colors.Highlight_added_bg).SetClosingBackground(resolved_colors.Added_bg)
case "remove":
ans.SetBackground(conf.Highlight_removed_bg).SetClosingBackground(conf.Removed_bg)
ans.SetBackground(resolved_colors.Highlight_removed_bg).SetClosingBackground(resolved_colors.Removed_bg)
}
return ans
}
@@ -469,7 +539,7 @@ func hunk_title(hunk *Hunk) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", hunk.left_start+1, hunk.left_count, hunk.right_start+1, hunk.right_count, hunk.title)
}
func lines_for_context_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine {
func lines_for_context_chunk(data *DiffData, _ int, chunk *Chunk, _ int, ans []*LogicalLine) []*LogicalLine {
for i := 0; i < chunk.left_count; i++ {
left_line_number := chunk.left_start + i
right_line_number := chunk.right_start + i
@@ -514,7 +584,7 @@ func render_half_line(line_number int, line, ltype string, available_cols int, c
return ans
}
func lines_for_diff_chunk(data *DiffData, hunk_num int, chunk *Chunk, chunk_num int, ans []*LogicalLine) []*LogicalLine {
func lines_for_diff_chunk(data *DiffData, _ int, chunk *Chunk, _ int, ans []*LogicalLine) []*LogicalLine {
common := utils.Min(chunk.left_count, chunk.right_count)
ll, rl := make([]HalfScreenLine, 0, 32), make([]HalfScreenLine, 0, 32)
for i := 0; i < utils.Max(chunk.left_count, chunk.right_count); i++ {

View File

@@ -61,6 +61,7 @@ type Handler struct {
collection *Collection
diff_map map[string]*Patch
logical_lines *LogicalLines
terminal_capabilities_received bool
lp *loop.Loop
current_context_count, original_context_count int
added_count, removed_count int
@@ -111,6 +112,31 @@ func (self *Handler) finalize() {
image_collection.Finalize(self.lp)
}
func set_terminal_colors(lp *loop.Loop) {
create_formatters()
lp.SetDefaultColor(loop.FOREGROUND, resolved_colors.Foreground)
lp.SetDefaultColor(loop.CURSOR, resolved_colors.Foreground)
lp.SetDefaultColor(loop.BACKGROUND, resolved_colors.Background)
lp.SetDefaultColor(loop.SELECTION_BG, resolved_colors.Select_bg)
if resolved_colors.Select_fg.IsSet {
lp.SetDefaultColor(loop.SELECTION_FG, resolved_colors.Select_fg.Color)
}
}
func (self *Handler) on_capabilities_received(tc loop.TerminalCapabilities) {
switch conf.Color_scheme {
case Color_scheme_auto:
use_dark_colors = tc.ColorPreference != loop.LIGHT_COLOR_PREFERENCE
case Color_scheme_light:
use_dark_colors = false
case Color_scheme_dark:
use_dark_colors = true
}
set_terminal_colors(self.lp)
self.terminal_capabilities_received = true
self.draw_screen()
}
func (self *Handler) initialize() {
self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
self.lp.OnEscapeCode = self.on_escape_code
@@ -122,13 +148,6 @@ func (self *Handler) initialize() {
sz, _ := self.lp.ScreenSize()
self.update_screen_size(sz)
self.original_context_count = self.current_context_count
self.lp.SetDefaultColor(loop.FOREGROUND, conf.Foreground)
self.lp.SetDefaultColor(loop.CURSOR, conf.Foreground)
self.lp.SetDefaultColor(loop.BACKGROUND, conf.Background)
self.lp.SetDefaultColor(loop.SELECTION_BG, conf.Select_bg)
if conf.Select_fg.IsSet {
self.lp.SetDefaultColor(loop.SELECTION_FG, conf.Select_fg.Color)
}
self.async_results = make(chan AsyncResult, 32)
go func() {
r := AsyncResult{}
@@ -315,7 +334,7 @@ func (self *Handler) render_diff() (err error) {
return nil
}
func (self *Handler) draw_image(key string, num_rows, starting_row int) {
func (self *Handler) draw_image(key string, _, starting_row int) {
image_collection.PlaceImageSubRect(self.lp, key, self.images_resized_to, 0, self.screen_size.cell_height*starting_row, -1, -1)
}
@@ -345,7 +364,7 @@ func (self *Handler) draw_screen() {
}
lp.MoveCursorTo(1, 1)
lp.ClearToEndOfScreen()
if self.logical_lines == nil || self.diff_map == nil || self.collection == nil {
if self.logical_lines == nil || self.diff_map == nil || self.collection == nil || !self.terminal_capabilities_received {
lp.Println(`Calculating diff, please wait...`)
return
}