okami.mom/ultraviolet

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

Ayman Bagabasrefactor(examples): move examples to advanced directoryf72cc87

main
11.7 KiB464 linesraw
1package main
2
3import (
4	"context"
5	"image/color"
6	"log"
7	"os"
8	"strings"
9
10	"charm.land/lipgloss/v2"
11	uv "github.com/charmbracelet/ultraviolet"
12	"github.com/charmbracelet/ultraviolet/screen"
13	"github.com/charmbracelet/x/ansi"
14	"github.com/lucasb-eyer/go-colorful"
15	"github.com/rivo/uniseg"
16)
17
18const (
19	width       = 96
20	columnWidth = 30
21)
22
23var (
24	hasDarkBG bool
25	lightDark lipgloss.LightDarkFunc
26)
27
28func init() {
29	hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
30	lightDark = lipgloss.LightDark(hasDarkBG)
31}
32
33func main() {
34	// Style definitions.
35	var (
36		subtle    = lightDark(lipgloss.Color("#D9DCCF"), lipgloss.Color("#383838"))
37		highlight = lightDark(lipgloss.Color("#874BFD"), lipgloss.Color("#7D56F4"))
38		special   = lightDark(lipgloss.Color("#43BF6D"), lipgloss.Color("#73F59F"))
39
40		divider = lipgloss.NewStyle().
41			SetString("•").
42			Padding(0, 1).
43			Foreground(subtle).
44			String()
45
46		url = lipgloss.NewStyle().Foreground(special).Render
47
48		activeTabBorder = lipgloss.Border{
49			Top:         "─",
50			Bottom:      " ",
51			Left:        "│",
52			Right:       "│",
53			TopLeft:     "╭",
54			TopRight:    "╮",
55			BottomLeft:  "┘",
56			BottomRight: "└",
57		}
58
59		tabBorder = lipgloss.Border{
60			Top:         "─",
61			Bottom:      "─",
62			Left:        "│",
63			Right:       "│",
64			TopLeft:     "╭",
65			TopRight:    "╮",
66			BottomLeft:  "┴",
67			BottomRight: "┴",
68		}
69
70		tab = lipgloss.NewStyle().
71			Border(tabBorder, true).
72			BorderForeground(highlight).
73			Padding(0, 1)
74
75		activeTab = tab.Border(activeTabBorder, true)
76
77		tabGap = tab.
78			BorderTop(false).
79			BorderLeft(false).
80			BorderRight(false)
81
82		titleStyle = lipgloss.NewStyle().
83				MarginLeft(1).
84				MarginRight(5).
85				Padding(0, 1).
86				Italic(true).
87				Foreground(lipgloss.Color("#FFF7DB")).
88				SetString("Lip Gloss")
89
90		descStyle = lipgloss.NewStyle().MarginTop(1)
91
92		infoStyle = lipgloss.NewStyle().
93				BorderStyle(lipgloss.NormalBorder()).
94				BorderTop(true).
95				BorderForeground(subtle)
96
97		dialogBoxStyle = lipgloss.NewStyle().
98				Border(lipgloss.RoundedBorder()).
99				BorderForeground(lipgloss.Color("#874BFD")).
100				Padding(1, 0).
101				BorderTop(true).
102				BorderLeft(true).
103				BorderRight(true).
104				BorderBottom(true)
105
106		buttonStyle = lipgloss.NewStyle().
107				Foreground(lipgloss.Color("#FFF7DB")).
108				Background(lipgloss.Color("#888B7E")).
109				Padding(0, 3).
110				MarginTop(1)
111
112		activeButtonStyle = buttonStyle.
113					Foreground(lipgloss.Color("#FFF7DB")).
114					Background(lipgloss.Color("#F25D94")).
115					MarginRight(2).
116					Underline(true)
117
118		list = lipgloss.NewStyle().
119			Border(lipgloss.NormalBorder(), false, true, false, false).
120			BorderForeground(subtle).
121			MarginRight(2).
122			Height(8).
123			Width(columnWidth + 1)
124
125		listHeader = lipgloss.NewStyle().
126				BorderStyle(lipgloss.NormalBorder()).
127				BorderBottom(true).
128				BorderForeground(subtle).
129				MarginRight(2).
130				Render
131
132		listItem = lipgloss.NewStyle().PaddingLeft(2).Render
133
134		checkMark = lipgloss.NewStyle().SetString("✓").
135				Foreground(special).
136				PaddingRight(1).
137				String()
138
139		listDone = func(s string) string {
140			return checkMark + lipgloss.NewStyle().
141				Strikethrough(true).
142				Foreground(lightDark(lipgloss.Color("#969B86"), lipgloss.Color("#696969"))).
143				Render(s)
144		}
145
146		historyStyle = lipgloss.NewStyle().
147				Align(lipgloss.Left).
148				Foreground(lipgloss.Color("#FAFAFA")).
149				Background(highlight).
150				Margin(1, 3, 0, 0).
151				Padding(1, 2).
152				Height(19).
153				Width(columnWidth)
154
155		statusNugget = lipgloss.NewStyle().
156				Foreground(lipgloss.Color("#FFFDF5")).
157				Padding(0, 1)
158
159		statusBarStyle = lipgloss.NewStyle().
160				Foreground(lightDark(lipgloss.Color("#343433"), lipgloss.Color("#C1C6B2"))).
161				Background(lightDark(lipgloss.Color("#D9DCCF"), lipgloss.Color("#353533")))
162
163		statusStyle = lipgloss.NewStyle().
164				Inherit(statusBarStyle).
165				Foreground(lipgloss.Color("#FFFDF5")).
166				Background(lipgloss.Color("#FF5F87")).
167				Padding(0, 1).
168				MarginRight(1)
169
170		encodingStyle = statusNugget.
171				Background(lipgloss.Color("#A550DF")).
172				Align(lipgloss.Right)
173
174		statusText = lipgloss.NewStyle().Inherit(statusBarStyle)
175
176		fishCakeStyle = statusNugget.Background(lipgloss.Color("#6124DF"))
177
178		docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2)
179	)
180
181	doc := strings.Builder{}
182
183	{
184		row := lipgloss.JoinHorizontal(
185			lipgloss.Top,
186			activeTab.Render("Lip Gloss"),
187			tab.Render("Blush"),
188			tab.Render("Eye Shadow"),
189			tab.Render("Mascara"),
190			tab.Render("Foundation"),
191		)
192		gap := tabGap.Render(strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2)))
193		row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap)
194		doc.WriteString(row + "\n\n")
195	}
196
197	{
198		var (
199			colors = colorGrid(1, 5)
200			title  strings.Builder
201		)
202
203		for i, v := range colors {
204			const offset = 2
205			c := lipgloss.Color(v[0])
206			_, _ = title.WriteString(titleStyle.MarginLeft(i * offset).Background(c).String())
207			if i < len(colors)-1 {
208				title.WriteRune('\n')
209			}
210		}
211
212		desc := lipgloss.JoinVertical(lipgloss.Left,
213			descStyle.Render("Style Definitions for Nice Terminal Layouts"),
214			infoStyle.Render("From Charm"+divider+url("https://github.com/charmbracelet/lipgloss")),
215		)
216
217		row := lipgloss.JoinHorizontal(lipgloss.Top, title.String(), desc)
218		doc.WriteString(row + "\n\n")
219	}
220
221	okButton := activeButtonStyle.Render("Yes")
222	cancelButton := buttonStyle.Render("Maybe")
223
224	grad := applyGradient(
225		lipgloss.NewStyle(),
226		"Are you sure you want to eat marmalade?",
227		lipgloss.Color("#EDFF82"),
228		lipgloss.Color("#F25D94"),
229	)
230
231	question := lipgloss.NewStyle().
232		Width(50).
233		Align(lipgloss.Center).
234		Render(grad)
235
236	buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton)
237	dialogUI := lipgloss.JoinVertical(lipgloss.Center, question, buttons)
238
239	dialog := lipgloss.Place(width, 9,
240		lipgloss.Center, lipgloss.Center,
241		"",
242		lipgloss.WithWhitespaceChars("猫咪"),
243		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(subtle)),
244	)
245
246	doc.WriteString(dialog + "\n\n")
247
248	colors := func() string {
249		colors := colorGrid(14, 8)
250
251		b := strings.Builder{}
252		for _, x := range colors {
253			for _, y := range x {
254				s := lipgloss.NewStyle().SetString("  ").Background(lipgloss.Color(y))
255				b.WriteString(s.String())
256			}
257			b.WriteRune('\n')
258		}
259
260		return b.String()
261	}()
262
263	lists := lipgloss.JoinHorizontal(lipgloss.Top,
264		list.Render(
265			lipgloss.JoinVertical(lipgloss.Left,
266				listHeader("Citrus Fruits to Try"),
267				listDone("Grapefruit"),
268				listDone("Yuzu"),
269				listItem("Citron"),
270				listItem("Kumquat"),
271				listItem("Pomelo"),
272			),
273		),
274		list.Width(columnWidth).Render(
275			lipgloss.JoinVertical(lipgloss.Left,
276				listHeader("Actual Lip Gloss Vendors"),
277				listItem("Glossier"),
278				listItem("Claire's Boutique"),
279				listDone("Nyx"),
280				listItem("Mac"),
281				listDone("Milk"),
282			),
283		),
284	)
285
286	doc.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, lists, colors))
287
288	{
289		const (
290			historyA = "The Romans learned from the Greeks that quinces slowly cooked with honey would \"set\" when cool. The Apicius gives a recipe for preserving whole quinces, stems and leaves attached, in a bath of honey diluted with defrutum: Roman marmalade. Preserves of quince and lemon appear (along with rose, apple, plum and pear) in the Book of ceremonies of the Byzantine Emperor Constantine VII Porphyrogennetos."
291			historyB = "Medieval quince preserves, which went by the French name cotignac, produced in a clear version and a fruit pulp version, began to lose their medieval seasoning of spices in the 16th century. In the 17th century, La Varenne provided recipes for both thick and clear cotignac."
292			historyC = "In 1524, Henry VIII, King of England, received a \"box of marmalade\" from Mr. Hull of Exeter. This was probably marmelada, a solid quince paste from Portugal, still made and sold in southern Europe today. It became a favourite treat of Anne Boleyn and her ladies in waiting."
293		)
294
295		doc.WriteString(lipgloss.JoinHorizontal(
296			lipgloss.Top,
297			historyStyle.Align(lipgloss.Right).Render(historyA),
298			historyStyle.Align(lipgloss.Center).Render(historyB),
299			historyStyle.MarginRight(0).Render(historyC),
300		))
301
302		doc.WriteString("\n\n")
303	}
304
305	{
306		w := lipgloss.Width
307
308		lightDarkState := "Light"
309		if hasDarkBG {
310			lightDarkState = "Dark"
311		}
312
313		statusKey := statusStyle.Render("STATUS")
314		encoding := encodingStyle.Render("UTF-8")
315		fishCake := fishCakeStyle.Render("🍥 Fish Cake")
316		statusVal := statusText.
317			Width(width - w(statusKey) - w(encoding) - w(fishCake)).
318			Render("Ravishingly " + lightDarkState + "!")
319
320		bar := lipgloss.JoinHorizontal(lipgloss.Top,
321			statusKey,
322			statusVal,
323			encoding,
324			fishCake,
325		)
326
327		doc.WriteString(statusBarStyle.Width(width).Render(bar))
328	}
329
330	t := uv.DefaultTerminal()
331	scr := t.Screen()
332
333	if err := t.Start(); err != nil {
334		log.Fatalf("starting program: %v", err)
335	}
336
337	defer t.Stop()
338
339	physicalWidth := scr.Bounds().Dx()
340
341	if physicalWidth > 0 {
342		docStyle = docStyle.MaxWidth(physicalWidth)
343	}
344
345	scr.EnterAltScreen()
346	scr.WriteString(ansi.SetMode(ansi.ModeMouseButtonEvent, ansi.ModeMouseExtSgr))
347
348	dialogWidth := lipgloss.Width(dialogUI) + dialogBoxStyle.GetHorizontalFrameSize()
349	dialogHeight := lipgloss.Height(dialogUI) + dialogBoxStyle.GetVerticalFrameSize()
350	dialogX, dialogY := physicalWidth/2-dialogWidth/2+docStyle.GetVerticalFrameSize()-1, 12
351	mainDoc := docStyle.Render(doc.String())
352
353	display := func() {
354		screen.Clear(scr)
355		mainSs := uv.NewStyledString(mainDoc)
356		mainSs.Draw(scr, scr.Bounds())
357		boxArea := uv.Rect(dialogX, dialogY, dialogWidth, dialogHeight)
358		box := uv.NewStyledString(dialogBoxStyle.Render(dialogUI))
359		box.Draw(scr, boxArea)
360		scr.Render()
361		scr.Flush()
362	}
363
364	display()
365
366	ctx, cancel := context.WithCancel(context.Background())
367	defer cancel()
368
369LOOP:
370	for {
371		select {
372		case <-ctx.Done():
373			break LOOP
374		case ev := <-t.Events():
375			log.Printf("event: %T", ev)
376			switch ev := ev.(type) {
377			case uv.WindowSizeEvent:
378				scr.Resize(ev.Width, ev.Height)
379			case uv.MouseClickEvent:
380				dialogX, dialogY = ev.X-dialogWidth/2, ev.Y-dialogHeight/2
381			case uv.KeyPressEvent:
382				log.Printf("%T %q %q", ev, ev.String(), ev.Keystroke())
383				switch {
384				case ev.MatchString("ctrl+c", "q"):
385					cancel()
386				case ev.MatchString("left", "h"):
387					dialogX--
388				case ev.MatchString("down", "j"):
389					dialogY++
390				case ev.MatchString("up", "k"):
391					dialogY--
392				case ev.MatchString("right", "l"):
393					dialogX++
394				}
395			}
396
397			display()
398		}
399	}
400
401	scr.WriteString(ansi.ResetMode(ansi.ModeMouseButtonEvent, ansi.ModeMouseExtSgr))
402}
403
404func colorGrid(xSteps, ySteps int) [][]string {
405	x0y0, _ := colorful.Hex("#F25D94")
406	x1y0, _ := colorful.Hex("#EDFF82")
407	x0y1, _ := colorful.Hex("#643AFF")
408	x1y1, _ := colorful.Hex("#14F9D5")
409
410	x0 := make([]colorful.Color, ySteps)
411	for i := range x0 {
412		x0[i] = x0y0.BlendLuv(x0y1, float64(i)/float64(ySteps))
413	}
414
415	x1 := make([]colorful.Color, ySteps)
416	for i := range x1 {
417		x1[i] = x1y0.BlendLuv(x1y1, float64(i)/float64(ySteps))
418	}
419
420	grid := make([][]string, ySteps)
421	for x := 0; x < ySteps; x++ {
422		y0 := x0[x]
423		grid[x] = make([]string, xSteps)
424		for y := 0; y < xSteps; y++ {
425			grid[x][y] = y0.BlendLuv(x1[x], float64(y)/float64(xSteps)).Hex()
426		}
427	}
428
429	return grid
430}
431
432func max(a, b int) int {
433	if a > b {
434		return a
435	}
436	return b
437}
438
439func applyGradient(base lipgloss.Style, input string, from, to color.Color) string {
440	g := uniseg.NewGraphemes(input)
441	var chars []string
442	for g.Next() {
443		chars = append(chars, g.Str())
444	}
445
446	a, _ := colorful.MakeColor(to)
447	b, _ := colorful.MakeColor(from)
448	var output strings.Builder
449	var hex string
450	for i := 0; i < len(chars); i++ {
451		hex = a.BlendLuv(b, float64(i)/float64(len(chars)-1)).Hex()
452		output.WriteString(base.Foreground(lipgloss.Color(hex)).Render(chars[i]))
453	}
454
455	return output.String()
456}
457
458func init() {
459	f, err := os.OpenFile("layout.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
460	if err != nil {
461		log.Fatal(err)
462	}
463	log.SetOutput(f)
464}