Merge pull request #3 from shokinn/codex/complete-implementation-of-phase-2

Finalize Phase 2 TUI with pane navigation
This commit is contained in:
Philip Henning 2025-08-13 16:22:01 +02:00 committed by GitHub
commit 0c60248d75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 175 additions and 73 deletions

View file

@ -3,8 +3,10 @@ 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"
)
@ -30,12 +32,21 @@ const (
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.
@ -51,11 +62,39 @@ func NewModel(hf *core.HostsFile) Model {
l.SetShowHelp(false)
l.SetShowPagination(false)
return Model{
list: l,
hosts: hf,
mode: ViewMode,
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.

View file

@ -12,13 +12,65 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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 "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
m.list.SetSize(msg.Width/2, msg.Height)
}
m.height = msg.Height - 1
m.list, cmd = m.list.Update(msg)
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

@ -2,43 +2,51 @@ package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
var (
listStyle = lipgloss.NewStyle().Padding(0, 1)
detailStyle = lipgloss.NewStyle().Padding(0, 1)
statusStyle = lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.Color("240")).Background(lipgloss.Color("236"))
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()
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)
// 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
}
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())
panes := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
// 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)
status := fmt.Sprintf("VIEW MODE • %d entries", len(m.hosts.Entries))
bar := statusStyle.Width(m.width).Render(status)
return lipgloss.JoinVertical(lipgloss.Left, panes, bar)

View file

@ -2,8 +2,8 @@
## Current Work Focus
**Status**: Phase 2 In Progress - Basic TUI prototype implemented
**Priority**: Expand Bubble Tea TUI with navigation and view features
**Status**: Phase 2 Complete - Basic TUI prototype finished
**Priority**: Begin Phase 3 - Edit mode and file integration
## Recent Changes
@ -17,6 +17,12 @@
- ✅ **Comprehensive testing**: 54 comprehensive tests covering all parser functionality (100% passing)
- ✅ **Demo application**: Full showcase of parser capabilities with real-world examples
### Phase 2: TUI Prototype (COMPLETED) ✅
- ✅ **Two-pane layout** with list and detail panes
- ✅ **Navigation system** with pane switching and detail scrolling
- ✅ **View mode** showing entry details and status bar
- ✅ **Parser integration** loading `/etc/hosts` into the UI
### Parser Capabilities Achieved
- ✅ **Standard entries**: IPv4, IPv6, multiple aliases, inline comments
- ✅ **Disabled entries**: Commented lines with `# IP hostname` format detection
@ -35,33 +41,14 @@
## Next Steps
### Immediate (Phase 2 - Current Priority)
1. **TUI Architecture Design**
- Main Bubble Tea model with list and detail panes
- State tracks selection and window size
2. **Two-Pane Layout Implementation**
- Left pane: entry list using Bubbles list component
- Right pane: detail view of selected entry
- Responsive sizing handled in update
3. **Navigation System** 🟡
- Basic keyboard navigation via list component
- Need scroll handling and pane switching
4. **View Mode Implementation** 🟡
- Read-only display of `/etc/hosts` entries
- Status bar and active/inactive indicators in list implemented
- Further styling improvements pending
### Medium-term (Phase 3)
### Immediate (Phase 3 - Current Priority)
1. **Edit Mode Implementation**
- Explicit mode transition with visual indicators
- Permission handling with sudo request
- Entry modification forms with validation
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
- Live validation and formatting preview

View file

@ -44,13 +44,13 @@
## What's Left to Build
### 🎨 Basic TUI (Phase 2 - Current Priority)
### 🎨 Basic TUI (Phase 2 - Completed)
- [x] **Main Bubble Tea model**: Core application state and structure
- [x] **Two-pane layout**: Left list + right detail view
- [x] **Entry list display**: Show IP and hostname columns
- [x] **Entry selection**: Navigate and select entries with keyboard
- [x] **View mode**: Safe browsing with status bar and active/inactive indicators
- [ ] **Integration**: Connect TUI with existing parser functionality
- [x] **View mode**: Safe browsing with status bar and active/inactive indicators
- [x] **Integration**: Connect TUI with existing parser functionality
### 🔧 Edit Functionality (Phase 3)
- [ ] **Edit mode transition**: Explicit mode switching with visual indicators
@ -74,9 +74,9 @@
## Current Status
### Project Phase: **Phase 1 Complete → Phase 2 (TUI Implementation)**
- **Completion**: ~65% (foundation and complete core parser functionality implemented)
- **Active work**: Ready to implement Bubble Tea TUI with two-pane layout (Phase 2)
### Project Phase: **Phase 2 Complete → Phase 3 (Edit Mode Implementation)**
- **Completion**: ~75% (parser and basic TUI implemented)
- **Active work**: Begin edit mode and file integration (Phase 3)
- **Blockers**: None - comprehensive parser foundation with 54 tests completed
- **Parser status**: Production-ready with all safety features implemented
@ -132,10 +132,10 @@
## Success Metrics & Milestones
### Milestone 1: Basic Functionality (Target: Week 1)
- [ ] Parse and display existing hosts file entries
- [ ] Navigate entries with keyboard
- [ ] View entry details in right pane
- **Success criteria**: Can safely browse hosts file without editing
- [x] Parse and display existing hosts file entries
- [x] Navigate entries with keyboard
- [x] View entry details in right pane
- **Success criteria**: Can safely browse hosts file without editing (achieved)
### Milestone 2: Core Editing (Target: Week 2)
- [ ] Toggle entries active/inactive
@ -179,15 +179,14 @@
19. ✅ **Verify atomic operations** (temp files with rollback for safe writes)
20. ✅ **Complete parser documentation** (comprehensive demo showing all capabilities)
### 🚧 NEXT Phase 2 Actions (TUI Implementation)
1. **Design TUI architecture** following Bubble Tea MVU pattern
2. **Create main application model** with state management
3. **Implement two-pane layout** (entry list + detail view)
4. **Add navigation controls** (keyboard-driven interaction)
5. **Integrate parser functionality** with TUI display
6. **Implement view mode** (safe browsing without modifications)
### 🚧 NEXT Phase 3 Actions (Edit Mode & File Integration)
1. **Edit mode transition** with explicit mode switching
2. **Permission handling** requesting sudo when entering edit mode
3. **Entry modification** forms for add/edit/delete/toggle
4. **File writing** with backups and atomic updates
5. **Input validation** in real time
**Phase 1 is fully complete with a production-ready parser foundation.**
**Phase 2 delivered a functional view-only TUI.**
### Parser Achievement Summary
- **54 comprehensive tests** covering all hosts file variations and edge cases
@ -197,4 +196,4 @@
- **Search and management** capabilities for finding and manipulating entries
- **Demo application** showcasing all functionality with realistic examples
The foundation is robust, tested, and ready for TUI implementation in Phase 2.
The foundation is robust, tested, and ready for edit mode implementation in Phase 3.

View file

@ -45,3 +45,20 @@ func TestViewModeStatusBar(t *testing.T) {
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)
}