Improve text preview keyboard navigation
This commit is contained in:
parent
cb5c98834e
commit
641ad17676
2 changed files with 651 additions and 24 deletions
|
|
@ -4,6 +4,8 @@ import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
type KeyMap struct {
|
type KeyMap struct {
|
||||||
Help key.Binding
|
Help key.Binding
|
||||||
|
Visual key.Binding
|
||||||
|
Caret key.Binding
|
||||||
View key.Binding
|
View key.Binding
|
||||||
Edit key.Binding
|
Edit key.Binding
|
||||||
Rename key.Binding
|
Rename key.Binding
|
||||||
|
|
@ -38,9 +40,11 @@ func DefaultKeyMap() KeyMap {
|
||||||
return KeyMap{
|
return KeyMap{
|
||||||
Help: key.NewBinding(key.WithKeys("f1", "?"), key.WithHelp("F1/?", "help")),
|
Help: key.NewBinding(key.WithKeys("f1", "?"), key.WithHelp("F1/?", "help")),
|
||||||
Rename: key.NewBinding(key.WithKeys("f2", "r"), key.WithHelp("F2/r", "rename")),
|
Rename: key.NewBinding(key.WithKeys("f2", "r"), key.WithHelp("F2/r", "rename")),
|
||||||
View: key.NewBinding(key.WithKeys("f3", "v"), key.WithHelp("F3/v", "view")),
|
View: key.NewBinding(key.WithKeys("f3"), key.WithHelp("F3", "view")),
|
||||||
|
Visual: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "visual")),
|
||||||
|
Caret: key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "caret")),
|
||||||
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("f9", "i"), key.WithHelp("F9/i", "info")),
|
Info: key.NewBinding(key.WithKeys("f9", "o"), key.WithHelp("F9/o", "info")),
|
||||||
SelectText: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("C-t", "text select")),
|
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")),
|
||||||
|
|
@ -49,7 +53,7 @@ func DefaultKeyMap() KeyMap {
|
||||||
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
||||||
SelectUp: key.NewBinding(key.WithKeys("shift+up", "K"), key.WithHelp("S-↑/K", "select up")),
|
SelectUp: key.NewBinding(key.WithKeys("shift+up", "K"), key.WithHelp("S-↑/K", "select up")),
|
||||||
SelectDown: key.NewBinding(key.WithKeys("shift+down", "J"), key.WithHelp("S-↓/J", "select down")),
|
SelectDown: key.NewBinding(key.WithKeys("shift+down", "J"), key.WithHelp("S-↓/J", "select down")),
|
||||||
PageUp: key.NewBinding(key.WithKeys("pgup", "b"), key.WithHelp("PgUp/b", "page up")),
|
PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("PgUp", "page up")),
|
||||||
PageDown: key.NewBinding(key.WithKeys("pgdown", "f"), key.WithHelp("PgDn/f", "page down")),
|
PageDown: key.NewBinding(key.WithKeys("pgdown", "f"), key.WithHelp("PgDn/f", "page down")),
|
||||||
Open: key.NewBinding(key.WithKeys("enter", "right"), key.WithHelp("Enter", "open")),
|
Open: key.NewBinding(key.WithKeys("enter", "right"), key.WithHelp("Enter", "open")),
|
||||||
Back: key.NewBinding(key.WithKeys("backspace", "left"), key.WithHelp("←", "parent")),
|
Back: key.NewBinding(key.WithKeys("backspace", "left"), key.WithHelp("←", "parent")),
|
||||||
|
|
@ -69,13 +73,13 @@ func DefaultKeyMap() KeyMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KeyMap) ShortHelp() []key.Binding {
|
func (k KeyMap) ShortHelp() []key.Binding {
|
||||||
return []key.Binding{k.Help, k.Rename, k.View, k.Copy, k.Move, k.Delete, k.Info, k.Quit}
|
return []key.Binding{k.Help, k.Rename, k.View, k.Visual, k.Copy, k.Delete, k.Info, k.Quit}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KeyMap) FullHelp() [][]key.Binding {
|
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{
|
return [][]key.Binding{
|
||||||
{k.Help, k.Up, k.Down, k.SelectUp, k.SelectDown, k.Open, k.Back},
|
{k.Help, k.Up, k.Down, k.SelectUp, k.SelectDown, k.Open, k.Back},
|
||||||
{k.Rename, k.View, k.Edit, k.Copy, k.Move, k.Mkdir, k.Delete},
|
{k.Rename, k.View, k.Caret, k.Visual, k.Edit, k.Copy, k.Move, k.Delete},
|
||||||
{k.SelectText, 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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/atotto/clipboard"
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
|
@ -152,13 +153,19 @@ 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
|
selectMode bool
|
||||||
viewMode bool
|
cursorMode bool
|
||||||
viewPrevInfo bool
|
cursorLine int
|
||||||
|
cursorCol int
|
||||||
|
visualMode bool
|
||||||
|
visualAnchor int
|
||||||
|
visualAnchorCol int
|
||||||
|
viewMode bool
|
||||||
|
viewPrevInfo bool
|
||||||
|
|
||||||
previewModel viewport.Model
|
previewModel viewport.Model
|
||||||
previewData vfs.Preview
|
previewData vfs.Preview
|
||||||
|
|
@ -169,6 +176,7 @@ type Model struct {
|
||||||
|
|
||||||
lastClick mouseClickState
|
lastClick mouseClickState
|
||||||
hover hoverState
|
hover hoverState
|
||||||
|
pendingY bool
|
||||||
|
|
||||||
copyJob *copyJobState
|
copyJob *copyJobState
|
||||||
nextCopyJob int
|
nextCopyJob int
|
||||||
|
|
@ -243,6 +251,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.selectMode = false
|
m.selectMode = false
|
||||||
return m, enableMouseCmd()
|
return m, enableMouseCmd()
|
||||||
}
|
}
|
||||||
|
if (m.cursorMode || m.visualMode) && msg.preview.Kind != vfs.PreviewKindText {
|
||||||
|
m.cursorMode = false
|
||||||
|
m.visualMode = false
|
||||||
|
m.status = "Text cursor mode: off"
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case dirSizeMsg:
|
case dirSizeMsg:
|
||||||
|
|
@ -426,24 +439,136 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if msg.String() != "y" {
|
||||||
|
m.pendingY = false
|
||||||
|
}
|
||||||
if m.modal.kind != modalNone {
|
if m.modal.kind != modalNone {
|
||||||
return m.handleModalKey(msg)
|
return m.handleModalKey(msg)
|
||||||
}
|
}
|
||||||
if m.viewMode {
|
if m.viewMode {
|
||||||
|
if m.previewData.Kind == vfs.PreviewKindText && m.infoMode && !m.selectMode {
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Visual):
|
||||||
|
return m.toggleVisualMode()
|
||||||
|
case msg.String() == "y":
|
||||||
|
return m.yankVisualSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, m.keys.View), key.Matches(msg, m.keys.Cancel), msg.String() == "q":
|
case key.Matches(msg, m.keys.Cancel):
|
||||||
|
if m.visualMode {
|
||||||
|
return m.exitVisualMode("Visual mode: off")
|
||||||
|
}
|
||||||
|
return m.exitViewMode()
|
||||||
|
case key.Matches(msg, m.keys.View), msg.String() == "q":
|
||||||
return m.exitViewMode()
|
return m.exitViewMode()
|
||||||
case key.Matches(msg, m.keys.Up):
|
case key.Matches(msg, m.keys.Up):
|
||||||
m.previewModel.LineUp(1)
|
if m.visualMode {
|
||||||
|
m.moveTextCursorLine(-1)
|
||||||
|
} else {
|
||||||
|
m.previewModel.LineUp(1)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "left":
|
||||||
|
if m.visualMode {
|
||||||
|
m.moveTextCursorCol(-1)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "h":
|
||||||
|
if m.visualMode {
|
||||||
|
m.moveTextCursorCol(-1)
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.Down):
|
case key.Matches(msg, m.keys.Down):
|
||||||
m.previewModel.LineDown(1)
|
if m.visualMode {
|
||||||
|
m.moveTextCursorLine(1)
|
||||||
|
} else {
|
||||||
|
m.previewModel.LineDown(1)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "right":
|
||||||
|
if m.visualMode {
|
||||||
|
m.moveTextCursorCol(1)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "l":
|
||||||
|
if m.visualMode {
|
||||||
|
m.moveTextCursorCol(1)
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.PageUp):
|
case key.Matches(msg, m.keys.PageUp):
|
||||||
m.previewModel.LineUp(max(m.previewModel.Height-2, 1))
|
if m.visualMode {
|
||||||
|
m.moveTextCursorLine(-max(m.previewModel.Height-2, 1))
|
||||||
|
} else {
|
||||||
|
m.previewModel.LineUp(max(m.previewModel.Height-2, 1))
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.PageDown):
|
case key.Matches(msg, m.keys.PageDown):
|
||||||
m.previewModel.LineDown(max(m.previewModel.Height-2, 1))
|
if m.visualMode {
|
||||||
|
m.moveTextCursorLine(max(m.previewModel.Height-2, 1))
|
||||||
|
} else {
|
||||||
|
m.previewModel.LineDown(max(m.previewModel.Height-2, 1))
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
default:
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.cursorMode || m.visualMode) && m.previewData.Kind == vfs.PreviewKindText {
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Caret):
|
||||||
|
return m.toggleCaretMode()
|
||||||
|
case key.Matches(msg, m.keys.Visual):
|
||||||
|
if m.visualMode {
|
||||||
|
return m.exitVisualMode("Visual mode: off")
|
||||||
|
}
|
||||||
|
return m.toggleVisualMode()
|
||||||
|
case key.Matches(msg, m.keys.Cancel), msg.String() == "q":
|
||||||
|
if m.visualMode {
|
||||||
|
return m.exitVisualMode("Visual mode: off")
|
||||||
|
}
|
||||||
|
return m.exitCaretMode("Caret mode: off")
|
||||||
|
case msg.String() == "y":
|
||||||
|
if !m.visualMode {
|
||||||
|
if m.pendingY {
|
||||||
|
m.pendingY = false
|
||||||
|
return m.yankCursorLine()
|
||||||
|
}
|
||||||
|
m.pendingY = true
|
||||||
|
m.status = "Press y again to copy current line"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m.yankVisualSelection()
|
||||||
|
case key.Matches(msg, m.keys.Up):
|
||||||
|
m.moveTextCursorLine(-1)
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "left":
|
||||||
|
m.moveTextCursorCol(-1)
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "h":
|
||||||
|
m.moveTextCursorCol(-1)
|
||||||
|
return m, nil
|
||||||
|
case key.Matches(msg, m.keys.Down):
|
||||||
|
m.moveTextCursorLine(1)
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "right":
|
||||||
|
m.moveTextCursorCol(1)
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "l":
|
||||||
|
m.moveTextCursorCol(1)
|
||||||
|
return m, nil
|
||||||
|
case key.Matches(msg, m.keys.PageUp):
|
||||||
|
m.moveTextCursorLine(-max(m.previewModel.Height-2, 1))
|
||||||
|
return m, nil
|
||||||
|
case key.Matches(msg, m.keys.PageDown):
|
||||||
|
m.moveTextCursorLine(max(m.previewModel.Height-2, 1))
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "w":
|
||||||
|
m.moveTextCursorWordForward()
|
||||||
|
return m, nil
|
||||||
|
case msg.String() == "b":
|
||||||
|
m.moveTextCursorWordBackward()
|
||||||
return m, nil
|
return m, nil
|
||||||
default:
|
default:
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -461,10 +586,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keys.Rename):
|
case key.Matches(msg, m.keys.Rename):
|
||||||
m.openRenameModal()
|
m.openRenameModal()
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.Cancel):
|
case key.Matches(msg, m.keys.Cancel), msg.String() == "q":
|
||||||
if m.infoMode {
|
if m.infoMode {
|
||||||
m.infoMode = false
|
m.infoMode = false
|
||||||
m.selectMode = false
|
m.selectMode = false
|
||||||
|
m.cursorMode = false
|
||||||
|
m.visualMode = false
|
||||||
m.status = "Info pane closed"
|
m.status = "Info pane closed"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
@ -476,6 +603,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.View):
|
case key.Matches(msg, m.keys.View):
|
||||||
return m.handleView()
|
return m.handleView()
|
||||||
|
case key.Matches(msg, m.keys.Caret):
|
||||||
|
return m.toggleCaretMode()
|
||||||
|
case key.Matches(msg, m.keys.Visual):
|
||||||
|
return m.toggleVisualMode()
|
||||||
case key.Matches(msg, m.keys.Edit):
|
case key.Matches(msg, m.keys.Edit):
|
||||||
return m.handleEdit()
|
return m.handleEdit()
|
||||||
case key.Matches(msg, m.keys.Info):
|
case key.Matches(msg, m.keys.Info):
|
||||||
|
|
@ -568,6 +699,10 @@ func (m Model) View() string {
|
||||||
Height(bodyHeight).
|
Height(bodyHeight).
|
||||||
Background(m.palette.Background).
|
Background(m.palette.Background).
|
||||||
Render("")
|
Render("")
|
||||||
|
} else if m.viewMode && m.previewData.Kind == vfs.PreviewKindText {
|
||||||
|
panels = renderSelectionPane(m.previewData, &m.previewModel, m.palette, m.width, bodyHeight)
|
||||||
|
} else if m.viewMode {
|
||||||
|
panels = renderPreviewPane(m.previewData, &m.previewModel, m.cfg, m.palette, m.width, bodyHeight)
|
||||||
} 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 {
|
||||||
|
|
@ -1003,12 +1138,16 @@ func (m *Model) handleView() (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
m.viewPrevInfo = m.infoMode
|
m.viewPrevInfo = m.infoMode
|
||||||
m.infoMode = true
|
m.infoMode = true
|
||||||
m.selectMode = true
|
m.selectMode = m.previewData.Kind == vfs.PreviewKindText
|
||||||
|
m.visualMode = false
|
||||||
m.viewMode = true
|
m.viewMode = true
|
||||||
m.resizePreview()
|
m.resizePreview()
|
||||||
m.syncPreviewContent()
|
m.syncPreviewContent()
|
||||||
m.status = "View mode: F3/Esc/q to close"
|
m.status = "View mode: F3/Esc/q to close"
|
||||||
return m, tea.Batch(m.loadPreviewCmd(), disableMouseCmd())
|
if m.selectMode {
|
||||||
|
return m, tea.Batch(m.loadPreviewCmd(), disableMouseCmd())
|
||||||
|
}
|
||||||
|
return m, tea.Batch(m.loadPreviewCmd(), enableMouseCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) exitViewMode() (tea.Model, tea.Cmd) {
|
func (m *Model) exitViewMode() (tea.Model, tea.Cmd) {
|
||||||
|
|
@ -1017,6 +1156,7 @@ func (m *Model) exitViewMode() (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
m.viewMode = false
|
m.viewMode = false
|
||||||
m.selectMode = false
|
m.selectMode = false
|
||||||
|
m.visualMode = false
|
||||||
m.infoMode = m.viewPrevInfo
|
m.infoMode = m.viewPrevInfo
|
||||||
m.resizePreview()
|
m.resizePreview()
|
||||||
m.syncPreviewContent()
|
m.syncPreviewContent()
|
||||||
|
|
@ -1127,6 +1267,8 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
case msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonRight:
|
case msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonRight:
|
||||||
if m.infoMode && m.mouseOverPreview(msg.X, msg.Y) {
|
if m.infoMode && m.mouseOverPreview(msg.X, msg.Y) {
|
||||||
m.infoMode = false
|
m.infoMode = false
|
||||||
|
m.cursorMode = false
|
||||||
|
m.visualMode = false
|
||||||
m.resizePreview()
|
m.resizePreview()
|
||||||
m.syncPreviewContent()
|
m.syncPreviewContent()
|
||||||
m.status = "Info mode: off"
|
m.status = "Info mode: off"
|
||||||
|
|
@ -1140,6 +1282,8 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
if m.infoMode && paneID == m.active && index == m.activePane().Cursor {
|
if m.infoMode && paneID == m.active && index == m.activePane().Cursor {
|
||||||
m.infoMode = false
|
m.infoMode = false
|
||||||
|
m.cursorMode = false
|
||||||
|
m.visualMode = false
|
||||||
m.hover = hoverState{pane: paneID, index: index, ok: true}
|
m.hover = hoverState{pane: paneID, index: index, ok: true}
|
||||||
m.status = "Info mode: off"
|
m.status = "Info mode: off"
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -1174,8 +1318,17 @@ 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()
|
||||||
}
|
}
|
||||||
|
wasSelect := m.selectMode
|
||||||
if m.selectMode {
|
if m.selectMode {
|
||||||
m.selectMode = false
|
m.selectMode = false
|
||||||
|
}
|
||||||
|
if m.cursorMode {
|
||||||
|
m.cursorMode = false
|
||||||
|
}
|
||||||
|
if m.visualMode {
|
||||||
|
m.visualMode = false
|
||||||
|
}
|
||||||
|
if wasSelect {
|
||||||
return m, enableMouseCmd()
|
return m, enableMouseCmd()
|
||||||
}
|
}
|
||||||
m.status = "Info mode: off"
|
m.status = "Info mode: off"
|
||||||
|
|
@ -1184,7 +1337,7 @@ func (m *Model) toggleInfo() (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m *Model) toggleSelectMode() (tea.Model, tea.Cmd) {
|
func (m *Model) toggleSelectMode() (tea.Model, tea.Cmd) {
|
||||||
if m.viewMode {
|
if m.viewMode {
|
||||||
m.status = "Close view mode first (F3/Esc/q)"
|
m.status = "Use v/y in F3 view for keyboard selection"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if m.selectMode {
|
if m.selectMode {
|
||||||
|
|
@ -1201,6 +1354,100 @@ func (m *Model) toggleSelectMode() (tea.Model, tea.Cmd) {
|
||||||
return m, disableMouseCmd()
|
return m, disableMouseCmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) toggleCaretMode() (tea.Model, tea.Cmd) {
|
||||||
|
if m.viewMode {
|
||||||
|
m.status = "F3 uses plain text mouse selection"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.selectMode {
|
||||||
|
m.status = "Disable Ctrl+T mouse selection first"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if !m.infoMode || m.previewData.Kind != vfs.PreviewKindText {
|
||||||
|
m.status = "Caret mode works only for text preview in info pane"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.cursorMode {
|
||||||
|
return m.exitCaretMode("Caret mode: off")
|
||||||
|
}
|
||||||
|
|
||||||
|
lineCount := len(m.previewPlainLines())
|
||||||
|
if lineCount == 0 {
|
||||||
|
m.status = "Nothing to navigate"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.cursorMode = true
|
||||||
|
m.cursorLine = clamp(m.previewModel.YOffset, 0, lineCount-1)
|
||||||
|
m.cursorCol = clamp(m.cursorCol, 0, m.lineRuneCount(m.cursorLine))
|
||||||
|
m.visualMode = false
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
m.status = "Caret mode: h/j/k/l move, v select, Esc exit"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) exitCaretMode(status string) (tea.Model, tea.Cmd) {
|
||||||
|
if !m.cursorMode {
|
||||||
|
m.status = status
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.cursorMode = false
|
||||||
|
m.visualMode = false
|
||||||
|
m.syncPreviewContent()
|
||||||
|
m.status = status
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) toggleVisualMode() (tea.Model, tea.Cmd) {
|
||||||
|
if m.viewMode {
|
||||||
|
m.status = "F3 uses plain text mouse selection; visual mode is for info pane"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.selectMode {
|
||||||
|
m.status = "Disable Ctrl+T mouse selection first"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if !m.infoMode || m.previewData.Kind != vfs.PreviewKindText {
|
||||||
|
m.status = "Visual mode works only for text preview"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.visualMode {
|
||||||
|
return m.exitVisualMode("Visual mode: off")
|
||||||
|
}
|
||||||
|
|
||||||
|
lineCount := len(m.previewPlainLines())
|
||||||
|
if lineCount == 0 {
|
||||||
|
m.status = "Nothing to select"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
start := clamp(m.previewModel.YOffset, 0, lineCount-1)
|
||||||
|
if m.cursorMode {
|
||||||
|
start = clamp(m.cursorLine, 0, lineCount-1)
|
||||||
|
} else {
|
||||||
|
m.cursorMode = true
|
||||||
|
m.cursorLine = start
|
||||||
|
m.cursorCol = 0
|
||||||
|
}
|
||||||
|
m.visualMode = true
|
||||||
|
m.visualAnchor = start
|
||||||
|
m.visualAnchorCol = m.cursorCol
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
m.status = "Visual mode: h/j/k/l move, y copy, Esc exit"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) exitVisualMode(status string) (tea.Model, tea.Cmd) {
|
||||||
|
if !m.visualMode {
|
||||||
|
m.status = status
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.visualMode = false
|
||||||
|
m.syncPreviewContent()
|
||||||
|
m.status = status
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
@ -1225,6 +1472,68 @@ func (m *Model) cycleTheme() (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyTextToClipboard(text string) error {
|
||||||
|
if err := clipboard.WriteAll(text); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := fmt.Fprint(os.Stderr, ansi.SetSystemClipboard(text))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) yankVisualSelection() (tea.Model, tea.Cmd) {
|
||||||
|
if !m.visualMode || m.previewData.Kind != vfs.PreviewKindText {
|
||||||
|
m.status = "Visual mode is not active"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := m.previewPlainLines()
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return m.exitVisualMode("Nothing to copy")
|
||||||
|
}
|
||||||
|
startLine, startCol, endLine, endCol := m.visualSelectionBounds()
|
||||||
|
startLine = clamp(startLine, 0, len(lines)-1)
|
||||||
|
endLine = clamp(endLine, 0, len(lines)-1)
|
||||||
|
|
||||||
|
parts := make([]string, 0, endLine-startLine+1)
|
||||||
|
for line := startLine; line <= endLine; line++ {
|
||||||
|
raw := lines[line]
|
||||||
|
lineStart := 0
|
||||||
|
lineEnd := len([]rune(raw))
|
||||||
|
if line == startLine {
|
||||||
|
lineStart = clamp(startCol, 0, lineEnd)
|
||||||
|
}
|
||||||
|
if line == endLine {
|
||||||
|
lineEnd = clamp(endCol, lineStart, len([]rune(raw)))
|
||||||
|
}
|
||||||
|
parts = append(parts, sliceRunes(raw, lineStart, lineEnd))
|
||||||
|
}
|
||||||
|
text := strings.Join(parts, "\n")
|
||||||
|
if err := copyTextToClipboard(text); err != nil {
|
||||||
|
m.status = fmt.Sprintf("Copy failed: %v", err)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m.exitVisualMode("Copied selection")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) yankCursorLine() (tea.Model, tea.Cmd) {
|
||||||
|
if !m.cursorMode || m.previewData.Kind != vfs.PreviewKindText {
|
||||||
|
m.status = "Caret mode is not active"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
lines := m.previewPlainLines()
|
||||||
|
if len(lines) == 0 {
|
||||||
|
m.status = "Nothing to copy"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
line := clamp(m.cursorLine, 0, len(lines)-1)
|
||||||
|
if err := copyTextToClipboard(lines[line]); err != nil {
|
||||||
|
m.status = fmt.Sprintf("Copy failed: %v", err)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.status = "Copied current line"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) cycleSort() (tea.Model, tea.Cmd) {
|
func (m *Model) cycleSort() (tea.Model, tea.Cmd) {
|
||||||
order := []string{"name", "modified", "size", "created", "extension"}
|
order := []string{"name", "modified", "size", "created", "extension"}
|
||||||
current := strings.ToLower(strings.TrimSpace(m.cfg.Browser.Sort.By))
|
current := strings.ToLower(strings.TrimSpace(m.cfg.Browser.Sort.By))
|
||||||
|
|
@ -1313,10 +1622,13 @@ func (m *Model) openHelpModal() {
|
||||||
" Ctrl+r refresh both panes",
|
" Ctrl+r refresh both panes",
|
||||||
"",
|
"",
|
||||||
"View and Panels",
|
"View and Panels",
|
||||||
" F9 / i toggle preview/info pane",
|
" F9 / o toggle preview/info pane",
|
||||||
" F3 / v text view mode or fullscreen image viewer",
|
" F3 plain text view or fullscreen image viewer",
|
||||||
|
" i show text caret in preview pane",
|
||||||
|
" v visual selection in text preview pane",
|
||||||
" F3 / Esc / q close view mode",
|
" F3 / Esc / q close view mode",
|
||||||
" Ctrl+t toggle text selection mode in text preview",
|
" y copy visual selection to clipboard",
|
||||||
|
" Ctrl+t mouse selection mode in text preview pane",
|
||||||
" Space calculate selected directory size",
|
" Space calculate selected directory size",
|
||||||
" s cycle sort mode",
|
" s cycle sort mode",
|
||||||
" . toggle hidden files",
|
" . toggle hidden files",
|
||||||
|
|
@ -1365,12 +1677,264 @@ func (m *Model) applyPreview(preview vfs.Preview) {
|
||||||
|
|
||||||
func (m *Model) syncPreviewContent() {
|
func (m *Model) syncPreviewContent() {
|
||||||
content := m.previewData.Body
|
content := m.previewData.Body
|
||||||
|
if (m.cursorMode || m.visualMode) && m.previewData.Kind == vfs.PreviewKindText {
|
||||||
|
content = m.renderTextCursorContent()
|
||||||
|
}
|
||||||
if m.cfg.Preview.WrapText && m.previewModel.Width > 0 {
|
if m.cfg.Preview.WrapText && m.previewModel.Width > 0 {
|
||||||
content = lipgloss.NewStyle().Width(m.previewModel.Width).Render(content)
|
content = lipgloss.NewStyle().Width(m.previewModel.Width).Render(content)
|
||||||
}
|
}
|
||||||
m.previewModel.SetContent(content)
|
m.previewModel.SetContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) previewPlainLines() []string {
|
||||||
|
content := m.previewData.PlainBody
|
||||||
|
if content == "" {
|
||||||
|
content = m.previewData.Body
|
||||||
|
}
|
||||||
|
content = strings.ReplaceAll(content, "\r\n", "\n")
|
||||||
|
return strings.Split(content, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) previewRenderedLines() []string {
|
||||||
|
content := m.previewData.Body
|
||||||
|
if content == "" {
|
||||||
|
content = m.previewData.PlainBody
|
||||||
|
}
|
||||||
|
content = strings.ReplaceAll(content, "\r\n", "\n")
|
||||||
|
return strings.Split(content, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) lineRuneCount(line int) int {
|
||||||
|
lines := m.previewPlainLines()
|
||||||
|
if line < 0 || line >= len(lines) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len([]rune(lines[line]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceRunes(text string, start int, end int) string {
|
||||||
|
runes := []rune(text)
|
||||||
|
start = clamp(start, 0, len(runes))
|
||||||
|
end = clamp(end, start, len(runes))
|
||||||
|
return string(runes[start:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWordRune(r rune) bool {
|
||||||
|
return r == '_' || r == '-' || r == '.' ||
|
||||||
|
(r >= '0' && r <= '9') ||
|
||||||
|
(r >= 'a' && r <= 'z') ||
|
||||||
|
(r >= 'A' && r <= 'Z') ||
|
||||||
|
(r >= 'А' && r <= 'я') ||
|
||||||
|
(r >= 'Ё' && r <= 'ё')
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSelection(startLine, startCol, endLine, endCol int) (int, int, int, int) {
|
||||||
|
if startLine == endLine && startCol == endCol {
|
||||||
|
return startLine, startCol, endLine, endCol + 1
|
||||||
|
}
|
||||||
|
if startLine < endLine || (startLine == endLine && startCol <= endCol) {
|
||||||
|
return startLine, startCol, endLine, endCol + 1
|
||||||
|
}
|
||||||
|
return endLine, endCol, startLine, startCol + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) visualSelectionBounds() (int, int, int, int) {
|
||||||
|
return normalizeSelection(m.visualAnchor, m.visualAnchorCol, m.cursorLine, m.cursorCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderTextCursorContent() string {
|
||||||
|
lines := append([]string(nil), m.previewRenderedLines()...)
|
||||||
|
plainLines := m.previewPlainLines()
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
startLine, startCol, endLine, endCol := m.visualSelectionBounds()
|
||||||
|
hasSelection := false
|
||||||
|
if m.visualMode {
|
||||||
|
startLine = clamp(startLine, 0, len(lines)-1)
|
||||||
|
endLine = clamp(endLine, 0, len(lines)-1)
|
||||||
|
hasSelection = startLine != endLine || startCol != endCol
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := lipgloss.NewStyle().
|
||||||
|
Background(m.palette.Marked).
|
||||||
|
Foreground(m.palette.Text)
|
||||||
|
cursor := lipgloss.NewStyle().
|
||||||
|
Background(m.palette.Warning).
|
||||||
|
Foreground(m.palette.Background).
|
||||||
|
Bold(true)
|
||||||
|
gutterBase := lipgloss.NewStyle().
|
||||||
|
Width(2).
|
||||||
|
Foreground(m.palette.Muted)
|
||||||
|
gutterAnchor := lipgloss.NewStyle().
|
||||||
|
Width(2).
|
||||||
|
Foreground(m.palette.Info).
|
||||||
|
Bold(true)
|
||||||
|
gutterCursor := lipgloss.NewStyle().
|
||||||
|
Width(2).
|
||||||
|
Foreground(m.palette.Accent).
|
||||||
|
Bold(true)
|
||||||
|
gutterBoth := lipgloss.NewStyle().
|
||||||
|
Width(2).
|
||||||
|
Foreground(m.palette.Warning).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
for idx := range lines {
|
||||||
|
marker := " "
|
||||||
|
switch {
|
||||||
|
case m.visualMode && idx == m.visualAnchor && idx == m.cursorLine:
|
||||||
|
marker = gutterBoth.Render("◆ ")
|
||||||
|
case m.visualMode && idx == m.visualAnchor:
|
||||||
|
marker = gutterAnchor.Render("│ ")
|
||||||
|
case idx == m.cursorLine:
|
||||||
|
marker = gutterCursor.Render("▶ ")
|
||||||
|
default:
|
||||||
|
marker = gutterBase.Render(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
line := lines[idx]
|
||||||
|
plain := ""
|
||||||
|
if idx < len(plainLines) {
|
||||||
|
plain = plainLines[idx]
|
||||||
|
}
|
||||||
|
lineLen := len([]rune(plain))
|
||||||
|
cursorCol := clamp(m.cursorCol, 0, lineLen)
|
||||||
|
|
||||||
|
if hasSelection && idx >= startLine && idx <= endLine {
|
||||||
|
segStart := 0
|
||||||
|
segEnd := lineLen
|
||||||
|
if idx == startLine {
|
||||||
|
segStart = clamp(startCol, 0, lineLen)
|
||||||
|
}
|
||||||
|
if idx == endLine {
|
||||||
|
segEnd = clamp(endCol, segStart, lineLen)
|
||||||
|
}
|
||||||
|
left := ansi.Cut(line, 0, segStart)
|
||||||
|
mid := ansi.Cut(line, segStart, segEnd)
|
||||||
|
right := ansi.Cut(line, segEnd, lineLen)
|
||||||
|
if mid != "" {
|
||||||
|
line = left + selected.Render(mid) + right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == m.cursorLine {
|
||||||
|
left := ansi.Cut(line, 0, cursorCol)
|
||||||
|
mid := ansi.Cut(line, cursorCol, min(cursorCol+1, max(lineLen, cursorCol+1)))
|
||||||
|
right := ansi.Cut(line, min(cursorCol+1, lineLen), lineLen)
|
||||||
|
if cursorCol >= lineLen {
|
||||||
|
mid = cursor.Render(" ")
|
||||||
|
right = ""
|
||||||
|
} else {
|
||||||
|
mid = cursor.Render(mid)
|
||||||
|
}
|
||||||
|
line = left + mid + right
|
||||||
|
}
|
||||||
|
lines[idx] = marker + line
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) moveTextCursorWordForward() {
|
||||||
|
if !m.cursorMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines := m.previewPlainLines()
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line := clamp(m.cursorLine, 0, len(lines)-1)
|
||||||
|
col := clamp(m.cursorCol, 0, len([]rune(lines[line])))
|
||||||
|
for {
|
||||||
|
runes := []rune(lines[line])
|
||||||
|
for col < len(runes) && isWordRune(runes[col]) {
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
for col < len(runes) && !isWordRune(runes[col]) {
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
if col < len(runes) {
|
||||||
|
m.cursorLine = line
|
||||||
|
m.cursorCol = col
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if line >= len(lines)-1 {
|
||||||
|
m.cursorLine = line
|
||||||
|
m.cursorCol = len(runes)
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line++
|
||||||
|
col = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) moveTextCursorWordBackward() {
|
||||||
|
if !m.cursorMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines := m.previewPlainLines()
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line := clamp(m.cursorLine, 0, len(lines)-1)
|
||||||
|
col := clamp(m.cursorCol, 0, len([]rune(lines[line])))
|
||||||
|
for {
|
||||||
|
runes := []rune(lines[line])
|
||||||
|
if col > len(runes) {
|
||||||
|
col = len(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start from the character immediately before the cursor.
|
||||||
|
if col == 0 {
|
||||||
|
if line == 0 {
|
||||||
|
m.cursorLine = 0
|
||||||
|
m.cursorCol = 0
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line--
|
||||||
|
col = len([]rune(lines[line]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
col--
|
||||||
|
for {
|
||||||
|
runes = []rune(lines[line])
|
||||||
|
for col >= 0 && !isWordRune(runes[col]) {
|
||||||
|
col--
|
||||||
|
}
|
||||||
|
if col >= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if line == 0 {
|
||||||
|
m.cursorLine = 0
|
||||||
|
m.cursorCol = 0
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line--
|
||||||
|
runes = []rune(lines[line])
|
||||||
|
col = len(runes) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for col > 0 && isWordRune(runes[col-1]) {
|
||||||
|
col--
|
||||||
|
}
|
||||||
|
m.cursorLine = line
|
||||||
|
m.cursorCol = col
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) activePane() *BrowserPane {
|
func (m *Model) activePane() *BrowserPane {
|
||||||
if m.active == PaneLeft {
|
if m.active == PaneLeft {
|
||||||
return &m.left
|
return &m.left
|
||||||
|
|
@ -1431,6 +1995,44 @@ func (m *Model) resizePreview() {
|
||||||
m.previewModel.Height = max(innerHeight-metaHeight-3, 3)
|
m.previewModel.Height = max(innerHeight-metaHeight-3, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) moveTextCursorLine(delta int) {
|
||||||
|
lines := m.previewPlainLines()
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !m.cursorMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.cursorLine = clamp(m.cursorLine+delta, 0, len(lines)-1)
|
||||||
|
m.cursorCol = clamp(m.cursorCol, 0, m.lineRuneCount(m.cursorLine))
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) moveTextCursorCol(delta int) {
|
||||||
|
if !m.cursorMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.cursorCol = clamp(m.cursorCol+delta, 0, m.lineRuneCount(m.cursorLine))
|
||||||
|
m.ensureTextCursorVisible()
|
||||||
|
m.syncPreviewContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) ensureTextCursorVisible() {
|
||||||
|
if !m.cursorMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visible := max(m.previewModel.Height, 1)
|
||||||
|
if m.cursorLine < m.previewModel.YOffset {
|
||||||
|
m.previewModel.SetYOffset(m.cursorLine)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bottom := m.previewModel.YOffset + visible - 1
|
||||||
|
if m.cursorLine > bottom {
|
||||||
|
m.previewModel.SetYOffset(m.cursorLine - visible + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
innerWidth := max(width-2, 1)
|
||||||
innerHeight := max(height-2, 1)
|
innerHeight := max(height-2, 1)
|
||||||
|
|
@ -1637,6 +2239,17 @@ func renderFooter(m Model) string {
|
||||||
}
|
}
|
||||||
line += modeLabel
|
line += modeLabel
|
||||||
}
|
}
|
||||||
|
if m.visualMode {
|
||||||
|
modeLabel := lipgloss.NewStyle().
|
||||||
|
Background(m.palette.Footer).
|
||||||
|
Foreground(m.palette.Marked).
|
||||||
|
Bold(true).
|
||||||
|
Render("VISUAL MODE")
|
||||||
|
if line != "" {
|
||||||
|
line += sep
|
||||||
|
}
|
||||||
|
line += modeLabel
|
||||||
|
}
|
||||||
line = prefix + line
|
line = prefix + line
|
||||||
line = ansi.Truncate(line, m.width, "")
|
line = ansi.Truncate(line, m.width, "")
|
||||||
fill := m.width - ansi.StringWidth(line)
|
fill := m.width - ansi.StringWidth(line)
|
||||||
|
|
@ -2666,6 +3279,16 @@ func min(a, b int) int {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clamp(n, low, high int) int {
|
||||||
|
if n < low {
|
||||||
|
return low
|
||||||
|
}
|
||||||
|
if n > high {
|
||||||
|
return high
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
func max(a, b int) int {
|
func max(a, b int) int {
|
||||||
if a > b {
|
if a > b {
|
||||||
return a
|
return a
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue