package tui import ( "fmt" "time" ) // A mood is a class of moment in a turn. Each class owns its own faces, so a // face always means something: what albedo is doing, or how a turn ended. type mood string const ( moodPreparing mood = "preparing" moodThinking mood = "thinking" moodResponding mood = "responding" moodWorking mood = "working" moodCompacting mood = "compacting" moodStopping mood = "stopping" moodConnecting mood = "connecting" moodDone mood = "done" moodQuick mood = "quick" moodLong mood = "long" moodTired mood = "tired" moodFailed mood = "failed" moodStopped mood = "stopped" ) // Faces avoid combining marks and right-to-left letters, which terminals // disagree on. // // A phase owns a pool of animations and each turn plays one of them. A face // keeps its parentheses in place across frames: only eyes, mouth, and what // follows the face move. var animations = map[mood][][]string{ moodPreparing: {{"(・・ )", "( ・・)"}}, moodThinking: { {"( ˘ω˘ )", "( ˘ω˘ ).", "( ˘ω˘ )..", "( ˘ω˘ )..."}, {"(・ω・ )", "( ・ω・)", "(・ω・ )?", "( ・ω・)?"}, {"(´-ω-`)", "(´・ω・`)", "(´-ω-`)", "(´・ω・`)!"}, }, moodResponding: { {"(ˊᗜˋ )", "(ˊ-ˋ )", "( ˊᗜˋ)", "( ˊ-ˋ)"}, {"(˶ᵔ ᗜ ᵔ˶)", "(˶ᵔ ᵕ ᵔ˶)"}, {"(・ᗜ・)", "(・-・)", "(・ᗜ・)ノ", "(・-・)ノ"}, }, moodWorking: { {"(˶•ᴗ•)カタ", "(˶•ᴗ•)カタカタ", "(˶•ᴗ•)カタカタカタ"}, {"(・_・ )", "( ・_・)", "(・ᴗ・ )✧"}, {"(っ•ᴗ•)っ・", "(っ•ᴗ•)っ ・", "(っ•ᴗ•)っ ・"}, {"ヽ(・ω・)ノ", "ヾ(・ω・)ノ"}, {"(๑•ᴗ•)", "(๑•ᴗ•)✧", "(๑•ᴗ•) ✧"}, }, moodCompacting: {{"(>_<)", "(>.<)"}}, moodStopping: {{"(・_・;)"}}, moodConnecting: { {"(・ω・)ノ", "(・ω・)ノ ・", "(・ω・)ノ ・・", "(・ω・)ノ ・・・"}, {"( ˘ω˘ )~", "( ˘ω˘ )~~", "( ˘ω˘ )~~~"}, {"(っ•ᴗ•)っ", "(っ•ᴗ•)っ ・", "(っ•ᴗ•)っ ・゚", "(っ•ᴗ•)っ ・゚✧"}, }, } // An outcome owns a pool of faces and each turn shows one of them. // Additional faces come from https://wikileaks.org/ciav7p1/cms/page_17760284.html. var faces = map[mood][]string{ moodDone: {"(˶ᵔ ᵕ ᵔ˶)", "(๑˃ᴗ˂)", "ヽ(・∀・)ノ", "(ᵔᴥᵔ)", "(っ˘ω˘ς)", "(◕‿◕)", "(✿◠‿◠)", "(o´ω`o)", "☻_☻", "ᶘ ᵒᴥᵒᶅ"}, moodQuick: {"(・ω・)ノ", "(`・ω・´)", "(^_−)☆", "(^▽^)"}, moodLong: {"(ง ˃ᴗ˂)ง", "(๑˃ᴗ˂)✧", "(ˊᗜˋ*)✧", "o(≧∀≦)o", "(ノ◕ヮ◕)ノ*:・゚✧"}, moodTired: {"( ̄ー ̄;)ゞ", "(・ω・;)ゞ", "( ˘ᴗ˘ )フゥ", "(´-ω-`)zzZ", "_(:3 」∠)_", "(︶ω︶)", "(-_- )ノ", "(n˘v˘•)¬"}, moodFailed: {"(╥﹏╥)", "(。•︿•。)", "(っ- ‸ – ς)", "(ಥ﹏ಥ)", "(╥_╥)", "☹_☹", "(ಠ~ಠ)", "(ಡ_ಡ)", "(ತಎತ)", "(ತ_ತ)", "(ಥдಥ)"}, moodStopped: {"(・_・;)", "(°ロ°)", "( ̄□ ̄;)", "(ಠ_ಠ)", "(°Д°)"}, } // pick chooses from n by seed, salted by the mood so one turn's picks for // different moods do not move in step. func (m mood) pick(seed int64, n int) int { return int(fnv1a(uint64(seed), string(m)) % uint64(n)) } // face picks a stable face for seed, so a rebuilt transcript keeps its faces. func (m mood) face(seed int64) string { set := faces[m] return set[m.pick(seed, len(set))] } // faceInterval is how long each frame of an animation shows. const faceInterval = 500 * time.Millisecond // frame is the turn's animation at tick, one frame per faceInterval. func (m mood) frame(seed int64, tick int) string { pool := animations[m] set := pool[m.pick(seed, len(pool))] return set[tick%len(set)] } // reachingText is what waiting on a host reads as, by its label: connecting, // or copying albedo's kernel over (step staging), which the first visit to a // host, and the first after albedo's python changed, waits for. func reachingText(label, step string) string { if step == "staging" { return "copying the kernel to " + label + "…" } return "connecting to " + label + "…" } // connectingFace is the animation for reaching a host, by its label, at // tick: the same one for a host wherever it shows. func connectingFace(host string, tick int) string { return moodConnecting.frame(int64(fnv1a(fnvOffset64, host)), tick) } // outcome classes a finished turn by how it ended and how long it took. func outcome(failed, stopped bool, elapsedMs int64) mood { switch { case stopped: return moodStopped case failed: return moodFailed case elapsedMs > 10*60_000: return moodTired case elapsedMs >= 3*60_000: return moodLong case elapsedMs > 0 && elapsedMs < 4_000: return moodQuick } return moodDone } func formatElapsed(ms int64) string { s := ms / 1000 switch { case s < 60: return fmt.Sprintf("%ds", s) case s < 3600: return fmt.Sprintf("%dm %ds", s/60, s%60) } return fmt.Sprintf("%dh %dm", s/3600, s/60%60) } // formatGap is how long you were away, in the largest whole unit. func formatGap(ms int64) string { m := ms / 60_000 switch { case m < 60: return fmt.Sprintf("%dm later", m) case m < 48*60: return fmt.Sprintf("%dh later", m/60) } return fmt.Sprintf("%dd later", m/(24*60)) }