ターミナルから起動できる簡易タイマーをつくった

  • 8
    いいね
  • 0
    コメント

この記事はIS17er Advent Calendar9日目の記事として書かれたものです。
8日目の記事はこちら。

概要

PythonのTkinterというライブラリを使ってターミナルから起動できるタイマーアプリを作りました。
動作方法は画像を見てもらえればわかるかと思います。ぜひぜひ使ってください!
tmr.png

そうだ、簡易タイマーをつくろう

最近集中力が落ちていて、パソコンの前に座っていたらいつの間にか2時間が経っていた、ということがざらにあるので、短時間集中できるようにターミナルから簡単に起動できるタイマーを作って時間管理をしようと思い立ちました(昨日の夕方)。

構想

(このセクションを書いていたときはまだプログラムを書く前でした)
今回制作するアプリケーションのイメージとしては、たとえば
$ tmr 5m30s -t kadai
みたいな感じのコマンドを叩けば、画面右上に小さなウィンドウが出てきて、カウントダウンしてくれる、というイメージです。-tはオプションでタイマーのタイトルです。あまり複雑な機能を実装しても使わないのでとりあえずはこれでいいかな、という具合です。あとオプションでSlackみたいな通知音が出ればいいかな。あとは起動するたびにランダムに色が変わったりしたら飽きずに使えるかな〜というイメージ。

書いていく

普段はウェブ系なのでデスクトップアプリケーションは全然書いたことがないのですが、ちょっとググってみたところTkinterというのが楽そうなのでこれを使うことにしました。Tkinterを使えばPythonでGUIを簡単につくれるそうです。

コード

コードが100行程度だったので全部貼り付けます。この短さなのでさすがに1ファイルにしました。
カスタマイズなどはご自由にどうぞ。

tmr.py
#!/usr/bin/env python
# -*- coding: utf8 -*-
import sys
import re
import argparse
import random
import Tkinter as tk

class App():
  def __init__(self):
    self.window_size = 150
    self.fps = 10
    self.font_size = 23
    self.option = self.parse_args()
    self.time = self.get_timesec()
    self.period = self.time
    self.root = self.set_root()
    self.canvas = self.set_canvas()
    self.label = self.set_label()
    self.progress = self.init_progress()
    self.update()
    self.run()

  def parse_args(self):
    parser = argparse.ArgumentParser(description="Show simple countdown timer on desktop.")
    parser.add_argument("period", action="store", type=str, help="Set period for your timer.")
    parser.add_argument("-t", dest="title", action="store", default="Timer", help="Set title for your timer.")
    parser.add_argument("-n", dest="notify", action="store_false", default=True, help="Disable notification.")
    return parser.parse_args()

  def get_timesec(self):
    if re.search("\A(?:\d+h)?(?:\d+m)?(?:\d+s)?$", self.option.period) is None:
      print "Incorrect format of period:", self.option.period
      print "Set period like 10m30s"
      sys.exit()
    time = 0
    if re.search("\d+h", self.option.period) is not None:
      time += int(re.search("\d+h", self.option.period).group(0)[:-1]) * 3600
    if re.search("\d+m", self.option.period) is not None:
      time += int(re.search("\d+m", self.option.period).group(0)[:-1]) * 60
    if re.search("\d+s", self.option.period) is not None:
      time += int(re.search("\d+s", self.option.period).group(0)[:-1])
    if time > 9 * 3600 + 59 * 60 + 59:
      print "Too long period."
      sys.exit()
    return time

  def set_root(self):
    root = tk.Tk()
    root.resizable(0,0)
    window_size = self.window_size
    colors = ["#f44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#607D8B"]
    root.title(self.option.title)
    root.geometry("%dx%d+%d+%d" % (window_size, window_size, root.winfo_screenwidth() - window_size, 0))
    root.configure(bg=random.choice(colors))
    root.attributes("-alpha", 0.5)
    root.attributes("-topmost", True)
    return root

  def set_label(self):
    window_size = self.window_size
    label = self.canvas.create_text((window_size / 2, window_size / 2), text="")
    self.canvas.itemconfig(label, font=("Menlo-Regular", self.font_size))
    return label

  def set_canvas(self):
    window_size = self.window_size
    canvas = tk.Canvas(self.root, width=window_size, height=window_size, highlightthickness=0)
    canvas.grid()
    return canvas

  def format_time(self, timesec):
    m, s = divmod(timesec, 60)
    h, m = divmod(m, 60)
    if h == 0:
      if m == 0:
        return "%02ds" % (s)
      else:
        return "%02dm%02ds" % (m, s)
    else:
      return "%dh%02dm%02ds" % (h, m, s)

  def init_progress(self):
    color = self.root["bg"]
    window_size = self.window_size
    progress = self.canvas.create_arc(window_size * 0.1, window_size * 0.1, window_size * 0.9, window_size * 0.9, style="arc", width="%d" % (window_size / 15), outline=color, start=90, extent=360)
    return progress

  def update(self):
    window_size = self.window_size
    self.canvas.itemconfig(self.label, text=self.format_time(self.time))
    extent = 360.0 * self.time / self.period
    self.canvas.itemconfig(self.progress, start=450-extent, extent=extent)
    self.time -= 1.0 / self.fps
    if self.time < 0:
      if self.option.notify:
        print '\a'
      sys.exit()
    self.root.after(1000 / self.fps, self.update)

  def run(self):
    self.root.mainloop()

app = App()

使い方

GitHubにも書きましたが、こちらではあえてcloneしない方法で。
1. 上のコードをコピーしてtmrというファイル名で保存する(拡張子なし)。
2. chmod 755 tmr
3. export PATH=$PATH:/Users/[username]/timerみたいなやつを~/.bash_rc~/.bash_profileに追加。ここらへんがわからないひとはPATHの追加をググろう。
4. source ~/.bash_rcもしくはsource ~/.bash_profileをやってターミナルを再起動
5. tmr 10m30sとか打ってみよう!

苦労した点(コードの解説)

実は結構あったりするんですが、とりあえずいくつか。

正規表現

みなさん大好き正規表現ですね。
"\A(?:\d+h)?(?:\d+m)?(?:\d+s)?$"の部分ですが、はじめ"\A(?:\d+h)?(?:\d+m)?(?:\d+s)?\z"と書いていて永遠にNoneを量産してました。正規表現慣れましょう。

いかにコードを簡潔に書くか

Pythonには便利な関数が標準で大量に用意されているので、自分で何か書こうと思っても先に調べるのが得策ですね。自分で書いたほうが早い場合がほとんどですが、やはりコーナーケースの処理が面倒だったり可読性が落ちたりとバッドプラクティスなので既存の関数をひたすらググってました。しかもPythonの場合、複数の既存の関数が同じふるまいをすることが多いので、既存の関数の中でも統一感がよくなるようにライブラリを選ぶのがまた時間がかかりました。ここらへんは職業病かも。

さいごに

GitHubからcloneできるのでぜひ使ってみてください(面倒だったらコードをそのままコピーしてもいいよ)。Starしてくれたら泣いてよろこびます!あと通知音などは執筆時点では未実装なのであとで追加するかも。

余談ですが、Macの場合はsayという天才コマンドがあって
$ sleep 100 && say 終了
とか実行すれば100秒後に「終了」と読み上げてくれたような気がします(言語を日本語に設定している場合)。

それではまた明日〜

[追記] 通知音実装しました(上記コード修正済み)。
print '\a'
でいけるんですね!Python神ではないかっ・・・!