package tui import "fmt" // 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" 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: {{"(・_・;)"}}, } // An outcome owns a pool of faces and each turn shows one of them. var faces = map[mood][]string{ moodDone: {"(˶ᵔ ᵕ ᵔ˶)", "(๑˃ᴗ˂)", "ヽ(・∀・)ノ", "(ᵔᴥᵔ)", "(っ˘ω˘ς)"}, moodQuick: {"(・ω・)ノ", "(`・ω・´)", "(^_−)☆"}, moodLong: {"(ง ˃ᴗ˂)ง", "(๑˃ᴗ˂)✧", "(ˊᗜˋ*)✧"}, moodTired: {"( ̄ー ̄;)ゞ", "(・ω・;)ゞ", "( ˘ᴗ˘ )フゥ", "(´-ω-`)zzZ", "_(:3 」∠)_"}, 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))] } // framesPerFace slows faces against the 80ms spinner tick, about 0.5s each. const framesPerFace = 6 // frame is the turn's animation at spinner tick. func (m mood) frame(seed int64, tick int) string { pool := animations[m] set := pool[m.pick(seed, len(pool))] return set[(tick/framesPerFace)%len(set)] } // 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)) }