fix: enable text selection, dashboard multi-column layout
All checks were successful
CI / build (push) Successful in 1m44s

- Remove WithMouseCellMotion to allow text selection in terminal
- Dashboard now uses 2-column layout (left: system/tools, right: actions/status)
- More compact dashboard with better space usage

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-20 00:33:12 +02:00
parent 2d6fc64b18
commit 82b281641d
2 changed files with 74 additions and 75 deletions

View File

@@ -108,7 +108,7 @@ func runTUI() {
result := scanner.ScanSystem() result := scanner.ScanSystem()
model := tui.NewModel(cfg, result) model := tui.NewModel(cfg, result)
p := tea.NewProgram(model, tea.WithAltScreen(), tea.WithMouseCellMotion()) p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)

View File

@@ -1173,32 +1173,37 @@ func (m Model) renderTermInput() string {
} }
func (m Model) renderDashboard() string { func (m Model) renderDashboard() string {
var b strings.Builder colWidth := m.width / 2
if colWidth < 30 {
colWidth = 30
}
b.WriteString(sectionStyle.Render("System")) var left, right strings.Builder
b.WriteString("\n")
left.WriteString(sectionStyle.Render("System"))
left.WriteString("\n")
if m.scanResult != nil { if m.scanResult != nil {
sysInfo := m.scanResult.System.String() sysInfo := m.scanResult.System.String()
b.WriteString(" ") left.WriteString(" ")
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(sysInfo)) left.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(sysInfo))
} }
b.WriteString("\n\n") left.WriteString("\n\n")
b.WriteString(sectionStyle.Render("Tools")) left.WriteString(sectionStyle.Render("Tools"))
b.WriteString("\n") left.WriteString("\n")
if m.scanResult != nil { if m.scanResult != nil {
installed := 0 installed := 0
total := len(m.scanResult.Tools) total := len(m.scanResult.Tools)
for _, t := range m.scanResult.Tools { for _, t := range m.scanResult.Tools {
if t.Installed { if t.Installed {
installed++ installed++
b.WriteString(" ") left.WriteString(" ")
b.WriteString(itemOKStyle.Render(" ")) left.WriteString(itemOKStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, extractVersion(t.Version))) left.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, extractVersion(t.Version)))
} else { } else {
b.WriteString(" ") left.WriteString(" ")
b.WriteString(itemMissingStyle.Render(" ")) left.WriteString(itemMissingStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, itemPendingStyle.Render("(not installed)"))) left.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, itemPendingStyle.Render("(not installed)")))
} }
} }
barWidth := 20 barWidth := 20
@@ -1208,30 +1213,13 @@ func (m Model) renderDashboard() string {
} }
bar := lipgloss.NewStyle().Foreground(successColor).Render(strings.Repeat("█", pct)) + bar := lipgloss.NewStyle().Foreground(successColor).Render(strings.Repeat("█", pct)) +
lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("░", barWidth-pct)) lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("░", barWidth-pct))
b.WriteString(fmt.Sprintf("\n %s %d/%d tools installed\n", bar, installed, total)) left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total))
}
b.WriteString("\n")
if len(m.updateStatus) > 0 {
b.WriteString(sectionStyle.Render("Updates"))
b.WriteString("\n")
for _, s := range m.updateStatus {
if s.NeedsUpdate {
b.WriteString(" ")
b.WriteString(itemWarnStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %s: %s -> %s\n", s.Tool, s.Current, s.Latest))
} else if s.Error == "" {
b.WriteString(" ")
b.WriteString(itemOKStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %s: up to date\n", s.Tool))
}
}
b.WriteString("\n")
} }
left.WriteString("\n")
if m.installing { if m.installing {
b.WriteString(sectionStyle.Render("Installing...")) left.WriteString(sectionStyle.Render("Installing..."))
b.WriteString("\n") left.WriteString("\n")
progBar := m.progressBar.View() progBar := m.progressBar.View()
label := "" label := ""
if m.installTool != "" { if m.installTool != "" {
@@ -1239,19 +1227,21 @@ func (m Model) renderDashboard() string {
} else { } else {
label = fmt.Sprintf(" %d/%d", m.installCurrent, m.installTotal) label = fmt.Sprintf(" %d/%d", m.installCurrent, m.installTotal)
} }
b.WriteString(fmt.Sprintf(" %s%s\n", progBar, label)) left.WriteString(fmt.Sprintf(" %s%s\n", progBar, label))
b.WriteString("\n") left.WriteString("\n")
} }
if len(m.installLog) > 0 { if len(m.installLog) > 0 {
b.WriteString(sectionStyle.Render("Install Log")) left.WriteString(sectionStyle.Render("Install Log"))
b.WriteString("\n") left.WriteString("\n")
for _, l := range m.installLog { for _, l := range m.installLog {
b.WriteString(l + "\n") left.WriteString(l + "\n")
} }
b.WriteString("\n") left.WriteString("\n")
} }
right.WriteString(sectionStyle.Render("Quick Actions"))
right.WriteString("\n")
actions := []struct { actions := []struct {
key string key string
desc string desc string
@@ -1262,68 +1252,77 @@ func (m Model) renderDashboard() string {
{"l", "Scan LSP servers"}, {"l", "Scan LSP servers"},
{"m", "Configure MCP servers"}, {"m", "Configure MCP servers"},
} }
b.WriteString(sectionStyle.Render("Quick Actions"))
b.WriteString("\n")
for _, a := range actions { for _, a := range actions {
b.WriteString(fmt.Sprintf(" %s %s\n", right.WriteString(fmt.Sprintf(" %s %s\n",
lipgloss.NewStyle().Foreground(baseColor).Bold(true).Render("["+a.key+"]"), lipgloss.NewStyle().Foreground(baseColor).Bold(true).Render("["+a.key+"]"),
a.desc)) a.desc))
} }
b.WriteString("\n") right.WriteString("\n")
if len(m.updateStatus) > 0 {
right.WriteString(sectionStyle.Render("Updates"))
right.WriteString("\n")
for _, s := range m.updateStatus {
if s.NeedsUpdate {
right.WriteString(" ")
right.WriteString(itemWarnStyle.Render(" "))
right.WriteString(fmt.Sprintf(" %s: %s -> %s\n", s.Tool, s.Current, s.Latest))
} else if s.Error == "" {
right.WriteString(" ")
right.WriteString(itemOKStyle.Render(" "))
right.WriteString(fmt.Sprintf(" %s: up to date\n", s.Tool))
}
}
right.WriteString("\n")
}
if len(m.lspServers) > 0 { if len(m.lspServers) > 0 {
b.WriteString(sectionStyle.Render("LSP Servers")) right.WriteString(sectionStyle.Render("LSP Servers"))
b.WriteString("\n") right.WriteString("\n")
lspInstalled := 0 lspInstalled := 0
for _, s := range m.lspServers { for _, s := range m.lspServers {
if s.Installed { if s.Installed {
lspInstalled++ lspInstalled++
b.WriteString(" ") right.WriteString(" ")
b.WriteString(itemOKStyle.Render(" ")) right.WriteString(itemOKStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-28s (%s)\n", s.Name, s.Language)) right.WriteString(fmt.Sprintf(" %-22s (%s)\n", s.Name, s.Language))
} else { } else {
b.WriteString(" ") right.WriteString(" ")
b.WriteString(itemPendingStyle.Render(" ")) right.WriteString(itemPendingStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-28s (%s)\n", s.Name, s.Language)) right.WriteString(fmt.Sprintf(" %-22s (%s)\n", s.Name, s.Language))
} }
} }
b.WriteString(fmt.Sprintf("\n Installed: %d/%d\n", lspInstalled, len(m.lspServers))) right.WriteString(fmt.Sprintf("\n Installed: %d/%d\n", lspInstalled, len(m.lspServers)))
b.WriteString("\n") right.WriteString("\n")
} }
if m.daemon != nil { if m.daemon != nil {
b.WriteString(sectionStyle.Render("Update Daemon")) right.WriteString(sectionStyle.Render("Daemon"))
b.WriteString("\n") right.WriteString("\n")
if m.daemon.IsRunning() { if m.daemon.IsRunning() {
b.WriteString(" ") right.WriteString(" ")
b.WriteString(itemOKStyle.Render("running")) right.WriteString(itemOKStyle.Render("running"))
lastCheck := m.daemon.LastCheck() lastCheck := m.daemon.LastCheck()
if !lastCheck.IsZero() { if !lastCheck.IsZero() {
b.WriteString(fmt.Sprintf(" last check: %s", lastCheck.Format("15:04:05"))) right.WriteString(fmt.Sprintf(" last: %s", lastCheck.Format("15:04:05")))
} }
} else { } else {
b.WriteString(" ") right.WriteString(" ")
b.WriteString(itemPendingStyle.Render("stopped")) right.WriteString(itemPendingStyle.Render("stopped"))
} }
b.WriteString("\n") right.WriteString("\n\n")
logs := m.daemon.Logs()
if len(logs) > 3 {
logs = logs[len(logs)-3:]
}
for _, l := range logs {
b.WriteString(" " + itemPendingStyle.Render(l) + "\n")
}
b.WriteString("\n")
} }
mcpStatus := itemPendingStyle.Render("not configured") mcpStatus := itemPendingStyle.Render("not configured")
if m.mcpConfigured { if m.mcpConfigured {
mcpStatus = itemOKStyle.Render("configured") mcpStatus = itemOKStyle.Render("configured")
} }
b.WriteString(fmt.Sprintf("MCP Servers: %s\n", mcpStatus)) right.WriteString(fmt.Sprintf("MCP: %s\n", mcpStatus))
return b.String() leftCol := lipgloss.NewStyle().Width(colWidth).Render(left.String())
rightCol := lipgloss.NewStyle().Width(colWidth).Render(right.String())
return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol)
} }
func (m Model) renderChat() string { func (m Model) renderChat() string {