Pad/internal/browser/types.go
Greg Pomerantz 06b1444207 gofmt: format all remaining files with the go1.27 toolchain
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.
2026-08-23 10:03:27 -04:00

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 = ""
}