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 (
"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,47 @@ 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.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
}

View file

@ -2,41 +2,29 @@ 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()
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)
}
detailView := m.detail.View()
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)
status := fmt.Sprintf("VIEW MODE • %d entries", len(m.hosts.Entries))