Skip to main content

kasl/commands/
inbox.rs

1//! Jira inbox command: sync, list, pin, dismiss, open, and import issues.
2//!
3//! Manages the local `jira_inbox` table populated from assigned open Jira issues.
4
5use crate::db::jira_inbox::JiraInbox;
6use crate::db::tasks::Tasks;
7use crate::libs::jira_inbox as inbox_lib;
8use crate::libs::messages::Message;
9use crate::libs::task::Task;
10use crate::libs::view::View;
11use crate::{msg_error, msg_info, msg_print, msg_success};
12use anyhow::Result;
13use clap::{Args, Subcommand};
14
15/// Command-line arguments for the inbox command.
16#[derive(Debug, Args)]
17pub struct InboxArgs {
18    #[command(subcommand)]
19    command: Option<InboxCommand>,
20
21    // Kept at the top level so the bare `kasl inbox -n 5` form keeps working
22    // as a shorthand for `kasl inbox list -n 5`.
23    /// Show only the top N issues (already sorted by pin / score / priority)
24    #[arg(long, short = 'n', value_name = "N")]
25    limit: Option<usize>,
26}
27
28/// Available inbox operations.
29#[derive(Debug, Subcommand)]
30enum InboxCommand {
31    /// Sync assigned open issues from Jira now
32    #[command(about = "Sync inbox from Jira")]
33    Sync,
34
35    /// List active (non-dismissed) inbox issues
36    #[command(about = "List active inbox issues")]
37    List {
38        /// Show only the top N issues
39        #[arg(long, short = 'n', value_name = "N")]
40        limit: Option<usize>,
41    },
42
43    /// Pin an issue so it stays on top
44    #[command(about = "Pin an inbox issue")]
45    Pin {
46        /// Issue key, e.g. PROJ-123
47        #[arg(value_name = "KEY")]
48        key: String,
49    },
50
51    /// Unpin a previously pinned issue
52    #[command(about = "Unpin an inbox issue")]
53    Unpin {
54        /// Issue key, e.g. PROJ-123
55        #[arg(value_name = "KEY")]
56        key: String,
57    },
58
59    /// Dismiss an issue, hiding it from the list
60    #[command(about = "Dismiss an inbox issue")]
61    Dismiss {
62        /// Issue key, e.g. PROJ-123
63        #[arg(value_name = "KEY")]
64        key: String,
65    },
66
67    /// Open an issue in the browser
68    #[command(about = "Open issue URL in browser")]
69    Open {
70        /// Issue key, e.g. PROJ-123
71        #[arg(value_name = "KEY")]
72        key: String,
73    },
74
75    /// Import an issue into local tasks
76    #[command(about = "Import issue into tasks")]
77    Take {
78        /// Issue key, e.g. PROJ-123
79        #[arg(value_name = "KEY")]
80        key: String,
81    },
82}
83
84/// Entry point for `kasl inbox`.
85pub async fn cmd(args: InboxArgs) -> Result<()> {
86    match args.command {
87        Some(InboxCommand::Sync) => {
88            let outcome = inbox_lib::sync_interactive(true).await?;
89            if !outcome.skipped {
90                msg_success!(Message::JiraInboxSynced {
91                    fetched: outcome.fetched,
92                    new_count: outcome.new_keys.len(),
93                    updated: outcome.updated,
94                });
95            }
96            Ok(())
97        }
98        Some(InboxCommand::List { limit }) => list_inbox(limit.or(args.limit)),
99        Some(InboxCommand::Pin { key }) => set_pinned(&key, true),
100        Some(InboxCommand::Unpin { key }) => set_pinned(&key, false),
101        Some(InboxCommand::Dismiss { key }) => dismiss(&key),
102        Some(InboxCommand::Open { key }) => open_issue(&key),
103        Some(InboxCommand::Take { key }) => take_issue(&key),
104        // Bare `kasl inbox` shows the list, as it always has.
105        None => list_inbox(args.limit),
106    }
107}
108
109fn list_inbox(limit: Option<usize>) -> Result<()> {
110    let mut items = JiraInbox::new()?.list_active()?;
111    if items.is_empty() {
112        msg_info!(Message::JiraInboxEmpty);
113        return Ok(());
114    }
115    if let Some(n) = limit {
116        items.truncate(n);
117    }
118    msg_print!(Message::JiraInboxListHeader, true);
119    View::jira_inbox(&items)
120}
121
122fn set_pinned(key: &str, pinned: bool) -> Result<()> {
123    let db = JiraInbox::new()?;
124    if !db.set_pinned(key, pinned)? {
125        msg_error!(Message::JiraInboxNotFound(key.to_string()));
126        return Ok(());
127    }
128    if pinned {
129        msg_success!(Message::JiraInboxPinned(key.to_string()));
130    } else {
131        msg_success!(Message::JiraInboxUnpinned(key.to_string()));
132    }
133    Ok(())
134}
135
136fn dismiss(key: &str) -> Result<()> {
137    let db = JiraInbox::new()?;
138    if !db.set_dismissed(key, true)? {
139        msg_error!(Message::JiraInboxNotFound(key.to_string()));
140        return Ok(());
141    }
142    msg_success!(Message::JiraInboxDismissed(key.to_string()));
143    Ok(())
144}
145
146fn open_issue(key: &str) -> Result<()> {
147    let db = JiraInbox::new()?;
148    let Some(item) = db.get_by_key(key)? else {
149        msg_error!(Message::JiraInboxNotFound(key.to_string()));
150        return Ok(());
151    };
152
153    match inbox_lib::open_url(&item.url) {
154        Ok(()) => msg_success!(Message::JiraInboxOpened(key.to_string())),
155        Err(e) => msg_error!(Message::JiraInboxOpenFailed(e.to_string())),
156    }
157    Ok(())
158}
159
160fn take_issue(key: &str) -> Result<()> {
161    let db = JiraInbox::new()?;
162    let Some(item) = db.get_by_key(key)? else {
163        msg_error!(Message::JiraInboxNotFound(key.to_string()));
164        return Ok(());
165    };
166
167    let name = format!("{} {}", item.issue_key, item.summary);
168    let task = Task::new(&name, "", Some(0));
169    Tasks::new()?.insert(&task)?;
170    let _ = db.set_dismissed(key, true)?;
171    msg_success!(Message::JiraInboxTaken(key.to_string()));
172    Ok(())
173}