🚴

Rust製ターミナルでタスク管理。ratatuiでTUI TODOアプリを作ってみた

に公開

どうも。最近まで私は色々なTODOアプリを使っていましたが
あちこちに移動するのが面倒すぎてターミナルで完結するTODOアプリを自作しました。

ということで今回は、ターミナルで動作するRust製のTODOアプリをご紹介します。
キーボード操作でタスクを管理でき、タスク整理に役立つかと思います。

目指せターミナル完結!(あと意識高い系になりたい)

作成中のもの

なんでターミナルアプリなのか?TUIの魅力について

「ターミナル?逆に面倒くさ…」と感じる方もいるかもしれません。
しかし、「TUI (Terminal User Interface)」アプリには、以下のような魅力があります。

  1. キーボード操作による快適さ
    すべての操作がキーボードで完結。
    Vimのような操作感で、効率的なタスク管理が可能。
  2. シンプルなUI
    ブラウザや他のアプリの通知に邪魔されることなく、タスク管理に集中。
    シンプルな画面構成により、やるべきことにフォーカスできるはず…
  3. 爆速❤️
    TUIアプリはグラフィカルなUIに比べて非常に軽量。
    PCの性能に左右されず快適に動作し、バッテリー消費も抑えられます。

とりあえず面倒くさがり&スピード狂なので爆速が正義という信念を持っています。

アプリについて

このアプリは、タスクを階層的に管理するようにしました。

1. サブタスクで複雑なプロジェクトも整理

個人的に大分類、中分類と分けるのが大好きな人間なのでサブタスクの機能もつけました。

  • Enterキーまたは(右矢印)で、選択中のタスクのサブタスクリストに移動。
  • Backspaceキーまたは(左矢印)で、親タスクのリストに戻る。

タスクの階層を移動して管理できます。サブタスクを持つタスクには (...) と表示。

2. 直感的なキーボード操作

シンプルな操作を心がけたつもりです。
操作としては

  • j / k または / : タスクの選択
  • a: 新しいタスク(またはサブタスク)を追加
  • d: 選択中のタスクを削除
  • スペース: 選択中のタスクの完了/未完了を切り替え
  • q: アプリを終了(タスクは自動保存されます)

アプリのコードを少し見る

このアプリがどのように動いているのか、主要なコードをいくつか紹介します。
備忘録&初心者の方にも分かりやすいように、ポイントを絞って解説します。

1. タスクのデータ構造 (Task struct)

タスクとそのサブタスクを表現するための構造体です。
subtasks: Vec<Task>とすることで、タスクの中にさらにタスクを持つ、階層的な構造を実現しています。

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
struct Task {
    id: usize,
    description: String,
    completed: bool,
    #[serde(default)]
    subtasks: Vec<Task>,
}
  • #[derive(...)]はよく見かけるやつですね。Rustの便利なマクロで、データのシリアライズ(保存)やデシリアライズ(読み込み)、デバッグ表示などを自動で実装してくれます。
  • subtasks: Vec<Task>これがサブタスクの所です。Vec<Task>は「Task型のリスト」を意味し、タスクが複数のサブタスクを持てるようにします。
  • #[serde(default)]``tasks.jsonsubtasksフィールドがない場合でも、エラーにならずに空のリストとして扱ってくれます。

2. キーボードイベントの処理 (handle_events 関数)

このアプリのインタラクティブな操作は、handle_events関数が担っています。キーボードからの入力を受け取り、それに応じてアプリの状態を変化させます。

fn handle_events(app: &mut App) -> io::Result<()> {
    if event::poll(Duration::from_millis(250))? {
        if let Event::Key(key) = event::read()? {
            if key.kind == KeyEventKind::Press {
                match app.mode {
                    Mode::Normal => match key.code {
                        KeyCode::Char('q') => app.should_quit = true,
                        KeyCode::Char('j') | KeyCode::Down => app.next(),
                        // ... 他のキー操作 ...
                        KeyCode::Enter | KeyCode::Right => app.zoom_in(),
                        KeyCode::Backspace | KeyCode::Left => app.zoom_out(),
                        _ => {}
                    },
                    Mode::Adding => match key.code {
                        KeyCode::Enter => {
                            app.add_task();
                            app.mode = Mode::Normal;
                        }
                        KeyCode::Esc => app.mode = Mode::Normal,
                        _ => {
                            app.input.handle_event(&Event::Key(key));
                        }
                    },
                }
            }
        }
    }
    Ok(())
}
  • event::read() キーボードからの入力を読み取り。
  • match app.mode アプリが「通常モード」なのか「タスク追加モード」なのかによって、キー操作の挙動を変更。
  • KeyCode::Char('q') qキーが押されたらアプリを終了。

3. 画面の描画 (ui 関数)

ターミナルにタスクリストやヘルプメッセージなどを描画するのがui関数です。
ratatuiライブラリを使って、TUIを実現しています。

fn ui(f: &mut Frame, app: &mut App) {
    // ... レイアウトの設定 ...

    let items: Vec<ListItem> = app
        .get_current_tasks()
        .iter()
        .map(|task| {
            let status = if task.completed { "[x]" } else { "[ ]" };
            let subtask_indicator = if task.subtasks.is_empty() { "" } else { " (...)" };
            let content = format!(
                "{} {} - {}{}",
                status, task.id, task.description, subtask_indicator
            );
            let style = if task.completed {
                Style::default().add_modifier(Modifier::CROSSED_OUT)
            } else {
                Style::default()
            };
            ListItem::new(Line::from(content)).style(style)
        })
        .collect();

    let list_title = app.get_breadcrumb();
    let list = List::new(items)
        .block(Block::default().borders(Borders::ALL).title(list_title))
        .highlight_style(
            Style::default()
                .bg(Color::LightBlue) 
                .add_modifier(Modifier::BOLD),
        )
        .highlight_symbol("> ");

    f.render_stateful_widget(list, chunks[0], &mut app.list_state);

    // ... ヘルプテキストや入力フィールドの描画 ...
}
  • app.get_current_tasks() 現在表示すべきタスクのリストを取得します。
  • map(|task| { ... }) 各タスクを画面に表示するためのListItemに変換しています。完了状態の表示やサブタスクの有無の表示、打ち消し線などのスタイル設定もここで行われます。
  • highlight_style(...) 選択中のタスクの背景色(Color::LightBlue)や文字の太さ(Modifier::BOLD)を設定しています。

全体のコード

https://github.com/kassa697/todo-tui

?? 「お前らのPR待ってるぜ!」

まとめ

このアプリはとりあえずミニマリスト的な感じで無駄を削ぎ落としたいというのと
とりあえずサクサク動くTODOアプリが欲しいという思いで作成しました。

夢は自分のPCからタッチパッドを消し去ることです。

Discussion