1use anyhow::{Result, bail};
35use chrono::Local;
36use dialoguer::{MultiSelect, Select, theme::ColorfulTheme};
37
38use crate::db::jira_inbox::JiraInboxItem;
39use crate::db::tags::Tag;
40use crate::db::templates::TaskTemplate;
41use crate::libs::pause::Pause;
42use crate::libs::prompt::ensure_interactive;
43use crate::libs::task::Task;
44
45fn select(prompt: &str, labels: &[String]) -> Result<usize> {
47 Ok(Select::with_theme(&ColorfulTheme::default())
48 .with_prompt(prompt)
49 .items(labels)
50 .default(0)
51 .interact()?)
52}
53
54pub fn inbox_issue(items: &[JiraInboxItem], prompt: &str) -> Result<String> {
60 if items.is_empty() {
61 bail!("the inbox is empty - run `kasl inbox sync` to fetch assigned issues");
62 }
63 ensure_interactive("issue key is required; pass KEY outside a terminal")?;
64
65 let now = Local::now().naive_local();
66 let width = items.iter().map(|i| i.issue_key.len()).max().unwrap_or(0);
67 let labels: Vec<String> = items
68 .iter()
69 .map(|item| {
70 let pin = if item.pinned { "*" } else { " " };
71 let badge = item.badge(now).map(|b| format!(" [{b}]")).unwrap_or_default();
72 format!("{pin}{:width$} {}{badge}", item.issue_key, item.summary)
73 })
74 .collect();
75
76 Ok(items[select(prompt, &labels)?].issue_key.clone())
77}
78
79pub fn tasks(tasks: &[Task], prompt: &str) -> Result<Vec<i32>> {
85 if tasks.is_empty() {
86 bail!("no tasks to choose from - `kasl task add` creates one");
87 }
88 ensure_interactive("task id is required; pass ID outside a terminal")?;
89
90 let labels: Vec<String> = tasks
91 .iter()
92 .map(|t| format!("[{}] {} ({}%)", t.id.unwrap_or(0), t.name, t.completeness.unwrap_or(0)))
93 .collect();
94
95 let chosen = MultiSelect::with_theme(&ColorfulTheme::default())
96 .with_prompt(prompt)
97 .items(&labels)
98 .interact()?;
99
100 Ok(chosen.into_iter().filter_map(|i| tasks[i].id).collect())
101}
102
103pub fn template(templates: &[TaskTemplate], prompt: &str) -> Result<String> {
105 if templates.is_empty() {
106 bail!("no templates yet - run `kasl template add` first");
107 }
108 ensure_interactive("template name is required; pass NAME outside a terminal")?;
109
110 let width = templates.iter().map(|t| t.name.len()).max().unwrap_or(0);
111 let labels: Vec<String> = templates.iter().map(|t| format!("{:width$} {}", t.name, t.task_name)).collect();
112
113 Ok(templates[select(prompt, &labels)?].name.clone())
114}
115
116pub fn tag(tags: &[Tag], prompt: &str) -> Result<String> {
121 if tags.is_empty() {
122 bail!("no tags yet - run `kasl tag add` first");
123 }
124 ensure_interactive("tag name is required; pass TAG outside a terminal")?;
125
126 let width = tags.iter().map(|t| t.name.len()).max().unwrap_or(0);
127 let labels: Vec<String> = tags
128 .iter()
129 .map(|t| match &t.color {
130 Some(color) if !color.is_empty() => format!("{:width$} {color}", t.name),
131 _ => t.name.clone(),
132 })
133 .collect();
134
135 Ok(tags[select(prompt, &labels)?].name.clone())
136}
137
138pub fn pause(pauses: &[Pause], prompt: &str) -> Result<i32> {
143 if pauses.is_empty() {
144 bail!("no pauses recorded for that day - `kasl pauses list` shows what is there");
145 }
146 ensure_interactive("pause id is required; pass ID outside a terminal")?;
147
148 let labels: Vec<String> = pauses
149 .iter()
150 .map(|p| {
151 let start = p.start.format("%H:%M");
152 let end = p.end.map(|e| e.format("%H:%M").to_string()).unwrap_or_else(|| "…".to_string());
153 match p.duration {
154 Some(d) => format!("{start} - {end} ({} min)", d.num_minutes()),
155 None => format!("{start} - {end} (ongoing)"),
156 }
157 })
158 .collect();
159
160 Ok(pauses[select(prompt, &labels)?].id)
161}