Polish panel layout and preview interaction
This commit is contained in:
parent
ef63a2479b
commit
941708970b
3 changed files with 120 additions and 39 deletions
|
|
@ -6,6 +6,7 @@ type KeyMap struct {
|
|||
View key.Binding
|
||||
Edit key.Binding
|
||||
Info key.Binding
|
||||
SelectText key.Binding
|
||||
ToggleHidden key.Binding
|
||||
CycleTheme key.Binding
|
||||
CycleSort key.Binding
|
||||
|
|
@ -32,6 +33,7 @@ func DefaultKeyMap() KeyMap {
|
|||
View: key.NewBinding(key.WithKeys("f3", "v"), key.WithHelp("F3/v", "view")),
|
||||
Edit: key.NewBinding(key.WithKeys("f4", "e"), key.WithHelp("F4/e", "edit")),
|
||||
Info: key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "info")),
|
||||
SelectText: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("C-t", "text select")),
|
||||
ToggleHidden: key.NewBinding(key.WithKeys("."), key.WithHelp(".", "hidden")),
|
||||
CycleTheme: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "theme")),
|
||||
CycleSort: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "sort")),
|
||||
|
|
@ -55,13 +57,13 @@ func DefaultKeyMap() KeyMap {
|
|||
}
|
||||
|
||||
func (k KeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{k.Info, k.Copy, k.Move, k.Mkdir, k.Delete, k.Quit}
|
||||
return []key.Binding{k.Info, k.SelectText, k.Copy, k.Move, k.Delete, k.Quit}
|
||||
}
|
||||
|
||||
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Open, k.Back, k.Switch, k.Info},
|
||||
{k.View, k.Edit, k.Copy, k.Move, k.Mkdir, k.Delete},
|
||||
{k.DirSize, k.Refresh, k.ToggleHidden, k.CycleSort, k.CycleTheme, k.Quit},
|
||||
{k.SelectText, k.DirSize, k.Refresh, k.ToggleHidden, k.CycleSort, k.CycleTheme, k.Quit},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ type Model struct {
|
|||
right BrowserPane
|
||||
active PaneID
|
||||
infoMode bool
|
||||
selectMode bool
|
||||
|
||||
helpModel help.Model
|
||||
previewModel viewport.Model
|
||||
|
|
@ -180,6 +181,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if selected, ok := m.activePane().Selected(); ok && selected.Path == msg.entryPath {
|
||||
m.applyPreview(msg.preview)
|
||||
}
|
||||
if m.selectMode && msg.preview.Kind != vfs.PreviewKindText {
|
||||
m.selectMode = false
|
||||
return m, enableMouseCmd()
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case dirSizeMsg:
|
||||
|
|
@ -236,6 +241,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m.handleEdit()
|
||||
case key.Matches(msg, m.keys.Info):
|
||||
return m.toggleInfo()
|
||||
case key.Matches(msg, m.keys.SelectText):
|
||||
return m.toggleSelectMode()
|
||||
case key.Matches(msg, m.keys.ToggleHidden):
|
||||
return m.toggleHidden()
|
||||
case key.Matches(msg, m.keys.CycleTheme):
|
||||
|
|
@ -329,12 +336,11 @@ func (m Model) View() string {
|
|||
)
|
||||
}
|
||||
|
||||
parts := make([]string, 0, 4)
|
||||
parts := make([]string, 0, 3)
|
||||
if m.cfg.UI.ShowTitleBar {
|
||||
parts = append(parts, renderTitleBar(m))
|
||||
}
|
||||
parts = append(parts, panels)
|
||||
parts = append(parts, renderStatus(m))
|
||||
if m.cfg.UI.ShowFooter {
|
||||
parts = append(parts, renderFooter(m))
|
||||
}
|
||||
|
|
@ -740,10 +746,29 @@ func (m *Model) toggleInfo() (tea.Model, tea.Cmd) {
|
|||
m.status = fmt.Sprintf("Info mode: %s selection", strings.ToUpper(string(m.active)))
|
||||
return m, m.loadPreviewCmd()
|
||||
}
|
||||
if m.selectMode {
|
||||
m.selectMode = false
|
||||
return m, enableMouseCmd()
|
||||
}
|
||||
m.status = "Info mode: off"
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) toggleSelectMode() (tea.Model, tea.Cmd) {
|
||||
if m.selectMode {
|
||||
m.selectMode = false
|
||||
m.status = "Text selection mode: off"
|
||||
return m, enableMouseCmd()
|
||||
}
|
||||
if !m.infoMode || m.previewData.Kind != vfs.PreviewKindText {
|
||||
m.status = "Text selection mode works only for text preview in info pane"
|
||||
return m, nil
|
||||
}
|
||||
m.selectMode = true
|
||||
m.status = "Text selection mode: on"
|
||||
return m, disableMouseCmd()
|
||||
}
|
||||
|
||||
func (m *Model) toggleHidden() (tea.Model, tea.Cmd) {
|
||||
m.cfg.Browser.ShowHidden = !m.cfg.Browser.ShowHidden
|
||||
return m.refreshAllPanes(fmt.Sprintf("Show hidden: %t", m.cfg.Browser.ShowHidden))
|
||||
|
|
@ -867,7 +892,7 @@ func (m *Model) layoutWidths() (int, int, int) {
|
|||
}
|
||||
|
||||
func (m *Model) bodyHeight() int {
|
||||
height := m.height - 1
|
||||
height := m.height
|
||||
if m.cfg.UI.ShowTitleBar {
|
||||
height--
|
||||
}
|
||||
|
|
@ -888,16 +913,19 @@ func (m *Model) resizePreview() {
|
|||
}
|
||||
|
||||
func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg config.Config, palette theme.Palette, width int, height int) string {
|
||||
innerWidth := max(width-2, 1)
|
||||
innerHeight := max(height-2, 1)
|
||||
|
||||
box := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Height(height).
|
||||
Width(innerWidth).
|
||||
Height(innerHeight).
|
||||
Background(palette.Panel).
|
||||
Foreground(palette.Text).
|
||||
BorderStyle(borderStyle(cfg.UI.Border)).
|
||||
BorderForeground(palette.BorderActive)
|
||||
|
||||
title := lipgloss.NewStyle().
|
||||
Width(width-2).
|
||||
Width(innerWidth).
|
||||
Padding(0, 1).
|
||||
Background(palette.Accent).
|
||||
Foreground(palette.Background).
|
||||
|
|
@ -906,9 +934,9 @@ func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg c
|
|||
|
||||
parts := []string{title}
|
||||
if cfg.Preview.ShowMetadata {
|
||||
parts = append(parts, renderMetadata(preview.Metadata, palette, width-2))
|
||||
parts = append(parts, renderMetadata(preview.Metadata, palette, innerWidth))
|
||||
}
|
||||
parts = append(parts, renderPreviewContent(viewportModel, palette, width-2))
|
||||
parts = append(parts, renderPreviewContent(viewportModel, palette, innerWidth))
|
||||
|
||||
return box.Render(lipgloss.JoinVertical(lipgloss.Left, parts...))
|
||||
}
|
||||
|
|
@ -1020,6 +1048,13 @@ func renderFooter(m Model) string {
|
|||
helpModel := m.helpModel
|
||||
helpModel.Width = max(m.width-28, 20)
|
||||
helpView := helpModel.View(m.keys)
|
||||
modeLabel := ""
|
||||
if m.selectMode {
|
||||
modeLabel = lipgloss.NewStyle().
|
||||
Foreground(m.palette.Accent).
|
||||
Bold(true).
|
||||
Render(" SELECT TEXT MODE")
|
||||
}
|
||||
legend := lipgloss.NewStyle().
|
||||
Foreground(m.palette.Muted).
|
||||
Render(" dir text config exec image bin")
|
||||
|
|
@ -1027,7 +1062,7 @@ func renderFooter(m Model) string {
|
|||
Width(m.width).
|
||||
Padding(0, 1).
|
||||
Background(m.palette.Panel).
|
||||
Render(lipgloss.JoinHorizontal(lipgloss.Top, helpView, " ", legend))
|
||||
Render(lipgloss.JoinHorizontal(lipgloss.Top, helpView, modeLabel, " ", legend))
|
||||
}
|
||||
|
||||
func renderModal(modal modalState, palette theme.Palette, width int) string {
|
||||
|
|
@ -1252,6 +1287,12 @@ func enableMouseCmd() tea.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func disableMouseCmd() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return tea.DisableMouse()
|
||||
}
|
||||
}
|
||||
|
||||
func resolveStartPath(raw string, fallback string) (string, error) {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
|
|
@ -1319,20 +1360,28 @@ func (m *Model) mouseTarget(x, y int) (PaneID, int, bool) {
|
|||
if m.infoMode && m.active == PaneRight {
|
||||
return "", 0, false
|
||||
}
|
||||
return PaneLeft, paneIndexFromMouse(y-top, bodyHeight, &m.left), true
|
||||
index, ok := paneIndexFromMouse(y-top, bodyHeight, &m.left)
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
return PaneLeft, index, true
|
||||
case x >= rightStart && x < rightStart+rightWidth:
|
||||
if m.infoMode && m.active == PaneLeft {
|
||||
return "", 0, false
|
||||
}
|
||||
return PaneRight, paneIndexFromMouse(y-top, bodyHeight, &m.right), true
|
||||
index, ok := paneIndexFromMouse(y-top, bodyHeight, &m.right)
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
return PaneRight, index, true
|
||||
default:
|
||||
return "", 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func paneIndexFromMouse(localY int, height int, pane *BrowserPane) int {
|
||||
func paneIndexFromMouse(localY int, height int, pane *BrowserPane) (int, bool) {
|
||||
if localY < 1 || localY >= height-1 {
|
||||
return pane.Cursor
|
||||
return 0, false
|
||||
}
|
||||
row := localY - 1
|
||||
index := pane.Offset + row
|
||||
|
|
@ -1340,12 +1389,9 @@ func paneIndexFromMouse(localY int, height int, pane *BrowserPane) int {
|
|||
index = 0
|
||||
}
|
||||
if index >= len(pane.Entries) {
|
||||
index = len(pane.Entries) - 1
|
||||
return 0, false
|
||||
}
|
||||
if index < 0 {
|
||||
return 0
|
||||
}
|
||||
return index
|
||||
return index, true
|
||||
}
|
||||
|
||||
func isEditableEntry(entry vfs.Entry) bool {
|
||||
|
|
|
|||
|
|
@ -114,10 +114,11 @@ func renderPane(
|
|||
}
|
||||
|
||||
innerWidth := max(width-2, 1)
|
||||
innerHeight := max(height-2, 1)
|
||||
|
||||
box := lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Height(height).
|
||||
Width(innerWidth).
|
||||
Height(innerHeight).
|
||||
Background(palette.PanelInactive).
|
||||
Foreground(palette.Text).
|
||||
BorderStyle(borderStyle(cfg.UI.Border)).
|
||||
|
|
@ -126,7 +127,7 @@ func renderPane(
|
|||
header := lipgloss.NewStyle().
|
||||
Render(renderPaneHeader(pane, cfg, palette, innerWidth, active, headerBg))
|
||||
|
||||
rowsHeight := max(height-4, 1)
|
||||
rowsHeight := max(innerHeight-2, 1)
|
||||
headerRow := renderColumnsHeader(cfg, innerWidth, palette)
|
||||
rows := renderPaneRows(pane, cfg, palette, innerWidth, rowsHeight, active, hoverIndex)
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, header, headerRow, rows)
|
||||
|
|
@ -293,8 +294,8 @@ func buildColumns(cfg config.Config, totalWidth int) []columnSpec {
|
|||
fixed = append(fixed, columnSpec{
|
||||
Key: "size",
|
||||
Title: "Size",
|
||||
Width: 10,
|
||||
MinWidth: 7,
|
||||
Width: 9,
|
||||
MinWidth: 6,
|
||||
AlignRight: true,
|
||||
Value: func(entry vfs.Entry, human bool) string {
|
||||
if entry.IsDir {
|
||||
|
|
@ -394,7 +395,7 @@ func columnSeparator(columnKey string, palette theme.Palette, background lipglos
|
|||
|
||||
func separatorWidth(columnKey string) int {
|
||||
if columnKey == "size" {
|
||||
return 3
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
|
@ -433,7 +434,7 @@ func truncateMiddle(value string, maxWidth int) string {
|
|||
return value
|
||||
}
|
||||
if maxWidth <= 3 {
|
||||
return value[:maxWidth]
|
||||
return trimToWidthRight(value, maxWidth)
|
||||
}
|
||||
left := maxWidth/2 - 1
|
||||
right := maxWidth - left - 1
|
||||
|
|
@ -443,7 +444,7 @@ func truncateMiddle(value string, maxWidth int) string {
|
|||
if right < 1 {
|
||||
right = 1
|
||||
}
|
||||
return value[:left] + "…" + value[len(value)-right:]
|
||||
return trimToWidthRight(value, left) + "…" + trimToWidthLeft(value, right)
|
||||
}
|
||||
|
||||
func truncateRight(value string, maxWidth int) string {
|
||||
|
|
@ -451,9 +452,9 @@ func truncateRight(value string, maxWidth int) string {
|
|||
return value
|
||||
}
|
||||
if maxWidth == 1 {
|
||||
return value[:1]
|
||||
return trimToWidthRight(value, 1)
|
||||
}
|
||||
return value[:maxWidth-1] + "…"
|
||||
return trimToWidthRight(value, maxWidth-1) + "…"
|
||||
}
|
||||
|
||||
func truncateForColumn(value string, maxWidth int, alignRight bool) string {
|
||||
|
|
@ -462,16 +463,48 @@ func truncateForColumn(value string, maxWidth int, alignRight bool) string {
|
|||
}
|
||||
if alignRight {
|
||||
if maxWidth <= 1 {
|
||||
return value[len(value)-1:]
|
||||
return trimToWidthLeft(value, 1)
|
||||
}
|
||||
if len(value) <= maxWidth {
|
||||
return value
|
||||
}
|
||||
return "…" + value[len(value)-maxWidth+1:]
|
||||
return "…" + trimToWidthLeft(value, maxWidth-1)
|
||||
}
|
||||
return truncateRight(value, maxWidth)
|
||||
}
|
||||
|
||||
func trimToWidthRight(value string, maxWidth int) string {
|
||||
if maxWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
width := 0
|
||||
var builder strings.Builder
|
||||
for _, r := range value {
|
||||
rw := lipgloss.Width(string(r))
|
||||
if width+rw > maxWidth {
|
||||
break
|
||||
}
|
||||
builder.WriteRune(r)
|
||||
width += rw
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func trimToWidthLeft(value string, maxWidth int) string {
|
||||
if maxWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
width := 0
|
||||
start := len(runes)
|
||||
for i := len(runes) - 1; i >= 0; i-- {
|
||||
rw := lipgloss.Width(string(runes[i]))
|
||||
if width+rw > maxWidth {
|
||||
break
|
||||
}
|
||||
width += rw
|
||||
start = i
|
||||
}
|
||||
return string(runes[start:])
|
||||
}
|
||||
|
||||
func entryIcon(entry vfs.Entry) string {
|
||||
switch entry.Category() {
|
||||
case "parent":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue