choose files: Add arbitrary command based previews
Needs testing. Someday.
This commit is contained in:
parent
7075f71da4
commit
772805b8ec
4 changed files with 168 additions and 1 deletions
100
kittens/choose_files/cmd_preview.go
Normal file
100
kittens/choose_files/cmd_preview.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package choose_files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/icons"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/humanize"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
const CMD_METADATA_KEY = "cmd-metadata.json"
|
||||
|
||||
type cmd_renderer struct {
|
||||
cmdline []string
|
||||
}
|
||||
|
||||
func (c cmd_renderer) String() string {
|
||||
return c.cmdline[0]
|
||||
}
|
||||
|
||||
type CmdResult struct {
|
||||
Lines []string `json:"lines"`
|
||||
Image string `json:"image"`
|
||||
TitleExtra string `json:"title_extra"`
|
||||
}
|
||||
|
||||
func (c cmd_renderer) Render(path string) (m map[string][]byte, mi metadata, img *images.ImageData, err error) {
|
||||
cmdline := append(c.cmdline, path)
|
||||
cmd := exec.Command(cmdline[0], cmdline[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.SysProcAttr = &unix.SysProcAttr{Setsid: true}
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err = cmd.Run(); err != nil {
|
||||
err = fmt.Errorf("failed to run %v to read metadata from %s with error: %w and stderr: %s", c.cmdline, path, err, stderr.String())
|
||||
return
|
||||
}
|
||||
var md CmdResult
|
||||
if err = json.Unmarshal(stdout.Bytes(), &md); err != nil {
|
||||
err = fmt.Errorf("could not decode JSON response from %v for %s: %w", c.cmdline, path, err)
|
||||
}
|
||||
if md.Image != "" {
|
||||
var ip ImagePreviewRenderer
|
||||
if m, mi, img, err = ip.Render(md.Image); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
mi.custom = &md
|
||||
return
|
||||
}
|
||||
|
||||
func (c cmd_renderer) Unmarshall(m map[string]string) (any, error) {
|
||||
data, err := os.ReadFile(m[CMD_METADATA_KEY])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ans CmdResult
|
||||
if err = json.Unmarshal(data, &ans); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ans, nil
|
||||
}
|
||||
|
||||
func (c cmd_renderer) ShowMetadata(h *Handler, s ShowData) (offset int) {
|
||||
w := func(text string, center bool) {
|
||||
if s.height > offset {
|
||||
offset += h.render_wrapped_text_in_region(text, s.x, s.y+offset, s.width, s.height-offset, center)
|
||||
}
|
||||
}
|
||||
ext := filepath.Ext(s.abspath)
|
||||
r := s.custom_metadata.custom.(*CmdResult)
|
||||
text := fmt.Sprintf("%s: %s%s", ext, humanize.Bytes(uint64(s.metadata.Size())), r.TitleExtra)
|
||||
icon := icons.IconForPath(s.abspath)
|
||||
w(icon+" "+text, true)
|
||||
for _, line := range r.Lines {
|
||||
w(line, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewCmdPreview(
|
||||
abspath string, metadata fs.FileInfo, opts Settings, WakeupMainThread func() bool, p previewer,
|
||||
) Preview {
|
||||
c := cmd_renderer{p.cmdline}
|
||||
if ans, err := NewImagePreview(abspath, metadata, opts, WakeupMainThread, c); err == nil {
|
||||
return ans
|
||||
} else {
|
||||
return NewErrorPreview(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -272,6 +272,38 @@ func (h *Handler) draw_screen() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
type previewer struct {
|
||||
cmdline []string
|
||||
pattern string
|
||||
use_mimetype bool
|
||||
}
|
||||
|
||||
func (p previewer) matches(fname, mt string) bool {
|
||||
q := utils.IfElse(p.use_mimetype, mt, fname)
|
||||
matched, err := filepath.Match(p.pattern, q)
|
||||
return matched && err == nil
|
||||
}
|
||||
|
||||
var previewers []previewer
|
||||
|
||||
func load_previewers(cfg *Config) (err error) {
|
||||
for _, spec := range cfg.Previewer {
|
||||
parts, err := shlex.Split(spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid previewer specification: %#v with error: %w", spec, err)
|
||||
}
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid previewer specification: %#v with error: no command specified", spec)
|
||||
}
|
||||
prefix, pattern, found := strings.Cut(parts[0], ":")
|
||||
if !found {
|
||||
return fmt.Errorf("invalid previewer specification: %#v with error: no prefix in pattern specification", spec)
|
||||
}
|
||||
previewers = append(previewers, previewer{parts[1:], pattern, prefix == "mime"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func load_config(opts *Options) (ans *Config, err error) {
|
||||
ans = NewConfig()
|
||||
p := config.ConfigParser{LineHandler: ans.Parse}
|
||||
|
|
@ -855,6 +887,9 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
|||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
if err = load_previewers(conf); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
lp, err := loop.New()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
|
|
|
|||
|
|
@ -85,7 +85,33 @@
|
|||
otherwise it will not affect already cached previews.
|
||||
''')
|
||||
|
||||
opt('+previewer', '', long_text='''
|
||||
Specify an arbitrary program based preview generator. The syntax is::
|
||||
|
||||
pattern program arguments...
|
||||
|
||||
Here, pattern can be used to match file names or mimetypes. For example:
|
||||
:code:`name:*.doc` matches files with the extension :code:`.doc`. Similarly,
|
||||
:code:`mime:image/*` matches all image files. :code:`program` can be any
|
||||
executable program in PATH. It will be run with the supplied arguments. The last argument
|
||||
will be the path to the file for which a preview must be generated.
|
||||
|
||||
Can be specified multiple times to setup different previewers for different types of files.
|
||||
Note that previewers specified using this option take precedence over the builtin
|
||||
previewers.
|
||||
|
||||
The command must output preview data to STDOUT, in JSON format, of the form::
|
||||
|
||||
{
|
||||
"lines": ["line1", "line2", ...],
|
||||
"image": "absolute path to generated image preview",
|
||||
"title_extra": "some text to show on the first line",
|
||||
}
|
||||
|
||||
The lines can contain SGR formatting escape codes and will be displayed as is at the
|
||||
top of the preview panel. The image is optional and must be in one of the JPEG, PNG, GIF, WEBP, APNG
|
||||
formats.
|
||||
''')
|
||||
egr() # }}}
|
||||
|
||||
agr('shortcuts', 'Keyboard shortcuts') # {{{
|
||||
|
|
|
|||
|
|
@ -332,7 +332,13 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
|
|||
}
|
||||
return NewDirectoryPreview(abspath, s)
|
||||
}
|
||||
mt := utils.GuessMimeType(filepath.Base(abspath))
|
||||
fname := filepath.Base(abspath)
|
||||
mt := utils.GuessMimeType(fname)
|
||||
for _, q := range previewers {
|
||||
if q.matches(fname, mt) {
|
||||
return NewCmdPreview(abspath, s, pm.settings, pm.WakeupMainThread, q)
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue