use crate::libs::data_storage::DataStorage;
use anyhow::{Result, bail};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
const MAILBOX_FILE: &str = "toast-actions.jsonl";
pub const SHORTCUT_DIR: &str = "toast-buttons";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToastAction {
Take,
Snooze,
Dismiss,
}
impl ToastAction {
pub fn as_str(&self) -> &'static str {
match self {
ToastAction::Take => "take",
ToastAction::Snooze => "snooze",
ToastAction::Dismiss => "dismiss",
}
}
pub fn label(&self) -> &'static str {
match self {
ToastAction::Take => "Take",
ToastAction::Snooze => "Snooze",
ToastAction::Dismiss => "Dismiss",
}
}
pub fn parse(name: &str) -> Option<Self> {
match name {
"take" => Some(ToastAction::Take),
"snooze" => Some(ToastAction::Snooze),
"dismiss" => Some(ToastAction::Dismiss),
_ => None,
}
}
pub const ALL: [ToastAction; 3] = [ToastAction::Take, ToastAction::Snooze, ToastAction::Dismiss];
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToastRequest {
pub action: ToastAction,
pub issue_key: String,
}
impl ToastRequest {
pub fn new(action: ToastAction, issue_key: &str) -> Self {
Self {
action,
issue_key: issue_key.to_string(),
}
}
fn to_line(&self) -> String {
format!("{}\t{}", self.action.as_str(), self.issue_key)
}
fn from_line(line: &str) -> Option<Self> {
let (action, key) = line.split_once('\t')?;
let key = key.trim();
if key.is_empty() {
return None;
}
Some(Self {
action: ToastAction::parse(action.trim())?,
issue_key: key.to_string(),
})
}
}
pub struct Mailbox {
path: PathBuf,
}
impl Mailbox {
pub fn open() -> Result<Self> {
Ok(Self {
path: DataStorage::new().get_path(MAILBOX_FILE)?,
})
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn post(&self, request: &ToastRequest) -> Result<()> {
let mut file = OpenOptions::new().create(true).append(true).open(&self.path)?;
file.write_all(format!("{}\n", request.to_line()).as_bytes())?;
file.flush()?;
Ok(())
}
pub fn collect(&self) -> Result<Vec<ToastRequest>> {
let contents = match fs::read_to_string(&self.path) {
Ok(contents) => contents,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
let requests: Vec<ToastRequest> = contents.lines().filter_map(ToastRequest::from_line).collect();
let _ = fs::remove_file(&self.path);
Ok(requests)
}
}
pub fn validate_issue_key(key: &str) -> Result<()> {
if key.is_empty() || key.len() > 64 {
bail!("issue key has an unusable length: {key:?}");
}
if !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
bail!("issue key has characters that cannot go in a shortcut: {key:?}");
}
Ok(())
}
pub fn shortcut_dir() -> Result<PathBuf> {
let dir = DataStorage::new().get_path(SHORTCUT_DIR)?;
fs::create_dir_all(&dir)?;
Ok(dir)
}