1package styles
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/charmbracelet/lipgloss"
8)
9
10const (
11 statusYes = "Yes"
12 statusNo = "No"
13)
14
15type DoctorRenderer struct {
16 theme *Theme
17}
18
19func NewDoctorRenderer(theme *Theme) *DoctorRenderer {
20 return &DoctorRenderer{theme: theme}
21}
22
23type DoctorReport struct {
24 OverallOK bool
25 Runtime DoctorRuntimeReport
26 Media *DoctorMediaReport
27}
28
29type DoctorRuntimeReport struct {
30 Prefix string
31 OK bool
32 Checks []DoctorRuntimeCheck
33}
34
35type DoctorRuntimeCheck struct {
36 Name string
37 PkgConfigName string
38 Installed bool
39 Version string
40 RequiredVersion string
41 OK bool
42 Error string
43}
44
45type DoctorMediaReport struct {
46 GStreamerAvailable bool
47 HWAccelAvailable bool
48 AV1HWAvailable bool
49
50 HasVAPlugin bool
51 HasVAAPIPlugin bool
52 HasNVCodecPlugin bool
53
54 AV1Decoders []string
55 H264Decoders []string
56 H265Decoders []string
57 VP9Decoders []string
58
59 VAAPIAvailable bool
60 VAAPIDriver string
61 VAAPIVersion string
62
63 Warnings []string
64}
65
66func (r *DoctorRenderer) Render(report DoctorReport) string {
67 header := r.renderHeader(report.OverallOK)
68
69 sections := []string{}
70 if len(report.Runtime.Checks) > 0 || strings.TrimSpace(report.Runtime.Prefix) != "" {
71 sections = append(sections, r.renderRuntime(report.Runtime))
72 }
73 if report.Media != nil {
74 sections = append(sections, r.renderMedia(*report.Media))
75 }
76
77 return lipgloss.JoinVertical(lipgloss.Left, header, "", strings.Join(sections, "\n\n"))
78}
79
80func (r *DoctorRenderer) renderHeader(ok bool) string {
81 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
82 statusStyle := r.theme.SuccessStyle
83 statusText := "OK"
84 if !ok {
85 statusStyle = r.theme.WarningStyle
86 statusText = "Needs attention"
87 }
88
89 title := fmt.Sprintf("%s %s", iconStyle.Render(IconDoctor), r.theme.Title.Render("Doctor"))
90 badge := r.theme.BadgeMuted.Render(statusStyle.Render(statusText))
91 return lipgloss.JoinHorizontal(lipgloss.Center, title, " ", badge)
92}
93
94func (r *DoctorRenderer) renderRuntime(rt DoctorRuntimeReport) string {
95 lines := make([]string, 0, len(rt.Checks)+2)
96
97 if strings.TrimSpace(rt.Prefix) != "" {
98 lines = append(lines, fmt.Sprintf(
99 "%s %s %s",
100 r.theme.Subtle.Render("Prefix"),
101 r.theme.Normal.Render(rt.Prefix),
102 r.theme.Subtle.Render("(runtime override)"),
103 ))
104 }
105
106 for _, c := range rt.Checks {
107 lines = append(lines, r.renderRuntimeCheck(c))
108 }
109
110 body := strings.Join(lines, "\n")
111 return r.theme.Box.Render(r.theme.BoxHeader.Render(fmt.Sprintf("%s Runtime", r.theme.Highlight.Render(IconPackage))) + "\n" + body)
112}
113
114func (r *DoctorRenderer) renderRuntimeCheck(c DoctorRuntimeCheck) string {
115 icon := IconCheck
116 statusStyle := r.theme.SuccessStyle
117 status := "OK"
118
119 summary := ""
120 if !c.Installed {
121 icon = IconX
122 statusStyle = r.theme.ErrorStyle
123 status = "Missing"
124 summary = c.Error
125 } else if !c.OK {
126 icon = IconWarning
127 statusStyle = r.theme.WarningStyle
128 status = "Too old"
129 summary = fmt.Sprintf("have %s, need >= %s", c.Version, c.RequiredVersion)
130 } else {
131 summary = fmt.Sprintf("%s (>= %s)", c.Version, c.RequiredVersion)
132 }
133
134 name := r.theme.Normal.Render(c.Name)
135 badge := r.theme.BadgeMuted.Render(statusStyle.Render(status))
136 info := r.theme.Subtle.Render(summary)
137
138 return fmt.Sprintf("%s %s %s\n %s", statusStyle.Render(icon), name, badge, info)
139}
140
141func (r *DoctorRenderer) renderMedia(m DoctorMediaReport) string {
142 lines := []string{}
143
144 gstIcon := IconCheck
145 gstStyle := r.theme.SuccessStyle
146 gstText := statusYes
147 if !m.GStreamerAvailable {
148 gstIcon = IconX
149 gstStyle = r.theme.ErrorStyle
150 gstText = statusNo
151 }
152 lines = append(lines, fmt.Sprintf("%s %s %s", gstStyle.Render(gstIcon), r.theme.Subtle.Render("GStreamer"), gstStyle.Render(gstText)))
153
154 hwIcon := IconWarning
155 hwStyle := r.theme.WarningStyle
156 hwText := statusNo
157 if m.HWAccelAvailable {
158 hwIcon = IconCheck
159 hwStyle = r.theme.SuccessStyle
160 hwText = statusYes
161 }
162 lines = append(lines, fmt.Sprintf("%s %s %s", hwStyle.Render(hwIcon), r.theme.Subtle.Render("HW decode"), hwStyle.Render(hwText)))
163
164 av1Icon := IconWarning
165 av1Style := r.theme.WarningStyle
166 av1Text := statusNo
167 if m.AV1HWAvailable {
168 av1Icon = IconCheck
169 av1Style = r.theme.SuccessStyle
170 av1Text = statusYes
171 }
172 lines = append(lines, fmt.Sprintf(
173 "%s %s %s %s",
174 av1Style.Render(av1Icon),
175 r.theme.Subtle.Render("AV1"),
176 av1Style.Render(av1Text),
177 r.theme.Subtle.Render("(preferred)"),
178 ))
179
180 plugins := []string{
181 fmt.Sprintf("%s VA (stateless): %s", r.theme.Subtle.Render("•"), pluginStatus(r.theme, m.HasVAPlugin, "recommended")),
182 fmt.Sprintf("%s VAAPI (legacy): %s", r.theme.Subtle.Render("•"), pluginStatus(r.theme, m.HasVAAPIPlugin, "gstreamer-vaapi")),
183 fmt.Sprintf("%s NVCodec: %s", r.theme.Subtle.Render("•"), pluginStatus(r.theme, m.HasNVCodecPlugin, "NVIDIA")),
184 }
185
186 lines = append(lines, "", r.theme.Subtle.Render("Plugins"), strings.Join(plugins, "\n"))
187
188 if m.VAAPIAvailable {
189 lines = append(lines, "", fmt.Sprintf("%s %s", r.theme.Subtle.Render("VA-API Driver"), r.theme.Normal.Render(m.VAAPIDriver)))
190 if strings.TrimSpace(m.VAAPIVersion) != "" {
191 lines = append(lines, fmt.Sprintf("%s %s", r.theme.Subtle.Render("VA-API Version"), r.theme.Normal.Render(m.VAAPIVersion)))
192 }
193 }
194
195 if len(m.Warnings) > 0 {
196 warnLines := make([]string, 0, len(m.Warnings))
197 for _, w := range m.Warnings {
198 warnLines = append(warnLines, fmt.Sprintf("%s %s", r.theme.WarningStyle.Render(IconWarning), r.theme.Normal.Render(w)))
199 }
200 lines = append(lines, "", r.theme.WarningStyle.Render("Warnings"), strings.Join(warnLines, "\n"))
201 }
202
203 body := strings.Join(lines, "\n")
204 return r.theme.Box.Render(r.theme.BoxHeader.Render(fmt.Sprintf("%s Media", r.theme.Highlight.Render(IconVideo))) + "\n" + body)
205}
206
207func pluginStatus(theme *Theme, ok bool, hint string) string {
208 if ok {
209 if hint != "" {
210 return theme.SuccessStyle.Render("Available") + " " + theme.Subtle.Render("("+hint+")")
211 }
212 return theme.SuccessStyle.Render("Available")
213 }
214 if hint != "" {
215 return theme.WarningStyle.Render("Missing") + " " + theme.Subtle.Render("("+hint+")")
216 }
217 return theme.WarningStyle.Render("Missing")
218}