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  }