t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

GUI開発が面倒ならCUIで作れば良いという気付き~Pythonリマインダー管理用コマンドツールを作成

今回は、以前作成したリマインダーシステムの管理ツールを作成した話。
thom.hateblo.jp

あれからリマインダーシステムはますます重要度を増し、周期的な作業も含めると既に40近くのTo Doが登録されている。

最初のうちは手修正で良かったが、これだけ数が増えると管理が煩雑なので別途仕組みを考えた。

考察

ドラッグ&ドロップで1日あるいは1週間延期できるような仕組みを作ったものの、これだけ項目が多いと間違って延期してしまったら埋もれてしまい気づかないリスクがある。

また、細かい調整はどうしても手動でファイル名を変えていたのだが、文字が小さいので入力ミスに気付かなかったり、存在しない日付を入力してしまったり、日を変えたつもりで月を変えてしまったりといったリスクも生じる。

そろそろもう少し安心して利用できる管理ツールが欲しいと思ってはいたのだが、画面レイアウトの設計と実装が面倒くさくて長らく放置していた。

そしてついに閃いた。

。。。CUIでいいのでは?

経緯

これは私の偏見も多分に混じっているかもしれないが、Windows文化においてはGUIが正義で今どきCUIアプリなんて見向きもされない。
例えばプログラミング入門書は大抵CUIを前提に解説しているのだが、黒画面にテキストを表示させるだけのアプリなんて作っても何も嬉しくないと感じる人が大半だと思う。

ところがしばらくUnix/Linux文化にどっぷりつかるとこの考えは180°変わる。
もう10年以上前になるけど、しばらく私はLinux文化に傾倒していた時期がある。当時はコマンドラインこそが正義という考えのものとWindowsを捨てて1年間ほどCUIのみのDebianノートをメインPCとしていた。
そのきっかけとなったのはこの書籍。

過去に記事も書いていたようだ。
thom.hateblo.jp

しばらく忘れていた感覚ではあるが、確かにコマンドにはコマンドの良さがあるのだ。
このことを再度思い返すきっかけとなったのが最近やっているLinux Essentialsの学習である。

ということでCUIアプリを作ることにした。

要件

機能としてほしいのは閲覧・登録・延期・削除である。

機能 動作イメージ
閲覧 showで10件の閲覧、nextで次の10件、prevで前の10件。
登録 create タスク名で1時間後を期限として新規タスクを作成。
延期 selectで複数選択し、extend n dでn日延期。extend n hでn時間延期。
削除 selectで複数選択し、removeで削除。

実装

取り急ぎpythonで作ったmanage.pyが以下のコード。

import os
import re
import datetime
from datetime import datetime as dt

def get_tasks():
    tasks = os.listdir(os.path.join(os.getcwd(), "tasks"))
    return [s for s in tasks if s.endswith(".txt")]

tasks = get_tasks()
selection = set()

def show_tasks(i):
    for x in tasks[i:i+10]:
        print(tasks.index(x), x)


cmd = ""
i=0
while cmd != "exit":
    cmd = input("$ ")
    if cmd == "show":
        show_tasks(i)

    if cmd == "next":
        if i + 10 < len(tasks):
            i = i + 10
            show_tasks(i)
        else:
            print("EOF")

    if cmd == "prev":
        if i - 10 >= 0:
            i = i - 10
            show_tasks(i)
        else:
            print("BOF")

    if cmd == "reload":
        i = 0
        tasks = get_tasks()
        selection = set()
        show_tasks(i)

    pattern = re.compile("^select( [0-9]+)*$")
    if pattern.match(cmd) is not None:
        for x in sorted(set(map(int,cmd.strip().split(" ")[1:]))):
            if len(tasks) >= int(x):
                selection.add(x)
        for y in sorted(selection):
            print(y, tasks[y])
    
    pattern = re.compile("^unselect( [0-9]+)*$")
    if pattern.match(cmd) is not None:
        for x in sorted(set(map(int,cmd.strip().split(" ")[1:]))):
            if len(tasks) >= int(x):
                selection.discard(x)
        for y in sorted(selection):
            print(y, tasks[y])

    pattern = re.compile("^extend -?[1-9][0-9]* (d|h)$")
    if pattern.match(cmd) is not None:
        for x in sorted(selection):
            exstr = cmd.strip().split(" ")[1:]
            if exstr[1] == "d":
                td = datetime.timedelta(days=int(exstr[0]))
            else:
                td = datetime.timedelta(hours=int(exstr[0]))

            newtime = (td + dt.strptime(tasks[x][0:16], "%Y_%m_%d_%H_%M")).strftime("%Y_%m_%d_%H_%M")
            old_path = os.path.join(os.getcwd(), "tasks", tasks[x])
            new_path = os.path.join(os.getcwd(), "tasks", newtime + tasks[x][16:])
            os.rename(old_path, new_path)
        tasks = get_tasks()
        selection = set()

    pattern = re.compile(r'^create [^ .\\/:*?"<>|]+$')
    if pattern.match(cmd) is not None:
        fname = (dt.now()+datetime.timedelta(hours=1)).strftime("%Y_%m_%d_%H_%M_") + cmd.split(" ")[1] + ".txt"
        fpath = os.path.join(os.getcwd(), "tasks", fname)
        open(fpath,"w").close()
        print(fpath)
        tasks = get_tasks()
        selection = set()

    if cmd == "remove":
        for x in sorted(selection):
            print(tasks[x])
        if input("Are you sure? ") == "sure":
            for y in sorted(selection):
                rm_path = os.path.join(os.getcwd(), "tasks", tasks[y])
                os.remove(rm_path)
                print("[Removed]" + rm_path)
        else:
            print("Cancelled.")
        tasks = get_tasks()
        selection = set()

雑な説明

python manage.pyとして実行するとプロンプトとして$マークを表示しコマンドを受け付ける。
コマンドを受け付けると処理に応じて出力し、次のコマンドループに入る。exitが入力されたらプログラムを終了する。

show等の引数無しコマンドはif文で比較し、extendなどの引数付きコマンドは正規表現でコマンドと引数の正しさをチェックしている。
selectコマンドは実行する度に選択アイテムが追加される仕組みなのでshow・next・prev等で別ページにあるアイテムを追加するのも簡単。
引数無しでselectを実行すると現在選択されているアイテムがずらっと表示される。
removeコマンドは確認が入るので、sureと入力すると削除、それ以外が入力されたらキャンセルされ、アイテムの選択が解除される。
※今のところunselect allは実装していないのでunselectで個別指定するかremoveをキャンセルするかexitしてやり直すことになる。

終わりに

CUIで面白いプログラムを書けないならGUIでも書けないという言葉をどこかで読んだ記憶がある。
その言葉を知った当時は少々過激な表現だなと思っていたけど、CUIでも十分に使えるプログラムを書けるということを改めて実感できたので、あながち間違いでもない気がしてきた。

プログラミング入門者が挫折するひとつの要因は、入門書で学習してもなかなか実用的なプログラムが作れるところまでたどり着かない点にあると思う。
実用的なプログラム=GUIアプリだという思い込みがこの傾向に拍車をかけているのかもしれない。
初心者はGUIアプリの作成に憧れを抱きがちだ。しかし初心者が作るとなるとなかなか難しい。

ひょっとするとその憧れの向き先をGUIではなくCUIに向けることができれば、案外すんなりとプログラミング言語を習得できるかもしれない。
確かにGUIの方が直感的に使えるけれど、慣れればCUIの方が効率的というケースはよくある。

それにマウスでポチポチ操作するよりも黒画面でカタカタやってるほうがなんとなく玄人っぽくて格好良い※。最初はそういう適当な憧れで良いんだと思う。

皆さんも自分だけのCUIツールを作ってみてはいかがだろうか。

※個人の主観です。PC音痴の知人によると黒画面で何かしてる人はみんなサイバー犯罪者に見えるようですが。。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。