プレゼンタイマー
code:scala
//> using scala 3.7.3
//> using dep dev.zio::zio::2.1.26
import zio.*
object Timer extends ZIOAppDefault:
// プレゼンテーション用タイマーの総時間
val totalMinutes = 15
val total = totalMinutes.minutes
val tick = 1.second
val notifyEvery = 5 // 分単位で通知する間隔
val barWidth = 40
/** notify-send でデスクトップ通知を送る。失敗しても本体は継続する。 */
def notify(title: String, body: String): UIOUnit = ZIO
.attemptBlocking {
val pb = new ProcessBuilder("notify-send", title, body)
pb.inheritIO()
pb.start().waitFor()
}
.unit
.catchAll(_ => ZIO.unit)
/** 経過秒数から進捗バーを組み立てて標準出力を上書きする。 */
def render(elapsed: Duration): UIOUnit = val totalSec = total.getSeconds
val elapsedSec = math.min(elapsed.getSeconds, totalSec)
val remainSec = totalSec - elapsedSec
val ratio = elapsedSec.toDouble / totalSec
val filled = (ratio * barWidth).toInt
val bar = "█" * filled + "░" * (barWidth - filled)
val percent = (ratio * 100).toInt
val remainMin = remainSec / 60
val remainS = remainSec % 60
val line =
f"\r$bar $percent%3d%% 残り $remainMin%02d:$remainS%02d" Console.print(line).orDie
/** 1秒ごとに進捗を更新し、指定分の節目で通知を送る。 */
def loop(elapsedSec: Long): UIOUnit = val elapsed = elapsedSec.seconds
for
_ <- render(elapsed)
// 5分ごと(ただし開始直後の0分と終了時は除く)に通知
_ <- ZIO.when(
elapsedSec > 0 &&
elapsedSec % (notifyEvery * 60) == 0 &&
elapsedSec < total.getSeconds
) {
val passedMin = elapsedSec / 60
val leftMin = totalMinutes - passedMin
notify(
"プレゼンタイマー",
s"${passedMin}分経過(残り${leftMin}分)"
)
}
_ <- ZIO.when(elapsedSec < total.getSeconds) {
ZIO.sleep(tick) *> loop(elapsedSec + 1)
}
yield ()
def run =
for
_ <- Console.printLine(s"プレゼンタイマーを開始します(${totalMinutes}分)")
_ <- notify("プレゼンタイマー", s"${totalMinutes}分のタイマーを開始しました")
_ <- loop(0)
_ <- render(total)
_ <- Console.printLine("")
_ <- Console.printLine("時間になりました。")
_ <- notify("プレゼンタイマー", s"${totalMinutes}分が経過しました。終了です。")
yield ()