main 5041353

doctor.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close