aimcal-cli 0.12.1

AIM - Analyze. Interact. Manage Your Time, with calendar support
Documentation
// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
//
// SPDX-License-Identifier: Apache-2.0

use std::cell::RefCell;

use aimcal_core::{Priority, TodoStatus};

use crate::tui::component_form::{Access, Form, FormItem, Input, RadioGroup};
use crate::tui::component_form_util::{FormItemSwitch, PositiveIntegerAccess, VisibleIf};
use crate::tui::component_page::SinglePage;
use crate::tui::dispatcher::{Action, Dispatcher};
use crate::tui::todo_store::TodoStoreLike;

pub fn new_todo_editor<S: TodoStoreLike + 'static>() -> SinglePage<S, Form<S, Box<dyn FormItem<S>>>>
{
    SinglePage::new(&"Todo Editor", new_todo_form())
}

pub fn new_todo_form<S: TodoStoreLike + 'static>() -> Form<S, Box<dyn FormItem<S>>> {
    Form::new(vec![
        Box::new(new_summary()),
        Box::new(new_due()),
        Box::new(new_priority()),
        Box::new(new_status()),
        Box::new(new_percent_complete()),
        Box::new(new_description()),
    ])
}

macro_rules! new_input {
    ($fn: ident, $title:expr, $acc: ident, $field: ident, $action: ident) => {
        fn $fn<S: TodoStoreLike>() -> Input<S, $acc> {
            Input::new(&$title)
        }

        struct $acc;

        impl<S: TodoStoreLike> Access<S, String> for $acc {
            fn get(store: &RefCell<S>) -> String {
                store.borrow().todo().data.$field.clone()
            }

            fn set(dispatcher: &mut Dispatcher, value: String) -> bool {
                dispatcher.dispatch(&Action::$action(value));
                true
            }
        }
    };
}

new_input!(
    new_summary,
    "Summary",
    SummaryAccess,
    summary,
    UpdateTodoSummary
);
new_input!(
    new_description,
    "Description",
    DescriptionAccess,
    description,
    UpdateTodoDescription
);
new_input!(new_due, "Due", DueAccess, due, UpdateTodoDue);

struct PercentCompleteAccess;

impl<S: TodoStoreLike> Access<S, Option<u8>> for PercentCompleteAccess {
    fn get(store: &RefCell<S>) -> Option<u8> {
        store.borrow().todo().data.percent_complete
    }

    fn set(dispatcher: &mut Dispatcher, value: Option<u8>) -> bool {
        dispatcher.dispatch(&Action::UpdateTodoPercentComplete(value));
        true
    }
}

type PercentCompleteInput<S> = Input<S, PositiveIntegerAccess<S, u8, PercentCompleteAccess>>;

fn new_percent_complete<S: TodoStoreLike>()
-> VisibleIf<S, PercentCompleteInput<S>, impl Fn(&RefCell<S>) -> bool> {
    VisibleIf::new(Input::new(&"Percent complete"), |store| {
        let s = store.borrow();
        let data = &s.todo().data;
        data.percent_complete.is_some() || matches!(data.status, TodoStatus::InProcess)
    })
}

fn new_status<S: TodoStoreLike>() -> RadioGroup<S, TodoStatus, StatusAccess> {
    use TodoStatus::{Cancelled, Completed, InProcess, NeedsAction};
    let values = vec![NeedsAction, Completed, InProcess, Cancelled];
    let options = values.iter().map(ToString::to_string).collect();
    RadioGroup::new(&"Status", values, options)
}

struct StatusAccess;

impl<S: TodoStoreLike> Access<S, TodoStatus> for StatusAccess {
    fn get(store: &RefCell<S>) -> TodoStatus {
        store.borrow().todo().data.status
    }

    fn set(dispatcher: &mut Dispatcher, value: TodoStatus) -> bool {
        dispatcher.dispatch(&Action::UpdateTodoStatus(value));
        true
    }
}

type RadioGroupPriority<S> = RadioGroup<S, Priority, PriorityAccess>;

fn new_priority<S: TodoStoreLike>()
-> FormItemSwitch<S, RadioGroupPriority<S>, RadioGroupPriority<S>, impl Fn(&RefCell<S>) -> bool> {
    use Priority::{None, P1, P2, P3, P4, P5, P6, P7, P8, P9};

    const TITLE: &str = "Priority";

    let values_verb = vec![P1, P2, P3, P4, P5, P6, P7, P8, P9, None];
    let values = vec![P2, P5, P8, None];

    let options_verb = values_verb
        .iter()
        .map(|a| fmt_priority(*a, true).to_string())
        .collect();

    let options = values
        .iter()
        .map(|a| fmt_priority(*a, false).to_string())
        .collect();

    let verbose = RadioGroup::new(TITLE, values_verb, options_verb);
    let concise = RadioGroup::new(TITLE, values, options);
    FormItemSwitch::new(verbose, concise, |store| {
        store.borrow().todo().verbose_priority
    })
}

const fn fmt_priority(priority: Priority, verbose: bool) -> &'static str {
    match priority {
        Priority::P2 if !verbose => "HIGH",
        Priority::P5 if !verbose => "MID",
        Priority::P8 if !verbose => "LOW",
        Priority::None => "NONE",
        Priority::P1 => "1",
        Priority::P2 => "2",
        Priority::P3 => "3",
        Priority::P4 => "4",
        Priority::P5 => "5",
        Priority::P6 => "6",
        Priority::P7 => "7",
        Priority::P8 => "8",
        Priority::P9 => "9",
    }
}

struct PriorityAccess;

impl<S: TodoStoreLike> Access<S, Priority> for PriorityAccess {
    fn get(store: &RefCell<S>) -> Priority {
        store.borrow().todo().data.priority
    }

    fn set(dispatcher: &mut Dispatcher, value: Priority) -> bool {
        dispatcher.dispatch(&Action::UpdateTodoPriority(value));
        true
    }
}