mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
Implement previews for plain text files
This commit is contained in:
parent
28fce006d6
commit
bd0f55531f
10 changed files with 185 additions and 25 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ known_extensions = {
|
|||
'yaml': 'text/yaml',
|
||||
'js': 'text/javascript',
|
||||
'json': 'text/json',
|
||||
'nix': 'text/nix',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue