Compare commits

3 Commits

Author SHA1 Message Date
5e1dce6082 update:multi selector 2026-04-18 04:42:35 +09:00
dc72658fe4 update:spacing 2026-04-18 04:35:09 +09:00
753329e2c8 update:scrollable list 2026-04-18 04:31:10 +09:00
7 changed files with 335 additions and 71 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.env .env
dist dist
.pi

View File

@@ -2,7 +2,8 @@ package pages
type FileActionPageMsg struct { type FileActionPageMsg struct {
ServerName string ServerName string
Filename string Filename string // single-file mode
Filenames []string // multi-file mode
} }
func FileActionItems() []MenuItem { func FileActionItems() []MenuItem {

View File

@@ -10,16 +10,14 @@ var (
Width(52) Width(52)
CardInnerStyle = lipgloss.NewStyle(). CardInnerStyle = lipgloss.NewStyle().
Padding(1, 3) Padding(0, 3)
CardTitleStyle = lipgloss.NewStyle(). CardTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("86")). Foreground(lipgloss.Color("86")).
Bold(true). Bold(true)
MarginBottom(1)
CardSubtitleStyle = lipgloss.NewStyle(). CardSubtitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("245")). Foreground(lipgloss.Color("245"))
MarginBottom(1)
// Menu items // Menu items
menuItemBase = lipgloss.NewStyle(). menuItemBase = lipgloss.NewStyle().
@@ -41,55 +39,45 @@ var (
// Form fields // Form fields
fieldLabelRequired = lipgloss.NewStyle(). fieldLabelRequired = lipgloss.NewStyle().
Foreground(lipgloss.Color("75")). Foreground(lipgloss.Color("75")).
Bold(true). Bold(true)
MarginTop(1)
fieldLabelOptional = lipgloss.NewStyle(). fieldLabelOptional = lipgloss.NewStyle().
Foreground(lipgloss.Color("245")). Foreground(lipgloss.Color("245"))
MarginTop(1)
FieldLegendStyle = lipgloss.NewStyle(). FieldLegendStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240")). Foreground(lipgloss.Color("240")).
Italic(true). Italic(true)
MarginTop(1)
// Buttons // Buttons
buttonActive = lipgloss.NewStyle(). buttonActive = lipgloss.NewStyle().
Foreground(lipgloss.Color("232")). Foreground(lipgloss.Color("232")).
Background(lipgloss.Color("75")). Background(lipgloss.Color("75")).
Bold(true). Bold(true).
Padding(0, 2). Padding(0, 2)
MarginTop(1)
buttonInactive = lipgloss.NewStyle(). buttonInactive = lipgloss.NewStyle().
Foreground(lipgloss.Color("232")). Foreground(lipgloss.Color("232")).
Background(lipgloss.Color("240")). Background(lipgloss.Color("240")).
Padding(0, 2). Padding(0, 2)
MarginTop(1)
buttonLocked = lipgloss.NewStyle(). buttonLocked = lipgloss.NewStyle().
Foreground(lipgloss.Color("238")). Foreground(lipgloss.Color("238")).
Background(lipgloss.Color("235")). Background(lipgloss.Color("235")).
Padding(0, 2). Padding(0, 2)
MarginTop(1)
// Status lines // Status lines
StatusOKStyle = lipgloss.NewStyle(). StatusOKStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("86")). Foreground(lipgloss.Color("86"))
MarginTop(1)
StatusWarnStyle = lipgloss.NewStyle(). StatusWarnStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("221")). Foreground(lipgloss.Color("221"))
MarginTop(1)
StatusErrStyle = lipgloss.NewStyle(). StatusErrStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("203")). Foreground(lipgloss.Color("203"))
MarginTop(1)
CleanWarningStyle = lipgloss.NewStyle(). CleanWarningStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("203")). Foreground(lipgloss.Color("203")).
Bold(true). Bold(true).
MarginBottom(1).
Width(44) Width(44)
// Footer // Footer
@@ -126,12 +114,10 @@ var (
// Storage file list // Storage file list
StorageFileSectionStyle = lipgloss.NewStyle(). StorageFileSectionStyle = lipgloss.NewStyle().
BorderTop(true). BorderTop(true).
BorderStyle(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("237")). BorderForeground(lipgloss.Color("237")).
MarginTop(1). Width(44)
PaddingTop(1).
Width(44)
StorageEmptyStyle = lipgloss.NewStyle(). StorageEmptyStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("243")). Foreground(lipgloss.Color("243")).
@@ -143,34 +129,30 @@ var (
Width(44) Width(44)
fileItemActive = lipgloss.NewStyle(). fileItemActive = lipgloss.NewStyle().
PaddingLeft(2). PaddingLeft(2).
Foreground(lipgloss.Color("75")). Foreground(lipgloss.Color("75")).
Bold(true). Bold(true).
Width(44). Width(44).
SetString("▸ ") SetString("▸ ")
FilenameLabelStyle = lipgloss.NewStyle(). FilenameLabelStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("243")). Foreground(lipgloss.Color("243"))
MarginBottom(1)
// Local directory label (above file list and in picker breadcrumb) // Local directory label (above file list and in picker breadcrumb)
LocalDirStyle = lipgloss.NewStyle(). LocalDirStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("243")). Foreground(lipgloss.Color("243")).
Italic(true). Italic(true)
MarginBottom(1)
// File picker // File picker
PickerQueryStyle = lipgloss.NewStyle(). PickerQueryStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("75")). Foreground(lipgloss.Color("75"))
MarginBottom(1)
PickerQueryBlurredStyle = lipgloss.NewStyle(). PickerQueryBlurredStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240")). Foreground(lipgloss.Color("240"))
MarginBottom(1)
pickerItemBase = lipgloss.NewStyle(). pickerItemBase = lipgloss.NewStyle().
PaddingLeft(4). PaddingLeft(4).
Width(44) Width(44)
pickerItemActive = lipgloss.NewStyle(). pickerItemActive = lipgloss.NewStyle().
PaddingLeft(2). PaddingLeft(2).
@@ -181,6 +163,9 @@ var (
pickerDirColor = lipgloss.Color("75") pickerDirColor = lipgloss.Color("75")
pickerFileColor = lipgloss.Color("252") pickerFileColor = lipgloss.Color("252")
ScrollIndicatorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240"))
) )
func MenuItemStyle(active, disabled bool) lipgloss.Style { func MenuItemStyle(active, disabled bool) lipgloss.Style {

88
internal/tui/scroll.go Normal file
View File

@@ -0,0 +1,88 @@
package tui
// visibleRange returns [start, end) indices of rows that should be rendered.
func visibleRange(total, selected, scrollOff, viewHeight int) (int, int) {
if total <= 0 {
return 0, 0
}
maxVis := viewHeight
if maxVis <= 0 {
maxVis = total // unconstrained
}
if maxVis >= total {
return 0, total
}
// Reserve lines for scroll indicators.
hasAbove := scrollOff > 0
hasBelow := scrollOff+maxVis < total
if hasAbove {
maxVis--
}
if hasBelow {
maxVis--
}
if maxVis < 1 {
maxVis = 1
}
start := scrollOff
end := start + maxVis
if selected < start {
start = selected
end = start + maxVis
}
if selected >= end {
end = selected + 1
start = end - maxVis
}
if start < 0 {
start = 0
end = min(maxVis, total)
}
if end > total {
end = total
start = max(0, end-maxVis)
}
return start, end
}
// syncScroll adjusts scrollOff so selected stays inside the viewport.
func syncScroll(selected, total, viewHeight, scrollOff int) int {
if total <= 0 {
return 0
}
maxVis := viewHeight
if maxVis <= 0 || maxVis >= total {
return 0
}
if selected < 0 {
selected = 0
}
if selected >= total {
selected = total - 1
}
if scrollOff < 0 {
scrollOff = 0
}
if scrollOff > total-1 {
scrollOff = total - 1
}
if selected < scrollOff {
scrollOff = selected
}
if selected >= scrollOff+maxVis {
scrollOff = selected - maxVis + 1
}
if scrollOff < 0 {
scrollOff = 0
}
return scrollOff
}

View File

@@ -39,33 +39,67 @@ type TUIInterface struct {
WindowWidth int WindowWidth int
WindowHeight int WindowHeight int
// server actions page // server actions page
ActiveServer string ActiveServer string
LocalDir string // user's cwd, destination for received files LocalDir string // user's cwd, destination for received files
StorageFiles []string StorageFiles []string
StorageLoading bool StorageLoading bool
StorageErr error StorageErr error
FileSelected int // cursor within StorageFiles FileSelected int // cursor within StorageFiles
FileFocused bool // true = ↑↓ drives file list, false = action menu FileFocused bool // true = ↑↓ drives file list, false = action menu
FileScrollOff int // first visible row in StorageFiles list
FileViewHeight int // available visible rows for StorageFiles list
FileMultiSelect map[int]bool // selected rows in StorageFiles
// file action page // file action page
ActiveFile string ActiveFile string
ActiveFiles []string
FileOpLoading bool FileOpLoading bool
FileOpErr error FileOpErr error
FileOpSuccess string FileOpSuccess string
// edit server page // edit server page
EditingServer string // original name of server being edited EditingServer string // original name of server being edited
// clean all confirmation page // clean all confirmation page
CleanInput textinput.Model CleanInput textinput.Model
CleanOpLoading bool CleanOpLoading bool
CleanOpErr error CleanOpErr error
// send / file picker page // send / file picker page
Picker picker Picker picker
} }
func NewTUIInterface(store *services.ServicesStore, localDir string) TUIInterface { func NewTUIInterface(store *services.ServicesStore, localDir string) TUIInterface {
return TUIInterface{ return TUIInterface{
Services: store, Services: store,
Page: pageHome, Page: pageHome,
MenuItems: pages.HomeMenuItems(), MenuItems: pages.HomeMenuItems(),
LocalDir: localDir, LocalDir: localDir,
FileMultiSelect: make(map[int]bool),
} }
} }
// fileListHeight calculates how many rows can fit in the server storage file list.
func (m TUIInterface) fileListHeight() int {
h := m.WindowHeight
if h == 0 {
return 0 // unconstrained until first WindowSizeMsg
}
// Card chrome overhead (rounded border + compact inner vertical padding)
const cardOverhead = 2
// Header (title + subtitle)
const headerLines = 2
// Footer (top border + content)
const footerLines = 2
// Server actions menu rows (Send / Clean All)
actionLines := len(m.MenuItems)
if actionLines < 1 {
actionLines = 2
}
// File section chrome: top border + local-dir label
const fileSectionOverhead = 2
used := cardOverhead + headerLines + footerLines + actionLines + fileSectionOverhead
available := h - used
if available < 1 {
available = 1
}
return available
}

View File

@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"fmt"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@@ -73,6 +74,62 @@ func deleteFileCmd(store *services.ServicesStore, serverName, filename string) t
} }
} }
func getFilesCmd(store *services.ServicesStore, serverName string, filenames []string, destDir string) tea.Cmd {
return func() tea.Msg {
storage, err := store.NewStorageService(serverName)
if err != nil {
return fileOpMsg{op: "get", err: err}
}
success := 0
var failed []string
for _, name := range filenames {
if err := storage.Get(name, destDir); err != nil {
failed = append(failed, fmt.Sprintf("%s: %v", name, err))
continue
}
success++
}
if len(failed) > 0 {
if success == 0 {
return fileOpMsg{op: "get", err: fmt.Errorf("download failed for %d file(s): %s", len(failed), strings.Join(failed, "; "))}
}
return fileOpMsg{op: "get", err: fmt.Errorf("downloaded %d/%d file(s), failed %d: %s", success, len(filenames), len(failed), strings.Join(failed, "; "))}
}
return fileOpMsg{op: "get", success: fmt.Sprintf("✓ Downloaded %d file(s)", success)}
}
}
func deleteFilesCmd(store *services.ServicesStore, serverName string, filenames []string) tea.Cmd {
return func() tea.Msg {
storage, err := store.NewStorageService(serverName)
if err != nil {
return fileOpMsg{op: "delete", err: err}
}
success := 0
var failed []string
for _, name := range filenames {
if err := storage.Delete(name); err != nil {
failed = append(failed, fmt.Sprintf("%s: %v", name, err))
continue
}
success++
}
if len(failed) > 0 {
if success == 0 {
return fileOpMsg{op: "delete", err: fmt.Errorf("delete failed for %d file(s): %s", len(failed), strings.Join(failed, "; "))}
}
return fileOpMsg{op: "delete", err: fmt.Errorf("deleted %d/%d file(s), failed %d: %s", success, len(filenames), len(failed), strings.Join(failed, "; "))}
}
return fileOpMsg{op: "delete", success: fmt.Sprintf("✓ Deleted %d file(s)", success)}
}
}
type cleanAllMsg struct { type cleanAllMsg struct {
err error err error
} }
@@ -160,6 +217,28 @@ func (m TUIInterface) nextSelectable(from, dir int) int {
return from return from
} }
func (m TUIInterface) selectedStorageFiles() []string {
if len(m.FileMultiSelect) == 0 || len(m.StorageFiles) == 0 {
return nil
}
idxs := make([]int, 0, len(m.FileMultiSelect))
for idx, selected := range m.FileMultiSelect {
if !selected {
continue
}
if idx < 0 || idx >= len(m.StorageFiles) {
continue
}
idxs = append(idxs, idx)
}
sort.Ints(idxs)
files := make([]string, 0, len(idxs))
for _, idx := range idxs {
files = append(files, m.StorageFiles[idx])
}
return files
}
func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -193,6 +272,11 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.Selected = 0 m.Selected = 0
m.FileSelected = 0 m.FileSelected = 0
m.FileFocused = true m.FileFocused = true
m.FileViewHeight = m.fileListHeight()
m.FileScrollOff = 0
m.FileMultiSelect = make(map[int]bool)
m.ActiveFiles = nil
m.ActiveFile = ""
m.StorageFiles = nil m.StorageFiles = nil
m.StorageErr = nil m.StorageErr = nil
m.StorageLoading = true m.StorageLoading = true
@@ -200,7 +284,16 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case pages.FileActionPageMsg: case pages.FileActionPageMsg:
m.Page = pageFileAction m.Page = pageFileAction
m.ActiveFile = msg.Filename targets := msg.Filenames
if len(targets) == 0 && msg.Filename != "" {
targets = []string{msg.Filename}
}
m.ActiveFiles = append([]string(nil), targets...)
if len(m.ActiveFiles) > 0 {
m.ActiveFile = m.ActiveFiles[0]
} else {
m.ActiveFile = ""
}
m.MenuItems = pages.FileActionItems() m.MenuItems = pages.FileActionItems()
m.Selected = 0 m.Selected = 0
m.FileOpLoading = false m.FileOpLoading = false
@@ -303,6 +396,8 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.StorageFiles = msg.files m.StorageFiles = msg.files
m.StorageErr = msg.err m.StorageErr = msg.err
m.FileSelected = 0 m.FileSelected = 0
m.FileMultiSelect = make(map[int]bool)
m.FileScrollOff = syncScroll(m.FileSelected, len(m.StorageFiles), m.FileViewHeight, 0)
return m, nil return m, nil
case configLoadedMsg: case configLoadedMsg:
@@ -332,6 +427,8 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.WindowWidth = msg.Width m.WindowWidth = msg.Width
m.WindowHeight = msg.Height m.WindowHeight = msg.Height
m.FileViewHeight = m.fileListHeight()
m.FileScrollOff = syncScroll(m.FileSelected, len(m.StorageFiles), m.FileViewHeight, m.FileScrollOff)
return m, nil return m, nil
case tea.KeyPressMsg: case tea.KeyPressMsg:
@@ -388,7 +485,7 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, func() tea.Msg { return pages.SelectEditServerPageMsg{} } return m, func() tea.Msg { return pages.SelectEditServerPageMsg{} }
case "remove": case "remove":
return m, func() tea.Msg { return pages.RemoveServerPageMsg{} } return m, func() tea.Msg { return pages.RemoveServerPageMsg{} }
// TODO: "edit" // TODO: "edit"
} }
case "ctrl+c": case "ctrl+c":
m.Quitting = true m.Quitting = true
@@ -420,6 +517,9 @@ func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.C
case "tab": case "tab":
if len(m.StorageFiles) > 0 || !m.StorageLoading { if len(m.StorageFiles) > 0 || !m.StorageLoading {
m.FileFocused = !m.FileFocused m.FileFocused = !m.FileFocused
if m.FileFocused {
m.FileScrollOff = syncScroll(m.FileSelected, len(m.StorageFiles), m.FileViewHeight, m.FileScrollOff)
}
} }
case "up", "k": case "up", "k":
@@ -427,6 +527,7 @@ func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.C
if m.FileSelected > 0 { if m.FileSelected > 0 {
m.FileSelected-- m.FileSelected--
} }
m.FileScrollOff = syncScroll(m.FileSelected, len(m.StorageFiles), m.FileViewHeight, m.FileScrollOff)
} else { } else {
if m.Selected > 0 { if m.Selected > 0 {
m.Selected-- m.Selected--
@@ -438,18 +539,34 @@ func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.C
if m.FileSelected < len(m.StorageFiles)-1 { if m.FileSelected < len(m.StorageFiles)-1 {
m.FileSelected++ m.FileSelected++
} }
m.FileScrollOff = syncScroll(m.FileSelected, len(m.StorageFiles), m.FileViewHeight, m.FileScrollOff)
} else { } else {
if m.Selected < len(m.MenuItems)-1 { if m.Selected < len(m.MenuItems)-1 {
m.Selected++ m.Selected++
} }
} }
case "space":
if m.FileFocused && len(m.StorageFiles) > 0 {
if m.FileMultiSelect == nil {
m.FileMultiSelect = make(map[int]bool)
}
idx := m.FileSelected
m.FileMultiSelect[idx] = !m.FileMultiSelect[idx]
if !m.FileMultiSelect[idx] {
delete(m.FileMultiSelect, idx)
}
}
case "enter": case "enter":
if m.FileFocused && len(m.StorageFiles) > 0 { if m.FileFocused && len(m.StorageFiles) > 0 {
file := m.StorageFiles[m.FileSelected] targets := m.selectedStorageFiles()
if len(targets) == 0 {
targets = []string{m.StorageFiles[m.FileSelected]}
}
server := m.ActiveServer server := m.ActiveServer
return m, func() tea.Msg { return m, func() tea.Msg {
return pages.FileActionPageMsg{ServerName: server, Filename: file} return pages.FileActionPageMsg{ServerName: server, Filenames: targets}
} }
} }
server := m.ActiveServer server := m.ActiveServer
@@ -492,18 +609,30 @@ func (m TUIInterface) updateFileAction(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
} }
case "enter": case "enter":
server := m.ActiveServer server := m.ActiveServer
file := m.ActiveFile targets := m.ActiveFiles
if len(targets) == 0 && m.ActiveFile != "" {
targets = []string{m.ActiveFile}
}
if len(targets) == 0 {
return m, nil
}
switch m.MenuItems[m.Selected].Key { switch m.MenuItems[m.Selected].Key {
case "get": case "get":
m.FileOpLoading = true m.FileOpLoading = true
m.FileOpErr = nil m.FileOpErr = nil
m.FileOpSuccess = "" m.FileOpSuccess = ""
return m, getFileCmd(m.Services, server, file, m.LocalDir) if len(targets) == 1 {
return m, getFileCmd(m.Services, server, targets[0], m.LocalDir)
}
return m, getFilesCmd(m.Services, server, targets, m.LocalDir)
case "delete": case "delete":
m.FileOpLoading = true m.FileOpLoading = true
m.FileOpErr = nil m.FileOpErr = nil
m.FileOpSuccess = "" m.FileOpSuccess = ""
return m, deleteFileCmd(m.Services, server, file) if len(targets) == 1 {
return m, deleteFileCmd(m.Services, server, targets[0])
}
return m, deleteFilesCmd(m.Services, server, targets)
} }
case "ctrl+c": case "ctrl+c":
m.Quitting = true m.Quitting = true

View File

@@ -38,6 +38,9 @@ func (m TUIInterface) subtitle() string {
} }
return "Server" return "Server"
case pageFileAction: case pageFileAction:
if len(m.ActiveFiles) > 1 {
return fmt.Sprintf("%d files selected", len(m.ActiveFiles))
}
return m.ActiveFile return m.ActiveFile
case pageSend: case pageSend:
return "Send File" return "Send File"
@@ -116,7 +119,9 @@ func (m TUIInterface) View() tea.View {
footerSep() + footerSep() +
footerHint("↑↓", "navigate") + footerHint("↑↓", "navigate") +
footerSep() + footerSep() +
footerHint("enter", "select") + footerHint("space", "select") +
footerSep() +
footerHint("enter", "actions") +
footerSep() + footerSep() +
footerHint("esc", "back") footerHint("esc", "back")
case pageFileAction: case pageFileAction:
@@ -220,6 +225,8 @@ func (m TUIInterface) viewServerActions() string {
// file list rows // file list rows
var fileRows []string var fileRows []string
showScrollUp := false
showScrollDown := false
switch { switch {
case m.StorageLoading: case m.StorageLoading:
fileRows = append(fileRows, styles.StatusWarnStyle.Render(" loading…")) fileRows = append(fileRows, styles.StatusWarnStyle.Render(" loading…"))
@@ -228,12 +235,27 @@ func (m TUIInterface) viewServerActions() string {
case len(m.StorageFiles) == 0: case len(m.StorageFiles) == 0:
fileRows = append(fileRows, styles.StorageEmptyStyle.Render(" no files in storage")) fileRows = append(fileRows, styles.StorageEmptyStyle.Render(" no files in storage"))
default: default:
for i, f := range m.StorageFiles { start, end := visibleRange(len(m.StorageFiles), m.FileSelected, m.FileScrollOff, m.FileViewHeight)
showScrollUp = start > 0
showScrollDown = end < len(m.StorageFiles)
for i := start; i < end; i++ {
f := m.StorageFiles[i]
active := m.FileFocused && i == m.FileSelected active := m.FileFocused && i == m.FileSelected
fileRows = append(fileRows, styles.FileItemStyle(active).Render(f)) checked := m.FileMultiSelect[i]
mark := "[ ] "
if checked {
mark = "[✓] "
}
fileRows = append(fileRows, styles.FileItemStyle(active).Render(mark+f))
} }
} }
fileList := lipgloss.JoinVertical(lipgloss.Left, fileRows...) fileList := lipgloss.JoinVertical(lipgloss.Left, fileRows...)
if showScrollUp {
fileList = styles.ScrollIndicatorStyle.Render(" ↑") + "\n" + fileList
}
if showScrollDown {
fileList = fileList + "\n" + styles.ScrollIndicatorStyle.Render(" ↓")
}
fileSection := styles.StorageFileSectionStyle.Render( fileSection := styles.StorageFileSectionStyle.Render(
lipgloss.JoinVertical(lipgloss.Left, localDirLabel, fileList), lipgloss.JoinVertical(lipgloss.Left, localDirLabel, fileList),
) )
@@ -242,7 +264,11 @@ func (m TUIInterface) viewServerActions() string {
} }
func (m TUIInterface) viewFileAction() string { func (m TUIInterface) viewFileAction() string {
filenameLabel := styles.FilenameLabelStyle.Render(m.ActiveFile) label := m.ActiveFile
if len(m.ActiveFiles) > 1 {
label = fmt.Sprintf("%d file(s) selected", len(m.ActiveFiles))
}
filenameLabel := styles.FilenameLabelStyle.Render(label)
var menuRows []string var menuRows []string
for i, item := range m.MenuItems { for i, item := range m.MenuItems {