mirror of
				https://github.com/shokinn/hosts-go.git
				synced 2025-11-03 20:18:32 +00:00 
			
		
		
		
	feat: begin tui implementation
This commit is contained in:
		
							parent
							
								
									b81f11f711
								
							
						
					
					
						commit
						1b66db10e2
					
				
					 9 changed files with 200 additions and 154 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=
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										57
									
								
								internal/tui/model.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								internal/tui/model.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					package tui
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"hosts-go/internal/core"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						list "github.com/charmbracelet/bubbles/list"
 | 
				
			||||||
 | 
						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       { return 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 Model struct {
 | 
				
			||||||
 | 
						list   list.Model
 | 
				
			||||||
 | 
						hosts  *core.HostsFile
 | 
				
			||||||
 | 
						width  int
 | 
				
			||||||
 | 
						height int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return Model{
 | 
				
			||||||
 | 
							list:  l,
 | 
				
			||||||
 | 
							hosts: hf,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								internal/tui/update.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/tui/update.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					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 tea.WindowSizeMsg:
 | 
				
			||||||
 | 
							m.width = msg.Width
 | 
				
			||||||
 | 
							m.height = msg.Height
 | 
				
			||||||
 | 
							m.list.SetSize(msg.Width/2, msg.Height)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m.list, cmd = m.list.Update(msg)
 | 
				
			||||||
 | 
						return m, cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										40
									
								
								internal/tui/view.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								internal/tui/view.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					package tui
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/charmbracelet/lipgloss"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						listStyle   = lipgloss.NewStyle().Padding(0, 1)
 | 
				
			||||||
 | 
						detailStyle = lipgloss.NewStyle().Padding(0, 1)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// View renders the two-pane layout.
 | 
				
			||||||
 | 
					func (m Model) View() string {
 | 
				
			||||||
 | 
						listView := m.list.View()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var detail strings.Builder
 | 
				
			||||||
 | 
						if len(m.hosts.Entries) > 0 {
 | 
				
			||||||
 | 
							entry := m.hosts.Entries[m.list.Index()]
 | 
				
			||||||
 | 
							status := "active"
 | 
				
			||||||
 | 
							if !entry.Active {
 | 
				
			||||||
 | 
								status = "inactive"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fmt.Fprintf(&detail, "IP: %s\n", entry.IP)
 | 
				
			||||||
 | 
							fmt.Fprintf(&detail, "Host: %s\n", entry.Hostname)
 | 
				
			||||||
 | 
							if len(entry.Aliases) > 0 {
 | 
				
			||||||
 | 
								fmt.Fprintf(&detail, "Aliases: %s\n", strings.Join(entry.Aliases, ", "))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if entry.Comment != "" {
 | 
				
			||||||
 | 
								fmt.Fprintf(&detail, "Comment: %s\n", entry.Comment)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fmt.Fprintf(&detail, "Status: %s", status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						left := listStyle.Width(m.width / 2).Height(m.height).Render(listView)
 | 
				
			||||||
 | 
						right := detailStyle.Width(m.width - m.width/2).Height(m.height).Render(detail.String())
 | 
				
			||||||
 | 
						return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,8 +2,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Current Work Focus
 | 
					## Current Work Focus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Status**: Phase 1 Complete - Ready for Phase 2 (TUI Implementation)
 | 
					**Status**: Phase 2 In Progress - Basic TUI prototype implemented
 | 
				
			||||||
**Priority**: Implementing Bubble Tea TUI with two-pane layout
 | 
					**Priority**: Expand Bubble Tea TUI with navigation and view features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Recent Changes
 | 
					## Recent Changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,25 +36,22 @@
 | 
				
			||||||
## Next Steps
 | 
					## Next Steps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Immediate (Phase 2 - Current Priority)
 | 
					### Immediate (Phase 2 - Current Priority)
 | 
				
			||||||
1. **TUI Architecture Design**
 | 
					1. **TUI Architecture Design** ✅
 | 
				
			||||||
   - Design main Bubble Tea model structure following MVU pattern
 | 
					   - Main Bubble Tea model with list and detail panes
 | 
				
			||||||
   - Plan state management for entries, selection, and modes
 | 
					   - State tracks selection and window size
 | 
				
			||||||
   - Define component hierarchy (main → list → detail → modal)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. **Two-Pane Layout Implementation**
 | 
					2. **Two-Pane Layout Implementation** ✅
 | 
				
			||||||
   - Create left pane: entry list with status indicators
 | 
					   - Left pane: entry list using Bubbles list component
 | 
				
			||||||
   - Create right pane: detailed entry view with editing capabilities
 | 
					   - Right pane: detail view of selected entry
 | 
				
			||||||
   - Implement responsive layout with proper sizing
 | 
					   - Responsive sizing handled in update
 | 
				
			||||||
 | 
					
 | 
				
			||||||
3. **Navigation System**
 | 
					3. **Navigation System** 🟡
 | 
				
			||||||
   - Keyboard navigation between panes and entries
 | 
					   - Basic keyboard navigation via list component
 | 
				
			||||||
   - Selection highlighting and status indicators
 | 
					   - Need scroll handling and pane switching
 | 
				
			||||||
   - Scroll handling for large hosts files
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
4. **View Mode Implementation**
 | 
					4. **View Mode Implementation** 🟡
 | 
				
			||||||
   - Safe browsing without modification capability
 | 
					   - Read-only display of `/etc/hosts` entries
 | 
				
			||||||
   - Display parsed entries with active/inactive status
 | 
					   - Needs status indicators and better styling
 | 
				
			||||||
   - Show entry details in right pane when selected
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Medium-term (Phase 3)
 | 
					### Medium-term (Phase 3)
 | 
				
			||||||
1. **Edit Mode Implementation**
 | 
					1. **Edit Mode Implementation**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,12 +44,12 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## What's Left to Build
 | 
					## What's Left to Build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 🎨 Basic TUI (Phase 2 - Current Priority)  
 | 
					### 🎨 Basic TUI (Phase 2 - Current Priority)
 | 
				
			||||||
- [ ] **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
 | 
					- [ ] **View mode**: Safe browsing with status indicators
 | 
				
			||||||
- [ ] **Integration**: Connect TUI with existing parser functionality
 | 
					- [ ] **Integration**: Connect TUI with existing parser functionality
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 🔧 Edit Functionality (Phase 3)
 | 
					### 🔧 Edit Functionality (Phase 3)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								tests/tui_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/tui_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue