diff --git a/cmd/hosts/main.go b/cmd/hosts/main.go index 4e0fbe4..6469ff4 100644 --- a/cmd/hosts/main.go +++ b/cmd/hosts/main.go @@ -1,22 +1,137 @@ package main import ( + "fmt" "log" + "strings" "hosts-go/internal/core" - "hosts-go/internal/tui" - - tea "github.com/charmbracelet/bubbletea" ) func main() { - hostsFile, _, err := core.ParseHostsFile("/etc/hosts") + fmt.Println("hosts-go - Phase 1: Core Functionality (Parser)") + fmt.Println("===============================================") + + // Demonstrate hosts file parsing with sample content + sampleHostsContent := `# Sample hosts file content +127.0.0.1 localhost # Local loopback +::1 ip6-localhost # IPv6 loopback +192.168.1.100 dev.example.com www.dev.example.com api.dev.example.com # Development server +# 10.0.0.50 staging.example.com # Disabled staging server +203.0.113.10 prod.example.com # Production server + +# Another comment +::ffff:192.168.1.200 test.example.com # Test server` + + fmt.Println("Sample hosts file content:") + fmt.Println(strings.Repeat("-", 50)) + fmt.Println(sampleHostsContent) + fmt.Println(strings.Repeat("-", 50)) + fmt.Println() + + // Parse the sample content + lines := strings.Split(sampleHostsContent, "\n") + hostsFile, warnings, err := core.ParseHostsContent(lines) if err != nil { - log.Fatalf("failed to parse hosts file: %v", err) + log.Fatalf("Failed to parse hosts content: %v", err) } - p := tea.NewProgram(tui.NewModel(hostsFile)) - if err := p.Start(); err != nil { - log.Fatalf("failed to start TUI: %v", err) + // Display parsing results + fmt.Printf("✅ Parsing successful!\n") + fmt.Printf(" Total entries: %d\n", len(hostsFile.Entries)) + fmt.Printf(" Active entries: %d\n", len(hostsFile.ActiveEntries())) + fmt.Printf(" Standalone comments: %d\n", len(hostsFile.Comments)) + fmt.Printf(" Warnings: %d\n", len(warnings)) + fmt.Println() + + // Show warnings if any + if len(warnings) > 0 { + fmt.Println("Parsing warnings:") + for _, warning := range warnings { + fmt.Printf(" Line %d: %s\n", warning.Line, warning.Message) + } + fmt.Println() } + + // Show standalone comments + if len(hostsFile.Comments) > 0 { + fmt.Println("Standalone comments found:") + for i, comment := range hostsFile.Comments { + fmt.Printf("%d. %s\n", i+1, comment) + } + fmt.Println() + } + + // Show parsed entries + fmt.Println("Parsed entries:") + for i, entry := range hostsFile.Entries { + status := "✅ Active" + if !entry.Active { + status = "❌ Disabled" + } + fmt.Printf("%d. [%s] %s -> %s", i+1, status, entry.IP, entry.Hostname) + if len(entry.Aliases) > 0 { + fmt.Printf(" (aliases: %s)", strings.Join(entry.Aliases, ", ")) + } + if entry.Comment != "" { + fmt.Printf(" # %s", entry.Comment) + } + fmt.Println() + } + fmt.Println() + + // Demonstrate intelligent formatting + fmt.Println("Intelligent formatting output:") + fmt.Println(strings.Repeat("-", 50)) + formattedLines := core.FormatHostsFile(hostsFile) + for _, line := range formattedLines { + fmt.Println(line) + } + fmt.Println(strings.Repeat("-", 50)) + fmt.Println() + + // Demonstrate formatting style detection + fmt.Println("Formatting style detection:") + style := core.DetectFormattingStyle(lines) + if style.UseTabs { + fmt.Printf(" Detected style: Tabs\n") + } else { + fmt.Printf(" Detected style: Spaces (%d per tab)\n", style.SpacesPerTab) + } + fmt.Printf(" Column widths: IP=%d, Host=%d\n", style.IPWidth, style.HostWidth) + fmt.Println() + + // Demonstrate search functionality + fmt.Println("Search demonstrations:") + if found := hostsFile.FindEntry("localhost"); found != nil { + fmt.Printf("✅ Found 'localhost': %s -> %s\n", found.IP, found.Hostname) + } + + if found := hostsFile.FindEntry("www.dev.example.com"); found != nil { + fmt.Printf("✅ Found alias 'www.dev.example.com': %s -> %s\n", found.IP, found.Hostname) + } + + if found := hostsFile.FindEntry("staging.example.com"); found != nil { + status := "active" + if !found.Active { + status = "disabled" + } + fmt.Printf("✅ Found 'staging.example.com': %s -> %s (%s)\n", found.IP, found.Hostname, status) + } + + if found := hostsFile.FindEntry("notfound.com"); found == nil { + fmt.Printf("❌ 'notfound.com' not found (as expected)\n") + } + fmt.Println() + + fmt.Println("🎉 Phase 1 Complete: Core Functionality (Parser)") + fmt.Println("✅ Hosts file parsing with format preservation") + fmt.Println("✅ Comment and disabled entry handling") + fmt.Println("✅ Intelligent formatting with column alignment") + fmt.Println("✅ Malformed line handling with warnings") + fmt.Println("✅ Round-trip parsing (parse → format → parse)") + fmt.Println("✅ Backup functionality") + fmt.Println("✅ Search and entry management") + fmt.Println() + fmt.Println("Ready for Phase 2: TUI Implementation!") } diff --git a/go.mod b/go.mod index e391812..dd1d3e3 100644 --- a/go.mod +++ b/go.mod @@ -2,22 +2,20 @@ module hosts-go go 1.24.5 -require ( - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.6 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/stretchr/testify v1.10.0 -) +require github.com/stretchr/testify v1.10.0 require ( - github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lrstanley/bubblezone v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -27,7 +25,6 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 950982e..eab93af 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= @@ -16,16 +12,14 @@ github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lrstanley/bubblezone v1.0.0 h1:bIpUaBilD42rAQwlg/4u5aTqVAt6DSRKYZuSdmkr8UA= +github.com/lrstanley/bubblezone v1.0.0/go.mod h1:kcTekA8HE/0Ll2bWzqHlhA2c513KDNLW7uDfDP4Mly8= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -45,14 +39,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/tui/model.go b/internal/tui/model.go deleted file mode 100644 index 9b9f9a2..0000000 --- a/internal/tui/model.go +++ /dev/null @@ -1,118 +0,0 @@ -package tui - -import ( - "fmt" - "hosts-go/internal/core" - "strings" - - list "github.com/charmbracelet/bubbles/list" - viewport "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" -) - -// entryItem wraps a HostEntry for display in a list component. -type entryItem struct{ entry *core.HostEntry } - -func (e entryItem) Title() string { - prefix := "[ ]" - if e.entry.Active { - prefix = "[✓]" - } - return fmt.Sprintf("%s %s", prefix, e.entry.Hostname) -} - -func (e entryItem) Description() string { return e.entry.IP } -func (e entryItem) FilterValue() string { return e.entry.Hostname } - -// Model is the main Bubble Tea model for the application. -type Mode int - -const ( - ViewMode Mode = iota - EditMode -) - -type pane int - -const ( - listPane pane = iota - detailPane -) - -type Model struct { - list list.Model - detail viewport.Model - hosts *core.HostsFile - width int - height int - mode Mode - focus pane -} - -// NewModel constructs the TUI model from a parsed HostsFile. -func NewModel(hf *core.HostsFile) Model { - items := make([]list.Item, len(hf.Entries)) - for i, e := range hf.Entries { - items[i] = entryItem{entry: e} - } - - l := list.New(items, list.NewDefaultDelegate(), 0, 0) - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.SetShowHelp(false) - l.SetShowPagination(false) - - m := Model{ - list: l, - detail: viewport.New(0, 0), - hosts: hf, - mode: ViewMode, - focus: listPane, - } - m.refreshDetail() - return m -} - -func (m *Model) refreshDetail() { - if len(m.hosts.Entries) == 0 { - m.detail.SetContent("") - return - } - entry := m.hosts.Entries[m.list.Index()] - var b strings.Builder - status := "active" - if !entry.Active { - status = "inactive" - } - fmt.Fprintf(&b, "IP: %s\n", entry.IP) - fmt.Fprintf(&b, "Host: %s\n", entry.Hostname) - if len(entry.Aliases) > 0 { - fmt.Fprintf(&b, "Aliases: %s\n", strings.Join(entry.Aliases, ", ")) - } - if entry.Comment != "" { - fmt.Fprintf(&b, "Comment: %s\n", entry.Comment) - } - fmt.Fprintf(&b, "Status: %s", status) - m.detail.SetContent(b.String()) - m.detail.YOffset = 0 -} - -// Init satisfies tea.Model. -func (m Model) Init() tea.Cmd { return nil } - -// SelectedEntry returns the currently selected host entry. -func (m Model) SelectedEntry() *core.HostEntry { - if len(m.hosts.Entries) == 0 { - return nil - } - idx := m.list.Index() - if idx < 0 || idx >= len(m.hosts.Entries) { - return nil - } - return m.hosts.Entries[idx] -} - -// Mode returns the current operating mode of the TUI. -func (m Model) Mode() Mode { - return m.mode -} diff --git a/internal/tui/update.go b/internal/tui/update.go deleted file mode 100644 index e29fd15..0000000 --- a/internal/tui/update.go +++ /dev/null @@ -1,90 +0,0 @@ -package tui - -import ( - tea "github.com/charmbracelet/bubbletea" -) - -// Update handles all messages for the TUI. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - return m, tea.Quit - case "tab": - if m.focus == listPane { - m.focus = detailPane - } else { - m.focus = listPane - } - case "e": - if m.mode == ViewMode { - m.mode = EditMode - } else { - m.mode = ViewMode - } - case "a": - if m.mode == EditMode { - if entry := m.SelectedEntry(); entry != nil { - entry.Active = !entry.Active - m.list.SetItem(m.list.Index(), entryItem{entry}) - m.refreshDetail() - } - } - case "up", "k": - if m.focus == detailPane { - m.detail.LineUp(1) - } else { - m.list, cmd = m.list.Update(msg) - m.refreshDetail() - } - return m, cmd - case "down", "j": - if m.focus == detailPane { - m.detail.LineDown(1) - } else { - m.list, cmd = m.list.Update(msg) - m.refreshDetail() - } - return m, cmd - } - if m.focus == listPane { - m.list, cmd = m.list.Update(msg) - m.refreshDetail() - } - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - 1 - - leftWidth := msg.Width / 2 - rightWidth := msg.Width - leftWidth - leftHeight := m.height - rightHeight := m.height - - if m.focus == listPane { - leftWidth -= 2 - leftHeight -= 2 - } else { - rightWidth -= 2 - rightHeight -= 2 - } - - leftWidth -= 2 - rightWidth -= 2 - - m.list.SetSize(leftWidth, leftHeight) - m.detail.Width = rightWidth - m.detail.Height = rightHeight - - if m.focus == listPane { - m.list, cmd = m.list.Update(msg) - } - default: - if m.focus == listPane { - m.list, cmd = m.list.Update(msg) - m.refreshDetail() - } - } - return m, cmd -} diff --git a/internal/tui/view.go b/internal/tui/view.go deleted file mode 100644 index 8190a0e..0000000 --- a/internal/tui/view.go +++ /dev/null @@ -1,57 +0,0 @@ -package tui - -import ( - "fmt" - - "github.com/charmbracelet/lipgloss" -) - -var ( - listStyle = lipgloss.NewStyle().Padding(0, 1) - detailStyle = lipgloss.NewStyle().Padding(0, 1) - focusedStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("62")) - statusStyle = lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.Color("240")).Background(lipgloss.Color("236")) -) - -// View renders the two-pane layout. -func (m Model) View() string { - listView := m.list.View() - detailView := m.detail.View() - - // compute dimensions for each pane accounting for padding and focus border - leftWidth := m.width / 2 - rightWidth := m.width - leftWidth - leftHeight := m.height - rightHeight := m.height - - if m.focus == listPane { - leftWidth -= 2 // border - leftHeight -= 2 - } else { - rightWidth -= 2 - rightHeight -= 2 - } - - // account for horizontal padding - leftWidth -= 2 - rightWidth -= 2 - - left := listStyle.Width(leftWidth).Height(leftHeight).Render(listView) - right := detailStyle.Width(rightWidth).Height(rightHeight).Render(detailView) - - if m.focus == listPane { - left = focusedStyle.Render(left) - } else { - right = focusedStyle.Render(right) - } - - // join panes and status bar - panes := lipgloss.JoinHorizontal(lipgloss.Top, left, right) - modeLabel := "VIEW" - if m.mode == EditMode { - modeLabel = "EDIT" - } - status := fmt.Sprintf("%s MODE • %d entries", modeLabel, len(m.hosts.Entries)) - bar := statusStyle.Width(m.width).Render(status) - return lipgloss.JoinVertical(lipgloss.Left, panes, bar) -} diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index c19e750..4fa6806 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -2,8 +2,8 @@ ## Current Work Focus -**Status**: Phase 3 In Progress - Edit mode basics implemented -**Priority**: Expand Phase 3 with file integration and advanced editing +**Status**: Phase 1 Complete - Ready for Phase 2 (TUI Implementation) +**Priority**: Implementing Bubble Tea TUI with two-pane layout ## Recent Changes @@ -17,17 +17,6 @@ - ✅ **Comprehensive testing**: 54 comprehensive tests covering all parser functionality (100% passing) - ✅ **Demo application**: Full showcase of parser capabilities with real-world examples -### Phase 2: TUI Prototype (COMPLETED) ✅ -- ✅ **Two-pane layout** with list and detail panes -- ✅ **Navigation system** with pane switching and detail scrolling -- ✅ **View mode** showing entry details and status bar -- ✅ **Parser integration** loading `/etc/hosts` into the UI - -### Phase 3: Edit Mode (IN PROGRESS) -- 🔄 **Edit mode toggle** via 'e' key with status bar indication -- 🔄 **Entry activation toggle** using 'a' key -- 🔄 **Model tests** covering edit mode behavior - ### Parser Capabilities Achieved - ✅ **Standard entries**: IPv4, IPv6, multiple aliases, inline comments - ✅ **Disabled entries**: Commented lines with `# IP hostname` format detection @@ -46,21 +35,42 @@ ## Next Steps -### Immediate (Phase 3 - Current Priority) +### Immediate (Phase 2 - Current Priority) +1. **TUI Architecture Design** + - Design main Bubble Tea model structure following MVU pattern + - Plan state management for entries, selection, and modes + - Define component hierarchy (main → list → detail → modal) + +2. **Two-Pane Layout Implementation** + - Create left pane: entry list with status indicators + - Create right pane: detailed entry view with editing capabilities + - Implement responsive layout with proper sizing + +3. **Navigation System** + - Keyboard navigation between panes and entries + - Selection highlighting and status indicators + - Scroll handling for large hosts files + +4. **View Mode Implementation** + - Safe browsing without modification capability + - Display parsed entries with active/inactive status + - Show entry details in right pane when selected + +### Medium-term (Phase 3) 1. **Edit Mode Implementation** - - ✅ Explicit mode transition with visual indicators - - 🔄 Permission handling with sudo request - - 🔄 Entry modification forms with validation + - Explicit mode transition with visual indicators + - Permission handling with sudo request + - Entry modification forms with validation 2. **File Integration** - - 🔄 Connect TUI with existing parser functionality for writes - - 🔄 Real-time display of actual `/etc/hosts` content - - 🔄 Live validation and formatting preview + - Connect TUI with existing parser functionality + - Real-time display of actual `/etc/hosts` content + - Live validation and formatting preview 3. **Advanced Features** - - ✅ Entry toggle (activate/deactivate) - - 🔄 Add/edit/delete operations - - 🔄 Sorting and filtering capabilities + - Entry toggle (activate/deactivate) + - Add/edit/delete operations + - Sorting and filtering capabilities ## Active Decisions and Considerations diff --git a/memory-bank/progress.md b/memory-bank/progress.md index aedc400..642491f 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -44,19 +44,18 @@ ## What's Left to Build -### 🎨 Basic TUI (Phase 2 - Completed) -- [x] **Main Bubble Tea model**: Core application state and structure -- [x] **Two-pane layout**: Left list + right detail view -- [x] **Entry list display**: Show IP and hostname columns -- [x] **Entry selection**: Navigate and select entries with keyboard -- [x] **View mode**: Safe browsing with status bar and active/inactive indicators -- [x] **Integration**: Connect TUI with existing parser functionality +### 🎨 Basic TUI (Phase 2 - Current Priority) +- [ ] **Main Bubble Tea model**: Core application state and structure +- [ ] **Two-pane layout**: Left list + right detail view +- [ ] **Entry list display**: Show active status, IP, hostname columns +- [ ] **Entry selection**: Navigate and select entries with keyboard +- [ ] **View mode**: Safe browsing without modification capability +- [ ] **Integration**: Connect TUI with existing parser functionality ### 🔧 Edit Functionality (Phase 3) -- [x] **Edit mode transition**: Explicit mode switching with visual indicators +- [ ] **Edit mode transition**: Explicit mode switching with visual indicators - [ ] **Permission handling**: Request sudo access when entering edit mode -- [x] **Entry modification**: Toggle active status -- [ ] **Entry modification**: Add, edit, delete operations +- [ ] **Entry modification**: Add, edit, delete, toggle active status - [ ] **File writing**: Atomic updates with backup and rollback - [ ] **Input validation**: Real-time validation of IP and hostname inputs @@ -68,16 +67,16 @@ - [ ] **Search/filter**: Find entries quickly in large files ### 🧪 Testing & Quality (Ongoing) -- [x] **TUI tests**: User interactions, state transitions +- [ ] **TUI tests**: User interactions, state transitions - [ ] **Integration tests**: Complete TUI workflows with file operations - [ ] **Permission tests**: sudo scenarios, graceful degradation - [ ] **End-to-end tests**: Full application workflows ## Current Status -### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)** -- **Completion**: ~80% (parser, TUI, and basic edit mode implemented) -- **Active work**: Expand edit mode with file integration and advanced editing +### Project Phase: **Phase 1 Complete → Phase 2 (TUI Implementation)** +- **Completion**: ~65% (foundation and complete core parser functionality implemented) +- **Active work**: Ready to implement Bubble Tea TUI with two-pane layout (Phase 2) - **Blockers**: None - comprehensive parser foundation with 54 tests completed - **Parser status**: Production-ready with all safety features implemented @@ -133,10 +132,10 @@ ## Success Metrics & Milestones ### Milestone 1: Basic Functionality (Target: Week 1) -- [x] Parse and display existing hosts file entries -- [x] Navigate entries with keyboard -- [x] View entry details in right pane -- **Success criteria**: Can safely browse hosts file without editing (achieved) +- [ ] Parse and display existing hosts file entries +- [ ] Navigate entries with keyboard +- [ ] View entry details in right pane +- **Success criteria**: Can safely browse hosts file without editing ### Milestone 2: Core Editing (Target: Week 2) - [ ] Toggle entries active/inactive @@ -180,14 +179,15 @@ 19. ✅ **Verify atomic operations** (temp files with rollback for safe writes) 20. ✅ **Complete parser documentation** (comprehensive demo showing all capabilities) -### 🚧 NEXT Phase 3 Actions (Edit Mode & File Integration) -1. **Edit mode transition** with explicit mode switching -2. **Permission handling** requesting sudo when entering edit mode -3. **Entry modification** forms for add/edit/delete/toggle -4. **File writing** with backups and atomic updates -5. **Input validation** in real time +### 🚧 NEXT Phase 2 Actions (TUI Implementation) +1. **Design TUI architecture** following Bubble Tea MVU pattern +2. **Create main application model** with state management +3. **Implement two-pane layout** (entry list + detail view) +4. **Add navigation controls** (keyboard-driven interaction) +5. **Integrate parser functionality** with TUI display +6. **Implement view mode** (safe browsing without modifications) -**Phase 2 delivered a functional view-only TUI.** +**Phase 1 is fully complete with a production-ready parser foundation.** ### Parser Achievement Summary - **54 comprehensive tests** covering all hosts file variations and edge cases @@ -197,4 +197,4 @@ - **Search and management** capabilities for finding and manipulating entries - **Demo application** showcasing all functionality with realistic examples -The foundation is robust, tested, and ready for edit mode implementation in Phase 3. +The foundation is robust, tested, and ready for TUI implementation in Phase 2. diff --git a/tests/tui_test.go b/tests/tui_test.go deleted file mode 100644 index a73719a..0000000 --- a/tests/tui_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package tests - -import ( - "strings" - "testing" - - "hosts-go/internal/core" - "hosts-go/internal/tui" - - tea "github.com/charmbracelet/bubbletea" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestModelSelection(t *testing.T) { - sample := `127.0.0.1 localhost -192.168.1.10 example.com` - lines := strings.Split(sample, "\n") - hf, _, err := core.ParseHostsContent(lines) - require.NoError(t, err) - - m := tui.NewModel(hf) - require.NotNil(t, m.SelectedEntry()) - assert.Equal(t, "localhost", m.SelectedEntry().Hostname) - - // Move selection down - nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown}) - m = nm.(tui.Model) - assert.Equal(t, "example.com", m.SelectedEntry().Hostname) -} - -func TestViewModeStatusBar(t *testing.T) { - sample := `127.0.0.1 localhost -# 192.168.1.10 example.com` - lines := strings.Split(sample, "\n") - hf, _, err := core.ParseHostsContent(lines) - require.NoError(t, err) - - m := tui.NewModel(hf) - nm, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 20}) - m = nm.(tui.Model) - view := m.View() - - assert.Contains(t, view, "VIEW MODE") - assert.Contains(t, view, "[✓] localhost") - assert.Contains(t, view, "[ ] example.com") -} - -func TestPaneSwitching(t *testing.T) { - sample := "127.0.0.1 localhost\n192.168.1.10 example.com" - lines := strings.Split(sample, "\n") - hf, _, err := core.ParseHostsContent(lines) - require.NoError(t, err) - - m := tui.NewModel(hf) - // Switch focus to detail pane - nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab}) - m = nm.(tui.Model) - - // Attempt to move down; selection should remain on first entry - nm, _ = m.Update(tea.KeyMsg{Type: tea.KeyDown}) - m = nm.(tui.Model) - assert.Equal(t, "localhost", m.SelectedEntry().Hostname) -} - -func TestEditModeToggleAndActivation(t *testing.T) { - sample := "127.0.0.1 localhost\n192.168.1.10 example.com" - lines := strings.Split(sample, "\n") - hf, _, err := core.ParseHostsContent(lines) - require.NoError(t, err) - - m := tui.NewModel(hf) - - // enter edit mode - nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) - m = nm.(tui.Model) - assert.Equal(t, tui.EditMode, m.Mode()) - - // toggle active state of first entry - nm, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}}) - m = nm.(tui.Model) - assert.False(t, m.SelectedEntry().Active) - - // status bar should reflect edit mode - view := m.View() - assert.Contains(t, view, "EDIT MODE") -}