The tree was formatted with an older gofmt; go1.27's gofmt additionally wants: EOF exactly one newline (no trailing blank lines), imports sorted alphabetically within a block, mixed-precedence binary expressions re-spaced for grouping ((a+b)/c), single-field composite literals un-aligned, adjacent one-line method signatures aligned, and one-line bodies containing a compound statement expanded. Applied repo-wide (31 files under internal/); pure formatting, no semantic changes — build and the full test suite pass.
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
package browser
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// Entry represents a single file or directory in the browser.
|
|
type Entry struct {
|
|
Path string // Relative path from configured directory
|
|
Name string // Display name (basename)
|
|
Size int64 // File size in bytes (0 for directories)
|
|
ModTime time.Time // Last modification time
|
|
IsDir bool // True if this is a directory
|
|
IsFirst bool // True if this is the first entry for its letter section
|
|
}
|
|
|
|
// Page represents a chunk of directory entries loaded from disk.
|
|
type Page struct {
|
|
Index int // Page number (0-based)
|
|
Entries []Entry // Entries in this page
|
|
Loaded bool // True if page data is in memory
|
|
Dirty bool // True if page needs refresh (external change detected)
|
|
}
|
|
|
|
// BrowserState holds all mutable browser state owned by the logic goroutine.
|
|
type BrowserState struct {
|
|
// Navigation
|
|
CurrentPath string // Currently browsed directory (relative to root)
|
|
History []string // Path history for navigation
|
|
ScrollOffset float64 // Vertical scroll offset in pixels (per-pixel scrolling)
|
|
EntryHeight float64 // Height of a single entry in pixels
|
|
VisibleCount int // Number of entries currently visible
|
|
|
|
// Lazy loading
|
|
Pages map[int]*Page // Loaded pages by page index
|
|
TotalEntries int // Total entry count (from cached index)
|
|
Loading bool // True if a page load is in flight
|
|
|
|
// Sorted index (position maps for each sort mode)
|
|
SortIndex *DirectoryIndex // Cached index with position maps
|
|
SortMode SortMode // Current sort mode
|
|
|
|
// Search
|
|
Query string // Current search query (forwarded from the main-owned search widget)
|
|
SearchResults []int // Indices of matching entries (empty = no filter)
|
|
// NOTE: the search bar's widget.Editor is intentionally NOT part of this
|
|
// state: Gio mutates widget state on the main goroutine during draw, and
|
|
// this state is owned by the logic goroutine (architecture.md §1). The
|
|
// widget lives in the renderer; its text is forwarded to the logic
|
|
// goroutine via the searchQuery channel, landing in Query.
|
|
|
|
// Alphabetical index
|
|
ActiveLetter string // Currently pressed letter (for highlighting)
|
|
LetterOffsets map[string]int // First entry index for each letter
|
|
|
|
// Interaction
|
|
SelectedIndex int // Currently selected entry (-1 = none)
|
|
TapTimestamp time.Time // For double-tap detection
|
|
}
|
|
|
|
const (
|
|
PageSize = 100 // Entries per page (tunable; ~5KB per page in memory)
|
|
PrefetchDist = 2 // Pages to prefetch beyond visible region
|
|
)
|
|
|
|
// NewEntry creates a new Entry from the given parameters.
|
|
func NewEntry(path, name string, size int64, modTime time.Time, isDir bool) Entry {
|
|
return Entry{
|
|
Path: path,
|
|
Name: name,
|
|
Size: size,
|
|
ModTime: modTime,
|
|
IsDir: isDir,
|
|
}
|
|
}
|
|
|
|
// NewPage creates a new Page with the given index and entries.
|
|
func NewPage(index int, entries []Entry) *Page {
|
|
return &Page{
|
|
Index: index,
|
|
Entries: entries,
|
|
Loaded: true,
|
|
}
|
|
}
|
|
|
|
// NewBrowserState creates a new BrowserState with default values.
|
|
func NewBrowserState() *BrowserState {
|
|
return &BrowserState{
|
|
Pages: make(map[int]*Page),
|
|
LetterOffsets: make(map[string]int),
|
|
SelectedIndex: -1,
|
|
SortMode: SortModeDateDesc, // Default sort mode (newest first)
|
|
EntryHeight: 48.0,
|
|
CurrentPath: "/",
|
|
}
|
|
}
|
|
|
|
// ToListItem converts an Entry to a ui.ListItem for rendering.
|
|
func (e *Entry) ToListItem() ui.ListItem {
|
|
subtext := formatSize(e.Size)
|
|
if e.IsDir {
|
|
subtext = "Directory"
|
|
}
|
|
return ui.ListItem{
|
|
Text: e.Name,
|
|
Subtext: subtext,
|
|
}
|
|
}
|
|
|
|
// formatSize returns a human-readable file size string.
|
|
func formatSize(bytes int64) string {
|
|
switch {
|
|
case bytes < 1024:
|
|
return fmt.Sprintf("%d B", bytes)
|
|
case bytes < 1024*1024:
|
|
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
|
|
case bytes < 1024*1024*1024:
|
|
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
|
|
default:
|
|
return fmt.Sprintf("%.1f GB", float64(bytes)/(1024*1024*1024))
|
|
}
|
|
}
|
|
|
|
// EvictPages removes pages that are far from the current scroll position.
|
|
func (s *BrowserState) EvictPages() {
|
|
minPage := s.GetScrollIndex()/PageSize - PrefetchDist
|
|
maxPage := (s.GetScrollIndex()+s.VisibleCount)/PageSize + PrefetchDist
|
|
for idx, page := range s.Pages {
|
|
if idx < minPage || idx > maxPage {
|
|
page.Entries = nil // Release memory
|
|
page.Loaded = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetScrollIndex returns the index of the first visible entry, derived from
|
|
// the per-pixel ScrollOffset and EntryHeight.
|
|
func (s *BrowserState) GetScrollIndex() int {
|
|
if s.EntryHeight <= 0 {
|
|
return 0
|
|
}
|
|
return int(s.ScrollOffset / s.EntryHeight)
|
|
}
|
|
|
|
// Reset clears all browser state for navigation.
|
|
func (s *BrowserState) Reset() {
|
|
s.CurrentPath = ""
|
|
s.Pages = make(map[int]*Page)
|
|
s.ScrollOffset = 0
|
|
s.SelectedIndex = -1
|
|
s.SearchResults = nil
|
|
s.Query = ""
|
|
}
|