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
|
View key.Binding
|
||||||
Edit key.Binding
|
Edit key.Binding
|
||||||
Info key.Binding
|
Info key.Binding
|
||||||
|
SelectText key.Binding
|
||||||
ToggleHidden key.Binding
|
ToggleHidden key.Binding
|
||||||
CycleTheme key.Binding
|
CycleTheme key.Binding
|
||||||
CycleSort key.Binding
|
CycleSort key.Binding
|
||||||
|
|
@ -32,6 +33,7 @@ func DefaultKeyMap() KeyMap {
|
||||||
View: key.NewBinding(key.WithKeys("f3", "v"), key.WithHelp("F3/v", "view")),
|
View: key.NewBinding(key.WithKeys("f3", "v"), key.WithHelp("F3/v", "view")),
|
||||||
Edit: key.NewBinding(key.WithKeys("f4", "e"), key.WithHelp("F4/e", "edit")),
|
Edit: key.NewBinding(key.WithKeys("f4", "e"), key.WithHelp("F4/e", "edit")),
|
||||||
Info: key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "info")),
|
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")),
|
ToggleHidden: key.NewBinding(key.WithKeys("."), key.WithHelp(".", "hidden")),
|
||||||
CycleTheme: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "theme")),
|
CycleTheme: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "theme")),
|
||||||
CycleSort: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "sort")),
|
CycleSort: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "sort")),
|
||||||
|
|
@ -55,13 +57,13 @@ func DefaultKeyMap() KeyMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KeyMap) ShortHelp() []key.Binding {
|
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 {
|
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{
|
return [][]key.Binding{
|
||||||
{k.Up, k.Down, k.Open, k.Back, k.Switch, k.Info},
|
{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.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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,11 @@ type Model struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
left BrowserPane
|
left BrowserPane
|
||||||
right BrowserPane
|
right BrowserPane
|
||||||
active PaneID
|
active PaneID
|
||||||
infoMode bool
|
infoMode bool
|
||||||
|
selectMode bool
|
||||||
|
|
||||||
helpModel help.Model
|
helpModel help.Model
|
||||||
previewModel viewport.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 {
|
if selected, ok := m.activePane().Selected(); ok && selected.Path == msg.entryPath {
|
||||||
m.applyPreview(msg.preview)
|
m.applyPreview(msg.preview)
|
||||||
}
|
}
|
||||||
|
if m.selectMode && msg.preview.Kind != vfs.PreviewKindText {
|
||||||
|
m.selectMode = false
|
||||||
|
return m, enableMouseCmd()
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case dirSizeMsg:
|
case dirSizeMsg:
|
||||||
|
|
@ -236,6 +241,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m.handleEdit()
|
return m.handleEdit()
|
||||||
case key.Matches(msg, m.keys.Info):
|
case key.Matches(msg, m.keys.Info):
|
||||||
return m.toggleInfo()
|
return m.toggleInfo()
|
||||||
|
case key.Matches(msg, m.keys.SelectText):
|
||||||
|
return m.toggleSelectMode()
|
||||||
case key.Matches(msg, m.keys.ToggleHidden):
|
case key.Matches(msg, m.keys.ToggleHidden):
|
||||||
return m.toggleHidden()
|
return m.toggleHidden()
|
||||||
case key.Matches(msg, m.keys.CycleTheme):
|
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 {
|
if m.cfg.UI.ShowTitleBar {
|
||||||
parts = append(parts, renderTitleBar(m))
|
parts = append(parts, renderTitleBar(m))
|
||||||
}
|
}
|
||||||
parts = append(parts, panels)
|
parts = append(parts, panels)
|
||||||
parts = append(parts, renderStatus(m))
|
|
||||||
if m.cfg.UI.ShowFooter {
|
if m.cfg.UI.ShowFooter {
|
||||||
parts = append(parts, renderFooter(m))
|
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)))
|
m.status = fmt.Sprintf("Info mode: %s selection", strings.ToUpper(string(m.active)))
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
}
|
}
|
||||||
|
if m.selectMode {
|
||||||
|
m.selectMode = false
|
||||||
|
return m, enableMouseCmd()
|
||||||
|
}
|
||||||
m.status = "Info mode: off"
|
m.status = "Info mode: off"
|
||||||
return m, nil
|
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) {
|
func (m *Model) toggleHidden() (tea.Model, tea.Cmd) {
|
||||||
m.cfg.Browser.ShowHidden = !m.cfg.Browser.ShowHidden
|
m.cfg.Browser.ShowHidden = !m.cfg.Browser.ShowHidden
|
||||||
return m.refreshAllPanes(fmt.Sprintf("Show hidden: %t", 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 {
|
func (m *Model) bodyHeight() int {
|
||||||
height := m.height - 1
|
height := m.height
|
||||||
if m.cfg.UI.ShowTitleBar {
|
if m.cfg.UI.ShowTitleBar {
|
||||||
height--
|
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 {
|
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().
|
box := lipgloss.NewStyle().
|
||||||
Width(width).
|
Width(innerWidth).
|
||||||
Height(height).
|
Height(innerHeight).
|
||||||
Background(palette.Panel).
|
Background(palette.Panel).
|
||||||
Foreground(palette.Text).
|
Foreground(palette.Text).
|
||||||
BorderStyle(borderStyle(cfg.UI.Border)).
|
BorderStyle(borderStyle(cfg.UI.Border)).
|
||||||
BorderForeground(palette.BorderActive)
|
BorderForeground(palette.BorderActive)
|
||||||
|
|
||||||
title := lipgloss.NewStyle().
|
title := lipgloss.NewStyle().
|
||||||
Width(width-2).
|
Width(innerWidth).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Background(palette.Accent).
|
Background(palette.Accent).
|
||||||
Foreground(palette.Background).
|
Foreground(palette.Background).
|
||||||
|
|
@ -906,9 +934,9 @@ func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg c
|
||||||
|
|
||||||
parts := []string{title}
|
parts := []string{title}
|
||||||
if cfg.Preview.ShowMetadata {
|
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...))
|
return box.Render(lipgloss.JoinVertical(lipgloss.Left, parts...))
|
||||||
}
|
}
|
||||||
|
|
@ -1020,6 +1048,13 @@ func renderFooter(m Model) string {
|
||||||
helpModel := m.helpModel
|
helpModel := m.helpModel
|
||||||
helpModel.Width = max(m.width-28, 20)
|
helpModel.Width = max(m.width-28, 20)
|
||||||
helpView := helpModel.View(m.keys)
|
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().
|
legend := lipgloss.NewStyle().
|
||||||
Foreground(m.palette.Muted).
|
Foreground(m.palette.Muted).
|
||||||
Render(" dir text config exec image bin")
|
Render(" dir text config exec image bin")
|
||||||
|
|
@ -1027,7 +1062,7 @@ func renderFooter(m Model) string {
|
||||||
Width(m.width).
|
Width(m.width).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Background(m.palette.Panel).
|
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 {
|
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) {
|
func resolveStartPath(raw string, fallback string) (string, error) {
|
||||||
value := strings.TrimSpace(raw)
|
value := strings.TrimSpace(raw)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
|
|
@ -1319,20 +1360,28 @@ func (m *Model) mouseTarget(x, y int) (PaneID, int, bool) {
|
||||||
if m.infoMode && m.active == PaneRight {
|
if m.infoMode && m.active == PaneRight {
|
||||||
return "", 0, false
|
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:
|
case x >= rightStart && x < rightStart+rightWidth:
|
||||||
if m.infoMode && m.active == PaneLeft {
|
if m.infoMode && m.active == PaneLeft {
|
||||||
return "", 0, false
|
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:
|
default:
|
||||||
return "", 0, false
|
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 {
|
if localY < 1 || localY >= height-1 {
|
||||||
return pane.Cursor
|
return 0, false
|
||||||
}
|
}
|
||||||
row := localY - 1
|
row := localY - 1
|
||||||
index := pane.Offset + row
|
index := pane.Offset + row
|
||||||
|
|
@ -1340,12 +1389,9 @@ func paneIndexFromMouse(localY int, height int, pane *BrowserPane) int {
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
if index >= len(pane.Entries) {
|
if index >= len(pane.Entries) {
|
||||||
index = len(pane.Entries) - 1
|
return 0, false
|
||||||
}
|
}
|
||||||
if index < 0 {
|
return index, true
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEditableEntry(entry vfs.Entry) bool {
|
func isEditableEntry(entry vfs.Entry) bool {
|
||||||
|
|
|
||||||
|
|
@ -114,10 +114,11 @@ func renderPane(
|
||||||
}
|
}
|
||||||
|
|
||||||
innerWidth := max(width-2, 1)
|
innerWidth := max(width-2, 1)
|
||||||
|
innerHeight := max(height-2, 1)
|
||||||
|
|
||||||
box := lipgloss.NewStyle().
|
box := lipgloss.NewStyle().
|
||||||
Width(width).
|
Width(innerWidth).
|
||||||
Height(height).
|
Height(innerHeight).
|
||||||
Background(palette.PanelInactive).
|
Background(palette.PanelInactive).
|
||||||
Foreground(palette.Text).
|
Foreground(palette.Text).
|
||||||
BorderStyle(borderStyle(cfg.UI.Border)).
|
BorderStyle(borderStyle(cfg.UI.Border)).
|
||||||
|
|
@ -126,7 +127,7 @@ func renderPane(
|
||||||
header := lipgloss.NewStyle().
|
header := lipgloss.NewStyle().
|
||||||
Render(renderPaneHeader(pane, cfg, palette, innerWidth, active, headerBg))
|
Render(renderPaneHeader(pane, cfg, palette, innerWidth, active, headerBg))
|
||||||
|
|
||||||
rowsHeight := max(height-4, 1)
|
rowsHeight := max(innerHeight-2, 1)
|
||||||
headerRow := renderColumnsHeader(cfg, innerWidth, palette)
|
headerRow := renderColumnsHeader(cfg, innerWidth, palette)
|
||||||
rows := renderPaneRows(pane, cfg, palette, innerWidth, rowsHeight, active, hoverIndex)
|
rows := renderPaneRows(pane, cfg, palette, innerWidth, rowsHeight, active, hoverIndex)
|
||||||
content := lipgloss.JoinVertical(lipgloss.Left, header, headerRow, rows)
|
content := lipgloss.JoinVertical(lipgloss.Left, header, headerRow, rows)
|
||||||
|
|
@ -293,8 +294,8 @@ func buildColumns(cfg config.Config, totalWidth int) []columnSpec {
|
||||||
fixed = append(fixed, columnSpec{
|
fixed = append(fixed, columnSpec{
|
||||||
Key: "size",
|
Key: "size",
|
||||||
Title: "Size",
|
Title: "Size",
|
||||||
Width: 10,
|
Width: 9,
|
||||||
MinWidth: 7,
|
MinWidth: 6,
|
||||||
AlignRight: true,
|
AlignRight: true,
|
||||||
Value: func(entry vfs.Entry, human bool) string {
|
Value: func(entry vfs.Entry, human bool) string {
|
||||||
if entry.IsDir {
|
if entry.IsDir {
|
||||||
|
|
@ -394,7 +395,7 @@ func columnSeparator(columnKey string, palette theme.Palette, background lipglos
|
||||||
|
|
||||||
func separatorWidth(columnKey string) int {
|
func separatorWidth(columnKey string) int {
|
||||||
if columnKey == "size" {
|
if columnKey == "size" {
|
||||||
return 3
|
return 2
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +434,7 @@ func truncateMiddle(value string, maxWidth int) string {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
if maxWidth <= 3 {
|
if maxWidth <= 3 {
|
||||||
return value[:maxWidth]
|
return trimToWidthRight(value, maxWidth)
|
||||||
}
|
}
|
||||||
left := maxWidth/2 - 1
|
left := maxWidth/2 - 1
|
||||||
right := maxWidth - left - 1
|
right := maxWidth - left - 1
|
||||||
|
|
@ -443,7 +444,7 @@ func truncateMiddle(value string, maxWidth int) string {
|
||||||
if right < 1 {
|
if right < 1 {
|
||||||
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 {
|
func truncateRight(value string, maxWidth int) string {
|
||||||
|
|
@ -451,9 +452,9 @@ func truncateRight(value string, maxWidth int) string {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
if maxWidth == 1 {
|
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 {
|
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 alignRight {
|
||||||
if maxWidth <= 1 {
|
if maxWidth <= 1 {
|
||||||
return value[len(value)-1:]
|
return trimToWidthLeft(value, 1)
|
||||||
}
|
}
|
||||||
if len(value) <= maxWidth {
|
return "…" + trimToWidthLeft(value, maxWidth-1)
|
||||||
return value
|
|
||||||
}
|
|
||||||
return "…" + value[len(value)-maxWidth+1:]
|
|
||||||
}
|
}
|
||||||
return truncateRight(value, maxWidth)
|
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 {
|
func entryIcon(entry vfs.Entry) string {
|
||||||
switch entry.Category() {
|
switch entry.Category() {
|
||||||
case "parent":
|
case "parent":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue