Compare commits

..

9 commits

9 changed files with 429 additions and 189 deletions

View file

@ -1,137 +1,22 @@
package main
import (
"fmt"
"log"
"strings"
"hosts-go/internal/core"
"hosts-go/internal/tui"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
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)
hostsFile, _, err := core.ParseHostsFile("/etc/hosts")
if err != nil {
log.Fatalf("Failed to parse hosts content: %v", err)
log.Fatalf("failed to parse hosts file: %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)
p := tea.NewProgram(tui.NewModel(hostsFile))
if err := p.Start(); err != nil {
log.Fatalf("failed to start TUI: %v", err)
}
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!")
}

13
go.mod
View file

@ -2,20 +2,22 @@ module hosts-go
go 1.24.5
require github.com/stretchr/testify v1.10.0
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/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
@ -25,6 +27,7 @@ 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

14
go.sum
View file

@ -1,5 +1,9 @@
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=
@ -12,14 +16,16 @@ 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/lrstanley/bubblezone v1.0.0 h1:bIpUaBilD42rAQwlg/4u5aTqVAt6DSRKYZuSdmkr8UA=
github.com/lrstanley/bubblezone v1.0.0/go.mod h1:kcTekA8HE/0Ll2bWzqHlhA2c513KDNLW7uDfDP4Mly8=
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/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=
@ -39,10 +45,14 @@ 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=

118
internal/tui/model.go Normal file
View file

@ -0,0 +1,118 @@
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
}

90
internal/tui/update.go Normal file
View file

@ -0,0 +1,90 @@
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
}

57
internal/tui/view.go Normal file
View file

@ -0,0 +1,57 @@
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)
}

View file

@ -2,8 +2,8 @@
## Current Work Focus
**Status**: Phase 1 Complete - Ready for Phase 2 (TUI Implementation)
**Priority**: Implementing Bubble Tea TUI with two-pane layout
**Status**: Phase 3 In Progress - Edit mode basics implemented
**Priority**: Expand Phase 3 with file integration and advanced editing
## Recent Changes
@ -17,6 +17,17 @@
- ✅ **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
@ -35,42 +46,21 @@
## Next Steps
### 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)
### Immediate (Phase 3 - Current Priority)
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
- Real-time display of actual `/etc/hosts` content
- Live validation and formatting preview
- 🔄 Connect TUI with existing parser functionality for writes
- 🔄 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

View file

@ -44,18 +44,19 @@
## What's Left to Build
### 🎨 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
### 🎨 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
### 🔧 Edit Functionality (Phase 3)
- [ ] **Edit mode transition**: Explicit mode switching with visual indicators
- [x] **Edit mode transition**: Explicit mode switching with visual indicators
- [ ] **Permission handling**: Request sudo access when entering edit mode
- [ ] **Entry modification**: Add, edit, delete, toggle active status
- [x] **Entry modification**: Toggle active status
- [ ] **Entry modification**: Add, edit, delete operations
- [ ] **File writing**: Atomic updates with backup and rollback
- [ ] **Input validation**: Real-time validation of IP and hostname inputs
@ -67,16 +68,16 @@
- [ ] **Search/filter**: Find entries quickly in large files
### 🧪 Testing & Quality (Ongoing)
- [ ] **TUI tests**: User interactions, state transitions
- [x] **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 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)
### 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
- **Blockers**: None - comprehensive parser foundation with 54 tests completed
- **Parser status**: Production-ready with all safety features implemented
@ -132,10 +133,10 @@
## Success Metrics & Milestones
### Milestone 1: Basic Functionality (Target: Week 1)
- [ ] 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
- [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)
### Milestone 2: Core Editing (Target: Week 2)
- [ ] Toggle entries active/inactive
@ -179,15 +180,14 @@
19. ✅ **Verify atomic operations** (temp files with rollback for safe writes)
20. ✅ **Complete parser documentation** (comprehensive demo showing all capabilities)
### 🚧 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)
### 🚧 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
**Phase 1 is fully complete with a production-ready parser foundation.**
**Phase 2 delivered a functional view-only TUI.**
### 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 TUI implementation in Phase 2.
The foundation is robust, tested, and ready for edit mode implementation in Phase 3.

87
tests/tui_test.go Normal file
View file

@ -0,0 +1,87 @@
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")
}