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