okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Kieran Klukaschore: satisfy the newer gofumpt6d3a341

main
11.4 KiB406 linesraw
1package uv
2
3import (
4	"fmt"
5	"image/color"
6	"io"
7
8	"github.com/charmbracelet/x/ansi"
9	"github.com/lucasb-eyer/go-colorful"
10)
11
12var (
13	// ErrNotTerminal is an error that indicates that the file is not a terminal.
14	ErrNotTerminal = fmt.Errorf("not a terminal")
15	// ErrPlatformNotSupported is an error that indicates that the platform is not supported.
16	ErrPlatformNotSupported = fmt.Errorf("platform not supported")
17)
18
19// Drawable represents a drawable component on a [Screen].
20type Drawable interface {
21	// Draw renders the component on the screen for the given area.
22	Draw(scr Screen, area Rectangle)
23}
24
25// DrawableFunc is a function that implements the [Drawable] interface.
26type DrawableFunc func(scr Screen, rect Rectangle)
27
28var _ Drawable = DrawableFunc(nil)
29
30// Draw implements the [Drawable] interface.
31func (f DrawableFunc) Draw(scr Screen, rect Rectangle) {
32	f(scr, rect)
33}
34
35// WidthMethod determines how many columns a grapheme occupies on the screen.
36type WidthMethod interface {
37	StringWidth(s string) int
38}
39
40// Screen represents a screen that can be drawn to.
41type Screen interface {
42	// Bounds returns the bounds of the screen. This is the rectangle that
43	// includes the start and end points of the screen.
44	Bounds() Rectangle
45
46	// CellAt returns the cell at the given position. If the position is out of
47	// bounds, it returns nil. Otherwise, it always returns a cell, even if it
48	// is empty (i.e., a cell with a space character and a width of 1).
49	CellAt(x, y int) *Cell
50
51	// SetCell sets the cell at the given position. A nil cell is treated as an
52	// empty cell with a space character and a width of 1.
53	SetCell(x, y int, c *Cell)
54
55	// WidthMethod returns the width method used by the screen.
56	WidthMethod() WidthMethod
57}
58
59// Cursor represents a cursor on the terminal screen.
60type Cursor struct {
61	// Position is a [Position] that determines the cursor's position on the
62	// screen relative to the top left corner of the frame.
63	Position
64
65	// Color is a [color.Color] that determines the cursor's color.
66	Color color.Color
67
68	// Shape is a [CursorShape] that determines the cursor's shape.
69	Shape CursorShape
70
71	// Blink is a boolean that determines whether the cursor should blink.
72	Blink bool
73
74	// Hidden is a boolean that determines whether the cursor is hidden. You
75	// can use this if you want to hide the cursor but still want to change its
76	// position.
77	Hidden bool
78}
79
80// NewCursor returns a new cursor with the default settings and the given
81// position.
82func NewCursor(x, y int) *Cursor {
83	return &Cursor{
84		Position: Position{X: x, Y: y},
85		Color:    nil,
86		Shape:    CursorBlock,
87		Blink:    true,
88	}
89}
90
91// ProgressBarState represents the state of the progress bar.
92type ProgressBarState int
93
94// Progress bar states.
95const (
96	ProgressBarNone ProgressBarState = iota
97	ProgressBarDefault
98	ProgressBarError
99	ProgressBarIndeterminate
100	ProgressBarWarning
101)
102
103// String return a human-readable value for the given [ProgressBarState].
104func (s ProgressBarState) String() string {
105	return [...]string{
106		"None",
107		"Default",
108		"Error",
109		"Indeterminate",
110		"Warning",
111	}[s]
112}
113
114// ProgressBar represents the terminal progress bar.
115//
116// Support depends on the terminal.
117//
118// See https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
119type ProgressBar struct {
120	// State is the current state of the progress bar. It can be one of
121	// [ProgressBarNone], [ProgressBarDefault], [ProgressBarError],
122	// [ProgressBarIndeterminate], and [ProgressBarWarning].
123	State ProgressBarState
124	// Value is the current value of the progress bar. It should be between
125	// 0 and 100.
126	Value int
127}
128
129// NewProgressBar returns a new progress bar with the given state and value.
130// The value is ignored if the state is [ProgressBarNone] or
131// [ProgressBarIndeterminate].
132func NewProgressBar(state ProgressBarState, value int) *ProgressBar {
133	return &ProgressBar{
134		State: state,
135		Value: min(max(value, 0), 100),
136	}
137}
138
139// KeyboardEnhancements defines different keyboard enhancement features that
140// can be requested from the terminal.
141type KeyboardEnhancements struct {
142	// DisambiguateEscapeCodes requests the terminal to report ambiguous keys
143	// such as Ctrl+i and Tab, and Ctrl+m and Enter, and others as distinct key
144	// code sequences.
145	// If supported, your program will receive distinct [KeyPressEvent]s for
146	// these keys.
147	DisambiguateEscapeCodes bool
148
149	// ReportEventTypes requests the terminal to report key repeat and release
150	// events.
151	// If supported, your program will receive [KeyReleaseEvent]s and
152	// [KeyPressEvent] with the [Key.IsRepeat] field set indicating that this
153	// is a it's part of a key repeat sequence.
154	ReportEventTypes bool
155
156	// ReportAlternateKeys requests the terminal to report alternate key values
157	// in addition to the main ones.
158	// Note that only key events represented as escape codes will affected by
159	// this enhancement.
160	ReportAlternateKeys bool
161
162	// ReportAllKeysAsEscapeCodes requests the terminal to report all key
163	// events, including plain text keys, as escape codes.
164	// When this is enabled, text won't be sent as plain text but instead as
165	// escape codes that encode the key value and modifiers.
166	ReportAllKeysAsEscapeCodes bool
167
168	// ReportAssociatedText requests the terminal to report the text associated
169	// with key events.
170	// Note that this is an enhancement to
171	// [KeyboardEnhancements.ReportAllKeysAsEscapeCodes] and only has an effect
172	// if that is enabled.
173	ReportAssociatedText bool
174}
175
176// NewKeyboardEnhancements returns a new [KeyboardEnhancements] with the given
177// options as flags.
178//
179// A zero, or negative, flags value is treated as no enhancements.
180//
181// See [ansi.KittyKeyboard] for more details on the supported keyboard enhancements.
182func NewKeyboardEnhancements(flags int) *KeyboardEnhancements {
183	if flags <= 0 {
184		return &KeyboardEnhancements{}
185	}
186
187	return &KeyboardEnhancements{
188		DisambiguateEscapeCodes: flags&ansi.KittyDisambiguateEscapeCodes != 0,
189		ReportEventTypes:        flags&ansi.KittyReportEventTypes != 0,
190	}
191}
192
193// Flags returns the keyboard enhancements as bits that can be used to set the
194// appropriate terminal modes.
195func (ke KeyboardEnhancements) Flags() int {
196	bits := 0
197	if ke.DisambiguateEscapeCodes {
198		bits |= ansi.KittyDisambiguateEscapeCodes
199	}
200	if ke.ReportEventTypes {
201		bits |= ansi.KittyReportEventTypes
202	}
203	if ke.ReportAlternateKeys {
204		bits |= ansi.KittyReportAlternateKeys
205	}
206	if ke.ReportAllKeysAsEscapeCodes {
207		bits |= ansi.KittyReportAllKeysAsEscapeCodes
208	}
209	if ke.ReportAssociatedText {
210		bits |= ansi.KittyReportAssociatedKeys
211	}
212
213	return bits
214}
215
216// EncodeBackgroundColor encodes the background color to the given writer. Use
217// nil to reset the background color to the default.
218func EncodeBackgroundColor(w io.Writer, c color.Color) error {
219	var seq string
220	if c == nil {
221		seq = ansi.ResetBackgroundColor
222	} else if col, ok := colorful.MakeColor(c); ok {
223		seq = ansi.SetBackgroundColor(col.Hex())
224	} else {
225		return fmt.Errorf("invalid color: %v", c)
226	}
227
228	_, err := io.WriteString(w, seq)
229	if err != nil {
230		return fmt.Errorf("failed to set background color: %w", err)
231	}
232
233	return nil
234}
235
236// EncodeForegroundColor encodes the foreground color to the given writer. Use
237// nil to reset the foreground color to the default.
238func EncodeForegroundColor(w io.Writer, c color.Color) error {
239	var seq string
240	if c == nil {
241		seq = ansi.ResetForegroundColor
242	} else if col, ok := colorful.MakeColor(c); ok {
243		seq = ansi.SetForegroundColor(col.Hex())
244	} else {
245		return fmt.Errorf("invalid color: %v", c)
246	}
247
248	_, err := io.WriteString(w, seq)
249	if err != nil {
250		return fmt.Errorf("failed to set foreground color: %w", err)
251	}
252
253	return nil
254}
255
256// EncodeCursorColor encodes the cursor color to the given writer. Use nil to
257// reset the cursor color to the default.
258func EncodeCursorColor(w io.Writer, c color.Color) error {
259	var seq string
260	if c == nil {
261		seq = ansi.ResetCursorColor
262	} else if col, ok := colorful.MakeColor(c); ok {
263		seq = ansi.SetCursorColor(col.Hex())
264	} else {
265		return fmt.Errorf("invalid color: %v", c)
266	}
267
268	_, err := io.WriteString(w, seq)
269	if err != nil {
270		return fmt.Errorf("failed to set cursor color: %w", err)
271	}
272
273	return nil
274}
275
276// EncodeCursorStyle encodes the cursor style to the given writer.
277func EncodeCursorStyle(w io.Writer, shape CursorShape, blink bool) error {
278	seq := ansi.SetCursorStyle(shape.Encode(blink))
279	_, err := io.WriteString(w, seq)
280	if err != nil {
281		return fmt.Errorf("failed to set cursor style: %w", err)
282	}
283
284	return nil
285}
286
287// EncodeBracketedPaste encodes the bracketed paste mode to the given writer.
288func EncodeBracketedPaste(w io.Writer, enable bool) error {
289	var seq string
290	if enable {
291		seq = ansi.SetModeBracketedPaste
292	} else {
293		seq = ansi.ResetModeBracketedPaste
294	}
295
296	_, err := io.WriteString(w, seq)
297	if err != nil {
298		return fmt.Errorf("failed to set bracketed paste mode: %w", err)
299	}
300
301	return nil
302}
303
304// EncodeMouseMode encodes the mouse tracking mode to the given writer.
305func EncodeMouseMode(w io.Writer, mode MouseMode) error {
306	var seq string
307	switch mode {
308	case MouseModeNone:
309		seq = ansi.ResetModeMouseX10 +
310			ansi.ResetModeMouseNormal +
311			ansi.ResetModeMouseButtonEvent +
312			ansi.ResetModeMouseAnyEvent
313	case MouseModePress:
314		seq = ansi.SetModeMouseX10
315	case MouseModeClick:
316		seq = ansi.SetModeMouseNormal
317	case MouseModeDrag:
318		seq = ansi.SetModeMouseButtonEvent
319	case MouseModeMotion:
320		seq = ansi.SetModeMouseAnyEvent
321	}
322
323	_, err := io.WriteString(w, seq) //nolint:errcheck
324	if err != nil {
325		return fmt.Errorf("failed to set mouse mode: %w", err)
326	}
327
328	return nil
329}
330
331// EncodeMouseEncoding encodes the mouse encoding mode to the given writer.
332// When enc is [MouseEncodingLegacy], all extended encodings are reset.
333func EncodeMouseEncoding(w io.Writer, enc MouseEncoding) error {
334	var seq string
335	switch enc {
336	case MouseEncodingLegacy:
337		seq = ansi.ResetModeMouseExtSgr +
338			ansi.ResetModeMouseExtUrxvt +
339			ansi.ResetModeMouseExtSgrPixel
340	case MouseEncodingSGR:
341		seq = ansi.SetModeMouseExtSgr
342	case MouseEncodingSGRPixel:
343		seq = ansi.SetModeMouseExtSgrPixel
344	}
345
346	_, err := io.WriteString(w, seq) //nolint:errcheck
347	if err != nil {
348		return fmt.Errorf("failed to set mouse encoding: %w", err)
349	}
350
351	return nil
352}
353
354// EncodeProgressBar encodes the progress bar to the given writer.
355func EncodeProgressBar(w io.Writer, pb *ProgressBar) error {
356	if pb == nil {
357		pb = &ProgressBar{State: ProgressBarNone}
358	}
359
360	var seq string
361	percent := clamp(pb.Value, 0, 100)
362	switch pb.State {
363	case ProgressBarNone:
364		seq = ansi.ResetProgressBar
365	case ProgressBarDefault:
366		seq = ansi.SetProgressBar(percent)
367	case ProgressBarError:
368		seq = ansi.SetErrorProgressBar(percent)
369	case ProgressBarIndeterminate:
370		seq = ansi.SetIndeterminateProgressBar
371	case ProgressBarWarning:
372		seq = ansi.SetWarningProgressBar(percent)
373	default:
374		return fmt.Errorf("invalid progress bar state: %d", pb.State)
375	}
376
377	_, err := io.WriteString(w, seq)
378	if err != nil {
379		return fmt.Errorf("failed to set progress bar: %w", err)
380	}
381
382	return nil
383}
384
385// EncodeKeyboardEnhancements encodes the keyboard enhancements to the given
386// writer.
387func EncodeKeyboardEnhancements(w io.Writer, ke *KeyboardEnhancements) error {
388	var flags int
389	if ke != nil {
390		flags = ke.Flags()
391	}
392	_, err := io.WriteString(w, ansi.KittyKeyboard(flags, 1))
393	if err != nil {
394		return fmt.Errorf("failed to set keyboard enhancements: %w", err)
395	}
396	return nil
397}
398
399// EncodeWindowTitle encodes the window title to the given writer.
400func EncodeWindowTitle(w io.Writer, title string) error {
401	_, err := io.WriteString(w, ansi.SetWindowTitle(title))
402	if err != nil {
403		return fmt.Errorf("failed to set window title: %w", err)
404	}
405	return nil
406}