package main import ( "fmt" "os" "strings" "time" "janouch.name/desktop-tools/liust-50/charset" ) // TODO(p): Make more elaberate animations with these. var kaomoji = []struct { kao, message string }{ {"(o_o)", ""}, {"(゚ロ゚)", ""}, {"(゚∩゚)", ""}, {"(^_^)", ""}, {" (^_^)", ""}, {"(^_^) ", ""}, {"(x_x)", "ズキズキ"}, {"(T_T)", "ズーン"}, {"=^.^=", "ニャー"}, {"(>_<)", "ゲップ"}, {"(O_O)", "ジー"}, {"(-_-)", ""}, {"(-_-)", "グーグー"}, {"(o_-)", ""}, {"(-_o)", ""}, } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const ( displayWidth = 20 displayHeight = 2 targetCharset = 0x63 ) type DisplayState struct { Display [displayHeight][displayWidth]uint8 } type Display struct { Current, Last DisplayState } func NewDisplay() *Display { t := &Display{} for y := 0; y < displayHeight; y++ { for x := 0; x < displayWidth; x++ { t.Current.Display[y][x] = ' ' t.Last.Display[y][x] = ' ' } } return t } func (t *Display) SetLine(row int, content string) { if row < 0 || row >= displayHeight { return } runes := []rune(content) for x := 0; x < displayWidth; x++ { if x < len(runes) { b, ok := charset.ResolveRune(runes[x], targetCharset) if ok { t.Current.Display[row][x] = b } else { t.Current.Display[row][x] = '?' } } else { t.Current.Display[row][x] = ' ' } } } func (t *Display) HasChanges() bool { for y := 0; y < displayHeight; y++ { for x := 0; x < displayWidth; x++ { if t.Current.Display[y][x] != t.Last.Display[y][x] { return true } } } return false } func (t *Display) Update() { for y := 0; y < displayHeight; y++ { x := 0 for x < displayWidth { if t.Current.Display[y][x] == t.Last.Display[y][x] { x++ continue } startX := x var changes strings.Builder for x < displayWidth && t.Current.Display[y][x] != t.Last.Display[y][x] { changes.WriteByte(t.Current.Display[y][x]) t.Last.Display[y][x] = t.Current.Display[y][x] x++ } fmt.Printf("\x1b[%d;%dH%s", y+1, startX+1, changes.String()) } } os.Stdout.Sync() } func centerText(text string, width int) string { textLen := len([]rune(text)) if textLen >= width { return text[:width] } leftPad := (width - textLen + 1) / 2 rightPad := width - textLen - leftPad return strings.Repeat(" ", leftPad) + text + strings.Repeat(" ", rightPad) } func kaomojiProducer(lines chan<- string) { ticker := time.NewTicker(30_000 * time.Millisecond) defer ticker.Stop() idx := 0 for { km := kaomoji[idx] line := centerText(km.kao, displayWidth) if km.message != "" { line = line[:14] + km.message line += strings.Repeat(" ", displayWidth-len([]rune(line))) } lines <- line idx = (idx + 1) % len(kaomoji) <-ticker.C } } func statusProducer(lines chan<- string) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() temperature, fetcher := "", NewWeatherFetcher() temperatureChan := make(chan string) go fetcher.Run(5*time.Minute, temperatureChan) for { select { case newTemperature := <-temperatureChan: temperature = newTemperature default: } now := time.Now() status := fmt.Sprintf("%s %3s %s", now.Format("Mon _2 Jan"), temperature, now.Format("15:04")) // Ensure exactly 20 characters. runes := []rune(status) if len(runes) > displayWidth { status = string(runes[:displayWidth]) } else if len(runes) < displayWidth { status = status + strings.Repeat(" ", displayWidth-len(runes)) } lines <- status <-ticker.C } } func main() { terminal := NewDisplay() kaomojiChan := make(chan string, 1) statusChan := make(chan string, 1) go kaomojiProducer(kaomojiChan) go statusProducer(statusChan) // TODO(p): And we might want to disable cursor visibility as well. fmt.Printf("\x1bR%c", targetCharset) fmt.Print("\x1b[2J") // Clear display go func() { kaomojiChan <- centerText(kaomoji[0].kao, displayWidth) statusChan <- strings.Repeat(" ", displayWidth) }() for { select { case line := <-kaomojiChan: terminal.SetLine(0, line) case line := <-statusChan: terminal.SetLine(1, line) } if terminal.HasChanges() { terminal.Update() } } }