Last active
August 6, 2025 13:41
-
-
Save 1021ky/dfdc316c4abe98943ebde11dc22724fd to your computer and use it in GitHub Desktop.
CLIで使う簡易ポモドーロタイマー
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# geminiに作ってもらった。まだ使い勝手がいまいち。 | |
# TODO: タスクの振り返りなどをgemini cliでやっていい感じにする | |
import json | |
import time | |
import os | |
from datetime import date, timedelta | |
# --- 設定 --- | |
WORK_MINUTES = 25 | |
SHORT_BREAK_MINUTES = 5 | |
LONG_BREAK_MINUTES = 30 | |
POMODOROS_PER_LONG_BREAK = 4 | |
TASK_FILE = 'tasks.json' | |
# --- プラットフォーム依存の通知 --- | |
def notify(message): | |
"""作業や休憩の開始・終了を音声で通知する""" | |
print(f"\n🔔 {message}\n") | |
try: | |
# macOS | |
if os.uname().sysname == 'Darwin': | |
os.system(f'say "{message}"') | |
# Linux (espeakが必要) | |
elif os.uname().sysname == 'Linux': | |
os.system(f'espeak "{message}"') | |
# Windowsの場合は、別途ライブラリ(winsoundなど)が必要です | |
except Exception as e: | |
print(f"音声通知に失敗しました: {e}") | |
def countdown(minutes): | |
"""指定された分数だけカウントダウンする""" | |
seconds = minutes * 60 | |
while seconds > 0: | |
mins, secs = divmod(seconds, 60) | |
timer = f'{mins:02d}:{secs:02d}' | |
print(f'\r⏳ {timer}', end="") | |
time.sleep(1) | |
seconds -= 1 | |
print("\r") | |
class Task: | |
"""タスクを表現するクラス""" | |
def __init__(self, name, estimated_pomodoros=0, actual_pomodoros=0, done=False): | |
self.name = name | |
self.estimated_pomodoros = estimated_pomodoros | |
self.actual_pomodoros = actual_pomodoros | |
self.done = done | |
def to_dict(self): | |
return self.__dict__ | |
@classmethod | |
def from_dict(cls, data): | |
return cls(**data) | |
class TaskManager: | |
"""タスクリストを管理するクラス""" | |
def __init__(self, filename): | |
self.filename = filename | |
self.tasks = self.load_tasks() | |
def load_tasks(self): | |
"""ファイルからタスクを読み込む""" | |
try: | |
with open(self.filename, 'r', encoding='utf-8') as f: | |
data = json.load(f) | |
today_str = str(date.today()) | |
if today_str in data: | |
return [Task.from_dict(t) for t in data[today_str]] | |
except (FileNotFoundError, json.JSONDecodeError): | |
pass | |
return [] | |
def save_tasks(self, tasks_to_save, target_date): | |
"""指定された日付のタスクをファイルに保存する""" | |
all_data = {} | |
try: | |
with open(self.filename, 'r', encoding='utf-8') as f: | |
all_data = json.load(f) | |
except (FileNotFoundError, json.JSONDecodeError): | |
pass | |
all_data[str(target_date)] = [t.to_dict() for t in tasks_to_save] | |
with open(self.filename, 'w', encoding='utf-8') as f: | |
json.dump(all_data, f, indent=4, ensure_ascii=False) | |
def get_task_by_index(self, index): | |
if 0 <= index < len(self.tasks): | |
return self.tasks[index] | |
return None | |
def add_task(self, task): | |
self.tasks.append(task) | |
def display_tasks(self, title="タスク一覧"): | |
"""タスクの一覧を表示する""" | |
print(f"\n--- {title} ---") | |
if not self.tasks: | |
print("タスクはありません。") | |
return | |
for i, task in enumerate(self.tasks): | |
status = "✅" if task.done else "🔲" | |
print(f"{i + 1}. {status} {task.name} " | |
f"(見積もり: {task.estimated_pomodoros}ポモ, " | |
f"実績: {task.actual_pomodoros}ポモ)") | |
print("--------------------") | |
def display_summary(self): | |
"""1日の作業サマリーを表示する""" | |
print("\n🎉 1日お疲れ様でした!本日のサマリーです 🎉") | |
done_tasks = [t for t in self.tasks if t.done] | |
if not done_tasks: | |
print("完了したタスクはありません。") | |
return | |
total_estimated = 0 | |
total_actual = 0 | |
for task in done_tasks: | |
diff = task.actual_pomodoros - task.estimated_pomodoros | |
diff_str = f"{diff:+}ポモ" if diff != 0 else "±0ポモ" | |
print(f"- {task.name}: 見積もり {task.estimated_pomodoros}, " | |
f"実績 {task.actual_pomodoros} (予実差: {diff_str})") | |
total_estimated += task.estimated_pomodoros | |
total_actual += task.actual_pomodoros | |
total_diff = total_actual - total_estimated | |
total_diff_str = f"{total_diff:+}ポモ" if total_diff != 0 else "±0ポモ" | |
print("\n[合計]") | |
print(f" 見積もり: {total_estimated} ポモドーロ") | |
print(f" 実 績: {total_actual} ポモドーロ") | |
print(f" 予実差 : {total_diff_str}") | |
print("----------------------------------------") | |
def get_initial_tasks(task_manager): | |
"""プログラム開始時に今日のタスクを準備する""" | |
if task_manager.tasks: | |
print(f"\n🗓️ {date.today()}のタスクです。") | |
task_manager.display_tasks("本日やること") | |
return | |
print("\nこんにちは!まず、今日終わらせるタスクをすべて入力してください。") | |
print("(入力が終わったら、何も入力せずにEnterキーを押してください)") | |
while True: | |
task_name = input("タスク名 > ").strip() | |
if not task_name: | |
break | |
task_manager.add_task(Task(task_name)) | |
task_manager.display_tasks("本日やること") | |
def main(): | |
"""メインの処理""" | |
tm = TaskManager(TASK_FILE) | |
pomodoro_count = 0 | |
print("🍅 ポモドーロ・タスクマネージャーへようこそ! 🍅") | |
get_initial_tasks(tm) | |
while True: | |
# --- メインメニュー --- | |
print("\n何をしますか? (番号を入力してください)") | |
print("1. タスクを開始する") | |
print("2. タスクを追加する") | |
print("3. 今日の作業を終わりにする") | |
choice = input("> ").strip() | |
if choice == '1': | |
# --- タスク選択 --- | |
tm.display_tasks("どのタスクを開始しますか?") | |
try: | |
task_index = int(input("タスク番号 > ")) - 1 | |
current_task = tm.get_task_by_index(task_index) | |
if not current_task: | |
print("⚠️ 無効な番号です。") | |
continue | |
if current_task.done: | |
print("⚠️ そのタスクは既に完了しています。") | |
continue | |
except ValueError: | |
print("⚠️ 番号で入力してください。") | |
continue | |
# --- 見積もり入力 --- | |
if current_task.estimated_pomodoros == 0: | |
try: | |
est = int(input(f"「{current_task.name}」は何ポモドーロかかりそうですか? > ")) | |
current_task.estimated_pomodoros = est | |
except ValueError: | |
print("⚠️ 数字で入力してください。見積もりは0のままです。") | |
# --- ポモドーロ開始 --- | |
pomodoro_count += 1 | |
notify(f"作業開始!「{current_task.name}」を頑張りましょう!") | |
countdown(WORK_MINUTES) | |
current_task.actual_pomodoros += 1 | |
# --- タスク完了確認 --- | |
done_choice = input(f"「{current_task.name}」は完了しましたか? (y/n) > ").lower() | |
if done_choice == 'y': | |
current_task.done = True | |
notify(f"タスク「{current_task.name}」完了!お疲れ様でした!") | |
print(f"実際にかかったポモドーロ数: {current_task.actual_pomodoros}") | |
# --- 休憩 --- | |
if pomodoro_count % POMODOROS_PER_LONG_BREAK == 0: | |
notify(f"{POMODOROS_PER_LONG_BREAK}ポモドーロお疲れ様です!{LONG_BREAK_MINUTES}分の長めの休憩に入ります。") | |
print("この時間に、追加でやるべきことがないか確認しましょう。") | |
countdown(LONG_BREAK_MINUTES) | |
else: | |
notify(f"お疲れ様でした!{SHORT_BREAK_MINUTES}分の短い休憩です。") | |
countdown(SHORT_BREAK_MINUTES) | |
notify("休憩終了です。次の作業の準備をしましょう!") | |
tm.save_tasks(tm.tasks, date.today()) # 休憩後に進捗を保存 | |
elif choice == '2': | |
# --- タスク追加 --- | |
task_name = input("追加するタスク名 > ").strip() | |
if task_name: | |
tm.add_task(Task(task_name)) | |
print(f"タスク「{task_name}」を追加しました。") | |
tm.save_tasks(tm.tasks, date.today()) | |
else: | |
print("⚠️ タスク名が空です。") | |
elif choice == '3': | |
# --- 終了処理 --- | |
tm.display_summary() | |
print("\n明日は何をしますか?タスクをすべて入力してください。") | |
print("(入力が終わったら、何も入力せずにEnterキーを押してください)") | |
next_day_tasks = [] | |
while True: | |
task_name = input("明日のタスク名 > ").strip() | |
if not task_name: | |
break | |
next_day_tasks.append(Task(task_name)) | |
next_day = date.today() + timedelta(days=1) | |
tm.save_tasks(next_day_tasks, next_day) | |
print(f"\n{next_day}のタスクを保存しました。ゆっくり休んでくださいね!") | |
break | |
else: | |
print("⚠️ 1, 2, 3のいずれかを入力してください。") | |
if __name__ == "__main__": | |
try: | |
main() | |
except KeyboardInterrupt: | |
print("\n\nプログラムを中断します。お疲れ様でした。") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment