package tui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/lipgloss" "proxyctl/internal/app" ) type dashboardRenderState struct { catalog targetCatalog selectedTargetID string focus dashboardPane detailsOffset int operationProgress map[string]app.TargetProgressPhase applyingFrame string terminalWidth int terminalHeight int } func (p terminalPresentation) renderDashboard(state dashboardRenderState, width, height int) string { if state.terminalWidth >= wideWidth && state.terminalHeight >= wideHeight { leftWidth := (width - 1) * 2 / 5 rightWidth := width - leftWidth - 1 left := p.renderTargetListPane(state, leftWidth, height) right := p.renderTargetDetailsPane(state, rightWidth, height) divider := strings.Repeat("│\n", max(0, height-1)) + "│" return lipgloss.JoinHorizontal(lipgloss.Top, left, divider, right) } listHeight := max(5, height/2) detailsHeight := height - listHeight - 1 if detailsHeight < 4 { detailsHeight = 4 listHeight = max(3, height-detailsHeight-1) } return p.renderTargetListPane(state, width, listHeight) + "\n" + p.renderTargetDetailsPane(state, width, detailsHeight) } func (p terminalPresentation) renderTargetListPane(state dashboardRenderState, width, height int) string { heading := paneHeading("Targets", state.focus == targetListPane) bodyHeight := max(1, height-2) lines := make([]string, 0, len(state.catalog.items)) selectedIndex := state.catalog.index(state.selectedTargetID) for _, item := range state.catalog.items { cursor := " " if item.ID == state.selectedTargetID { cursor = "> " } badge := p.targetBadge(item, state.operationProgress[item.ID], state.applyingFrame) lines = append(lines, cursor+badge+" "+item.Description.Name) } if len(lines) == 0 { lines = append(lines, "No Targets are currently Available.") selectedIndex = 0 } offset := 0 if selectedIndex >= bodyHeight { offset = selectedIndex - bodyHeight + 1 } body := fixedViewport(protectRenderedText(strings.Join(lines, "\n")), width, bodyHeight, offset, false) return fitPane(heading+"\n"+paneRule(width)+"\n"+body, width, height) } func (p terminalPresentation) renderTargetDetailsPane(state dashboardRenderState, width, height int) string { heading := paneHeading("Target details", state.focus == targetDetailsPane) bodyHeight := max(1, height-2) selected, ok := state.catalog.selected(state.selectedTargetID) content := "Select a Target to review its details." if ok { content = p.targetDetails(selected, state.operationProgress[selected.ID], state.applyingFrame) } body := fixedViewport(content, width, bodyHeight, state.detailsOffset, true) return fitPane(heading+"\n"+paneRule(width)+"\n"+body, width, height) } func paneHeading(label string, focused bool) string { if focused { return "▶ " + label + " [focused]" } return " " + label } func paneRule(width int) string { return strings.Repeat("─", max(1, width)) } func fitPane(content string, width, height int) string { return lipgloss.NewStyle().Width(width).Height(height).MaxWidth(width).MaxHeight(height).Render(content) } func fixedViewport(content string, width, height, offset int, wrap bool) string { if wrap { content = lipgloss.NewStyle().Width(width).Render(content) } view := viewport.New(width, height) view.SetContent(strings.TrimRight(content, "\n")) view.SetYOffset(max(0, offset)) return view.View() } func (p terminalPresentation) targetBadge(item app.TargetSnapshot, progress app.TargetProgressPhase, applyingFrame string) string { switch item.DetectionPhase { case app.TargetChecking: return p.theme.checking.Render("… Checking") case app.TargetDetectionFailed: return p.theme.checkFailed.Render("! Failed") } if !item.Available { return p.theme.unavailable.Render("○ Unavailable") } actual := p.theme.renderTargetState(item.State, false) switch progress { case app.TargetProgressRunning: if applyingFrame == "" { applyingFrame = "|" } return p.theme.applying.Render(applyingFrame + " Applying") case app.TargetProgressFailed: return actual + " " + p.theme.checkFailed.Render("! Failed") default: return actual } } func (p terminalPresentation) targetDetails(item app.TargetSnapshot, progress app.TargetProgressPhase, applyingFrame string) string { state := p.targetBadge(item, progress, applyingFrame) evidence := item.Evidence switch item.DetectionPhase { case app.TargetChecking: evidence = "Availability and configuration inspection are in progress." case app.TargetDetectionFailed: if evidence == "" { evidence = item.AvailabilityEvidence } default: if !item.Available { evidence = item.AvailabilityEvidence } } if evidence == "" { evidence = "No detection evidence reported." } var details strings.Builder details.WriteString(p.theme.section.Render(item.Description.Name)) fmt.Fprintf(&details, "\n%s\n%s", state, evidence) action := targetActionLabel(item.PrimaryAction.Kind) if item.PrimaryAction.Available { fmt.Fprintf(&details, "\n\nSpace: %s", action) } else if item.PrimaryAction.DisabledReason != "" { fmt.Fprintf(&details, "\n\nUnavailable: %s", item.PrimaryAction.DisabledReason) } if item.DetectionPhase == app.TargetDetectionFailed && item.DetectionErr != nil { fmt.Fprintf(&details, "\nCheck failed: %s", item.DetectionErr) } if item.Description.Summary != "" { fmt.Fprintf(&details, "\n\n%s", item.Description.Summary) } return protectRenderedText(details.String()) }