From e813704f8161781f8815846e6c81a394852cbe67 Mon Sep 17 00:00:00 2001 From: Philip Henning Date: Thu, 14 Aug 2025 10:09:30 +0200 Subject: [PATCH] feat: save hosts file --- cmd/hosts/main.go | 2 +- internal/tui/model.go | 30 ++++++++++++++++-------------- internal/tui/update.go | 10 ++++++++++ memory-bank/activeContext.md | 2 +- memory-bank/progress.md | 6 +++--- tests/tui_test.go | 33 +++++++++++++++++++++++++++++---- 6 files changed, 60 insertions(+), 23 deletions(-) diff --git a/cmd/hosts/main.go b/cmd/hosts/main.go index 4e0fbe4..d134d1e 100644 --- a/cmd/hosts/main.go +++ b/cmd/hosts/main.go @@ -15,7 +15,7 @@ func main() { log.Fatalf("failed to parse hosts file: %v", err) } - p := tea.NewProgram(tui.NewModel(hostsFile)) + p := tea.NewProgram(tui.NewModel(hostsFile, "/etc/hosts")) if err := p.Start(); err != nil { log.Fatalf("failed to start TUI: %v", err) } diff --git a/internal/tui/model.go b/internal/tui/model.go index 9b9f9a2..2b178c8 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -40,17 +40,18 @@ const ( ) type Model struct { - list list.Model - detail viewport.Model - hosts *core.HostsFile - width int - height int - mode Mode - focus pane + list list.Model + detail viewport.Model + hosts *core.HostsFile + hostsPath string + width int + height int + mode Mode + focus pane } -// NewModel constructs the TUI model from a parsed HostsFile. -func NewModel(hf *core.HostsFile) Model { +// NewModel constructs the TUI model from a parsed HostsFile and its path. +func NewModel(hf *core.HostsFile, path string) Model { items := make([]list.Item, len(hf.Entries)) for i, e := range hf.Entries { items[i] = entryItem{entry: e} @@ -63,11 +64,12 @@ func NewModel(hf *core.HostsFile) Model { l.SetShowPagination(false) m := Model{ - list: l, - detail: viewport.New(0, 0), - hosts: hf, - mode: ViewMode, - focus: listPane, + list: l, + detail: viewport.New(0, 0), + hosts: hf, + hostsPath: path, + mode: ViewMode, + focus: listPane, } m.refreshDetail() return m diff --git a/internal/tui/update.go b/internal/tui/update.go index 8092961..7180492 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -1,6 +1,10 @@ package tui import ( + "fmt" + + "hosts-go/internal/core" + tea "github.com/charmbracelet/bubbletea" ) @@ -24,6 +28,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { m.mode = ViewMode } + case "ctrl+s": + if m.mode == EditMode && m.hostsPath != "" { + if err := core.WriteHostsFile(m.hostsPath, m.hosts); err != nil { + fmt.Println("save failed:", err) + } + } case " ": if m.mode == EditMode { if entry := m.SelectedEntry(); entry != nil { diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 5a6e125..aa36b71 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -53,7 +53,7 @@ - 🔄 Entry modification forms with validation 2. **File Integration** - - 🔄 Connect TUI with existing parser functionality for writes + - ✅ Connect TUI with parser to persist changes (Ctrl+S) - 🔄 Real-time display of actual `/etc/hosts` content - 🔄 Live validation and formatting preview diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 13272c7..42f055d 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -57,7 +57,7 @@ - [ ] **Permission handling**: Request sudo access when entering edit mode - [x] **Entry modification**: Toggle active status - [ ] **Entry modification**: Add, edit, delete operations -- [ ] **File writing**: Atomic updates with backup and rollback +- [x] **File writing**: Atomic updates with backup and rollback - [ ] **Input validation**: Real-time validation of IP and hostname inputs ### 🌐 Advanced Features (Phase 4) @@ -77,10 +77,10 @@ ### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)** - **Completion**: ~80% (parser, TUI, and basic edit mode implemented) -- **Active work**: Expand edit mode with file integration and advanced editing +- **Active work**: Expand edit mode with advanced editing features - **Blockers**: None - comprehensive parser foundation with 54 tests completed - **Parser status**: Production-ready with all safety features implemented -- **Key bindings updated**: `ctrl+e` for edit mode, spacebar to toggle entries +- **Key bindings updated**: `ctrl+e` for edit mode, spacebar to toggle entries, `ctrl+s` to save ### Development Readiness - ✅ **Architecture designed**: Clear technical approach documented diff --git a/tests/tui_test.go b/tests/tui_test.go index ae7ce72..2e1d0ed 100644 --- a/tests/tui_test.go +++ b/tests/tui_test.go @@ -1,6 +1,7 @@ package tests import ( + "os" "strings" "testing" @@ -19,7 +20,7 @@ func TestModelSelection(t *testing.T) { hf, _, err := core.ParseHostsContent(lines) require.NoError(t, err) - m := tui.NewModel(hf) + m := tui.NewModel(hf, "") require.NotNil(t, m.SelectedEntry()) assert.Equal(t, "localhost", m.SelectedEntry().Hostname) @@ -36,7 +37,7 @@ func TestViewModeStatusBar(t *testing.T) { hf, _, err := core.ParseHostsContent(lines) require.NoError(t, err) - m := tui.NewModel(hf) + m := tui.NewModel(hf, "") nm, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 20}) m = nm.(tui.Model) view := m.View() @@ -52,7 +53,7 @@ func TestPaneSwitching(t *testing.T) { hf, _, err := core.ParseHostsContent(lines) require.NoError(t, err) - m := tui.NewModel(hf) + m := tui.NewModel(hf, "") // Switch focus to detail pane nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyTab}) m = nm.(tui.Model) @@ -69,7 +70,7 @@ func TestEditModeToggleAndActivation(t *testing.T) { hf, _, err := core.ParseHostsContent(lines) require.NoError(t, err) - m := tui.NewModel(hf) + m := tui.NewModel(hf, "") // enter edit mode nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlE}) @@ -85,3 +86,27 @@ func TestEditModeToggleAndActivation(t *testing.T) { view := m.View() assert.Contains(t, view, "EDIT MODE") } + +func TestSaveWritesToFile(t *testing.T) { + content := "127.0.0.1 localhost\n192.168.1.10 example.com\n" + tmp, err := os.CreateTemp(t.TempDir(), "hosts") + require.NoError(t, err) + _, err = tmp.WriteString(content) + require.NoError(t, err) + require.NoError(t, tmp.Close()) + + hf, _, err := core.ParseHostsFile(tmp.Name()) + require.NoError(t, err) + + m := tui.NewModel(hf, tmp.Name()) + + nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlE}) + m = nm.(tui.Model) + nm, _ = m.Update(tea.KeyMsg{Type: tea.KeySpace}) + m = nm.(tui.Model) + _, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlS}) + + saved, _, err := core.ParseHostsFile(tmp.Name()) + require.NoError(t, err) + assert.False(t, saved.Entries[0].Active) +}