use super::Refusal;
use crate::core::entity::{Handoff, Note, Notes, Subject};
#[derive(Debug, Clone, PartialEq)]
pub enum Request {
Deliver(Handoff),
}
pub const HINT: &str = "`linear-tui context` shows this view with the row under my cursor; \
`linear-tui issue show|comment|status <ID>` and `linear-tui issue create --team <key> --title <text>` \
act on Linear.";
#[derive(Debug, Clone, PartialEq)]
pub enum Delivery {
Herdr(Request),
Clipboard(String),
}
pub fn add(notes: &mut Notes, about: Option<Subject>, body: &str) -> Option<usize> {
let body = body.trim();
if body.is_empty() {
return None;
}
notes.push(Note {
about,
body: body.to_string(),
});
Some(notes.len())
}
pub fn prompt(notes: &Notes, view: String) -> Handoff {
let lines: Vec<String> = notes
.iter()
.map(|note| {
let about = match ¬e.about {
Some(subject) => format!("**{}** {}", subject.identifier, subject.title),
None => "**This view**".to_string(),
};
let body = note.body.replace('\n', "\n ");
format!("- {about}: {body}")
})
.collect();
let notes = lines.join("\n");
let text =
format!("My notes on what I am looking at in linear-tui ({view}):\n\n{notes}\n\n({HINT})");
Handoff::Prompt {
text,
notes,
view,
hint: HINT.to_string(),
}
}
pub fn send(notes: &mut Notes, view: String, herdr: bool) -> Result<Delivery, Refusal> {
if notes.is_empty() {
return Err(Refusal::NoNotes);
}
let handoff = prompt(notes, view);
notes.clear();
Ok(if herdr {
Delivery::Herdr(Request::Deliver(handoff))
} else {
Delivery::Clipboard(salvage(&handoff).unwrap_or_default())
})
}
pub fn discard(notes: &mut Notes) -> usize {
let count = notes.len();
notes.clear();
count
}
pub fn salvage(handoff: &Handoff) -> Option<String> {
match handoff {
Handoff::Prompt { text, .. } => Some(text.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn eng_42() -> Option<Subject> {
Some(Subject {
identifier: "ENG-42".into(),
title: "Checkout fails".into(),
})
}
fn two_notes() -> Notes {
let mut notes = Notes::default();
add(&mut notes, eng_42(), "reproduce first\nthen fix");
add(&mut notes, None, "the top three are one bug");
notes
}
#[test]
fn a_note_is_trimmed_and_counted() {
let mut notes = Notes::default();
assert_eq!(add(&mut notes, None, " look here \n"), Some(1));
assert_eq!(notes.iter().next().unwrap().body, "look here");
assert_eq!(add(&mut notes, eng_42(), "and here"), Some(2));
}
#[test]
fn a_blank_note_is_dropped() {
let mut notes = Notes::default();
assert_eq!(add(&mut notes, None, " \n "), None);
assert!(notes.is_empty());
}
#[test]
fn the_prompt_names_the_view_and_each_notes_issue() {
let Handoff::Prompt {
text, notes, view, ..
} = prompt(&two_notes(), "Engineering › Issues".into())
else {
panic!("notes make a prompt");
};
assert_eq!(view, "Engineering › Issues");
assert_eq!(
notes,
"- **ENG-42** Checkout fails: reproduce first\n then fix\n\
- **This view**: the top three are one bug"
);
assert!(
text.starts_with(
"My notes on what I am looking at in linear-tui (Engineering › Issues):"
)
);
}
#[test]
fn the_prompt_ends_with_the_hint() {
let Handoff::Prompt { text, hint, .. } = prompt(&two_notes(), "Views".into()) else {
panic!("notes make a prompt");
};
assert!(text.ends_with(&format!("({HINT})")));
assert_eq!(hint, HINT);
}
#[test]
fn outside_herdr_the_notes_go_to_the_clipboard() {
let mut notes = two_notes();
let Ok(Delivery::Clipboard(text)) = send(&mut notes, "Views".into(), false) else {
panic!("expected the clipboard");
};
assert!(text.contains("**ENG-42**"));
assert!(notes.is_empty());
}
#[test]
fn inside_herdr_the_notes_are_handed_to_the_agent() {
let mut notes = two_notes();
let delivery = send(&mut notes, "Views".into(), true);
assert!(matches!(
delivery,
Ok(Delivery::Herdr(Request::Deliver(Handoff::Prompt { .. })))
));
assert!(notes.is_empty());
}
#[test]
fn with_no_notes_there_is_nothing_to_send() {
assert_eq!(
send(&mut Notes::default(), "Views".into(), false),
Err(Refusal::NoNotes)
);
}
#[test]
fn discarding_drops_every_note() {
let mut notes = two_notes();
assert_eq!(discard(&mut notes), 2);
assert!(notes.is_empty());
assert_eq!(discard(&mut notes), 0);
}
#[test]
fn a_prompt_herdr_refused_is_kept_for_the_clipboard() {
let handoff = prompt(&two_notes(), "Views".into());
assert!(salvage(&handoff).is_some_and(|t| t.contains("ENG-42")));
assert_eq!(salvage(&Handoff::Focus { pane: "p".into() }), None);
}
}