feat: add copy path button in info panel
This commit is contained in:
parent
ea8c596ef6
commit
e890e1871b
2 changed files with 86 additions and 27 deletions
|
|
@ -183,6 +183,7 @@ type Model struct {
|
||||||
copyJob *copyJobState
|
copyJob *copyJobState
|
||||||
nextCopyJob int
|
nextCopyJob int
|
||||||
copyProgress chan tea.Msg
|
copyProgress chan tea.Msg
|
||||||
|
copyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(cfg config.Config, configPath string) (Model, error) {
|
func NewModel(cfg config.Config, configPath string) (Model, error) {
|
||||||
|
|
@ -585,6 +586,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy file path when info mode is open
|
||||||
|
if m.infoMode && m.copyPath != "" {
|
||||||
|
switch msg.String() {
|
||||||
|
case "y", "Y", "ctrl+c":
|
||||||
|
if err := clipboard.WriteAll(m.copyPath); err != nil {
|
||||||
|
m.status = fmt.Sprintf("Copy path error: %v", err)
|
||||||
|
} else {
|
||||||
|
m.status = fmt.Sprintf("Path copied: %s", m.copyPath)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, m.keys.Quit):
|
case key.Matches(msg, m.keys.Quit):
|
||||||
m.cleanupArchiveMounts()
|
m.cleanupArchiveMounts()
|
||||||
|
|
@ -602,6 +616,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.selectMode = false
|
m.selectMode = false
|
||||||
m.cursorMode = false
|
m.cursorMode = false
|
||||||
m.visualMode = false
|
m.visualMode = false
|
||||||
|
m.copyPath = ""
|
||||||
m.status = "Info pane closed"
|
m.status = "Info pane closed"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
@ -715,7 +730,7 @@ func (m Model) View() string {
|
||||||
} else if m.viewMode && m.previewData.Kind == vfs.PreviewKindText {
|
} else if m.viewMode && m.previewData.Kind == vfs.PreviewKindText {
|
||||||
panels = renderSelectionPane(m.previewData, &m.previewModel, m.palette, m.width, bodyHeight)
|
panels = renderSelectionPane(m.previewData, &m.previewModel, m.palette, m.width, bodyHeight)
|
||||||
} else if m.viewMode {
|
} else if m.viewMode {
|
||||||
panels = renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, m.width, bodyHeight)
|
panels = renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, m.width, bodyHeight, m.nerdIcons)
|
||||||
} else if m.selectMode && m.infoMode {
|
} else if m.selectMode && m.infoMode {
|
||||||
panels = renderSelectionPane(m.previewData, &m.previewModel, m.palette, m.width, bodyHeight)
|
panels = renderSelectionPane(m.previewData, &m.previewModel, m.palette, m.width, bodyHeight)
|
||||||
} else if m.infoMode {
|
} else if m.infoMode {
|
||||||
|
|
@ -724,12 +739,12 @@ func (m Model) View() string {
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
renderPane(m.left, m.cfg, m.palette, leftWidth, bodyHeight, true, m.hoverIndexFor(PaneLeft), m.nerdIcons),
|
renderPane(m.left, m.cfg, m.palette, leftWidth, bodyHeight, true, m.hoverIndexFor(PaneLeft), m.nerdIcons),
|
||||||
gap,
|
gap,
|
||||||
renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, previewWidth, bodyHeight),
|
renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, previewWidth, bodyHeight, m.nerdIcons),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
panels = lipgloss.JoinHorizontal(
|
panels = lipgloss.JoinHorizontal(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, previewWidth, bodyHeight),
|
renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, previewWidth, bodyHeight, m.nerdIcons),
|
||||||
gap,
|
gap,
|
||||||
renderPane(m.right, m.cfg, m.palette, rightWidth, bodyHeight, true, m.hoverIndexFor(PaneRight), m.nerdIcons),
|
renderPane(m.right, m.cfg, m.palette, rightWidth, bodyHeight, true, m.hoverIndexFor(PaneRight), m.nerdIcons),
|
||||||
)
|
)
|
||||||
|
|
@ -1253,6 +1268,15 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
m.moveCursor(1)
|
m.moveCursor(1)
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
case msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft:
|
case msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft:
|
||||||
|
if m.copyPath != "" && m.mouseOverPathLine(msg.X, msg.Y) {
|
||||||
|
if err := clipboard.WriteAll(m.copyPath); err != nil {
|
||||||
|
m.status = fmt.Sprintf("Copy path error: %v", err)
|
||||||
|
} else {
|
||||||
|
m.status = fmt.Sprintf("Path copied: %s", m.copyPath)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
paneID, index, ok := m.mouseTarget(msg.X, msg.Y)
|
paneID, index, ok := m.mouseTarget(msg.X, msg.Y)
|
||||||
if !ok {
|
if !ok {
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -1284,6 +1308,7 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
m.visualMode = false
|
m.visualMode = false
|
||||||
m.resizePreview()
|
m.resizePreview()
|
||||||
m.syncPreviewContent()
|
m.syncPreviewContent()
|
||||||
|
m.copyPath = ""
|
||||||
m.status = "Info mode: off"
|
m.status = "Info mode: off"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
@ -1298,6 +1323,7 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
m.cursorMode = false
|
m.cursorMode = false
|
||||||
m.visualMode = false
|
m.visualMode = false
|
||||||
m.hover = hoverState{pane: paneID, index: index, ok: true}
|
m.hover = hoverState{pane: paneID, index: index, ok: true}
|
||||||
|
m.copyPath = ""
|
||||||
m.status = "Info mode: off"
|
m.status = "Info mode: off"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
@ -1691,6 +1717,9 @@ func (m *Model) applyDirSize(path string, size int64) {
|
||||||
func (m *Model) applyPreview(preview vfs.Preview) {
|
func (m *Model) applyPreview(preview vfs.Preview) {
|
||||||
m.previewData = preview
|
m.previewData = preview
|
||||||
m.syncPreviewContent()
|
m.syncPreviewContent()
|
||||||
|
if m.infoMode {
|
||||||
|
m.copyPath = preview.Metadata.Path
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) syncPreviewContent() {
|
func (m *Model) syncPreviewContent() {
|
||||||
|
|
@ -2059,7 +2088,7 @@ func (m *Model) ensureTextCursorVisible() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg config.Config, palette theme.Palette, width int, height int) string {
|
func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg config.Config, palette theme.Palette, width int, height int, useNerdfont bool) string {
|
||||||
innerWidth := max(width-2, 1)
|
innerWidth := max(width-2, 1)
|
||||||
innerHeight := max(height-2, 1)
|
innerHeight := max(height-2, 1)
|
||||||
contentWidth := max(innerWidth-2, 1)
|
contentWidth := max(innerWidth-2, 1)
|
||||||
|
|
@ -2084,7 +2113,7 @@ func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg c
|
||||||
parts := []string{title}
|
parts := []string{title}
|
||||||
usedHeight := lipgloss.Height(title)
|
usedHeight := lipgloss.Height(title)
|
||||||
if cfg.Preview.ShowMetadata {
|
if cfg.Preview.ShowMetadata {
|
||||||
metaView := renderMetadata(preview.Metadata, palette, innerWidth)
|
metaView := renderMetadata(preview.Metadata, palette, innerWidth, useNerdfont)
|
||||||
parts = append(parts, metaView)
|
parts = append(parts, metaView)
|
||||||
usedHeight += lipgloss.Height(metaView)
|
usedHeight += lipgloss.Height(metaView)
|
||||||
}
|
}
|
||||||
|
|
@ -2118,7 +2147,7 @@ func renderSelectionPane(preview vfs.Preview, viewportModel *viewport.Model, pal
|
||||||
Render(viewportModel.View())
|
Render(viewportModel.View())
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderMetadata(meta vfs.Metadata, palette theme.Palette, width int) string {
|
func renderMetadata(meta vfs.Metadata, palette theme.Palette, width int, useNerdfont bool) string {
|
||||||
outerWidth := max(width-2, 1)
|
outerWidth := max(width-2, 1)
|
||||||
innerWidth := max(outerWidth-2, 1)
|
innerWidth := max(outerWidth-2, 1)
|
||||||
leftRows := []string{
|
leftRows := []string{
|
||||||
|
|
@ -2153,11 +2182,17 @@ func renderMetadata(meta vfs.Metadata, palette theme.Palette, width int) string
|
||||||
Foreground(palette.Text).
|
Foreground(palette.Text).
|
||||||
Render(strings.Join(rightRows, "\n"))
|
Render(strings.Join(rightRows, "\n"))
|
||||||
|
|
||||||
|
copyIcon := "📋"
|
||||||
|
if useNerdfont {
|
||||||
|
copyIcon = ""
|
||||||
|
}
|
||||||
|
iconWidth := lipgloss.Width(copyIcon)
|
||||||
|
pathAvailable := max(innerWidth-6-iconWidth-3, 10) // "path: "=6, icon, spacing
|
||||||
pathLine := lipgloss.NewStyle().
|
pathLine := lipgloss.NewStyle().
|
||||||
Width(innerWidth).
|
Width(innerWidth).
|
||||||
Background(palette.PanelElevated).
|
Background(palette.PanelElevated).
|
||||||
Foreground(palette.Text).
|
Foreground(palette.Text).
|
||||||
Render(fmt.Sprintf("path: %s", truncateMiddle(meta.Path, max(innerWidth-8, 16))))
|
Render(fmt.Sprintf("path: %s %s", truncateMiddle(meta.Path, pathAvailable), copyIcon))
|
||||||
|
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Width(outerWidth).
|
Width(outerWidth).
|
||||||
|
|
@ -3434,7 +3469,7 @@ func (m Model) syncImageOverlay(leftWidth int, previewWidth int, bodyHeight int)
|
||||||
innerWidth := max(previewWidth-2, 1)
|
innerWidth := max(previewWidth-2, 1)
|
||||||
metaHeight := 0
|
metaHeight := 0
|
||||||
if m.cfg.Preview.ShowMetadata {
|
if m.cfg.Preview.ShowMetadata {
|
||||||
metaHeight = lipgloss.Height(renderMetadata(m.previewData.Metadata, m.palette, innerWidth))
|
metaHeight = lipgloss.Height(renderMetadata(m.previewData.Metadata, m.palette, innerWidth, m.nerdIcons))
|
||||||
}
|
}
|
||||||
titleHeight := 1
|
titleHeight := 1
|
||||||
topInset := 1
|
topInset := 1
|
||||||
|
|
@ -3491,3 +3526,35 @@ func (m *Model) mouseOverPreview(x, y int) bool {
|
||||||
|
|
||||||
return x >= 0 && x < previewWidth
|
return x >= 0 && x < previewWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) mouseOverPathLine(x, y int) bool {
|
||||||
|
if !m.infoMode || !m.cfg.Preview.ShowMetadata || m.width <= 0 || m.height <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
leftWidth, previewWidth, _ := m.layoutWidths()
|
||||||
|
bodyHeight := m.bodyHeight()
|
||||||
|
if y < 0 || y >= bodyHeight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview pane x-range
|
||||||
|
var startX int
|
||||||
|
if m.active == PaneLeft {
|
||||||
|
startX = leftWidth + m.cfg.UI.PaneGap
|
||||||
|
} else {
|
||||||
|
startX = 0
|
||||||
|
}
|
||||||
|
if x < startX || x >= startX+previewWidth {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The path line is within the metadata section (approximate Y range 1-7).
|
||||||
|
// Check that Y is in the metadata area and X is in the right half where the icon is.
|
||||||
|
if y < 1 || y > 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// The icon is at the far-right end of the preview pane content area
|
||||||
|
iconStartX := startX + previewWidth/2
|
||||||
|
return x >= iconStartX
|
||||||
|
}
|
||||||
|
|
|
||||||
30
vcom.toml
30
vcom.toml
|
|
@ -1,15 +1,15 @@
|
||||||
[startup]
|
[startup]
|
||||||
# left_path = "~/Downloads"
|
left_path = ''
|
||||||
# right_path = "~/Projects"
|
right_path = ''
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
app_title = "vcom"
|
app_title = 'vcom'
|
||||||
theme = "catppuccin-mocha"
|
theme = 'catppuccin-mocha'
|
||||||
icon_mode = "auto" # auto | nerd | ascii
|
icon_mode = 'auto'
|
||||||
show_title_bar = true
|
show_title_bar = true
|
||||||
show_footer = true
|
show_footer = true
|
||||||
border = "rounded"
|
border = 'rounded'
|
||||||
path_display = "smart"
|
path_display = 'smart'
|
||||||
pane_gap = 1
|
pane_gap = 1
|
||||||
center_width_percent = 30
|
center_width_percent = 30
|
||||||
|
|
||||||
|
|
@ -19,21 +19,16 @@ dirs_first = true
|
||||||
human_readable_size = true
|
human_readable_size = true
|
||||||
|
|
||||||
[browser.sort]
|
[browser.sort]
|
||||||
by = "name"
|
by = 'name'
|
||||||
reverse = false
|
reverse = false
|
||||||
|
|
||||||
# by = "modified"
|
|
||||||
# by = "size"
|
|
||||||
# reverse = true
|
|
||||||
|
|
||||||
[browser.columns]
|
[browser.columns]
|
||||||
name = true
|
name = true
|
||||||
size = true
|
size = true
|
||||||
modified = true
|
modified = true
|
||||||
|
created = false
|
||||||
# created = true
|
permissions = false
|
||||||
# permissions = true
|
extension = false
|
||||||
# extension = true
|
|
||||||
|
|
||||||
[preview]
|
[preview]
|
||||||
show_metadata = true
|
show_metadata = true
|
||||||
|
|
@ -41,9 +36,6 @@ wrap_text = false
|
||||||
max_preview_bytes = 65536
|
max_preview_bytes = 65536
|
||||||
directory_preview_limit = 80
|
directory_preview_limit = 80
|
||||||
|
|
||||||
# wrap_text = true
|
|
||||||
# max_preview_bytes = 131072
|
|
||||||
|
|
||||||
[behavior]
|
[behavior]
|
||||||
confirm_delete = true
|
confirm_delete = true
|
||||||
confirm_overwrite = true
|
confirm_overwrite = true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue