Compare commits

..

No commits in common. "250091bb8a497ecde70f86feefc6200f7fc63e38" and "b81f11f711c432af80eff117950798a46277c1db" have entirely different histories.

9 changed files with 189 additions and 429 deletions

View file

@ -1,22 +1,137 @@
package main package main
import ( import (
"fmt"
"log" "log"
"strings"
"hosts-go/internal/core" "hosts-go/internal/core"
"hosts-go/internal/tui"
tea "github.com/charmbracelet/bubbletea"
) )
func main() { 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 { 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)) // Display parsing results
if err := p.Start(); err != nil { fmt.Printf("✅ Parsing successful!\n")
log.Fatalf("failed to start TUI: %v", err) 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!")
} }

13
go.mod
View file

@ -2,22 +2,20 @@ module hosts-go
go 1.24.5 go 1.24.5
require ( require github.com/stretchr/testify v1.10.0
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 ( require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/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/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // 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/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // 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 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect

14
go.sum
View file

@ -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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 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= 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/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 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 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 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/lrstanley/bubblezone v1.0.0 h1:bIpUaBilD42rAQwlg/4u5aTqVAt6DSRKYZuSdmkr8UA=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 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 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -2,8 +2,8 @@
## Current Work Focus ## Current Work Focus
**Status**: Phase 3 In Progress - Edit mode basics implemented **Status**: Phase 1 Complete - Ready for Phase 2 (TUI Implementation)
**Priority**: Expand Phase 3 with file integration and advanced editing **Priority**: Implementing Bubble Tea TUI with two-pane layout
## Recent Changes ## Recent Changes
@ -17,17 +17,6 @@
- ✅ **Comprehensive testing**: 54 comprehensive tests covering all parser functionality (100% passing) - ✅ **Comprehensive testing**: 54 comprehensive tests covering all parser functionality (100% passing)
- ✅ **Demo application**: Full showcase of parser capabilities with real-world examples - ✅ **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 ### Parser Capabilities Achieved
- ✅ **Standard entries**: IPv4, IPv6, multiple aliases, inline comments - ✅ **Standard entries**: IPv4, IPv6, multiple aliases, inline comments
- ✅ **Disabled entries**: Commented lines with `# IP hostname` format detection - ✅ **Disabled entries**: Commented lines with `# IP hostname` format detection
@ -46,21 +35,42 @@
## Next Steps ## 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** 1. **Edit Mode Implementation**
- ✅ Explicit mode transition with visual indicators - Explicit mode transition with visual indicators
- 🔄 Permission handling with sudo request - Permission handling with sudo request
- 🔄 Entry modification forms with validation - Entry modification forms with validation
2. **File Integration** 2. **File Integration**
- 🔄 Connect TUI with existing parser functionality for writes - Connect TUI with existing parser functionality
- 🔄 Real-time display of actual `/etc/hosts` content - Real-time display of actual `/etc/hosts` content
- 🔄 Live validation and formatting preview - Live validation and formatting preview
3. **Advanced Features** 3. **Advanced Features**
- ✅ Entry toggle (activate/deactivate) - Entry toggle (activate/deactivate)
- 🔄 Add/edit/delete operations - Add/edit/delete operations
- 🔄 Sorting and filtering capabilities - Sorting and filtering capabilities
## Active Decisions and Considerations ## Active Decisions and Considerations

View file

@ -44,19 +44,18 @@
## What's Left to Build ## What's Left to Build
### 🎨 Basic TUI (Phase 2 - Completed) ### 🎨 Basic TUI (Phase 2 - Current Priority)
- [x] **Main Bubble Tea model**: Core application state and structure - [ ] **Main Bubble Tea model**: Core application state and structure
- [x] **Two-pane layout**: Left list + right detail view - [ ] **Two-pane layout**: Left list + right detail view
- [x] **Entry list display**: Show IP and hostname columns - [ ] **Entry list display**: Show active status, IP, hostname columns
- [x] **Entry selection**: Navigate and select entries with keyboard - [ ] **Entry selection**: Navigate and select entries with keyboard
- [x] **View mode**: Safe browsing with status bar and active/inactive indicators - [ ] **View mode**: Safe browsing without modification capability
- [x] **Integration**: Connect TUI with existing parser functionality - [ ] **Integration**: Connect TUI with existing parser functionality
### 🔧 Edit Functionality (Phase 3) ### 🔧 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 - [ ] **Permission handling**: Request sudo access when entering edit mode
- [x] **Entry modification**: Toggle active status - [ ] **Entry modification**: Add, edit, delete, toggle active status
- [ ] **Entry modification**: Add, edit, delete operations
- [ ] **File writing**: Atomic updates with backup and rollback - [ ] **File writing**: Atomic updates with backup and rollback
- [ ] **Input validation**: Real-time validation of IP and hostname inputs - [ ] **Input validation**: Real-time validation of IP and hostname inputs
@ -68,16 +67,16 @@
- [ ] **Search/filter**: Find entries quickly in large files - [ ] **Search/filter**: Find entries quickly in large files
### 🧪 Testing & Quality (Ongoing) ### 🧪 Testing & Quality (Ongoing)
- [x] **TUI tests**: User interactions, state transitions - [ ] **TUI tests**: User interactions, state transitions
- [ ] **Integration tests**: Complete TUI workflows with file operations - [ ] **Integration tests**: Complete TUI workflows with file operations
- [ ] **Permission tests**: sudo scenarios, graceful degradation - [ ] **Permission tests**: sudo scenarios, graceful degradation
- [ ] **End-to-end tests**: Full application workflows - [ ] **End-to-end tests**: Full application workflows
## Current Status ## Current Status
### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)** ### Project Phase: **Phase 1 Complete → Phase 2 (TUI Implementation)**
- **Completion**: ~80% (parser, TUI, and basic edit mode implemented) - **Completion**: ~65% (foundation and complete core parser functionality implemented)
- **Active work**: Expand edit mode with file integration and advanced editing - **Active work**: Ready to implement Bubble Tea TUI with two-pane layout (Phase 2)
- **Blockers**: None - comprehensive parser foundation with 54 tests completed - **Blockers**: None - comprehensive parser foundation with 54 tests completed
- **Parser status**: Production-ready with all safety features implemented - **Parser status**: Production-ready with all safety features implemented
@ -133,10 +132,10 @@
## Success Metrics & Milestones ## Success Metrics & Milestones
### Milestone 1: Basic Functionality (Target: Week 1) ### Milestone 1: Basic Functionality (Target: Week 1)
- [x] Parse and display existing hosts file entries - [ ] Parse and display existing hosts file entries
- [x] Navigate entries with keyboard - [ ] Navigate entries with keyboard
- [x] View entry details in right pane - [ ] View entry details in right pane
- **Success criteria**: Can safely browse hosts file without editing (achieved) - **Success criteria**: Can safely browse hosts file without editing
### Milestone 2: Core Editing (Target: Week 2) ### Milestone 2: Core Editing (Target: Week 2)
- [ ] Toggle entries active/inactive - [ ] Toggle entries active/inactive
@ -180,14 +179,15 @@
19. ✅ **Verify atomic operations** (temp files with rollback for safe writes) 19. ✅ **Verify atomic operations** (temp files with rollback for safe writes)
20. ✅ **Complete parser documentation** (comprehensive demo showing all capabilities) 20. ✅ **Complete parser documentation** (comprehensive demo showing all capabilities)
### 🚧 NEXT Phase 3 Actions (Edit Mode & File Integration) ### 🚧 NEXT Phase 2 Actions (TUI Implementation)
1. **Edit mode transition** with explicit mode switching 1. **Design TUI architecture** following Bubble Tea MVU pattern
2. **Permission handling** requesting sudo when entering edit mode 2. **Create main application model** with state management
3. **Entry modification** forms for add/edit/delete/toggle 3. **Implement two-pane layout** (entry list + detail view)
4. **File writing** with backups and atomic updates 4. **Add navigation controls** (keyboard-driven interaction)
5. **Input validation** in real time 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 ### Parser Achievement Summary
- **54 comprehensive tests** covering all hosts file variations and edge cases - **54 comprehensive tests** covering all hosts file variations and edge cases
@ -197,4 +197,4 @@
- **Search and management** capabilities for finding and manipulating entries - **Search and management** capabilities for finding and manipulating entries
- **Demo application** showcasing all functionality with realistic examples - **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.

View file

@ -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")
}