use std::sync::{Arc, Mutex, OnceLock};
use skim::prelude::*;
use worktrunk::git::Repository;
use worktrunk::styling::{StyledLine, strip_osc8_hyperlinks};
use super::items::{HeaderSkimItem, PreviewCache, WorktreeSkimItem};
use super::preview::PreviewMode;
use super::preview_orchestrator::PreviewOrchestrator;
use crate::commands::list::collect::PickerProgressHandler;
use crate::commands::list::model::ListItem;
pub(super) struct PickerHandler {
pub(super) tx: SkimItemSender,
pub(super) shared_items: Arc<Mutex<Vec<Arc<dyn SkimItem>>>>,
pub(super) rendered_slots: OnceLock<Box<[Arc<Mutex<String>>]>>,
pub(super) preview_cache: PreviewCache,
pub(super) orchestrator: Arc<PreviewOrchestrator>,
pub(super) preview_dims: (usize, usize),
pub(super) llm_command: Option<String>,
pub(super) repo: Repository,
pub(super) summary_hint: Option<String>,
}
impl PickerProgressHandler for PickerHandler {
fn on_skeleton(&self, items: Vec<ListItem>, rendered: Vec<String>, header: StyledLine) {
debug_assert_eq!(items.len(), rendered.len());
let mut slots: Vec<Arc<Mutex<String>>> = Vec::with_capacity(items.len());
let mut skim_items: Vec<Arc<dyn SkimItem>> = Vec::with_capacity(items.len() + 1);
let mut list_items: Vec<Arc<ListItem>> = Vec::with_capacity(items.len());
skim_items.push(Arc::new(HeaderSkimItem {
display_text: header.plain_text(),
display_text_with_ansi: header.render(),
}) as Arc<dyn SkimItem>);
for (item, rendered_line) in items.into_iter().zip(rendered) {
let branch_name = item.branch_name().to_string();
let path_str = item
.worktree_path()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let search_text = if path_str.is_empty() {
branch_name.clone()
} else {
format!("{branch_name} {path_str}")
};
let rendered_arc = Arc::new(Mutex::new(strip_osc8_hyperlinks(&rendered_line)));
slots.push(Arc::clone(&rendered_arc));
let item_arc = Arc::new(item);
list_items.push(Arc::clone(&item_arc));
skim_items.push(Arc::new(WorktreeSkimItem {
search_text,
rendered: rendered_arc,
branch_name,
item: item_arc,
preview_cache: Arc::clone(&self.preview_cache),
}) as Arc<dyn SkimItem>);
}
let _ = self.rendered_slots.set(slots.into_boxed_slice());
*self.shared_items.lock().unwrap() = skim_items.clone();
for skim_item in &skim_items {
let _ = self.tx.send(Arc::clone(skim_item));
}
self.spawn_precompute(&list_items);
}
fn on_update(&self, idx: usize, rendered: String) {
if let Some(slots) = self.rendered_slots.get()
&& let Some(slot) = slots.get(idx)
{
*slot.lock().unwrap() = strip_osc8_hyperlinks(&rendered);
}
}
fn on_reveal(&self, rendered: Vec<Option<String>>) {
let Some(slots) = self.rendered_slots.get() else {
return;
};
for (slot, line) in slots.iter().zip(rendered) {
if let Some(line) = line {
*slot.lock().unwrap() = strip_osc8_hyperlinks(&line);
}
}
}
}
impl PickerHandler {
fn spawn_precompute(&self, list_items: &[Arc<ListItem>]) {
let modes = [
PreviewMode::WorkingTree,
PreviewMode::Log,
PreviewMode::BranchDiff,
PreviewMode::UpstreamDiff,
];
if let Some(first) = list_items.first() {
for mode in modes {
self.orchestrator
.spawn_preview(Arc::clone(first), mode, self.preview_dims);
}
}
for mode in modes {
for item in list_items.iter().skip(1) {
self.orchestrator
.spawn_preview(Arc::clone(item), mode, self.preview_dims);
}
}
if let Some(llm) = self.llm_command.as_ref() {
if let Some(first) = list_items.first() {
self.orchestrator
.spawn_summary(Arc::clone(first), llm.clone(), self.repo.clone());
}
for item in list_items.iter().skip(1) {
self.orchestrator
.spawn_summary(Arc::clone(item), llm.clone(), self.repo.clone());
}
} else if let Some(hint) = self.summary_hint.as_ref() {
for item in list_items {
let branch = item.branch_name().to_string();
self.preview_cache
.insert((branch, PreviewMode::Summary), hint.clone());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::list::model::ListItem;
use worktrunk::testing::TestRepo;
fn make_handler() -> (
PickerHandler,
TestRepo,
crossbeam_channel::Receiver<Arc<dyn SkimItem>>,
) {
let test = TestRepo::with_initial_commit();
let (tx, rx) = crossbeam_channel::unbounded::<Arc<dyn SkimItem>>();
let shared_items = Arc::new(Mutex::new(Vec::new()));
let repo = test.repo.clone();
let orchestrator = Arc::new(PreviewOrchestrator::new(repo.clone()));
let preview_cache: PreviewCache = Arc::clone(&orchestrator.cache);
let handler = PickerHandler {
tx,
shared_items,
rendered_slots: OnceLock::new(),
preview_cache,
orchestrator,
preview_dims: (80, 24),
llm_command: None,
repo,
summary_hint: Some("disabled".to_string()),
};
(handler, test, rx)
}
fn header(text: &str) -> StyledLine {
let mut line = StyledLine::new();
line.push_raw(text);
line
}
#[test]
fn handler_updates_render_strings_in_place() {
let (handler, _test, rx) = make_handler();
let items = vec![
ListItem::new_branch("aaa".into(), "one".into()),
ListItem::new_branch("bbb".into(), "two".into()),
];
let rendered = vec!["skel-one".to_string(), "skel-two".to_string()];
handler.on_skeleton(items, rendered, header("hdr"));
let received: Vec<Arc<dyn SkimItem>> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
assert_eq!(received.len(), 3, "expected header + 2 items");
let slots = handler.rendered_slots.get().unwrap();
assert_eq!(slots.len(), 2);
assert_eq!(*slots[0].lock().unwrap(), "skel-one");
assert_eq!(*slots[1].lock().unwrap(), "skel-two");
handler.on_update(1, "updated-two".into());
assert_eq!(*slots[0].lock().unwrap(), "skel-one", "row 0 untouched");
assert_eq!(*slots[1].lock().unwrap(), "updated-two");
handler.on_reveal(vec![Some("rev-one".into()), None]);
assert_eq!(*slots[0].lock().unwrap(), "rev-one");
assert_eq!(
*slots[1].lock().unwrap(),
"updated-two",
"row 1 had data — reveal must not clobber it"
);
}
#[test]
fn skeleton_publishes_header_then_items() {
let (handler, _test, rx) = make_handler();
let items = vec![
ListItem::new_branch("aaa".into(), "feat-a".into()),
ListItem::new_branch("bbb".into(), "feat-b".into()),
];
handler.on_skeleton(
items,
vec!["skel-a".into(), "skel-b".into()],
header("Branch Status"),
);
let received: Vec<Arc<dyn SkimItem>> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
assert_eq!(received.len(), 3);
assert_eq!(received[0].output().as_ref(), "");
assert_eq!(received[1].output().as_ref(), "feat-a");
assert_eq!(received[2].output().as_ref(), "feat-b");
let shared = handler.shared_items.lock().unwrap();
assert_eq!(shared.len(), 3);
assert_eq!(shared[1].output().as_ref(), "feat-a");
assert_eq!(shared[2].output().as_ref(), "feat-b");
}
}