github.com/swaros/contxt/module/runner@v0.0.0-20240305083542-3dbd4436ac40/randcolor.go (about) 1 // MIT License 2 // 3 // Copyright (c) 2020 Thomas Ziegler <thomas.zglr@googlemail.com>. All rights reserved. 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the Software), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in all 13 // copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 // SOFTWARE. 22 23 // AINC-NOTE-0815 24 25 package runner 26 27 import ( 28 "math/rand" 29 "sync" 30 ) 31 32 // solving the problem of having different color combination for each task that is running. 33 // so any task can identify itself by a color. 34 // these colors have to readable in the combination of foreground and background. 35 // also we store the used colors per run in memory, with the name of the task as key. 36 // the colors are just the key-names from the ctxout color table. 37 // to use them in ctxout we have to wrap them in the ctxout markup. 38 // the color combinations are picked from the looksGoodCombinations list. 39 // the list is not complete, but it is a good start. 40 41 // example: 42 // randColor := runner.NewRandColorStore() 43 // colorPicked := randColor.GetColor("myTask") 44 // ctxout.PrintLn(ctxout.NewMOWrap(), colorPicked.ColorMarkup(), "myTask", ctxout.CleanTag) 45 46 var ( 47 lastInstace *RandColorStore 48 mu sync.Mutex 49 ) 50 51 type RandColor struct { 52 foreColor string // foreground color as string 53 backColor string // background color as string 54 } 55 56 type RandColorStore struct { 57 colors sync.Map 58 lastIndex int 59 } 60 61 // LastRandColorInstance returns the last instance of RandColorStore 62 // if no instance exists, a new one is created 63 // this is a singleton (kind of) 64 // the reason is just to have a global instance so you have access to the colors 65 // assigned to an task, from everywhere in the code. 66 // this logic can fail, of course if you have multiple instances of RandColorStore created. 67 func LastRandColorInstance() *RandColorStore { 68 mu.Lock() 69 defer mu.Unlock() 70 if lastInstace == nil { 71 lastInstace = NewRandColorStore() 72 } 73 return lastInstace 74 } 75 76 var ( 77 // looksGoodCombinations is a list of color combinations that are readable 78 looksGoodCombinations = []RandColor{ 79 {foreColor: "white", backColor: "black"}, 80 {foreColor: "green", backColor: "black"}, 81 {foreColor: "yellow", backColor: "black"}, 82 {foreColor: "blue", backColor: "black"}, 83 {foreColor: "magenta", backColor: "black"}, 84 {foreColor: "cyan", backColor: "black"}, 85 {foreColor: "dark-grey", backColor: "black"}, 86 {foreColor: "light-green", backColor: "black"}, 87 {foreColor: "light-grey", backColor: "black"}, 88 {foreColor: "light-red", backColor: "black"}, 89 {foreColor: "light-blue", backColor: "black"}, 90 {foreColor: "light-yellow", backColor: "black"}, 91 {foreColor: "light-cyan", backColor: "black"}, 92 {foreColor: "light-magenta", backColor: "black"}, 93 {foreColor: "white", backColor: "dark-grey"}, 94 {foreColor: "light-green", backColor: "dark-grey"}, 95 {foreColor: "light-blue", backColor: "dark-grey"}, 96 {foreColor: "light-yellow", backColor: "dark-grey"}, 97 {foreColor: "light-cyan", backColor: "dark-grey"}, 98 {foreColor: "light-magenta", backColor: "dark-grey"}, 99 {foreColor: "yellow", backColor: "dark-grey"}, 100 {foreColor: "green", backColor: "dark-grey"}, 101 {foreColor: "cyan", backColor: "dark-grey"}, 102 {foreColor: "black", backColor: "green"}, 103 {foreColor: "blue", backColor: "green"}, 104 {foreColor: "white", backColor: "green"}, 105 {foreColor: "magenta", backColor: "green"}, 106 {foreColor: "black", backColor: "yellow"}, 107 {foreColor: "black", backColor: "blue"}, 108 {foreColor: "black", backColor: "magenta"}, 109 {foreColor: "black", backColor: "cyan"}, 110 {foreColor: "black", backColor: "white"}, 111 {foreColor: "white", backColor: "yellow"}, 112 {foreColor: "white", backColor: "blue"}, 113 {foreColor: "white", backColor: "magenta"}, 114 {foreColor: "white", backColor: "cyan"}, 115 {foreColor: "blue", backColor: "yellow"}, 116 {foreColor: "blue", backColor: "cyan"}, 117 {foreColor: "blue", backColor: "white"}, 118 {foreColor: "yellow", backColor: "blue"}, 119 {foreColor: "yellow", backColor: "magenta"}, 120 {foreColor: "yellow", backColor: "cyan"}, 121 {foreColor: "yellow", backColor: "white"}, 122 } 123 ) 124 125 // NewRandColorStore creates a new RandColorStore 126 // and stores the last instance in a global variable 127 func NewRandColorStore() *RandColorStore { 128 rn := &RandColorStore{ 129 colors: sync.Map{}, 130 lastIndex: 0, 131 } 132 lastInstace = rn 133 return rn 134 } 135 136 // PickRandColor picks a random color combination from the looksGoodCombinations list 137 func PickRandColor() RandColor { 138 return looksGoodCombinations[rand.Intn(len(looksGoodCombinations))] 139 } 140 141 // PickRandColorByIndex picks a color combination from the looksGoodCombinations list 142 // by the given index 143 // if the index is out of range, the first color combination is returned 144 func PickRandColorByIndex(index int) RandColor { 145 // should not happen but just in case someone removes all colors 146 if len(looksGoodCombinations) == 0 { 147 panic("no color combinations available") 148 } 149 if index > len(looksGoodCombinations) { 150 index = 0 151 } 152 return looksGoodCombinations[index] 153 } 154 155 // PickUnusedRandColor picks a random color combination from the looksGoodCombinations list 156 // that is not in usage. 157 // if all colors are in usage, the first color combination is returned 158 // so there is no guarantee that you get an unused color combination. 159 // the second return value is true if the color combination is unused 160 // the second return value is false if the color combination is in usage 161 func (rcs *RandColorStore) PickUnusedRandColor() (RandColor, bool) { 162 tries := 0 163 for { 164 color := PickRandColor() 165 if !rcs.IsInusage(color) { 166 return color, true 167 } 168 tries++ 169 // if we failed after 10 times * max variants, we just return the color 170 if tries > rcs.GetMaxVariants()*10 { 171 return color, false 172 } 173 } 174 } 175 176 // GetOrSetIndexColor returns a color combination for the given taskName 177 func (rcs *RandColorStore) GetOrSetIndexColor(taskName string) RandColor { 178 if col, ok := rcs.colors.Load(taskName); ok { 179 return col.(RandColor) 180 } 181 col := PickRandColorByIndex(rcs.lastIndex) 182 rcs.colors.Store(taskName, col) 183 rcs.lastIndex++ 184 return col 185 } 186 187 // GetOrSetIndexColor returns a color combination for the given taskName 188 // this color is randomly picked from the looksGoodCombinations list 189 // if the color is not already in usage, the second return value is false 190 // if the color is already in usage, the second return value is true 191 // this is also the case,if we ask for a color that is already stored 192 func (rcs *RandColorStore) GetOrSetRandomColor(taskName string) (RandColor, bool) { 193 if col, ok := rcs.colors.Load(taskName); ok { 194 return col.(RandColor), true 195 } 196 col, inUse := rcs.PickUnusedRandColor() 197 rcs.colors.Store(taskName, col) 198 rcs.lastIndex++ 199 return col, inUse 200 } 201 202 // GetMaxVariants returns the number of color combinations in the looksGoodCombinations list 203 func (rcs *RandColorStore) GetMaxVariants() int { 204 return len(looksGoodCombinations) 205 } 206 207 // IsInusage returns true if the given color is already in usage 208 func (rcs *RandColorStore) IsInusage(color RandColor) bool { 209 var inusage bool 210 rcs.colors.Range(func(key, value interface{}) bool { 211 if value == color { 212 inusage = true 213 return false 214 } 215 return true 216 }, 217 ) 218 return inusage 219 } 220 221 // GetColorAsCtxMarkup returns the color combination for the given taskName 222 // as ctxout markup 223 // the first return value is the foreground color as markup 224 // the second return value is the background color as markup 225 // the third return value is the sign color as markup. this simply the foreground color 226 // used as background color, so cou can draw a sign with the foreground color. 227 func (rcs *RandColorStore) GetColorAsCtxMarkup(taskName string) (string, string, string) { 228 col, _ := rcs.GetOrSetRandomColor(taskName) 229 return col.ForeColor(), col.BackColor(), col.AsSignColor() 230 } 231 232 // ColorMarkup returns the color combination as ctxout markup 233 func (r *RandColor) ColorMarkup() string { 234 return "<f:" + r.foreColor + "><b:" + r.backColor + ">" 235 } 236 237 // ForeColor returns the foreground color as ctxout markup 238 func (r *RandColor) ForeColor() string { 239 return "<f:" + r.foreColor + ">" 240 } 241 242 // BackColor returns the background color as ctxout markup 243 func (r *RandColor) BackColor() string { 244 return "<b:" + r.backColor + ">" 245 } 246 247 // AsSignColor returns the sign color as ctxout markup 248 func (r *RandColor) AsSignColor() string { 249 return "<f:" + r.backColor + ">" 250 }