Implement TUI pane navigation

This commit is contained in:
Philip Henning 2025-08-13 15:57:35 +02:00
parent 3561d15858
commit a24041d664
6 changed files with 137 additions and 73 deletions

View file

@ -3,8 +3,10 @@ package tui
import ( import (
"fmt" "fmt"
"hosts-go/internal/core" "hosts-go/internal/core"
"strings"
list "github.com/charmbracelet/bubbles/list" list "github.com/charmbracelet/bubbles/list"
viewport "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@ -30,12 +32,21 @@ const (
EditMode EditMode
) )
type pane int
const (
listPane pane = iota
detailPane
)
type Model struct { type Model struct {
list list.Model list list.Model
detail viewport.Model
hosts *core.HostsFile hosts *core.HostsFile
width int width int
height int height int
mode Mode mode Mode
focus pane
} }
// NewModel constructs the TUI model from a parsed HostsFile. // NewModel constructs the TUI model from a parsed HostsFile.
@ -51,11 +62,39 @@ func NewModel(hf *core.HostsFile) Model {
l.SetShowHelp(false) l.SetShowHelp(false)
l.SetShowPagination(false) l.SetShowPagination(false)
return Model{ m := Model{
list: l, list: l,
detail: viewport.New(0, 0),
hosts: hf, hosts: hf,
mode: ViewMode, 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. // Init satisfies tea.Model.

View file

@ -12,13 +12,47 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "q", "ctrl+c": case "q", "ctrl+c":
return m, tea.Quit 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: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height - 1
m.list.SetSize(msg.Width/2, msg.Height) m.list.SetSize(msg.Width/2, m.height)
} m.detail.Width = msg.Width - msg.Width/2
m.detail.Height = m.height
if m.focus == listPane {
m.list, cmd = m.list.Update(msg) m.list, cmd = m.list.Update(msg)
}
default:
if m.focus == listPane {
m.list, cmd = m.list.Update(msg)
m.refreshDetail()
}
}
return m, cmd return m, cmd
} }

View file

@ -2,7 +2,6 @@ package tui
import ( import (
"fmt" "fmt"
"strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@ -10,33 +9,22 @@ import (
var ( var (
listStyle = lipgloss.NewStyle().Padding(0, 1) listStyle = lipgloss.NewStyle().Padding(0, 1)
detailStyle = 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")) statusStyle = lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.Color("240")).Background(lipgloss.Color("236"))
) )
// View renders the two-pane layout. // View renders the two-pane layout.
func (m Model) View() string { func (m Model) View() string {
listView := m.list.View() 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)
}
left := listStyle.Width(m.width / 2).Height(m.height).Render(listView) 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()) right := detailStyle.Width(m.width - m.width/2).Height(m.height).Render(detailView)
if m.focus == listPane {
left = focusedStyle.Render(left)
} else {
right = focusedStyle.Render(right)
}
panes := lipgloss.JoinHorizontal(lipgloss.Top, left, right) panes := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
status := fmt.Sprintf("VIEW MODE • %d entries", len(m.hosts.Entries)) status := fmt.Sprintf("VIEW MODE • %d entries", len(m.hosts.Entries))

View file

@ -2,8 +2,8 @@
## Current Work Focus ## Current Work Focus
**Status**: Phase 2 In Progress - Basic TUI prototype implemented **Status**: Phase 2 Complete - Basic TUI prototype finished
**Priority**: Expand Bubble Tea TUI with navigation and view features **Priority**: Begin Phase 3 - Edit mode and file integration
## Recent Changes ## Recent Changes
@ -17,6 +17,12 @@
- ✅ **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
### 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,33 +41,14 @@
## Next Steps ## Next Steps
### Immediate (Phase 2 - Current Priority) ### Immediate (Phase 3 - 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)
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

View file

@ -44,13 +44,13 @@
## What's Left to Build ## 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] **Main Bubble Tea model**: Core application state and structure
- [x] **Two-pane layout**: Left list + right detail view - [x] **Two-pane layout**: Left list + right detail view
- [x] **Entry list display**: Show IP and hostname columns - [x] **Entry list display**: Show IP and hostname columns
- [x] **Entry selection**: Navigate and select entries with keyboard - [x] **Entry selection**: Navigate and select entries with keyboard
- [x] **View mode**: Safe browsing with status bar and active/inactive indicators - [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 - [ ] **Edit mode transition**: Explicit mode switching with visual indicators
@ -74,9 +74,9 @@
## 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**: ~75% (parser and basic TUI implemented)
- **Active work**: Ready to implement Bubble Tea TUI with two-pane layout (Phase 2) - **Active work**: Begin edit mode and file integration (Phase 3)
- **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 +132,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 +179,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 +196,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.

View file

@ -45,3 +45,20 @@ func TestViewModeStatusBar(t *testing.T) {
assert.Contains(t, view, "[✓] localhost") assert.Contains(t, view, "[✓] localhost")
assert.Contains(t, view, "[ ] example.com") 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)
}