git_branchless_navigation/
prompt.rs

1//! An interactive prompt to select a commit.
2
3use lib::core::node_descriptors::NodeDescriptor;
4use lib::git::{Commit, NonZeroOid};
5
6/// Prompt the user to select a commit from the provided list
7/// of commits, and returns the OID of the selected commit.
8#[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) // Consume all items before displaying selector.
131            .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}