Pad/internal/browser/types.go
Greg Pomerantz 6a43e3c0db Add e2e test harness for testing editor logic without Gio display
- internal/test/e2e/: FrameCapture, Harness, ElementAssertions, helpers
- internal/editor/logic.go: Add done channel and Done() method for graceful shutdown
- internal/editor/mock_setup.go: Mock filesystem for tests
- internal/browser/: Browser layout and search logic
- internal/io/: Worker pool for async tasks
- Update architecture docs and spec
2026-05-31 08:56:30 -04:00

139 lines
4.0 KiB
Go

package browser
import (
"fmt"
"time"
"gioui.org/widget"
"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)
ScrollIndex int // Index of first visible entry (not pixel offset)
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
SearchResults []int // Indices of matching entries (empty = no filter)
SearchEditor widget.Editor // Gio editor for search input
// 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: SortModeNameAsc, // Default sort mode
}
}
// 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.ScrollIndex/PageSize - PrefetchDist
maxPage := (s.ScrollIndex + s.VisibleCount)/PageSize + PrefetchDist
for idx, page := range s.Pages {
if idx < minPage || idx > maxPage {
page.Entries = nil // Release memory
page.Loaded = false
}
}
}
// Reset clears all browser state for navigation.
func (s *BrowserState) Reset() {
s.Pages = make(map[int]*Page)
s.ScrollIndex = 0
s.SelectedIndex = -1
s.SearchResults = nil
s.Query = ""
}