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 @@ Anchored patterns match with respect to whatever directory is currently being di
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 @@ known_extensions = {
'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