git_branchless_navigation/
prompt.rs1use lib::core::node_descriptors::NodeDescriptor;
4use lib::git::{Commit, NonZeroOid};
5
6#[cfg(unix)]
9pub fn prompt_select_commit(
10 header: Option<&str>,
11 initial_query: &str,
12 commits: Vec<Commit>,
13 commit_descriptors: &mut [&mut dyn NodeDescriptor],
14) -> eyre::Result<Option<NonZeroOid>> {
15 skim::prompt_skim(header, initial_query, commits, commit_descriptors)
16}
17
18#[cfg(not(unix))]
19pub fn prompt_select_commit(
20 header: Option<&str>,
21 initial_query: &str,
22 commits: Vec<Commit>,
23 commit_descriptors: &mut [&mut dyn NodeDescriptor],
24) -> eyre::Result<Option<NonZeroOid>> {
25 unimplemented!("Non-unix targets are currently unsupported for prompting")
26}
27
28#[cfg(unix)]
29mod skim {
30 use eyre::eyre;
31 use std::borrow::Cow;
32 use std::sync::Arc;
33
34 use itertools::Itertools;
35
36 use lib::core::formatting::Glyphs;
37 use lib::core::node_descriptors::{render_node_descriptors, NodeDescriptor, NodeObject};
38 use lib::git::{Commit, NonZeroOid};
39
40 use skim::{
41 prelude::SkimOptionsBuilder, AnsiString, DisplayContext, ItemPreview, Matches,
42 PreviewContext, Skim, SkimItem, SkimItemReceiver, SkimItemSender,
43 };
44
45 #[derive(Debug)]
46 pub struct CommitSkimItem {
47 pub oid: NonZeroOid,
48 pub styled_summary: String,
49 pub styled_preview: String,
50 }
51
52 impl SkimItem for CommitSkimItem {
53 fn text(&self) -> Cow<str> {
54 AnsiString::parse(&self.styled_summary).into_inner()
55 }
56
57 fn display<'b>(&'b self, context: DisplayContext<'b>) -> AnsiString<'b> {
58 let mut text = AnsiString::parse(&self.styled_summary);
59 match context.matches {
60 Matches::CharIndices(indices) => {
61 text.override_attrs(
62 indices
63 .iter()
64 .map(|&i| {
65 (
66 context.highlight_attr,
67 (u32::try_from(i).unwrap(), u32::try_from(i + 1).unwrap()),
68 )
69 })
70 .collect(),
71 );
72 }
73 Matches::CharRange(start, end) => {
74 text.override_attrs(vec![(
75 context.highlight_attr,
76 (u32::try_from(start).unwrap(), u32::try_from(end).unwrap()),
77 )]);
78 }
79 Matches::ByteRange(start, end) => {
80 let start = text.stripped()[..start].chars().count();
81 let end = start + text.stripped()[start..end].chars().count();
82 text.override_attrs(vec![(
83 context.highlight_attr,
84 (u32::try_from(start).unwrap(), u32::try_from(end).unwrap()),
85 )]);
86 }
87 Matches::None => (),
88 }
89 text
90 }
91
92 fn preview(&self, _context: PreviewContext) -> ItemPreview {
93 ItemPreview::AnsiText(self.styled_preview.to_owned())
94 }
95 }
96
97 impl CommitSkimItem {
98 fn from_descriptors(
99 commit: &Commit,
100 commit_descriptors: &mut [&mut dyn NodeDescriptor],
101 ) -> eyre::Result<Self> {
102 let glyphs = Glyphs::pretty();
103 let styled_summary = render_node_descriptors(
104 &glyphs,
105 &NodeObject::Commit {
106 commit: commit.clone(),
107 },
108 commit_descriptors,
109 )?;
110
111 Ok(CommitSkimItem {
112 oid: commit.get_oid(),
113 styled_summary: glyphs.render(styled_summary)?,
114 styled_preview: Glyphs::pretty().render(commit.friendly_preview()?)?,
115 })
116 }
117 }
118
119 #[cfg(unix)]
120 pub fn prompt_skim(
121 header: Option<&str>,
122 initial_query: &str,
123 commits: Vec<Commit>,
124 commit_descriptors: &mut [&mut dyn NodeDescriptor],
125 ) -> eyre::Result<Option<NonZeroOid>> {
126 let options = SkimOptionsBuilder::default()
127 .height(Some("100%"))
128 .preview(Some(""))
129 .preview_window(Some("up:70%"))
130 .sync(true) .bind(vec!["Enter:accept"])
132 .header(header)
133 .query(Some(initial_query))
134 .build()
135 .map_err(|e| eyre!("building Skim options failed: {}", e))?;
136
137 let items: Vec<CommitSkimItem> = commits
138 .iter()
139 .map(|commit| CommitSkimItem::from_descriptors(commit, commit_descriptors))
140 .try_collect()?;
141
142 let rx_item = {
143 let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = skim::prelude::unbounded();
144 for i in items {
145 tx_item.send(Arc::new(i))?;
146 }
147 rx_item
148 };
149
150 match Skim::run_with(&options, Some(rx_item)) {
151 Some(result) => {
152 if result.is_abort {
153 return Ok(None);
154 }
155 let selected = result
156 .selected_items
157 .first()
158 .and_then(|item| (*item).as_any().downcast_ref::<CommitSkimItem>());
159 Ok(selected.map(|c| c.oid))
160 }
161 None => Ok(None),
162 }
163 }
164}