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::libs::pause::Pause;
41use crate::libs::prompt::ensure_interactive;
42use crate::libs::task::Task;
43
44fn select(prompt: &str, labels: &[String]) -> Result<usize> {
46 Ok(Select::with_theme(&ColorfulTheme::default())
47 .with_prompt(prompt)
48 .items(labels)
49 .default(0)
50 .interact()?)
51}
52
53pub fn inbox_issue(items: &[JiraInboxItem], prompt: &str) -> Result<String> {
59 if items.is_empty() {
60 bail!("the inbox is empty - run `kasl inbox sync` to fetch assigned issues");
61 }
62 ensure_interactive("issue key is required; pass KEY outside a terminal")?;
63
64 let now = Local::now().naive_local();
65 let width = items.iter().map(|i| i.issue_key.len()).max().unwrap_or(0);
66 let labels: Vec<String> = items
67 .iter()
68 .map(|item| {
69 let pin = if item.pinned { "*" } else { " " };
70 let badge = item.badge(now).map(|b| format!(" [{b}]")).unwrap_or_default();
71 format!("{pin}{:width$} {}{badge}", item.issue_key, item.summary)
72 })
73 .collect();
74
75 Ok(items[select(prompt, &labels)?].issue_key.clone())
76}
77
78pub fn tasks(tasks: &[Task], prompt: &str) -> Result<Vec<i32>> {
84 if tasks.is_empty() {
85 bail!("no tasks to choose from - `kasl task add` creates one");
86 }
87 ensure_interactive("task id is required; pass ID outside a terminal")?;
88
89 let labels: Vec<String> = tasks
90 .iter()
91 .map(|t| format!("[{}] {} ({}%)", t.id.unwrap_or(0), t.name, t.completeness.unwrap_or(0)))
92 .collect();
93
94 let chosen = MultiSelect::with_theme(&ColorfulTheme::default())
95 .with_prompt(prompt)
96 .items(&labels)
97 .interact()?;
98
99 Ok(chosen.into_iter().filter_map(|i| tasks[i].id).collect())
100}
101
102pub fn tag(tags: &[Tag], prompt: &str) -> Result<String> {
107 if tags.is_empty() {
108 bail!("no tags yet - run `kasl tag add` first");
109 }
110 ensure_interactive("tag name is required; pass TAG outside a terminal")?;
111
112 let width = tags.iter().map(|t| t.name.len()).max().unwrap_or(0);
113 let labels: Vec<String> = tags
114 .iter()
115 .map(|t| match &t.color {
116 Some(color) if !color.is_empty() => format!("{:width$} {color}", t.name),
117 _ => t.name.clone(),
118 })
119 .collect();
120
121 Ok(tags[select(prompt, &labels)?].name.clone())
122}
123
124pub fn pause(pauses: &[Pause], prompt: &str) -> Result<i32> {
129 if pauses.is_empty() {
130 bail!("no pauses recorded for that day - `kasl pauses list` shows what is there");
131 }
132 ensure_interactive("pause id is required; pass ID outside a terminal")?;
133
134 let labels: Vec<String> = pauses
135 .iter()
136 .map(|p| {
137 let start = p.start.format("%H:%M");
138 let end = p.end.map(|e| e.format("%H:%M").to_string()).unwrap_or_else(|| "…".to_string());
139 match p.duration {
140 Some(d) => format!("{start} - {end} ({} min)", d.num_minutes()),
141 None => format!("{start} - {end} (ongoing)"),
142 }
143 })
144 .collect();
145
146 Ok(pauses[select(prompt, &labels)?].id)
147}