Loading/progress indicator for shell with aging emojis


Recently, while waiting for a long-running script to finish, I thought that it would be nice to have some sort of loader with aging emojis. **TLDR:** [we-are-waiting](https://github.com/nvbn/we-are-waiting).

The "life" of an emoji is simple:

šŸ‘¶šŸæ → šŸ‘§šŸæ → šŸ‘©šŸæ → šŸ‘±šŸæā€ā™€ļø → šŸ‘©šŸæā€āš•ļø → šŸ‘µšŸæ

It contains aging from a baby to grown-up person, one profession, and oldness.

And as we have four colors, two genders, five ages, and 22 professions. We can have a great variety of lives. So as the first thing to do I decided to generate all those variants. Initially, I was planning to implement everything in Go, but it's not possible to use emojis in Go code, only codepoints. Because of that, I decided to write a little Python script, that will generate Go code with all variants with codepoints instead of emojis.

For that I just copied lines with emojis from getemoji.com and put them in lists:

ages = [
    "šŸ‘¶ šŸ‘¦ šŸ‘§ šŸ‘Ø šŸ‘© šŸ‘±ā€ā™€ļø šŸ‘± šŸ‘“ šŸ‘µ",
    "šŸ‘¶šŸ» šŸ‘¦šŸ» šŸ‘§šŸ» šŸ‘ØšŸ» šŸ‘©šŸ» šŸ‘±šŸ»ā€ā™€ļø šŸ‘±šŸ» šŸ‘“šŸ» šŸ‘µšŸ»",
    "šŸ‘¶šŸ¼ šŸ‘¦šŸ¼ šŸ‘§šŸ¼ šŸ‘ØšŸ¼ šŸ‘©šŸ¼ šŸ‘±šŸ¼ā€ā™€ļø šŸ‘±šŸ¼ šŸ‘“šŸ¼ šŸ‘µšŸ¼",
    "šŸ‘¶šŸ½ šŸ‘¦šŸ½ šŸ‘§šŸ½ šŸ‘ØšŸ½ šŸ‘©šŸ½ šŸ‘±šŸ½ā€ā™€ļø šŸ‘±šŸ½ šŸ‘“šŸ½ šŸ‘µšŸ½",
    "šŸ‘¶šŸ¾ šŸ‘¦šŸ¾ šŸ‘§šŸ¾ šŸ‘ØšŸ¾ šŸ‘©šŸ¾ šŸ‘±šŸ¾ā€ā™€ļø šŸ‘±šŸ¾ šŸ‘“šŸ¾ šŸ‘µšŸ¾",
    "šŸ‘¶šŸæ šŸ‘¦šŸæ šŸ‘§šŸæ šŸ‘ØšŸæ šŸ‘©šŸæ šŸ‘±šŸæā€ā™€ļø šŸ‘±šŸæ šŸ‘“šŸæ šŸ‘µšŸæ",
]
ages = [x.split(' ') for x in ages]

roles = [
    "šŸ‘®ā€ā™€ļø šŸ‘® šŸ‘·ā€ā™€ļø šŸ‘· šŸ’‚ā€ā™€ļø šŸ’‚ šŸ•µļøā€ā™€ļø šŸ•µļø šŸ‘©ā€āš•ļø šŸ‘Øā€āš•ļø šŸ‘©ā€šŸŒ¾ šŸ‘Øā€šŸŒ¾ šŸ‘©ā€šŸ³ šŸ‘Øā€šŸ³ šŸ‘©ā€šŸŽ“ šŸ‘Øā€šŸŽ“ šŸ‘©ā€šŸŽ¤ šŸ‘Øā€šŸŽ¤ šŸ‘©ā€šŸ« šŸ‘Øā€šŸ« šŸ‘©ā€šŸ­ šŸ‘Øā€šŸ­ šŸ‘©ā€šŸ’» šŸ‘Øā€šŸ’» šŸ‘©ā€šŸ’¼ šŸ‘Øā€šŸ’¼ šŸ‘©ā€šŸ”§ šŸ‘Øā€šŸ”§ šŸ‘©ā€šŸ”¬ šŸ‘Øā€šŸ”¬ šŸ‘©ā€šŸŽØ šŸ‘Øā€šŸŽØ šŸ‘©ā€šŸš’ šŸ‘Øā€šŸš’ šŸ‘©ā€āœˆļø šŸ‘Øā€āœˆļø šŸ‘©ā€šŸš€ šŸ‘Øā€šŸš€ šŸ‘©ā€āš–ļø šŸ‘Øā€āš–ļø 🤶 šŸŽ… šŸ‘ø 🤓",
    "šŸ‘®šŸ»ā€ā™€ļø šŸ‘®šŸ» šŸ‘·šŸ»ā€ā™€ļø šŸ‘·šŸ» šŸ’‚šŸ»ā€ā™€ļø šŸ’‚šŸ» šŸ•µšŸ»ā€ā™€ļø šŸ•µšŸ» šŸ‘©šŸ»ā€āš•ļø šŸ‘ØšŸ»ā€āš•ļø šŸ‘©šŸ»ā€šŸŒ¾ šŸ‘ØšŸ»ā€šŸŒ¾ šŸ‘©šŸ»ā€šŸ³ šŸ‘ØšŸ»ā€šŸ³ šŸ‘©šŸ»ā€šŸŽ“ šŸ‘ØšŸ»ā€šŸŽ“ šŸ‘©šŸ»ā€šŸŽ¤ šŸ‘ØšŸ»ā€šŸŽ¤ šŸ‘©šŸ»ā€šŸ« šŸ‘ØšŸ»ā€šŸ« šŸ‘©šŸ»ā€šŸ­ šŸ‘ØšŸ»ā€šŸ­ šŸ‘©šŸ»ā€šŸ’» šŸ‘ØšŸ»ā€šŸ’» šŸ‘©šŸ»ā€šŸ’¼ šŸ‘ØšŸ»ā€šŸ’¼ šŸ‘©šŸ»ā€šŸ”§ šŸ‘ØšŸ»ā€šŸ”§ šŸ‘©šŸ»ā€šŸ”¬ šŸ‘ØšŸ»ā€šŸ”¬ šŸ‘©šŸ»ā€šŸŽØ šŸ‘ØšŸ»ā€šŸŽØ šŸ‘©šŸ»ā€šŸš’ šŸ‘ØšŸ»ā€šŸš’ šŸ‘©šŸ»ā€āœˆļø šŸ‘ØšŸ»ā€āœˆļø šŸ‘©šŸ»ā€šŸš€ šŸ‘ØšŸ»ā€šŸš€ šŸ‘©šŸ»ā€āš–ļø šŸ‘ØšŸ»ā€āš–ļø šŸ¤¶šŸ» šŸŽ…šŸ» šŸ‘øšŸ» šŸ¤“šŸ»",
    "šŸ‘®šŸ¼ā€ā™€ļø šŸ‘®šŸ¼ šŸ‘·šŸ¼ā€ā™€ļø šŸ‘·šŸ¼ šŸ’‚šŸ¼ā€ā™€ļø šŸ’‚šŸ¼ šŸ•µšŸ¼ā€ā™€ļø šŸ•µšŸ¼ šŸ‘©šŸ¼ā€āš•ļø šŸ‘ØšŸ¼ā€āš•ļø šŸ‘©šŸ¼ā€šŸŒ¾ šŸ‘ØšŸ¼ā€šŸŒ¾ šŸ‘©šŸ¼ā€šŸ³ šŸ‘ØšŸ¼ā€šŸ³ šŸ‘©šŸ¼ā€šŸŽ“ šŸ‘ØšŸ¼ā€šŸŽ“ šŸ‘©šŸ¼ā€šŸŽ¤ šŸ‘ØšŸ¼ā€šŸŽ¤ šŸ‘©šŸ¼ā€šŸ« šŸ‘ØšŸ¼ā€šŸ« šŸ‘©šŸ¼ā€šŸ­ šŸ‘ØšŸ¼ā€šŸ­ šŸ‘©šŸ¼ā€šŸ’» šŸ‘ØšŸ¼ā€šŸ’» šŸ‘©šŸ¼ā€šŸ’¼ šŸ‘ØšŸ¼ā€šŸ’¼ šŸ‘©šŸ¼ā€šŸ”§ šŸ‘ØšŸ¼ā€šŸ”§ šŸ‘©šŸ¼ā€šŸ”¬ šŸ‘ØšŸ¼ā€šŸ”¬ šŸ‘©šŸ¼ā€šŸŽØ šŸ‘ØšŸ¼ā€šŸŽØ šŸ‘©šŸ¼ā€šŸš’ šŸ‘ØšŸ¼ā€šŸš’ šŸ‘©šŸ¼ā€āœˆļø šŸ‘ØšŸ¼ā€āœˆļø šŸ‘©šŸ¼ā€šŸš€ šŸ‘ØšŸ¼ā€šŸš€ šŸ‘©šŸ¼ā€āš–ļø šŸ‘ØšŸ¼ā€āš–ļø šŸ¤¶šŸ¼ šŸŽ…šŸ¼ šŸ‘øšŸ¼ šŸ¤“šŸ¼",
    "šŸ‘®šŸ½ā€ā™€ļø šŸ‘®šŸ½ šŸ‘·šŸ½ā€ā™€ļø šŸ‘·šŸ½ šŸ’‚šŸ½ā€ā™€ļø šŸ’‚šŸ½ šŸ•µšŸ½ā€ā™€ļø šŸ•µšŸ½ šŸ‘©šŸ½ā€āš•ļø šŸ‘ØšŸ½ā€āš•ļø šŸ‘©šŸ½ā€šŸŒ¾ šŸ‘ØšŸ½ā€šŸŒ¾ šŸ‘©šŸ½ā€šŸ³ šŸ‘ØšŸ½ā€šŸ³ šŸ‘©šŸ½ā€šŸŽ“ šŸ‘ØšŸ½ā€šŸŽ“ šŸ‘©šŸ½ā€šŸŽ¤ šŸ‘ØšŸ½ā€šŸŽ¤ šŸ‘©šŸ½ā€šŸ« šŸ‘ØšŸ½ā€šŸ« šŸ‘©šŸ½ā€šŸ­ šŸ‘ØšŸ½ā€šŸ­ šŸ‘©šŸ½ā€šŸ’» šŸ‘ØšŸ½ā€šŸ’» šŸ‘©šŸ½ā€šŸ’¼ šŸ‘ØšŸ½ā€šŸ’¼ šŸ‘©šŸ½ā€šŸ”§ šŸ‘ØšŸ½ā€šŸ”§ šŸ‘©šŸ½ā€šŸ”¬ šŸ‘ØšŸ½ā€šŸ”¬ šŸ‘©šŸ½ā€šŸŽØ šŸ‘ØšŸ½ā€šŸŽØ šŸ‘©šŸ½ā€šŸš’ šŸ‘ØšŸ½ā€šŸš’ šŸ‘©šŸ½ā€āœˆļø šŸ‘ØšŸ½ā€āœˆļø šŸ‘©šŸ½ā€šŸš€ šŸ‘ØšŸ½ā€šŸš€ šŸ‘©šŸ½ā€āš–ļø šŸ‘ØšŸ½ā€āš–ļø šŸ¤¶šŸ½ šŸŽ…šŸ½ šŸ‘øšŸ½ šŸ¤“šŸ½",
    "šŸ‘®šŸ¾ā€ā™€ļø šŸ‘®šŸ¾ šŸ‘·šŸ¾ā€ā™€ļø šŸ‘·šŸ¾ šŸ’‚šŸ¾ā€ā™€ļø šŸ’‚šŸ¾ šŸ•µšŸ¾ā€ā™€ļø šŸ•µšŸ¾ šŸ‘©šŸ¾ā€āš•ļø šŸ‘ØšŸ¾ā€āš•ļø šŸ‘©šŸ¾ā€šŸŒ¾ šŸ‘ØšŸ¾ā€šŸŒ¾ šŸ‘©šŸ¾ā€šŸ³ šŸ‘ØšŸ¾ā€šŸ³ šŸ‘©šŸ¾ā€šŸŽ“ šŸ‘ØšŸ¾ā€šŸŽ“ šŸ‘©šŸ¾ā€šŸŽ¤ šŸ‘ØšŸ¾ā€šŸŽ¤ šŸ‘©šŸ¾ā€šŸ« šŸ‘ØšŸ¾ā€šŸ« šŸ‘©šŸ¾ā€šŸ­ šŸ‘ØšŸ¾ā€šŸ­ šŸ‘©šŸ¾ā€šŸ’» šŸ‘ØšŸ¾ā€šŸ’» šŸ‘©šŸ¾ā€šŸ’¼ šŸ‘ØšŸ¾ā€šŸ’¼ šŸ‘©šŸ¾ā€šŸ”§ šŸ‘ØšŸ¾ā€šŸ”§ šŸ‘©šŸ¾ā€šŸ”¬ šŸ‘ØšŸ¾ā€šŸ”¬ šŸ‘©šŸ¾ā€šŸŽØ šŸ‘ØšŸ¾ā€šŸŽØ šŸ‘©šŸ¾ā€šŸš’ šŸ‘ØšŸ¾ā€šŸš’ šŸ‘©šŸ¾ā€āœˆļø šŸ‘ØšŸ¾ā€āœˆļø šŸ‘©šŸ¾ā€šŸš€ šŸ‘ØšŸ¾ā€šŸš€ šŸ‘©šŸ¾ā€āš–ļø šŸ‘ØšŸ¾ā€āš–ļø šŸ¤¶šŸ¾ šŸŽ…šŸ¾ šŸ‘øšŸ¾ šŸ¤“šŸ¾",
    "šŸ‘®šŸæā€ā™€ļø šŸ‘®šŸæ šŸ‘·šŸæā€ā™€ļø šŸ‘·šŸæ šŸ’‚šŸæā€ā™€ļø šŸ’‚šŸæ šŸ•µšŸæā€ā™€ļø šŸ•µšŸæ šŸ‘©šŸæā€āš•ļø šŸ‘ØšŸæā€āš•ļø šŸ‘©šŸæā€šŸŒ¾ šŸ‘ØšŸæā€šŸŒ¾ šŸ‘©šŸæā€šŸ³ šŸ‘ØšŸæā€šŸ³ šŸ‘©šŸæā€šŸŽ“ šŸ‘ØšŸæā€šŸŽ“ šŸ‘©šŸæā€šŸŽ¤ šŸ‘ØšŸæā€šŸŽ¤ šŸ‘©šŸæā€šŸ« šŸ‘ØšŸæā€šŸ« šŸ‘©šŸæā€šŸ­ šŸ‘ØšŸæā€šŸ­ šŸ‘©šŸæā€šŸ’» šŸ‘ØšŸæā€šŸ’» šŸ‘©šŸæā€šŸ’¼ šŸ‘ØšŸæā€šŸ’¼ šŸ‘©šŸæā€šŸ”§ šŸ‘ØšŸæā€šŸ”§ šŸ‘©šŸæā€šŸ”¬ šŸ‘ØšŸæā€šŸ”¬ šŸ‘©šŸæā€šŸŽØ šŸ‘ØšŸæā€šŸŽØ šŸ‘©šŸæā€šŸš’ šŸ‘ØšŸæā€šŸš’ šŸ‘©šŸæā€āœˆļø šŸ‘ØšŸæā€āœˆļø šŸ‘©šŸæā€šŸš€ šŸ‘ØšŸæā€šŸš€ šŸ‘©šŸæā€āš–ļø šŸ‘ØšŸæā€āš–ļø šŸ¤¶šŸæ šŸŽ…šŸæ šŸ‘øšŸæ šŸ¤“šŸæ",
]
roles = [x.split(' ') for x in roles]

As emojis have a strange order, generation of all variants is a bit tricky, but it's easier than rearranging them in code because my editor doesn't work quite well with emojis:

def get_life(color, gender, role):
    yield ages[color][0]
    yield ages[color][1 + gender]
    yield ages[color][3 + gender]
    yield ages[color][6 - gender]
    yield roles[color][role * 2 + 1 - gender]
    yield ages[color][7 + gender]
>>> list(get_life(0, 0, 0))
['šŸ‘¶', 'šŸ‘¦', 'šŸ‘Ø', 'šŸ‘±', 'šŸ‘®', 'šŸ‘“']
def get_variants():
    for color in range(len(ages)):
        for gender in (0, 1):
            for role in range(len(roles[0]) // 2):
                yield color, gender, role
>>> list(get_life(*list(get_variants())[23]))
['šŸ‘¶', 'šŸ‘§', 'šŸ‘©', 'šŸ‘±\u200dā™€ļø', 'šŸ‘·\u200dā™€ļø', 'šŸ‘µ']

And after that it's very easy to generate Go package with all possible variants:

code = b'package variants\n\nvar All = [][]string{\n'

for variant in get_variants():
    code += b'\t{\n'
    for emoji in get_life(*variant):
        code += b'\t\t"' + emoji.encode('unicode-escape') + b'",\n'
    code += b'\t},\n'

code += b'}\n'

with open('variants/variants.go', 'wb') as f:
    f.write(code)

So we'll have something like this in variants/variants.go:

package variants

var All = [][]string{
	{
		"\U0001f476",
		"\U0001f466",
		"\U0001f468",
		"\U0001f471",
		"\U0001f46e",
		"\U0001f474",
	},
	...
}

The logic of the loader isn't that interesting, although I want to highlight some moments. At the high level we just read lines from a pipe, if there's no new line arrived before tick seconds, we update our emojis:

func main() {
        ...
        go watchApp(lines)
        
        for {
            select {
            case line, isOpen := <-lines:
                ...
                os.Stdout.WriteString(line)
                ...
                printPeople(people)
            case <-time.After(time.Duration(*tick) * time.Second):
                people = updatePeople(people, *count)
                printPeople(people)
            }
        }
}

While updating, we can add new emoji, make one emoji older or "kill" the oldest:

func updatePeople(people []*human, count int) []*human {
	addNew := rand.Intn(5) == 0
	toMakeOlder := canMakeOlder(people)

	if addNew || len(toMakeOlder) == 0 {
		people = append(people, getRandomHuman())
	} else {
		index := toMakeOlder[rand.Intn(len(toMakeOlder))]
		people[index].position += 1
	}

	if len(people) > count {
		oldest := getOldest(people)
		return append(people[:oldest], people[oldest+1:]...)
	} else {
		return people
	}
}

And that's all. You can find the source code on GitHub.



comments powered by Disqus