mirror of
https://github.com/shokinn/hosts-go.git
synced 2025-08-23 16:43:02 +00:00
Implement TUI pane navigation
This commit is contained in:
parent
3561d15858
commit
a24041d664
6 changed files with 137 additions and 73 deletions
|
@ -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,
|
||||||
hosts: hf,
|
detail: viewport.New(0, 0),
|
||||||
mode: ViewMode,
|
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.
|
// Init satisfies tea.Model.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if m.focus == listPane {
|
||||||
|
m.list, cmd = m.list.Update(msg)
|
||||||
|
m.refreshDetail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.list, cmd = m.list.Update(msg)
|
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,41 +2,29 @@ package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
statusStyle = lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.Color("240")).Background(lipgloss.Color("236"))
|
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.
|
// 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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue