okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
c7cac3e
upstream
1package uv 2 3import ( 4"image/color" 5"strings" 6 7"github.com/charmbracelet/colorprofile" 8"github.com/charmbracelet/x/ansi" 9) 10 11// EmptyCell is a cell with a single space, width of 1, and no style or link. 12var EmptyCell = Cell {Content :" " ,Width :1 } 13 14// Cell represents a single cell in the terminal screen. 15type Cell struct { 16// Content is the [Cell]'s content, which consists of a single grapheme 17// cluster. Most of the time, this will be a single rune as well, but it 18// can also be a combination of runes that form a grapheme cluster. 19Content string 20 21// The style of the cell. Nil style means no style. Zero value prints a 22// reset sequence. 23Style Style 24 25// Link is the hyperlink of the cell. 26Link Link 27 28// Width is the mono-spaced width of the grapheme cluster. 29Width int 30} 31 32// NewCell creates a new cell from the given string grapheme. It will only use 33// the first grapheme in the string and ignore the rest. The width of the cell 34// is determined using the given width method. 35func NewCell (method WidthMethod ,gr string )* Cell { 36if len (gr )== 0 { 37return & Cell {} 38 } 39if gr == " " { 40return EmptyCell .Clone () 41 } 42return & Cell { 43Content :gr , 44Width :method .StringWidth (gr ), 45 } 46} 47 48// String returns the string content of the cell excluding any styles, links, 49// and escape sequences. 50func (c * Cell )String ()string { 51return c .Content 52} 53 54// Equal returns whether the cell is equal to the other cell. 55func (c * Cell )Equal (o * Cell )bool { 56return o != nil && 57c .Width == o .Width && 58c .Content == o .Content && 59c .Style .Equal (& o .Style )&& 60c .Link .Equal (& o .Link ) 61} 62 63// IsZero returns whether the cell is an empty cell. 64func (c * Cell )IsZero ()bool { 65return * c == Cell {} 66} 67 68// isWidePlaceholder reports whether the cell is the continuation column of a 69// wide cell, marked with a zero display width. 70func (c * Cell )isWidePlaceholder ()bool { 71return c != nil && c .Width == 0 72} 73 74// Clone returns a copy of the cell. 75func (c * Cell )Clone () (n * Cell ) { 76n = new (Cell ) 77* n = * c 78return 79} 80 81// Empty makes the cell an empty cell by setting its content to a single space 82// and width to 1. 83func (c * Cell )Empty () { 84c .Content = " " 85c .Width = 1 86} 87 88// NewLink creates a new hyperlink with the given URL and parameters. 89func NewLink (url string ,params ... string )Link { 90return Link { 91URL :url , 92Params :strings .Join (params ,":" ), 93 } 94} 95 96// Link represents a hyperlink in the terminal screen. 97type Link struct { 98URL string 99Params string 100} 101 102// String returns a string representation of the hyperlink. 103func (h * Link )String ()string { 104return h .URL 105} 106 107// Equal returns whether the hyperlink is equal to the other hyperlink. 108func (h * Link )Equal (o * Link )bool { 109return o != nil && * h == * o 110} 111 112// IsZero returns whether the hyperlink is empty. 113func (h * Link )IsZero ()bool { 114return * h == Link {} 115} 116 117// These are the available text attributes that can be combined to create 118// different styles. 119const ( 120AttrBold = 1 << iota 121AttrFaint 122AttrItalic 123AttrBlink 124AttrRapidBlink // Not widely supported 125AttrReverse 126AttrConceal 127AttrStrikethrough 128 129AttrReset = 0 130) 131 132// AttrSlowBlink is an alias for AttrBlink. 133// 134// Deprecated: Use [AttrBlink] instead. 135const ( 136AttrSlowBlink = AttrBlink 137) 138 139// Underline is the style of underline to use for text. 140type Underline = ansi.Underline 141 142// UnderlineStyle is the style of underline to use for text. 143// 144// Deprecated: Use [Underline] instead. 145type UnderlineStyle = ansi.Underline 146 147// These are the available underline styles. 148const ( 149UnderlineNone = ansi .UnderlineNone 150UnderlineSingle = ansi .UnderlineSingle 151UnderlineDouble = ansi .UnderlineDouble 152UnderlineCurly = ansi .UnderlineCurly 153UnderlineDotted = ansi .UnderlineDotted 154UnderlineDashed = ansi .UnderlineDashed 155) 156 157// These are the available underline styles. 158// 159// Deprecated: Use the constants from [Underline] instead. 160const ( 161UnderlineStyleNone = ansi .UnderlineNone 162UnderlineStyleSingle = ansi .UnderlineSingle 163UnderlineStyleDouble = ansi .UnderlineDouble 164UnderlineStyleCurly = ansi .UnderlineCurly 165UnderlineStyleDotted = ansi .UnderlineDotted 166UnderlineStyleDashed = ansi .UnderlineDashed 167) 168 169// Style represents the style of a cell. 170type Style struct { 171Fg color.Color 172Bg color.Color 173UnderlineColor color.Color 174Underline Underline 175Attrs uint8 176} 177 178// Equal returns true if the style is equal to the other style. 179func (s * Style )Equal (o * Style )bool { 180return s .Attrs == o .Attrs && 181s .Underline == o .Underline && 182colorEqual (s .Fg ,o .Fg )&& 183colorEqual (s .Bg ,o .Bg )&& 184colorEqual (s .UnderlineColor ,o .UnderlineColor ) 185} 186 187// Styled wraps the given string with the style's ANSI sequences and resets. 188func (s * Style )Styled (str string )string { 189if s .IsZero () { 190return str 191 } 192return s .String ()+ str + ansi .ResetStyle 193} 194 195// String returns the ANSI SGR sequence for the style. 196func (s * Style )String ()string { 197if s .IsZero () { 198return ansi .ResetStyle 199 } 200 201var b ansi.Style 202 203if s .Attrs != 0 {//nolint:nestif 204if s .Attrs & AttrBold != 0 { 205b = b .Bold () 206 } 207if s .Attrs & AttrFaint != 0 { 208b = b .Faint () 209 } 210if s .Attrs & AttrItalic != 0 { 211b = b .Italic (true ) 212 } 213if s .Attrs & AttrBlink != 0 { 214b = b .Blink (true ) 215 } 216if s .Attrs & AttrRapidBlink != 0 { 217b = b .RapidBlink (true ) 218 } 219if s .Attrs & AttrReverse != 0 { 220b = b .Reverse (true ) 221 } 222if s .Attrs & AttrConceal != 0 { 223b = b .Conceal (true ) 224 } 225if s .Attrs & AttrStrikethrough != 0 { 226b = b .Strikethrough (true ) 227 } 228 } 229if s .Underline != UnderlineStyleNone { 230switch s .Underline { 231case UnderlineStyleSingle : 232b = b .Underline (true ) 233case UnderlineStyleDouble : 234b = b .UnderlineStyle (UnderlineStyleDouble ) 235case UnderlineStyleCurly : 236b = b .UnderlineStyle (UnderlineStyleCurly ) 237case UnderlineStyleDotted : 238b = b .UnderlineStyle (UnderlineStyleDotted ) 239case UnderlineStyleDashed : 240b = b .UnderlineStyle (UnderlineStyleDashed ) 241 } 242 } 243if s .Fg != nil { 244b = b .ForegroundColor (s .Fg ) 245 } 246if s .Bg != nil { 247b = b .BackgroundColor (s .Bg ) 248 } 249if s .UnderlineColor != nil { 250b = b .UnderlineColor (s .UnderlineColor ) 251 } 252 253return b .String () 254} 255 256// Diff returns the ANSI sequence that sets the style as a diff from 257// another style. 258func (s * Style )Diff (from * Style )string { 259return StyleDiff (from ,s ) 260} 261 262// StyleDiff returns the SGR ANSI sequence necessary to transition from the 263// "from" style to the "to" style. 264func StyleDiff (from ,to * Style )string { 265if from == nil && to == nil { 266return "" 267 } 268if from != nil && to != nil && from .Equal (to ) { 269return "" 270 } 271if from == nil { 272return to .String () 273 } 274if to == nil || to .IsZero () { 275// Resetting all styles is cheaper than calculating diffs. 276// "\x1b[m" is 3 bytes vs potentially much longer sequences. 277return ansi .ResetStyle 278 } 279 280// TODO: Optimize further by checking if a full reset is cheaper than 281// calculating diffs. Often, it might be cheaper to reset everything and 282// then set the desired styles rather than calculating diffs. Also more 283// compatible with terminals that have buggy SGR implementations. 284 285var b ansi.Style 286 287if ! colorEqual (from .Fg ,to .Fg ) { 288b = b .ForegroundColor (to .Fg ) 289 } 290 291if ! colorEqual (from .Bg ,to .Bg ) { 292b = b .BackgroundColor (to .Bg ) 293 } 294 295if ! colorEqual (from .UnderlineColor ,to .UnderlineColor ) { 296// TODO: Investigate this. For backward compatibility, we might want to 297// set this at the end instead. Because on terminals that don't support 298// underline color, this might mess up some other attributes if set in 299// the middle. 300b = b .UnderlineColor (to .UnderlineColor ) 301 } 302 303fromBold := from .Attrs & AttrBold != 0 304fromFaint := from .Attrs & AttrFaint != 0 305fromItalic := from .Attrs & AttrItalic != 0 306fromUnderline := from .Underline != UnderlineStyleNone 307fromBlink := from .Attrs & AttrBlink != 0 308fromRapidBlink := from .Attrs & AttrRapidBlink != 0 309fromReverse := from .Attrs & AttrReverse != 0 310fromConceal := from .Attrs & AttrConceal != 0 311fromStrikethrough := from .Attrs & AttrStrikethrough != 0 312toBold := to .Attrs & AttrBold != 0 313toFaint := to .Attrs & AttrFaint != 0 314toItalic := to .Attrs & AttrItalic != 0 315toUnderline := to .Underline != UnderlineStyleNone 316toBlink := to .Attrs & AttrBlink != 0 317toRapidBlink := to .Attrs & AttrRapidBlink != 0 318toReverse := to .Attrs & AttrReverse != 0 319toConceal := to .Attrs & AttrConceal != 0 320toStrikethrough := to .Attrs & AttrStrikethrough != 0 321 322// We perform the resets first since they are single attributes and 323// shouldn't interfere with others being set. 324 325boldChanged := fromBold != toBold 326faintChanged := fromFaint != toFaint 327if boldChanged || faintChanged { 328if fromBold && ! toBold || fromFaint && ! toFaint { 329b = b .Normal () 330boldChanged = true 331faintChanged = true 332 } 333 } 334 335italicChanged := fromItalic != toItalic 336if italicChanged && ! toItalic { 337b = b .Italic (false ) 338 } 339 340underlineChanged := fromUnderline != toUnderline || from .Underline != to .Underline 341if underlineChanged && ! toUnderline { 342b = b .Underline (false ) 343 } 344 345blinkChanged := fromBlink != toBlink 346rapidBlinkChanged := fromRapidBlink != toRapidBlink 347if blinkChanged || rapidBlinkChanged { 348if fromBlink && ! toBlink || fromRapidBlink && ! toRapidBlink { 349b = b .Blink (false ) 350blinkChanged = true 351rapidBlinkChanged = true 352 } 353 } 354 355reverseChanged := fromReverse != toReverse 356if reverseChanged && ! toReverse { 357b = b .Reverse (false ) 358 } 359 360concealChanged := fromConceal != toConceal 361if concealChanged && ! toConceal { 362b = b .Conceal (false ) 363 } 364 365strikethroughChanged := fromStrikethrough != toStrikethrough 366if strikethroughChanged && ! toStrikethrough { 367b = b .Strikethrough (false ) 368 } 369 370if boldChanged && toBold { 371b = b .Bold () 372 } 373 374if faintChanged && toFaint { 375b = b .Faint () 376 } 377 378if italicChanged && toItalic { 379b = b .Italic (true ) 380 } 381 382if underlineChanged && toUnderline && to .Underline == UnderlineStyleSingle { 383// We only handle single underline here since others require more 384// specific handling at the end. 385b = b .Underline (true ) 386 } 387 388if blinkChanged && toBlink { 389b = b .Blink (true ) 390 } 391 392if rapidBlinkChanged && toRapidBlink { 393b = b .RapidBlink (true ) 394 } 395 396if reverseChanged && toReverse { 397b = b .Reverse (true ) 398 } 399 400if concealChanged && toConceal { 401b = b .Conceal (true ) 402 } 403 404if strikethroughChanged && toStrikethrough { 405b = b .Strikethrough (true ) 406 } 407 408// Handle special underline styles. 409if underlineChanged && toUnderline && to .Underline > UnderlineStyleSingle { 410b = b .UnderlineStyle (to .Underline ) 411 } 412 413return b .String () 414} 415 416func colorEqual (c ,o color.Color )bool { 417if c == nil && o == nil { 418return true 419 } 420if c == nil || o == nil { 421return false 422 } 423cr ,cg ,cb ,ca := c .RGBA () 424or ,og ,ob ,oa := o .RGBA () 425return cr == or && cg == og && cb == ob && ca == oa 426} 427 428// IsZero returns true if the style is empty. 429func (s * Style )IsZero ()bool { 430return * s == Style {} 431} 432 433// ConvertStyle converts a style to respect the given color profile. 434func ConvertStyle (s Style ,p colorprofile.Profile )Style { 435switch p { 436case colorprofile .TrueColor : 437return s 438case colorprofile .ANSI ,colorprofile .ANSI256 : 439case colorprofile .Ascii : 440s .Fg = nil 441s .Bg = nil 442s .UnderlineColor = nil 443case colorprofile .NoTTY : 444return Style {} 445 } 446 447if s .Fg != nil { 448s .Fg = p .Convert (s .Fg ) 449 } 450if s .Bg != nil { 451s .Bg = p .Convert (s .Bg ) 452 } 453if s .UnderlineColor != nil { 454s .UnderlineColor = p .Convert (s .UnderlineColor ) 455 } 456return s 457} 458 459// ConvertLink converts a hyperlink to respect the given color profile. 460func ConvertLink (h Link ,p colorprofile.Profile )Link { 461if p == colorprofile .NoTTY { 462return Link {} 463 } 464return h 465}