mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-25 17:52:02 +02:00
Implement previews for plain text files
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <https://pygments.org/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 <https://pygments.org/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') # {{{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -630,6 +630,8 @@ type Settings interface {
|
||||
SortByLastModified() bool
|
||||
Filter() Filter
|
||||
GlobalIgnores() ignorefiles.IgnoreFile
|
||||
HighlightStyles() (string, string)
|
||||
SyntaxAliases() map[string]string
|
||||
}
|
||||
|
||||
type ResultManager struct {
|
||||
|
||||
Reference in New Issue
Block a user