mirror of
https://github.com/shokinn/hosts-go.git
synced 2025-08-23 08:33:02 +00:00
Compare commits
9 commits
b81f11f711
...
250091bb8a
Author | SHA1 | Date | |
---|---|---|---|
250091bb8a | |||
51966f766c | |||
0c60248d75 | |||
0bcb821c33 | |||
a24041d664 | |||
3561d15858 | |||
9748c2dde8 | |||
49bf61f8e5 | |||
1b66db10e2 |
9 changed files with 429 additions and 189 deletions
|
@ -1,137 +1,22 @@
|
||||||
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() {
|
||||||
fmt.Println("hosts-go - Phase 1: Core Functionality (Parser)")
|
hostsFile, _, err := core.ParseHostsFile("/etc/hosts")
|
||||||
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 content: %v", err)
|
log.Fatalf("failed to parse hosts file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display parsing results
|
p := tea.NewProgram(tui.NewModel(hostsFile))
|
||||||
fmt.Printf("✅ Parsing successful!\n")
|
if err := p.Start(); err != nil {
|
||||||
fmt.Printf(" Total entries: %d\n", len(hostsFile.Entries))
|
log.Fatalf("failed to start TUI: %v", err)
|
||||||
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
13
go.mod
|
@ -2,20 +2,22 @@ module hosts-go
|
||||||
|
|
||||||
go 1.24.5
|
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 (
|
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
|
||||||
|
@ -25,6 +27,7 @@ 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
14
go.sum
|
@ -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 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=
|
||||||
|
@ -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/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/lrstanley/bubblezone v1.0.0 h1:bIpUaBilD42rAQwlg/4u5aTqVAt6DSRKYZuSdmkr8UA=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/lrstanley/bubblezone v1.0.0/go.mod h1:kcTekA8HE/0Ll2bWzqHlhA2c513KDNLW7uDfDP4Mly8=
|
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 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=
|
||||||
|
@ -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.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=
|
||||||
|
|
118
internal/tui/model.go
Normal file
118
internal/tui/model.go
Normal 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
90
internal/tui/update.go
Normal 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
57
internal/tui/view.go
Normal 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)
|
||||||
|
}
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
## Current Work Focus
|
## Current Work Focus
|
||||||
|
|
||||||
**Status**: Phase 1 Complete - Ready for Phase 2 (TUI Implementation)
|
**Status**: Phase 3 In Progress - Edit mode basics implemented
|
||||||
**Priority**: Implementing Bubble Tea TUI with two-pane layout
|
**Priority**: Expand Phase 3 with file integration and advanced editing
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
|
||||||
|
@ -17,6 +17,17 @@
|
||||||
- ✅ **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
|
||||||
|
@ -35,42 +46,21 @@
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
### Immediate (Phase 2 - Current Priority)
|
### Immediate (Phase 3 - 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
|
- 🔄 Connect TUI with existing parser functionality for writes
|
||||||
- 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
|
||||||
|
|
||||||
|
|
|
@ -44,18 +44,19 @@
|
||||||
|
|
||||||
## What's Left to Build
|
## What's Left to Build
|
||||||
|
|
||||||
### 🎨 Basic TUI (Phase 2 - Current Priority)
|
### 🎨 Basic TUI (Phase 2 - Completed)
|
||||||
- [ ] **Main Bubble Tea model**: Core application state and structure
|
- [x] **Main Bubble Tea model**: Core application state and structure
|
||||||
- [ ] **Two-pane layout**: Left list + right detail view
|
- [x] **Two-pane layout**: Left list + right detail view
|
||||||
- [ ] **Entry list display**: Show active status, IP, hostname columns
|
- [x] **Entry list display**: Show IP and hostname columns
|
||||||
- [ ] **Entry selection**: Navigate and select entries with keyboard
|
- [x] **Entry selection**: Navigate and select entries with keyboard
|
||||||
- [ ] **View mode**: Safe browsing without modification capability
|
- [x] **View mode**: Safe browsing with status bar and active/inactive indicators
|
||||||
- [ ] **Integration**: Connect TUI with existing parser functionality
|
- [x] **Integration**: Connect TUI with existing parser functionality
|
||||||
|
|
||||||
### 🔧 Edit Functionality (Phase 3)
|
### 🔧 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
|
- [ ] **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
|
- [ ] **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
|
||||||
|
|
||||||
|
@ -67,16 +68,16 @@
|
||||||
- [ ] **Search/filter**: Find entries quickly in large files
|
- [ ] **Search/filter**: Find entries quickly in large files
|
||||||
|
|
||||||
### 🧪 Testing & Quality (Ongoing)
|
### 🧪 Testing & Quality (Ongoing)
|
||||||
- [ ] **TUI tests**: User interactions, state transitions
|
- [x] **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 1 Complete → Phase 2 (TUI Implementation)**
|
### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)**
|
||||||
- **Completion**: ~65% (foundation and complete core parser functionality implemented)
|
- **Completion**: ~80% (parser, TUI, and basic edit mode implemented)
|
||||||
- **Active work**: Ready to implement Bubble Tea TUI with two-pane layout (Phase 2)
|
- **Active work**: Expand edit mode with file integration and advanced editing
|
||||||
- **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
|
||||||
|
|
||||||
|
@ -132,10 +133,10 @@
|
||||||
## Success Metrics & Milestones
|
## Success Metrics & Milestones
|
||||||
|
|
||||||
### Milestone 1: Basic Functionality (Target: Week 1)
|
### Milestone 1: Basic Functionality (Target: Week 1)
|
||||||
- [ ] Parse and display existing hosts file entries
|
- [x] Parse and display existing hosts file entries
|
||||||
- [ ] Navigate entries with keyboard
|
- [x] Navigate entries with keyboard
|
||||||
- [ ] View entry details in right pane
|
- [x] View entry details in right pane
|
||||||
- **Success criteria**: Can safely browse hosts file without editing
|
- **Success criteria**: Can safely browse hosts file without editing (achieved)
|
||||||
|
|
||||||
### Milestone 2: Core Editing (Target: Week 2)
|
### Milestone 2: Core Editing (Target: Week 2)
|
||||||
- [ ] Toggle entries active/inactive
|
- [ ] Toggle entries active/inactive
|
||||||
|
@ -179,15 +180,14 @@
|
||||||
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 2 Actions (TUI Implementation)
|
### 🚧 NEXT Phase 3 Actions (Edit Mode & File Integration)
|
||||||
1. **Design TUI architecture** following Bubble Tea MVU pattern
|
1. **Edit mode transition** with explicit mode switching
|
||||||
2. **Create main application model** with state management
|
2. **Permission handling** requesting sudo when entering edit mode
|
||||||
3. **Implement two-pane layout** (entry list + detail view)
|
3. **Entry modification** forms for add/edit/delete/toggle
|
||||||
4. **Add navigation controls** (keyboard-driven interaction)
|
4. **File writing** with backups and atomic updates
|
||||||
5. **Integrate parser functionality** with TUI display
|
5. **Input validation** in real time
|
||||||
6. **Implement view mode** (safe browsing without modifications)
|
|
||||||
|
|
||||||
**Phase 1 is fully complete with a production-ready parser foundation.**
|
**Phase 2 delivered a functional view-only TUI.**
|
||||||
|
|
||||||
### 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 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
87
tests/tui_test.go
Normal 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")
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue