1// Copyright 2019 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2// that can be found in the LICENSE file. 3 4package gowid 5 6import ( 7 "fmt" 8 "regexp" 9 "strconv" 10 11 "github.com/gcla/gowid/gwutil" 12 "github.com/gdamore/tcell" 13 lru "github.com/hashicorp/golang-lru" 14 "github.com/lucasb-eyer/go-colorful" 15 "github.com/pkg/errors" 16) 17 18//====================================================================== 19 20// These are used as bitmasks - a style is two AttrMasks. The first bitmask says whether or not the style declares an 21// e.g. underline setting; if it's declared, the second bitmask says whether or not underline is affirmatively on or off. 22// This allows styles to be layered e.g. the lower style declares underline is on, the upper style does not declare 23// an underline preference, so when layered, the cell is rendered with an underline. 24const ( 25 StyleNoneSet tcell.AttrMask = 0 // Just unstyled text. 26 StyleAllSet tcell.AttrMask = tcell.AttrBold | tcell.AttrBlink | tcell.AttrReverse | tcell.AttrUnderline | tcell.AttrDim 27) 28 29// StyleAttrs allows the user to represent a set of styles, either affirmatively set (on) or unset (off) 30// with the rest of the styles being unspecified, meaning they can be determined by styles layered 31// "underneath". 32type StyleAttrs struct { 33 OnOff tcell.AttrMask // If the specific bit in Set is 1, then the specific bit on OnOff says whether the style is on or off 34 Set tcell.AttrMask // If the specific bit in Set is 0, then no style preference is declared (e.g. for underline) 35} 36 37// AllStyleMasks is an array of all the styles that can be applied to a Cell. 38var AllStyleMasks = [...]tcell.AttrMask{tcell.AttrBold, tcell.AttrBlink, tcell.AttrDim, tcell.AttrReverse, tcell.AttrUnderline} 39 40// StyleNone expresses no preference for any text styles. 41var StyleNone = StyleAttrs{} 42 43// StyleBold specifies the text should be bold, but expresses no preference for other text styles. 44var StyleBold = StyleAttrs{tcell.AttrBold, tcell.AttrBold} 45 46// StyleBlink specifies the text should blink, but expresses no preference for other text styles. 47var StyleBlink = StyleAttrs{tcell.AttrBlink, tcell.AttrBlink} 48 49// StyleDim specifies the text should be dim, but expresses no preference for other text styles. 50var StyleDim = StyleAttrs{tcell.AttrDim, tcell.AttrDim} 51 52// StyleReverse specifies the text should be displayed as reverse-video, but expresses no preference for other text styles. 53var StyleReverse = StyleAttrs{tcell.AttrReverse, tcell.AttrReverse} 54 55// StyleUnderline specifies the text should be underlined, but expresses no preference for other text styles. 56var StyleUnderline = StyleAttrs{tcell.AttrUnderline, tcell.AttrUnderline} 57 58// StyleBoldOnly specifies the text should be bold, and no other styling should apply. 59var StyleBoldOnly = StyleAttrs{tcell.AttrBold, StyleAllSet} 60 61// StyleBlinkOnly specifies the text should blink, and no other styling should apply. 62var StyleBlinkOnly = StyleAttrs{tcell.AttrBlink, StyleAllSet} 63 64// StyleDimOnly specifies the text should be dim, and no other styling should apply. 65var StyleDimOnly = StyleAttrs{tcell.AttrDim, StyleAllSet} 66 67// StyleReverseOnly specifies the text should be displayed reverse-video, and no other styling should apply. 68var StyleReverseOnly = StyleAttrs{tcell.AttrReverse, StyleAllSet} 69 70// StyleUnderlineOnly specifies the text should be underlined, and no other styling should apply. 71var StyleUnderlineOnly = StyleAttrs{tcell.AttrUnderline, StyleAllSet} 72 73// MergeUnder merges cell styles. E.g. if a is {underline, underline}, and upper is {!bold, bold}, that 74// means a declares that it should be rendered with underline and doesn't care about other styles; and 75// upper declares it should NOT be rendered bold, and doesn't declare about other styles. When merged, 76// the result is {underline|!bold, underline|bold}. 77func (a StyleAttrs) MergeUnder(upper StyleAttrs) StyleAttrs { 78 res := a 79 for _, am := range AllStyleMasks { 80 if (upper.Set & am) != 0 { 81 if (upper.OnOff & am) != 0 { 82 res.OnOff |= am 83 } else { 84 res.OnOff &= ^am 85 } 86 res.Set |= am 87 } 88 } 89 return res 90} 91 92//====================================================================== 93 94// ColorMode represents the color capability of a terminal. 95type ColorMode int 96 97const ( 98 // Mode256Colors represents a terminal with 256-color support. 99 Mode256Colors = ColorMode(iota) 100 101 // Mode88Colors represents a terminal with 88-color support such as rxvt. 102 Mode88Colors 103 104 // Mode16Colors represents a terminal with 16-color support. 105 Mode16Colors 106 107 // Mode8Colors represents a terminal with 8-color support. 108 Mode8Colors 109 110 // Mode8Colors represents a terminal with support for monochrome only. 111 ModeMonochrome 112 113 // Mode24BitColors represents a terminal with 24-bit color support like KDE's terminal. 114 Mode24BitColors 115) 116 117func (c ColorMode) String() string { 118 switch c { 119 case Mode256Colors: 120 return "256 colors" 121 case Mode88Colors: 122 return "88 colors" 123 case Mode16Colors: 124 return "16 colors" 125 case Mode8Colors: 126 return "8 colors" 127 case ModeMonochrome: 128 return "monochrome" 129 case Mode24BitColors: 130 return "24-bit truecolor" 131 default: 132 return fmt.Sprintf("Unknown (%d)", int(c)) 133 } 134} 135 136const ( 137 colorDefaultName = "default" 138 colorBlackName = "black" 139 colorRedName = "red" 140 colorDarkRedName = "dark red" 141 colorGreenName = "green" 142 colorDarkGreenName = "dark green" 143 colorBrownName = "brown" 144 colorBlueName = "blue" 145 colorDarkBlueName = "dark blue" 146 colorMagentaName = "magenta" 147 colorDarkMagentaName = "dark magenta" 148 colorCyanName = "cyan" 149 colorDarkCyanName = "dark cyan" 150 colorLightGrayName = "light gray" 151 colorDarkGrayName = "dark gray" 152 colorLightRedName = "light red" 153 colorLightGreenName = "light green" 154 colorYellowName = "yellow" 155 colorLightBlueName = "light blue" 156 colorLightMagentaName = "light magenta" 157 colorLightCyanName = "light cyan" 158 colorWhiteName = "white" 159) 160 161var ( 162 basicColors = map[string]int{ 163 colorDefaultName: 0, 164 colorBlackName: 1, 165 colorDarkRedName: 2, 166 colorDarkGreenName: 3, 167 colorBrownName: 4, 168 colorDarkBlueName: 5, 169 colorDarkMagentaName: 6, 170 colorDarkCyanName: 7, 171 colorLightGrayName: 8, 172 colorDarkGrayName: 9, 173 colorLightRedName: 10, 174 colorLightGreenName: 11, 175 colorYellowName: 12, 176 colorLightBlueName: 13, 177 colorLightMagentaName: 14, 178 colorLightCyanName: 15, 179 colorWhiteName: 16, 180 colorRedName: 10, 181 colorGreenName: 11, 182 colorBlueName: 13, 183 colorMagentaName: 14, 184 colorCyanName: 15, 185 } 186 187 tBasicColors = map[string]int{ 188 colorDefaultName: 0, 189 colorBlackName: 1, 190 colorDarkRedName: 2, 191 colorDarkGreenName: 3, 192 colorBrownName: 4, 193 colorDarkBlueName: 5, 194 colorDarkMagentaName: 6, 195 colorDarkCyanName: 7, 196 colorLightGrayName: 8, 197 colorDarkGrayName: 1, 198 colorLightRedName: 2, 199 colorLightGreenName: 3, 200 colorYellowName: 4, 201 colorLightBlueName: 5, 202 colorLightMagentaName: 6, 203 colorLightCyanName: 7, 204 colorWhiteName: 8, 205 colorRedName: 2, 206 colorGreenName: 3, 207 colorBlueName: 5, 208 colorMagentaName: 6, 209 colorCyanName: 7, 210 } 211 212 CubeStart = 16 // first index of color cube 213 CubeSize256 = 6 // one side of the color cube 214 graySize256 = 24 215 grayStart256 = gwutil.IPow(CubeSize256, 3) + CubeStart 216 cubeWhite256 = grayStart256 - 1 217 cubeSize88 = 4 218 graySize88 = 8 219 grayStart88 = gwutil.IPow(cubeSize88, 3) + CubeStart 220 cubeWhite88 = grayStart88 - 1 221 cubeBlack = CubeStart 222 223 cubeSteps256 = []int{0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff} 224 graySteps256 = []int{ 225 0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62, 226 0x6c, 0x76, 0x80, 0x84, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0, 227 0xda, 0xe4, 0xee, 228 } 229 230 cubeSteps88 = []int{0x00, 0x8b, 0xcd, 0xff} 231 graySteps88 = []int{0x2e, 0x5c, 0x73, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7} 232 233 cubeLookup256 = makeColorLookup(cubeSteps256, 256) 234 grayLookup256 = makeColorLookup(append([]int{0x00}, append(graySteps256, 0xff)...), 256) 235 236 cubeLookup88 = makeColorLookup(cubeSteps88, 256) 237 grayLookup88 = makeColorLookup(append([]int{0x00}, append(graySteps88, 0xff)...), 256) 238 239 cubeLookup256_16 []int 240 grayLookup256_101 []int 241 242 cubeLookup88_16 []int 243 grayLookup88_101 []int 244 245 // ColorNone means no preference if anything is layered underneath 246 ColorNone = MakeTCellNoColor() 247 248 // ColorDefault is an affirmative preference for the default terminal color 249 ColorDefault = MakeTCellColorExt(tcell.ColorDefault) 250 251 // Some pre-initialized color objects for use in applications e.g. 252 // MakePaletteEntry(ColorBlack, ColorRed) 253 ColorBlack = MakeTCellColorExt(tcell.ColorBlack) 254 ColorRed = MakeTCellColorExt(tcell.ColorRed) 255 ColorGreen = MakeTCellColorExt(tcell.ColorGreen) 256 ColorLightGreen = MakeTCellColorExt(tcell.ColorLightGreen) 257 ColorYellow = MakeTCellColorExt(tcell.ColorYellow) 258 ColorBlue = MakeTCellColorExt(tcell.ColorBlue) 259 ColorLightBlue = MakeTCellColorExt(tcell.ColorLightBlue) 260 ColorMagenta = MakeTCellColorExt(tcell.ColorDarkMagenta) 261 ColorCyan = MakeTCellColorExt(tcell.ColorDarkCyan) 262 ColorWhite = MakeTCellColorExt(tcell.ColorWhite) 263 ColorDarkRed = MakeTCellColorExt(tcell.ColorDarkRed) 264 ColorDarkGreen = MakeTCellColorExt(tcell.ColorDarkGreen) 265 ColorDarkBlue = MakeTCellColorExt(tcell.ColorDarkBlue) 266 ColorLightGray = MakeTCellColorExt(tcell.ColorLightGray) 267 ColorDarkGray = MakeTCellColorExt(tcell.ColorDarkGray) 268 ColorPurple = MakeTCellColorExt(tcell.ColorPurple) 269 ColorOrange = MakeTCellColorExt(tcell.ColorOrange) 270 271 longColorRE = regexp.MustCompile(`^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$`) 272 shortColorRE = regexp.MustCompile(`^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$`) 273 grayHexColorRE = regexp.MustCompile(`^g#([0-9a-fA-F][0-9a-fA-F])$`) 274 grayDecColorRE = regexp.MustCompile(`^g(1?[0-9][0-9]?)$`) 275 276 colorfulBlack8 = colorful.Color{R: 0.0, G: 0.0, B: 0.0} 277 colorfulWhite8 = colorful.Color{R: 1.0, G: 1.0, B: 1.0} 278 colorfulRed8 = colorful.Color{R: 1.0, G: 0.0, B: 0.0} 279 colorfulGreen8 = colorful.Color{R: 0.0, G: 1.0, B: 0.0} 280 colorfulBlue8 = colorful.Color{R: 0.0, G: 0.0, B: 1.0} 281 colorfulYellow8 = colorful.Color{R: 1.0, G: 1.0, B: 0.0} 282 colorfulMagenta8 = colorful.Color{R: 1.0, G: 0.0, B: 1.0} 283 colorfulCyan8 = colorful.Color{R: 0.0, G: 1.0, B: 1.0} 284 285 colorfulBlack16 = colorful.Color{R: 0.0, G: 0.0, B: 0.0} 286 colorfulWhite16 = colorful.Color{R: 0.66, G: 0.66, B: 0.66} 287 colorfulRed16 = colorful.Color{R: 0.5, G: 0.0, B: 0.0} 288 colorfulGreen16 = colorful.Color{R: 0.0, G: 0.5, B: 0.0} 289 colorfulBlue16 = colorful.Color{R: 0.0, G: 0.0, B: 0.5} 290 colorfulYellow16 = colorful.Color{R: 0.5, G: 0.5, B: 0.5} 291 colorfulMagenta16 = colorful.Color{R: 0.5, G: 0.0, B: 0.5} 292 colorfulCyan16 = colorful.Color{R: 0.0, G: 0.5, B: 0.5} 293 colorfulBrightBlack16 = colorful.Color{R: 0.33, G: 0.33, B: 0.33} 294 colorfulBrightWhite16 = colorful.Color{R: 1.0, G: 1.0, B: 1.0} 295 colorfulBrightRed16 = colorful.Color{R: 1.0, G: 0.0, B: 0.0} 296 colorfulBrightGreen16 = colorful.Color{R: 0.0, G: 1.0, B: 0.0} 297 colorfulBrightBlue16 = colorful.Color{R: 0.0, G: 0.0, B: 1.0} 298 colorfulBrightYellow16 = colorful.Color{R: 1.0, G: 1.0, B: 1.0} 299 colorfulBrightMagenta16 = colorful.Color{R: 1.0, G: 0.0, B: 1.0} 300 colorfulBrightCyan16 = colorful.Color{R: 0.0, G: 1.0, B: 1.0} 301 302 // Used in mapping RGB colors down to 8 terminal colors. 303 colorful8 = []colorful.Color{ 304 colorfulBlack8, 305 colorfulWhite8, 306 colorfulRed8, 307 colorfulGreen8, 308 colorfulBlue8, 309 colorfulYellow8, 310 colorfulMagenta8, 311 colorfulCyan8, 312 } 313 314 // Used in mapping RGB colors down to 16 terminal colors. 315 colorful16 = []colorful.Color{ 316 colorfulBlack16, 317 colorfulWhite16, 318 colorfulRed16, 319 colorfulGreen16, 320 colorfulBlue16, 321 colorfulYellow16, 322 colorfulMagenta16, 323 colorfulCyan16, 324 colorfulBrightBlack16, 325 colorfulBrightWhite16, 326 colorfulBrightRed16, 327 colorfulBrightGreen16, 328 colorfulBrightBlue16, 329 colorfulBrightYellow16, 330 colorfulBrightMagenta16, 331 colorfulBrightCyan16, 332 } 333 334 term8 = []TCellColor{ 335 ColorBlack, 336 ColorWhite, 337 ColorRed, 338 ColorGreen, 339 ColorBlue, 340 ColorYellow, 341 ColorMagenta, 342 ColorCyan, 343 } 344 345 term16 = []TCellColor{ 346 ColorBlack, 347 ColorLightGray, 348 ColorDarkRed, 349 ColorDarkGreen, 350 ColorDarkBlue, 351 ColorYellow, 352 ColorMagenta, 353 ColorCyan, 354 ColorDarkGray, 355 ColorWhite, 356 ColorRed, 357 ColorGreen, 358 ColorBlue, 359 ColorYellow, 360 ColorMagenta, 361 ColorCyan, // TODO - figure out these colors 362 } 363 364 term2Cache *lru.Cache 365 term8Cache *lru.Cache 366 term16Cache *lru.Cache 367) 368 369//====================================================================== 370 371func init() { 372 cubeLookup256_16 = make([]int, 16) 373 cubeLookup88_16 = make([]int, 16) 374 grayLookup256_101 = make([]int, 101) 375 grayLookup88_101 = make([]int, 101) 376 377 for i := 0; i < 16; i++ { 378 cubeLookup256_16[i] = cubeLookup256[intScale(i, 16, 0x100)] 379 cubeLookup88_16[i] = cubeLookup88[intScale(i, 16, 0x100)] 380 } 381 for i := 0; i < 101; i++ { 382 grayLookup256_101[i] = grayLookup256[intScale(i, 101, 0x100)] 383 grayLookup88_101[i] = grayLookup88[intScale(i, 101, 0x100)] 384 } 385 386 var err error 387 for _, cache := range []**lru.Cache{&term2Cache, &term8Cache, &term16Cache} { 388 *cache, err = lru.New(100) 389 if err != nil { 390 panic(err) 391 } 392 } 393} 394 395// makeColorLookup([0, 7, 9], 10) 396// [0, 0, 0, 0, 1, 1, 1, 1, 2, 2] 397// 398func makeColorLookup(vals []int, length int) []int { 399 res := make([]int, length) 400 401 vi := 0 402 for i := 0; i < len(res); i++ { 403 if vi+1 < len(vals) { 404 if i <= (vals[vi]+vals[vi+1])/2 { 405 res[i] = vi 406 } else { 407 vi++ 408 res[i] = vi 409 } 410 } else if vi < len(vals) { 411 // only last vi is valid 412 res[i] = vi 413 } 414 } 415 return res 416} 417 418// Scale val in the range [0, val_range-1] to an integer in the range 419// [0, out_range-1]. This implementation uses the "round-half-up" rounding 420// method. 421// 422func intScale(val int, val_range int, out_range int) int { 423 num := val*(out_range-1)*2 + (val_range - 1) 424 dem := (val_range - 1) * 2 425 return num / dem 426} 427 428//====================================================================== 429 430type ColorModeMismatch struct { 431 Color IColor 432 Mode ColorMode 433} 434 435var _ error = ColorModeMismatch{} 436 437func (e ColorModeMismatch) Error() string { 438 return fmt.Sprintf("Color %v of type %T not supported in mode %v", e.Color, e.Color, e.Mode) 439} 440 441type InvalidColor struct { 442 Color interface{} 443} 444 445var _ error = InvalidColor{} 446 447func (e InvalidColor) Error() string { 448 return fmt.Sprintf("Color %v of type %T is invalid", e.Color, e.Color) 449} 450 451//====================================================================== 452 453// ICellStyler is an analog to urwid's AttrSpec (http://urwid.org/reference/attrspec.html). When provided 454// a RenderContext (specifically the color mode in which to be rendered), the GetStyle() function will 455// return foreground, background and style values with which a cell should be rendered. The IRenderContext 456// argument provides access to the global palette, so an ICellStyle implementation can look up palette 457// entries by name. 458type ICellStyler interface { 459 GetStyle(IRenderContext) (IColor, IColor, StyleAttrs) 460} 461 462// IColor is implemented by any object that can turn itself into a TCellColor, meaning a color with 463// which a cell can be rendered. The display mode (e.g. 256 colors) is provided. If no TCellColor is 464// available, the second argument should be set to false e.g. no color can be found given a particular 465// string name. 466type IColor interface { 467 ToTCellColor(mode ColorMode) (TCellColor, bool) 468} 469 470// MakeCellStyle constructs a tcell.Style from gowid colors and styles. The return value can be provided 471// to tcell in order to style a particular region of the screen. 472func MakeCellStyle(fg TCellColor, bg TCellColor, attr StyleAttrs) tcell.Style { 473 var fgt, bgt tcell.Color 474 if fg == ColorNone { 475 fgt = tcell.ColorDefault 476 } else { 477 fgt = fg.ToTCell() 478 } 479 if bg == ColorNone { 480 bgt = tcell.ColorDefault 481 } else { 482 bgt = bg.ToTCell() 483 } 484 st := StyleNone.MergeUnder(attr) 485 return tcell.Style(st.OnOff).Foreground(fgt).Background(bgt) 486} 487 488//====================================================================== 489 490// Color satisfies IColor, embeds an IColor, and allows a gowid Color to be 491// constructed from a string alone. Each of the more specific color types is 492// tried in turn with the string until one succeeds. 493type Color struct { 494 IColor 495 Id string 496} 497 498func (c Color) String() string { 499 return fmt.Sprintf("%v", c.IColor) 500} 501 502// MakeColorSafe returns a Color struct specified by the string argument, in a 503// do-what-I-mean fashion - it tries the Color struct maker functions in 504// a pre-determined order until one successfully initialized a Color, or 505// until all fail - in which case an error is returned. The order tried is 506// TCellColor, RGBColor, GrayColor, UrwidColor. 507func MakeColorSafe(s string) (Color, error) { 508 var col IColor 509 var err error 510 col, err = MakeTCellColor(s) 511 if err == nil { 512 return Color{col, s}, nil 513 } 514 col, err = MakeRGBColorSafe(s) 515 if err == nil { 516 return Color{col, s}, nil 517 } 518 col, err = MakeGrayColorSafe(s) 519 if err == nil { 520 return Color{col, s}, nil 521 } 522 col, err = NewUrwidColorSafe(s) 523 if err == nil { 524 return Color{col, s}, nil 525 } 526 527 return Color{}, errors.WithStack(InvalidColor{Color: s}) 528} 529 530func MakeColor(s string) Color { 531 res, err := MakeColorSafe(s) 532 if err != nil { 533 panic(err) 534 } 535 return res 536} 537 538//====================================================================== 539 540type ColorByMode struct { 541 Colors map[ColorMode]IColor // Indexed by ColorMode 542} 543 544var _ IColor = (*ColorByMode)(nil) 545 546func MakeColorByMode(cols map[ColorMode]IColor) ColorByMode { 547 res, err := MakeColorByModeSafe(cols) 548 if err != nil { 549 panic(err) 550 } 551 return res 552} 553 554func MakeColorByModeSafe(cols map[ColorMode]IColor) (ColorByMode, error) { 555 return ColorByMode{Colors: cols}, nil 556} 557 558func (c ColorByMode) ToTCellColor(mode ColorMode) (TCellColor, bool) { 559 if col, ok := c.Colors[mode]; ok { 560 col2, ok := col.ToTCellColor(mode) 561 return col2, ok 562 } 563 panic(ColorModeMismatch{Color: c, Mode: mode}) 564} 565 566//====================================================================== 567 568// RGBColor allows for use of colors specified as three components, each with values from 0x0 to 0xf. 569// Note that an RGBColor should render as close to the components specify regardless of the color mode 570// of the terminal - 24-bit color, 256-color, 88-color. Gowid constructs a color cube, just like urwid, 571// and for each color mode, has a lookup table that maps the rgb values to a color cube value which is 572// closest to the intended color. Note that RGBColor is not supported in 16-color, 8-color or 573// monochrome. 574type RGBColor struct { 575 Red, Green, Blue int 576} 577 578var _ IColor = (*RGBColor)(nil) 579 580// MakeRGBColor constructs an RGBColor from a string e.g. "#f00" is red. Note that 581// MakeRGBColorSafe should be used unless you are sure the string provided is valid 582// (otherwise there will be a panic). 583func MakeRGBColor(s string) RGBColor { 584 res, err := MakeRGBColorSafe(s) 585 if err != nil { 586 panic(err) 587 } 588 return res 589} 590 591func (r RGBColor) String() string { 592 return fmt.Sprintf("RGBColor(#%02x,#%02x,#%02x)", r.Red, r.Green, r.Blue) 593} 594 595// MakeRGBColorSafe does the same as MakeRGBColor except will return an 596// error if provided with invalid input. 597func MakeRGBColorSafe(s string) (RGBColor, error) { 598 var mult int64 = 1 599 match := longColorRE.FindAllStringSubmatch(s, -1) 600 if len(match) == 0 { 601 match = shortColorRE.FindAllStringSubmatch(s, -1) 602 if len(match) == 0 { 603 return RGBColor{}, errors.WithStack(InvalidColor{Color: s}) 604 } 605 mult = 16 606 } 607 608 d1, _ := strconv.ParseInt(match[0][1], 16, 16) 609 d2, _ := strconv.ParseInt(match[0][2], 16, 16) 610 d3, _ := strconv.ParseInt(match[0][3], 16, 16) 611 612 d1 *= mult 613 d2 *= mult 614 d3 *= mult 615 616 x := MakeRGBColorExt(int(d1), int(d2), int(d3)) 617 return x, nil 618} 619 620// MakeRGBColorExtSafe builds an RGBColor from the red, green and blue components 621// provided as integers. If the values are out of range, an error is returned. 622func MakeRGBColorExtSafe(r, g, b int) (RGBColor, error) { 623 col := RGBColor{r, g, b} 624 if r > 0xff || g > 0xff || b > 0xff { 625 return RGBColor{}, errors.WithStack(errors.WithMessage(InvalidColor{Color: col}, "RGBColor parameters must be between 0x00 and 0xfff")) 626 } 627 return col, nil 628} 629 630// MakeRGBColorExt builds an RGBColor from the red, green and blue components 631// provided as integers. If the values are out of range, the function will panic. 632func MakeRGBColorExt(r, g, b int) RGBColor { 633 res, err := MakeRGBColorExtSafe(r, g, b) 634 if err != nil { 635 panic(err) 636 } 637 638 return res 639} 640 641// Implements golang standard library's color.Color 642func (rgb RGBColor) RGBA() (r, g, b, a uint32) { 643 r = uint32(rgb.Red << 8) 644 g = uint32(rgb.Green << 8) 645 b = uint32(rgb.Blue << 8) 646 a = 0xffff 647 return 648} 649 650func (r RGBColor) findClosest(from []colorful.Color, corresponding []TCellColor, cache *lru.Cache) TCellColor { 651 var best float64 = 100.0 652 var j int 653 654 if res, ok := cache.Get(r); ok { 655 return res.(TCellColor) 656 } 657 658 ccol, _ := colorful.MakeColor(r) 659 660 for i, c := range from { 661 x := c.DistanceLab(ccol) 662 if x < best { 663 best = x 664 j = i 665 } 666 } 667 668 cache.Add(r, corresponding[j]) 669 670 return corresponding[j] 671} 672 673// ToTCellColor converts an RGBColor to a TCellColor, suitable for rendering to the screen 674// with tcell. It lets RGBColor conform to IColor. 675func (r RGBColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { 676 switch mode { 677 case Mode24BitColors: 678 c := tcell.Color((r.Red << 16) | (r.Green << 8) | (r.Blue << 0) | int(tcell.ColorIsRGB)) 679 return MakeTCellColorExt(c), true 680 case Mode256Colors: 681 rd := cubeLookup256_16[r.Red>>4] 682 g := cubeLookup256_16[r.Green>>4] 683 b := cubeLookup256_16[r.Blue>>4] 684 c := tcell.Color((CubeStart + (((rd * CubeSize256) + g) * CubeSize256) + b) + 0) 685 return MakeTCellColorExt(c), true 686 case Mode88Colors: 687 rd := cubeLookup88_16[r.Red>>4] 688 g := cubeLookup88_16[r.Green>>4] 689 b := cubeLookup88_16[r.Blue>>4] 690 c := tcell.Color((CubeStart + (((rd * cubeSize88) + g) * cubeSize88) + b) + 0) 691 return MakeTCellColorExt(c), true 692 case Mode16Colors: 693 return r.findClosest(colorful16, term16, term16Cache), true 694 case Mode8Colors: 695 return r.findClosest(colorful8, term8, term8Cache), true 696 case ModeMonochrome: 697 return r.findClosest(colorful8[0:1], term8[0:1], term2Cache), true 698 default: 699 return TCellColor{}, false 700 } 701} 702 703//====================================================================== 704 705// UrwidColor is a gowid Color implementing IColor and which allows urwid color names to be used 706// (http://urwid.org/manual/displayattributes.html#foreground-and-background-settings) e.g. 707// "dark blue", "light gray". 708type UrwidColor struct { 709 Id string 710 cached bool 711 cache [2]TCellColor 712} 713 714var _ IColor = (*UrwidColor)(nil) 715 716// NewUrwidColorSafe returns a pointer to an UrwidColor struct and builds the UrwidColor from 717// a string argument e.g. "yellow". Note that in urwid proper (python), a color can also specify 718// a style, like "yellow, underline". UrwidColor does not support specifying styles in that manner. 719func NewUrwidColorSafe(val string) (*UrwidColor, error) { 720 return &UrwidColor{ 721 Id: val, 722 }, nil 723} 724 725// NewUrwidColorSafe returns a pointer to an UrwidColor struct and builds the UrwidColor from 726// a string argument e.g. "yellow"; this function will panic if the there is an error during 727// initialization. 728func NewUrwidColor(val string) *UrwidColor { 729 res, err := NewUrwidColorSafe(val) 730 if err != nil { 731 panic(err) 732 } 733 734 return res 735} 736 737func (r UrwidColor) String() string { 738 return fmt.Sprintf("UrwidColor(%s)", r.Id) 739} 740 741// ToTCellColor converts the receiver UrwidColor to a TCellColor, ready for rendering to a 742// tcell screen. This lets UrwidColor conform to IColor. 743func (s *UrwidColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { 744 if s.cached { 745 switch mode { 746 case Mode24BitColors, Mode256Colors, Mode88Colors, Mode16Colors: 747 return s.cache[0], true 748 case Mode8Colors, ModeMonochrome: 749 return s.cache[1], true 750 default: 751 panic(errors.WithStack(ColorModeMismatch{Color: s, Mode: mode})) 752 } 753 } 754 755 idx := -1 756 switch mode { 757 case Mode24BitColors, Mode256Colors, Mode88Colors, Mode16Colors: 758 idx = posInMap(s.Id, basicColors) 759 case Mode8Colors, ModeMonochrome: 760 idx = posInMap(s.Id, tBasicColors) 761 default: 762 panic(errors.WithStack(ColorModeMismatch{Color: s, Mode: mode})) 763 } 764 765 if idx == -1 { 766 panic(errors.WithStack(InvalidColor{Color: s})) 767 } 768 769 idx = idx - 1 // offset for tcell, which stores default at -1 770 771 c := MakeTCellColorExt(tcell.Color(idx)) 772 773 switch mode { 774 case Mode24BitColors, Mode256Colors, Mode88Colors, Mode16Colors: 775 s.cache[0] = c 776 case Mode8Colors, ModeMonochrome: 777 s.cache[1] = c 778 } 779 s.cached = true 780 781 return c, true 782} 783 784//====================================================================== 785 786// GrayColor is an IColor that represents a greyscale specified by the 787// same syntax as urwid - http://urwid.org/manual/displayattributes.html 788// and search for "gray scale entries". Strings may be of the form "g3", 789// "g100" or "g#a1", "g#ff" if hexadecimal is preferred. These index the 790// grayscale color cube. 791type GrayColor struct { 792 Val int 793} 794 795func (g GrayColor) String() string { 796 return fmt.Sprintf("GrayColor(%d)", g.Val) 797} 798 799// MakeGrayColorSafe returns an initialized GrayColor provided with a string 800// input like "g50" or "g#ab". If the input is invalid, an error is returned. 801func MakeGrayColorSafe(val string) (GrayColor, error) { 802 var d uint64 803 match := grayDecColorRE.FindAllStringSubmatch(val, -1) 804 if len(match) == 0 || len(match[0]) != 2 { 805 match := grayHexColorRE.FindAllStringSubmatch(val, -1) 806 if len(match) == 0 || len(match[0]) != 2 { 807 return GrayColor{}, errors.WithStack(InvalidColor{Color: val}) 808 } 809 d, _ = strconv.ParseUint(match[0][1], 16, 8) 810 } else { 811 d, _ = strconv.ParseUint(match[0][1], 10, 8) 812 if d > 100 { 813 return GrayColor{}, errors.WithStack(InvalidColor{Color: val}) 814 } 815 } 816 817 return GrayColor{int(d)}, nil 818} 819 820// MakeGrayColor returns an initialized GrayColor provided with a string 821// input like "g50" or "g#ab". If the input is invalid, the function panics. 822func MakeGrayColor(val string) GrayColor { 823 res, err := MakeGrayColorSafe(val) 824 if err != nil { 825 panic(err) 826 } 827 828 return res 829} 830 831func grayAdjustment88(val int) int { 832 if val == 0 { 833 return cubeBlack 834 } 835 val -= 1 836 if val == graySize88 { 837 return cubeWhite88 838 } 839 y := grayStart88 + val 840 return y 841} 842 843func grayAdjustment256(val int) int { 844 if val == 0 { 845 return cubeBlack 846 } 847 val -= 1 848 if val == graySize256 { 849 return cubeWhite256 850 } 851 y := grayStart256 + val 852 return y 853} 854 855// ToTCellColor converts the receiver GrayColor to a TCellColor, ready for rendering to a 856// tcell screen. This lets GrayColor conform to IColor. 857func (s GrayColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { 858 switch mode { 859 case Mode24BitColors: 860 adj := intScale(s.Val, 101, 0x100) 861 c := tcell.Color((adj << 16) | (adj << 8) | (adj << 0) | int(tcell.ColorIsRGB)) 862 return MakeTCellColorExt(c), true 863 case Mode256Colors: 864 x := tcell.Color(grayAdjustment256(grayLookup256_101[s.Val]) + 1) 865 return MakeTCellColorExt(x), true 866 case Mode88Colors: 867 x := tcell.Color(grayAdjustment88(grayLookup88_101[s.Val]) + 1) 868 return MakeTCellColorExt(x), true 869 default: 870 panic(errors.WithStack(ColorModeMismatch{Color: s, Mode: mode})) 871 } 872} 873 874//====================================================================== 875 876// TCellColor is an IColor using tcell's color primitives. If you are not porting from urwid or translating 877// from urwid, this is the simplest approach to using color. Note that the underlying tcell.Color value is 878// stored offset by 2 from the value tcell would use to actually render a colored cell. Tcell represents 879// e.g. black by 0, maroon by 1, and so on - that means the default/empty/zero value for a tcell.Color object 880// is the color black. Gowid's layering approach means that the empty value for a color should mean "no color 881// preference" - so we want the zero value to mean that. A tcell.Color of -1 means "default color". So gowid 882// coopts -2 to mean "no color preference". We store the tcell.Color offset by 2 so the empty value for a 883// TCellColor means "no color preference". When we convert to a tcell.Color, we subtract 2 (but since a value 884// of -2 is meaningless to tcell, the caller should check and not pass on a value of -2 to tcell APIs - see 885// gowid.ColorNone) 886type TCellColor struct { 887 tc tcell.Color 888} 889 890var ( 891 _ IColor = (*TCellColor)(nil) 892 _ fmt.Stringer = (*TCellColor)(nil) 893) 894 895// MakeTCellColor returns an initialized TCellColor given a string input like "yellow". The names that can be 896// used are provided here: https://github.com/gdamore/tcell/blob/master/color.go#L821. 897func MakeTCellColor(val string) (TCellColor, error) { 898 if col, ok := tcell.ColorNames[val]; !ok { 899 return TCellColor{}, errors.WithStack(InvalidColor{Color: val}) 900 } else { 901 return MakeTCellColorExt(col), nil 902 } 903} 904 905// MakeTCellColor returns an initialized TCellColor given a tcell.Color input. The values that can be 906// used are provided here: https://github.com/gdamore/tcell/blob/master/color.go#L41. 907func MakeTCellColorSafe(val tcell.Color) (TCellColor, error) { 908 return TCellColor{val + 2}, nil 909} 910 911// MakeTCellColor returns an initialized TCellColor given a tcell.Color input. The values that can be 912// used are provided here: https://github.com/gdamore/tcell/blob/master/color.go#L41. 913func MakeTCellColorExt(val tcell.Color) TCellColor { 914 res, _ := MakeTCellColorSafe(val) 915 return res 916} 917 918// MakeTCellNoColor returns an initialized TCellColor that represents "no color" - meaning if another 919// color is rendered "under" this one, then the color underneath will be displayed. 920func MakeTCellNoColor() TCellColor { 921 res := MakeTCellColorExt(-2) 922 return res 923} 924 925// String implements Stringer for '%v' support. 926func (r TCellColor) String() string { 927 c := r.tc - 2 928 if c == -2 { 929 return "[no-color]" 930 } else { 931 return fmt.Sprintf("TCellColor(%v)", tcell.Color(c)) 932 } 933} 934 935// ToTCell converts a TCellColor back to a tcell.Color for passing to tcell APIs. 936func (r TCellColor) ToTCell() tcell.Color { 937 return r.tc - 2 938} 939 940// ToTCellColor is a no-op, and exists so that TCellColor conforms to the IColor interface. 941func (r TCellColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { 942 return r, true 943} 944 945//====================================================================== 946 947// NoColor implements IColor, and represents "no color preference", distinct from the default terminal color, 948// white, black, etc. This means that if a NoColor is rendered over another color, the color underneath will 949// be displayed. 950type NoColor struct{} 951 952// ToTCellColor converts NoColor to TCellColor. This lets NoColor conform to the IColor interface. 953func (r NoColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { 954 return ColorNone, true 955} 956 957func (r NoColor) String() string { 958 return "NoColor" 959} 960 961//====================================================================== 962 963// DefaultColor implements IColor and means use whatever the default terminal color is. This is 964// different to NoColor, which expresses no preference. 965type DefaultColor struct{} 966 967// ToTCellColor converts DefaultColor to TCellColor. This lets DefaultColor conform to the IColor interface. 968func (r DefaultColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { 969 return MakeTCellColorExt(tcell.ColorDefault), true 970} 971 972func (r DefaultColor) String() string { 973 return "DefaultColor" 974} 975 976//====================================================================== 977 978// ColorInverter implements ICellStyler, and simply swaps foreground and background colors. 979type ColorInverter struct { 980 ICellStyler 981} 982 983func (c ColorInverter) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 984 y, x, z = c.ICellStyler.GetStyle(prov) 985 return 986} 987 988//====================================================================== 989 990// PaletteEntry is typically used by a gowid application to represent a set of color and style 991// preferences for use by different application widgets e.g. black text on a white background 992// with text underlined. PaletteEntry implements the ICellStyler interface meaning it can 993// provide a triple of foreground and background IColor, and a StyleAttrs struct. 994type PaletteEntry struct { 995 FG IColor 996 BG IColor 997 Style StyleAttrs 998} 999 1000var _ ICellStyler = (*PaletteEntry)(nil) 1001 1002// MakeStyledPaletteEntry simply stores the three parameters provided - a foreground and 1003// background IColor, and a StyleAttrs struct. 1004func MakeStyledPaletteEntry(fg, bg IColor, style StyleAttrs) PaletteEntry { 1005 return PaletteEntry{fg, bg, style} 1006} 1007 1008// MakePaletteEntry stores the two IColor parameters provided, and has no style preference. 1009func MakePaletteEntry(fg, bg IColor) PaletteEntry { 1010 return PaletteEntry{fg, bg, StyleNone} 1011} 1012 1013// GetStyle returns the individual colors and style attributes. 1014func (a PaletteEntry) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1015 x, y, z = a.FG, a.BG, a.Style 1016 return 1017} 1018 1019//====================================================================== 1020 1021// PaletteRef is intended to represent a PaletteEntry, looked up by name. The ICellStyler 1022// API GetStyle() provides an IRenderContext and should return two colors and style attributes. 1023// PaletteRef provides these by looking up the IRenderContext with the name (string) provided 1024// to it at initialization. 1025type PaletteRef struct { 1026 Name string 1027} 1028 1029var _ ICellStyler = (*PaletteRef)(nil) 1030 1031// MakePaletteRef returns a PaletteRef struct storing the (string) name of the PaletteEntry 1032// which will be looked up in the IRenderContext. 1033func MakePaletteRef(name string) PaletteRef { 1034 return PaletteRef{name} 1035} 1036 1037// GetStyle returns the two colors and a style, looked up in the IRenderContext by name. 1038func (a PaletteRef) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1039 spec, ok := prov.CellStyler(a.Name) 1040 if ok { 1041 x, y, z = spec.GetStyle(prov) 1042 } else { 1043 x, y, z = NoColor{}, NoColor{}, StyleAttrs{} 1044 } 1045 return 1046} 1047 1048//====================================================================== 1049 1050// EmptyPalette implements ICellStyler and returns no preference for any colors or styling. 1051type EmptyPalette struct{} 1052 1053var _ ICellStyler = (*EmptyPalette)(nil) 1054 1055func MakeEmptyPalette() EmptyPalette { 1056 return EmptyPalette{} 1057} 1058 1059// GetStyle implements ICellStyler. 1060func (a EmptyPalette) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1061 x, y, z = NoColor{}, NoColor{}, StyleAttrs{} 1062 return 1063} 1064 1065//====================================================================== 1066 1067// StyleMod implements ICellStyler. It returns colors and styles from its Cur field unless they are 1068// overridden by settings in its Mod field. This provides a way for a layering of ICellStylers. 1069type StyleMod struct { 1070 Cur ICellStyler 1071 Mod ICellStyler 1072} 1073 1074var _ ICellStyler = (*StyleMod)(nil) 1075 1076// MakeStyleMod implements ICellStyler and stores two ICellStylers, one to layer on top of the 1077// other. 1078func MakeStyleMod(cur, mod ICellStyler) StyleMod { 1079 return StyleMod{cur, mod} 1080} 1081 1082// GetStyle returns the IColors and StyleAttrs from the Mod ICellStyler if they express an 1083// affirmative preference, otherwise defers to the values from the Cur ICellStyler. 1084func (a StyleMod) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1085 fcur, bcur, scur := a.Cur.GetStyle(prov) 1086 fmod, bmod, smod := a.Mod.GetStyle(prov) 1087 var ok bool 1088 _, ok = fmod.ToTCellColor(prov.GetColorMode()) 1089 if ok { 1090 x = fmod 1091 } else { 1092 x = fcur 1093 } 1094 _, ok = bmod.ToTCellColor(prov.GetColorMode()) 1095 if ok { 1096 y = bmod 1097 } else { 1098 y = bcur 1099 } 1100 z = scur.MergeUnder(smod) 1101 return 1102} 1103 1104//====================================================================== 1105 1106// ForegroundColor is an ICellStyler that expresses a specific foreground color and no preference for 1107// background color or style. 1108type ForegroundColor struct { 1109 IColor 1110} 1111 1112var _ ICellStyler = (*ForegroundColor)(nil) 1113 1114func MakeForeground(c IColor) ForegroundColor { 1115 return ForegroundColor{c} 1116} 1117 1118// GetStyle implements ICellStyler. 1119func (a ForegroundColor) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1120 x = a.IColor 1121 y = NoColor{} 1122 z = StyleNone 1123 return 1124} 1125 1126//====================================================================== 1127 1128// BackgroundColor is an ICellStyler that expresses a specific background color and no preference for 1129// foreground color or style. 1130type BackgroundColor struct { 1131 IColor 1132} 1133 1134var _ ICellStyler = (*BackgroundColor)(nil) 1135 1136func MakeBackground(c IColor) BackgroundColor { 1137 return BackgroundColor{c} 1138} 1139 1140// GetStyle implements ICellStyler. 1141func (a BackgroundColor) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1142 x = NoColor{} 1143 y = a.IColor 1144 z = StyleNone 1145 return 1146} 1147 1148//====================================================================== 1149 1150// StyledAs is an ICellStyler that expresses a specific text style and no preference for 1151// foreground and background color. 1152type StyledAs struct { 1153 StyleAttrs 1154} 1155 1156var _ ICellStyler = (*StyledAs)(nil) 1157 1158func MakeStyledAs(s StyleAttrs) StyledAs { 1159 return StyledAs{s} 1160} 1161 1162// GetStyle implements ICellStyler. 1163func (a StyledAs) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { 1164 x = NoColor{} 1165 y = NoColor{} 1166 z = a.StyleAttrs 1167 return 1168} 1169 1170//====================================================================== 1171 1172// Palette implements IPalette and is a trivial implementation of a type that can store 1173// cell stylers and provide access to them via iteration. 1174type Palette map[string]ICellStyler 1175 1176var _ IPalette = (*Palette)(nil) 1177 1178// CellStyler will return an ICellStyler by name, if it exists. 1179func (m Palette) CellStyler(name string) (ICellStyler, bool) { 1180 i, ok := m[name] 1181 return i, ok 1182} 1183 1184// RangeOverPalette applies the supplied function to each member of the 1185// palette. If the function returns false, the loop terminates early. 1186func (m Palette) RangeOverPalette(f func(k string, v ICellStyler) bool) { 1187 for k, v := range m { 1188 if !f(k, v) { 1189 break 1190 } 1191 } 1192} 1193 1194//====================================================================== 1195 1196// IColorToTCell is a utility function that will convert an IColor to a TCellColor 1197// in preparation for passing to tcell to render; if the conversion fails, a default 1198// TCellColor is returned (provided to the function via a parameter) 1199func IColorToTCell(color IColor, def TCellColor, mode ColorMode) TCellColor { 1200 res := def 1201 colTC, ok := color.ToTCellColor(mode) // Is there a color specified affirmatively? (i.e. not NoColor) 1202 if ok && colTC != ColorNone { // Yes a color specified 1203 res = colTC 1204 } 1205 return res 1206} 1207 1208//====================================================================== 1209// Local Variables: 1210// mode: Go 1211// fill-column: 110 1212// End: 1213