diff --git a/kittens/choose_files/__init__.py b/kittens/choose_files/__init__.py
index e69de29bb..e74008787 100644
--- a/kittens/choose_files/__init__.py
+++ b/kittens/choose_files/__init__.py
@@ -0,0 +1,6 @@
+def syntax_aliases(x: str) -> dict[str, str]:
+ ans = {}
+ for x in x.split():
+ k, _, v = x.partition(':')
+ ans[k] = v
+ return ans
diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go
index ced324e14..5efa080e2 100644
--- a/kittens/choose_files/main.go
+++ b/kittens/choose_files/main.go
@@ -102,26 +102,28 @@ type render_state struct {
}
type State struct {
- base_dir string
- current_dir string
- select_dirs bool
- multiselect bool
- search_text string
- mode Mode
- suggested_save_file_name string
- suggested_save_file_path string
- window_title string
- screen Screen
- current_filter string
- filter_map map[string]Filter
- filter_names []string
- show_hidden bool
- show_preview bool
- respect_ignores bool
- sort_by_last_modified bool
- global_ignores ignorefiles.IgnoreFile
- keyboard_shortcuts []*config.KeyAction
- display_title bool
+ base_dir string
+ current_dir string
+ select_dirs bool
+ multiselect bool
+ search_text string
+ mode Mode
+ suggested_save_file_name string
+ suggested_save_file_path string
+ window_title string
+ screen Screen
+ current_filter string
+ filter_map map[string]Filter
+ filter_names []string
+ show_hidden bool
+ show_preview bool
+ respect_ignores bool
+ sort_by_last_modified bool
+ global_ignores ignorefiles.IgnoreFile
+ keyboard_shortcuts []*config.KeyAction
+ display_title bool
+ pygments_style, dark_pygments_style string
+ syntax_aliases map[string]string
selections []string
current_idx CollectionIndex
@@ -130,6 +132,8 @@ type State struct {
redraw_needed bool
}
+func (s State) HighlightStyles() (string, string) { return s.pygments_style, s.dark_pygments_style }
+func (s State) SyntaxAliases() map[string]string { return s.syntax_aliases }
func (s State) DisplayTitle() bool { return s.display_title }
func (s State) ShowHidden() bool { return s.show_hidden }
func (s State) ShowPreview() bool { return s.show_preview }
@@ -709,6 +713,9 @@ func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error)
}
h.state.keyboard_shortcuts = conf.KeyboardShortcuts
h.state.display_title = opts.DisplayTitle
+ h.state.pygments_style = conf.Pygments_style
+ h.state.dark_pygments_style = conf.Dark_pygments_style
+ h.state.syntax_aliases = conf.Syntax_aliases
return
}
diff --git a/kittens/choose_files/main.py b/kittens/choose_files/main.py
index 1d8b402fc..2726a7deb 100644
--- a/kittens/choose_files/main.py
+++ b/kittens/choose_files/main.py
@@ -40,10 +40,34 @@
Can be specified multiple times to use multiple patterns. Note that every pattern
has to be checked against every file, so use sparingly.
''')
+egr() # }}}
+
+agr('appearance', 'Appearance') # {{{
opt('show_preview', 'last', choices=('last', 'yes', 'y', 'true', 'no', 'n', 'false'), long_text='''
Whether to show a preview of the current file/directory. The default value of :code:`last` means remember the last
used value. This setting can be toggled withing the program.''')
+
+opt('pygments_style', 'default', long_text='''
+The pygments color scheme to use for syntax highlighting of file previews. See :link:`pygments
+builtin styles ` for a list of schemes.
+This sets the colors used for light color schemes, use :opt:`dark_pygments_style` to change the
+colors for dark color schemes.
+''')
+
+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 ` for a list of schemes.
+This sets the colors used for dark color schemes, use :opt:`pygments_style` to change the
+colors for light color schemes.''')
+
+opt('syntax_aliases', 'pyj:py pyi:py recipe:py', ctype='strdict_ _:', option_type='syntax_aliases',
+ long_text='''
+File extension aliases for syntax highlight. For example, to syntax highlight
+:file:`file.xyz` as :file:`file.abc` use a setting of :code:`xyz:abc`.
+Multiple aliases must be separated by spaces.
+''')
+
egr() # }}}
agr('shortcuts', 'Keyboard shortcuts') # {{{
diff --git a/kittens/choose_files/preview.go b/kittens/choose_files/preview.go
index 0ef7aae68..0771fbacb 100644
--- a/kittens/choose_files/preview.go
+++ b/kittens/choose_files/preview.go
@@ -9,8 +9,11 @@ import (
"slices"
"strings"
"sync"
+ "unicode/utf8"
+ "github.com/kovidgoyal/kitty/tools/highlight"
"github.com/kovidgoyal/kitty/tools/icons"
+ "github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/utils/humanize"
"github.com/kovidgoyal/kitty/tools/utils/style"
@@ -30,12 +33,14 @@ type PreviewManager struct {
WakeupMainThread func() bool
cache map[string]Preview
lock sync.Mutex
+ highlighter highlight.Highlighter
}
-func NewPreviewManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *PreviewManager {
+func NewPreviewManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) (ans *PreviewManager) {
+ defer func() { sanitize = ans.highlighter.Sanitize }()
return &PreviewManager{
report_errors: err_chan, settings: settings, WakeupMainThread: WakeupMainThread,
- cache: make(map[string]Preview),
+ cache: make(map[string]Preview), highlighter: highlight.NewHighlighter(nil),
}
}
@@ -104,6 +109,8 @@ func NewErrorPreview(err error) Preview {
return &MessagePreview{msg: text}
}
+var sanitize func(string) string
+
func write_file_metadata(abspath string, metadata fs.FileInfo, entries []fs.DirEntry) (header string, trailers []string) {
buf := strings.Builder{}
buf.Grow(4096)
@@ -115,7 +122,7 @@ func write_file_metadata(abspath string, metadata fs.FileInfo, entries []fs.DirE
add("Size", humanize.Bytes(uint64(metadata.Size())))
case fs.ModeSymlink:
if tgt, err := os.Readlink(abspath); err == nil {
- add("Target", tgt)
+ add("Target", sanitize(tgt))
} else {
add("Target", err.Error())
}
@@ -145,7 +152,7 @@ func write_file_metadata(abspath string, metadata fs.FileInfo, entries []fs.DirE
slices.SortFunc(names, func(a, b string) int { return strings.Compare(type_map[a].lname, type_map[b].lname) })
fmt.Fprintln(&buf, "Contents:")
for _, n := range names {
- trailers = append(trailers, icons.IconForFileWithMode(n, type_map[n].ftype, false)+" "+n)
+ trailers = append(trailers, icons.IconForFileWithMode(n, type_map[n].ftype, false)+" "+sanitize(n))
}
}
return buf.String(), trailers
@@ -167,6 +174,96 @@ func NewFileMetadataPreview(abspath string, metadata fs.FileInfo) Preview {
return &MessagePreview{title: title, msg: h, trailers: t}
}
+type highlighed_data struct {
+ text string
+ light bool
+ err error
+}
+
+type TextFilePreview struct {
+ plain_text, highlighted_text string
+ highlighted_chan chan highlighed_data
+ light bool
+}
+
+func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
+
+func (p TextFilePreview) Render(h *Handler, x, y, width, height int) {
+ if p.highlighted_chan != nil {
+ select {
+ case hd := <-p.highlighted_chan:
+ p.highlighted_chan = nil
+ if hd.err == nil {
+ p.highlighted_text = hd.text
+ }
+ default:
+ }
+ }
+ text := p.highlighted_text
+ if text == "" {
+ text = p.plain_text
+ }
+ s := utils.NewLineScanner(text)
+ buf := strings.Builder{}
+ buf.Grow(1024 * height)
+ for num := 0; s.Scan() && num < height; num++ {
+ line := s.Text()
+ truncated := wcswidth.TruncateToVisualLength(line, width)
+ buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+num, x))
+ buf.WriteString(truncated)
+ if len(truncated) < len(line) {
+ wcswidth.KeepOnlyCSI(line[len(truncated):], &buf)
+ }
+ }
+ buf.WriteString("\x1b[m") // reset any highlight styles
+ h.lp.QueueWriteString(buf.String())
+}
+
+func NewTextFilePreview(abspath string, metadata fs.FileInfo, highlighted_chan chan highlighed_data, sanitize func(string) string) Preview {
+ data, err := os.ReadFile(abspath)
+ if err != nil {
+ return NewFileMetadataPreview(abspath, metadata)
+ }
+ text := utils.UnsafeBytesToString(data)
+ if !utf8.ValidString(text) {
+ text = "Error: not valid utf-8 text"
+ }
+ return &TextFilePreview{plain_text: sanitize(text), highlighted_chan: highlighted_chan, light: use_light_colors}
+}
+
+type style_resolver struct {
+ light bool
+ light_style, dark_style string
+ syntax_aliases map[string]string
+}
+
+func (s style_resolver) StyleName() string {
+ return utils.IfElse(s.light, s.light_style, s.dark_style)
+}
+func (s style_resolver) UseLightColors() bool { return s.light }
+func (s style_resolver) SyntaxAliases() map[string]string { return s.syntax_aliases }
+func (s style_resolver) TextForPath(path string) (string, error) {
+ ans, err := os.ReadFile(path)
+ if err == nil {
+ return utils.UnsafeBytesToString(ans), nil
+ }
+ return "", err
+}
+
+func (pm *PreviewManager) highlight_file_async(path string, output chan highlighed_data) {
+ s := style_resolver{light: use_light_colors, syntax_aliases: pm.settings.SyntaxAliases()}
+ s.light_style, s.dark_style = pm.settings.HighlightStyles()
+ go func() {
+ highlighted, err := pm.highlighter.HighlightFile(path, &s)
+ if err != nil {
+ debugprintln(fmt.Sprintf("Failed to highlight: %s with error: %s", path, err))
+ }
+ output <- highlighed_data{text: highlighted, err: err, light: s.light}
+ close(output)
+ pm.WakeupMainThread()
+ }()
+}
+
func (pm *PreviewManager) invalidate_color_scheme_based_cached_items() {
pm.lock.Lock()
defer pm.lock.Unlock()
@@ -192,6 +289,13 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
}
return NewDirectoryPreview(abspath, s)
}
+ mt := utils.GuessMimeType(filepath.Base(abspath))
+ const MAX_TEXT_FILE_SIZE = 16 * 1024 * 1024
+ if s.Size() <= MAX_TEXT_FILE_SIZE && (utils.KnownTextualMimes[mt] || strings.HasPrefix(mt, "text/")) {
+ ch := make(chan highlighed_data, 2)
+ pm.highlight_file_async(abspath, ch)
+ return NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
+ }
return NewFileMetadataPreview(abspath, s)
}
diff --git a/kittens/choose_files/results.go b/kittens/choose_files/results.go
index c7a69b4d5..d816ac32a 100644
--- a/kittens/choose_files/results.go
+++ b/kittens/choose_files/results.go
@@ -26,6 +26,7 @@ func (h *Handler) draw_results_title() {
if strings.HasPrefix(text, home) {
text = "~" + text[len(home):]
}
+ text = sanitize(text)
available_width := h.screen_size.width - 9
if available_width < 2 {
return
@@ -137,7 +138,7 @@ func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x
} else {
icon = icon_for(filepath.Join(root_dir, m.text), m.ftype)
}
- text := m.text
+ text := sanitize(m.text)
add_ellipsis := false
width := wcswidth.Stringwidth(text)
if width > available_width-3 {
diff --git a/kittens/choose_files/scan.go b/kittens/choose_files/scan.go
index 1a1942857..6565fe15a 100644
--- a/kittens/choose_files/scan.go
+++ b/kittens/choose_files/scan.go
@@ -630,6 +630,8 @@ type Settings interface {
SortByLastModified() bool
Filter() Filter
GlobalIgnores() ignorefiles.IgnoreFile
+ HighlightStyles() (string, string)
+ SyntaxAliases() map[string]string
}
type ResultManager struct {
diff --git a/kitty/guess_mime_type.py b/kitty/guess_mime_type.py
index 44f349ec2..b9954e2ff 100644
--- a/kitty/guess_mime_type.py
+++ b/kitty/guess_mime_type.py
@@ -18,6 +18,7 @@
'yaml': 'text/yaml',
'js': 'text/javascript',
'json': 'text/json',
+ 'nix': 'text/nix',
}
diff --git a/tools/highlight/api.go b/tools/highlight/api.go
index 5068e91a7..281e89688 100644
--- a/tools/highlight/api.go
+++ b/tools/highlight/api.go
@@ -44,6 +44,7 @@ func NewSanitizeControlCodes(replace_tab_by string) *SanitizeControlCodes {
type Highlighter interface {
HighlightFile(path string, srd StyleResolveData) (highlighted_string string, err error)
+ Sanitize(string) string
}
func NewHighlighter(sanitize func(string) string) Highlighter {
diff --git a/tools/highlight/impl.go b/tools/highlight/impl.go
index 878d18828..7e6af8ab4 100644
--- a/tools/highlight/impl.go
+++ b/tools/highlight/impl.go
@@ -180,6 +180,8 @@ type highlighter struct {
sanitize func(string) string
}
+func (h *highlighter) Sanitize(x string) string { return h.sanitize(x) }
+
func (h *highlighter) HighlightFile(path string, srd StyleResolveData) (highlighted_string string, err error) {
defer func() {
if r := recover(); r != nil {
diff --git a/tools/wcswidth/truncate.go b/tools/wcswidth/truncate.go
index 7085629db..7aa0e50b9 100644
--- a/tools/wcswidth/truncate.go
+++ b/tools/wcswidth/truncate.go
@@ -5,6 +5,7 @@ package wcswidth
import (
"errors"
"fmt"
+ "io"
"strconv"
"github.com/kovidgoyal/kitty/tools/utils"
@@ -50,6 +51,17 @@ func (self *truncate_iterator) handle_st_terminated_escape_code(body []byte) err
return nil
}
+func KeepOnlyCSI(text string, output io.Writer) {
+ var w WCWidthIterator
+ w.parser.HandleCSI = func(data []byte) (err error) {
+ _, err = output.Write([]byte{'\x1b', '['})
+ if err == nil {
+ _, err = output.Write(data)
+ }
+ return
+ }
+}
+
func create_truncate_iterator() *truncate_iterator {
var ans truncate_iterator
ans.w.parser.HandleRune = ans.handle_rune