Refine image preview integration
This commit is contained in:
parent
9dcef02e0d
commit
912de45e19
5 changed files with 43 additions and 48 deletions
|
|
@ -147,6 +147,5 @@ Release artifacts:
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- File creation time depends on filesystem/OS support; unavailable values are shown as `n/a`.
|
- File creation time depends on filesystem/OS support; unavailable values are shown as `n/a`.
|
||||||
- Image preview in info pane (`F9`) and image full-screen view (`F3`) use `chafa`.
|
|
||||||
|
|
||||||
Architecture notes: [docs/architecture.md](/home/vrubel/projects/vcom/docs/architecture.md)
|
Architecture notes: [docs/architecture.md](/home/vrubel/projects/vcom/docs/architecture.md)
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,6 @@
|
||||||
package = pkgs.symlinkJoin {
|
package = pkgs.symlinkJoin {
|
||||||
name = "vcom";
|
name = "vcom";
|
||||||
paths = [ packageBase ];
|
paths = [ packageBase ];
|
||||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
|
||||||
postBuild = ''
|
|
||||||
wrapProgram "$out/bin/vcom" \
|
|
||||||
--prefix PATH : "${lib.makeBinPath [ pkgs.chafa ]}"
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
packages.default = package;
|
packages.default = package;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -123,9 +122,7 @@ func BuildPreview(entry Entry, options PreviewOptions) Preview {
|
||||||
preview.Metadata.ImageFormat = format
|
preview.Metadata.ImageFormat = format
|
||||||
preview.Metadata.ImageSize = dimensions
|
preview.Metadata.ImageSize = dimensions
|
||||||
inline := renderImageInlinePreview(entry.Path, options.ImagePreviewWidth, options.ImagePreviewHeight)
|
inline := renderImageInlinePreview(entry.Path, options.ImagePreviewWidth, options.ImagePreviewHeight)
|
||||||
if inline == "" {
|
if inline != "" {
|
||||||
preview.Body = "Image preview unavailable.\n\nInstall `chafa` for inline preview in info pane."
|
|
||||||
} else {
|
|
||||||
preview.Body = inline
|
preview.Body = inline
|
||||||
}
|
}
|
||||||
preview.PlainBody = preview.Body
|
preview.PlainBody = preview.Body
|
||||||
|
|
@ -381,36 +378,7 @@ func previewIcon(entry Entry, useNerdIcons bool) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderImageInlinePreview(path string, width int, height int) string {
|
func renderImageInlinePreview(path string, width int, height int) string {
|
||||||
if width < 20 {
|
return ""
|
||||||
width = 20
|
|
||||||
}
|
|
||||||
if height < 8 {
|
|
||||||
height = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := exec.LookPath("chafa"); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(
|
|
||||||
"chafa",
|
|
||||||
"--format=symbols",
|
|
||||||
"--symbols=vhalf",
|
|
||||||
"--animate=off",
|
|
||||||
"--fg-only",
|
|
||||||
"--size", fmt.Sprintf("%dx%d", width, height),
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
view := strings.TrimSpace(string(out))
|
|
||||||
if view == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectImage(data []byte) (string, string, bool) {
|
func detectImage(data []byte) (string, string, bool) {
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ func (m *imageOverlayManager) backendOutput() string {
|
||||||
case os.Getenv("DISPLAY") != "":
|
case os.Getenv("DISPLAY") != "":
|
||||||
order = append(order, "x11")
|
order = append(order, "x11")
|
||||||
}
|
}
|
||||||
order = append(order, "wayland", "x11", "sixel", "kitty", "chafa")
|
order = append(order, "wayland", "x11", "sixel", "kitty")
|
||||||
|
|
||||||
unique := make([]string, 0, len(order))
|
unique := make([]string, 0, len(order))
|
||||||
for _, backend := range order {
|
for _, backend := range order {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package ui
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -111,6 +112,10 @@ type copyDoneMsg struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dismissNoticeMsg struct{}
|
type dismissNoticeMsg struct{}
|
||||||
|
type externalOpenMsg struct {
|
||||||
|
path string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
type copyJobState struct {
|
type copyJobState struct {
|
||||||
id int
|
id int
|
||||||
|
|
@ -412,6 +417,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case externalOpenMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.status = fmt.Sprintf("Open failed: %v", msg.err)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.status = fmt.Sprintf("Opened %s", filepath.Base(msg.path))
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if m.modal.kind != modalNone {
|
if m.modal.kind != modalNone {
|
||||||
return m.handleModalKey(msg)
|
return m.handleModalKey(msg)
|
||||||
|
|
@ -1026,9 +1039,7 @@ func (m *Model) handleOpenExternal() (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
m.cleanupImageOverlay()
|
m.cleanupImageOverlay()
|
||||||
m.status = fmt.Sprintf("Opening %s with %s", selected.DisplayName(), name)
|
m.status = fmt.Sprintf("Opening %s with %s", selected.DisplayName(), name)
|
||||||
return m, tea.ExecProcess(command, func(err error) tea.Msg {
|
return m, startExternalOpenCmd(command, selected.Path)
|
||||||
return opMsg{kind: opView, sourcePath: selected.Path, err: err}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleEdit() (tea.Model, tea.Cmd) {
|
func (m *Model) handleEdit() (tea.Model, tea.Cmd) {
|
||||||
|
|
@ -2598,6 +2609,18 @@ func externalCommandFromEnv(envVars []string, fallbacks []string, path string) (
|
||||||
return exec.Command(parts[0], args...), filepath.Base(parts[0]), nil
|
return exec.Command(parts[0], args...), filepath.Base(parts[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startExternalOpenCmd(command *exec.Cmd, path string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
command.Stdin = nil
|
||||||
|
command.Stdout = io.Discard
|
||||||
|
command.Stderr = io.Discard
|
||||||
|
if err := command.Start(); err != nil {
|
||||||
|
return externalOpenMsg{path: path, err: err}
|
||||||
|
}
|
||||||
|
return externalOpenMsg{path: path}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func enableMouseCmd() tea.Cmd {
|
func enableMouseCmd() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
return tea.EnableMouseCellMotion()
|
return tea.EnableMouseCellMotion()
|
||||||
|
|
@ -2753,11 +2776,21 @@ func (m Model) syncImageOverlay(leftWidth int, previewWidth int, bodyHeight int)
|
||||||
if m.active == PaneLeft {
|
if m.active == PaneLeft {
|
||||||
startX = leftWidth + m.cfg.UI.PaneGap
|
startX = leftWidth + m.cfg.UI.PaneGap
|
||||||
}
|
}
|
||||||
|
innerWidth := max(previewWidth-2, 1)
|
||||||
|
metaHeight := 0
|
||||||
|
if m.cfg.Preview.ShowMetadata {
|
||||||
|
metaHeight = lipgloss.Height(renderMetadata(m.previewData.Metadata, m.palette, innerWidth))
|
||||||
|
}
|
||||||
|
titleHeight := 1
|
||||||
|
topInset := 1
|
||||||
|
contentBorder := 1
|
||||||
|
safetyGap := 1
|
||||||
|
contentTop := topInset + titleHeight + metaHeight + contentBorder + safetyGap
|
||||||
rect = overlayRect{
|
rect = overlayRect{
|
||||||
x: startX + 2,
|
x: startX + 3,
|
||||||
y: 9,
|
y: contentTop,
|
||||||
width: max(previewWidth-4, 1),
|
width: max(previewWidth-6, 1),
|
||||||
height: max(bodyHeight-11, 1),
|
height: max(bodyHeight-contentTop-2, 1),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.overlay.hide()
|
m.overlay.hide()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue