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()
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 {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

View File

@@ -1173,32 +1173,37 @@ func (m Model) renderTermInput() string {
}
func (m Model) renderDashboard() string {
var b strings.Builder
colWidth := m.width / 2
if colWidth < 30 {
colWidth = 30
}
b.WriteString(sectionStyle.Render("System"))
b.WriteString("\n")
var left, right strings.Builder
left.WriteString(sectionStyle.Render("System"))
left.WriteString("\n")
if m.scanResult != nil {
sysInfo := m.scanResult.System.String()
b.WriteString(" ")
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(sysInfo))
left.WriteString(" ")
left.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#E0E0E0")).Render(sysInfo))
}
b.WriteString("\n\n")
left.WriteString("\n\n")
b.WriteString(sectionStyle.Render("Tools"))
b.WriteString("\n")
left.WriteString(sectionStyle.Render("Tools"))
left.WriteString("\n")
if m.scanResult != nil {
installed := 0
total := len(m.scanResult.Tools)
for _, t := range m.scanResult.Tools {
if t.Installed {
installed++
b.WriteString(" ")
b.WriteString(itemOKStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, extractVersion(t.Version)))
left.WriteString(" ")
left.WriteString(itemOKStyle.Render(" "))
left.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, extractVersion(t.Version)))
} else {
b.WriteString(" ")
b.WriteString(itemMissingStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, itemPendingStyle.Render("(not installed)")))
left.WriteString(" ")
left.WriteString(itemMissingStyle.Render(" "))
left.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, itemPendingStyle.Render("(not installed)")))
}
}
barWidth := 20
@@ -1208,30 +1213,13 @@ func (m Model) renderDashboard() string {
}
bar := lipgloss.NewStyle().Foreground(successColor).Render(strings.Repeat("█", pct)) +
lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("░", barWidth-pct))
b.WriteString(fmt.Sprintf("\n %s %d/%d tools installed\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(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total))
}
left.WriteString("\n")
if m.installing {
b.WriteString(sectionStyle.Render("Installing..."))
b.WriteString("\n")
left.WriteString(sectionStyle.Render("Installing..."))
left.WriteString("\n")
progBar := m.progressBar.View()
label := ""
if m.installTool != "" {
@@ -1239,19 +1227,21 @@ func (m Model) renderDashboard() string {
} else {
label = fmt.Sprintf(" %d/%d", m.installCurrent, m.installTotal)
}
b.WriteString(fmt.Sprintf(" %s%s\n", progBar, label))
b.WriteString("\n")
left.WriteString(fmt.Sprintf(" %s%s\n", progBar, label))
left.WriteString("\n")
}
if len(m.installLog) > 0 {
b.WriteString(sectionStyle.Render("Install Log"))
b.WriteString("\n")
left.WriteString(sectionStyle.Render("Install Log"))
left.WriteString("\n")
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 {
key string
desc string
@@ -1262,68 +1252,77 @@ func (m Model) renderDashboard() string {
{"l", "Scan LSP servers"},
{"m", "Configure MCP servers"},
}
b.WriteString(sectionStyle.Render("Quick Actions"))
b.WriteString("\n")
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+"]"),
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 {
b.WriteString(sectionStyle.Render("LSP Servers"))
b.WriteString("\n")
right.WriteString(sectionStyle.Render("LSP Servers"))
right.WriteString("\n")
lspInstalled := 0
for _, s := range m.lspServers {
if s.Installed {
lspInstalled++
b.WriteString(" ")
b.WriteString(itemOKStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-28s (%s)\n", s.Name, s.Language))
right.WriteString(" ")
right.WriteString(itemOKStyle.Render(" "))
right.WriteString(fmt.Sprintf(" %-22s (%s)\n", s.Name, s.Language))
} else {
b.WriteString(" ")
b.WriteString(itemPendingStyle.Render(" "))
b.WriteString(fmt.Sprintf(" %-28s (%s)\n", s.Name, s.Language))
right.WriteString(" ")
right.WriteString(itemPendingStyle.Render(" "))
right.WriteString(fmt.Sprintf(" %-22s (%s)\n", s.Name, s.Language))
}
}
b.WriteString(fmt.Sprintf("\n Installed: %d/%d\n", lspInstalled, len(m.lspServers)))
b.WriteString("\n")
right.WriteString(fmt.Sprintf("\n Installed: %d/%d\n", lspInstalled, len(m.lspServers)))
right.WriteString("\n")
}
if m.daemon != nil {
b.WriteString(sectionStyle.Render("Update Daemon"))
b.WriteString("\n")
right.WriteString(sectionStyle.Render("Daemon"))
right.WriteString("\n")
if m.daemon.IsRunning() {
b.WriteString(" ")
b.WriteString(itemOKStyle.Render("running"))
right.WriteString(" ")
right.WriteString(itemOKStyle.Render("running"))
lastCheck := m.daemon.LastCheck()
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 {
b.WriteString(" ")
b.WriteString(itemPendingStyle.Render("stopped"))
right.WriteString(" ")
right.WriteString(itemPendingStyle.Render("stopped"))
}
b.WriteString("\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")
right.WriteString("\n\n")
}
mcpStatus := itemPendingStyle.Render("not configured")
if m.mcpConfigured {
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 {