1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
use lib::core::node_descriptors::NodeDescriptor;
use lib::git::{Commit, NonZeroOid};

/// Prompt the user to select a commit from the provided list
/// of commits, and returns the OID of the selected commit.
#[cfg(unix)]
pub fn prompt_select_commit(
    header: Option<&str>,
    initial_query: &str,
    commits: Vec<Commit>,
    commit_descriptors: &mut [&mut dyn NodeDescriptor],
) -> eyre::Result<Option<NonZeroOid>> {
    skim::prompt_skim(header, initial_query, commits, commit_descriptors)
}

#[cfg(not(unix))]
pub fn prompt_select_commit(
    header: Option<&str>,
    initial_query: &str,
    commits: Vec<Commit>,
    commit_descriptors: &mut [&mut dyn NodeDescriptor],
) -> eyre::Result<Option<NonZeroOid>> {
    unimplemented!("Non-unix targets are currently unsupported for prompting")
}

#[cfg(unix)]
mod skim {
    use eyre::eyre;
    use std::borrow::Cow;
    use std::convert::TryFrom;
    use std::sync::Arc;

    use itertools::Itertools;

    use lib::core::formatting::Glyphs;
    use lib::core::node_descriptors::{render_node_descriptors, NodeDescriptor, NodeObject};
    use lib::git::{Commit, NonZeroOid};

    use skim::{
        prelude::SkimOptionsBuilder, AnsiString, DisplayContext, ItemPreview, Matches,
        PreviewContext, Skim, SkimItem, SkimItemReceiver, SkimItemSender,
    };

    #[derive(Debug)]
    pub struct CommitSkimItem {
        pub oid: NonZeroOid,
        pub styled_summary: String,
        pub styled_preview: String,
    }

    impl SkimItem for CommitSkimItem {
        fn text(&self) -> Cow<str> {
            AnsiString::parse(&self.styled_summary).into_inner()
        }

        fn display<'b>(&'b self, context: DisplayContext<'b>) -> AnsiString<'b> {
            let mut text = AnsiString::parse(&self.styled_summary);
            match context.matches {
                Matches::CharIndices(indices) => {
                    text.override_attrs(
                        indices
                            .iter()
                            .map(|&i| {
                                (
                                    context.highlight_attr,
                                    (u32::try_from(i).unwrap(), u32::try_from(i + 1).unwrap()),
                                )
                            })
                            .collect(),
                    );
                }
                Matches::CharRange(start, end) => {
                    text.override_attrs(vec![(
                        context.highlight_attr,
                        (u32::try_from(start).unwrap(), u32::try_from(end).unwrap()),
                    )]);
                }
                Matches::ByteRange(start, end) => {
                    let start = text.stripped()[..start].chars().count();
                    let end = start + text.stripped()[start..end].chars().count();
                    text.override_attrs(vec![(
                        context.highlight_attr,
                        (u32::try_from(start).unwrap(), u32::try_from(end).unwrap()),
                    )]);
                }
                Matches::None => (),
            }
            text
        }

        fn preview(&self, _context: PreviewContext) -> ItemPreview {
            ItemPreview::AnsiText(self.styled_preview.to_owned())
        }
    }

    impl CommitSkimItem {
        fn from_descriptors(
            commit: &Commit,
            commit_descriptors: &mut [&mut dyn NodeDescriptor],
        ) -> eyre::Result<Self> {
            let glyphs = Glyphs::pretty();
            let styled_summary = render_node_descriptors(
                &glyphs,
                &NodeObject::Commit {
                    commit: commit.clone(),
                },
                commit_descriptors,
            )?;

            Ok(CommitSkimItem {
                oid: commit.get_oid(),
                styled_summary: glyphs.render(styled_summary)?,
                styled_preview: Glyphs::pretty().render(commit.friendly_preview()?)?,
            })
        }
    }

    #[cfg(unix)]
    pub fn prompt_skim(
        header: Option<&str>,
        initial_query: &str,
        commits: Vec<Commit>,
        commit_descriptors: &mut [&mut dyn NodeDescriptor],
    ) -> eyre::Result<Option<NonZeroOid>> {
        let options = SkimOptionsBuilder::default()
            .height(Some("100%"))
            .preview(Some(""))
            .preview_window(Some("up:70%"))
            .sync(true) // Consume all items before displaying selector.
            .bind(vec!["Enter:accept"])
            .header(header)
            .query(Some(initial_query))
            .build()
            .map_err(|e| eyre!("building Skim options failed: {}", e))?;

        let items: Vec<CommitSkimItem> = commits
            .iter()
            .map(|commit| CommitSkimItem::from_descriptors(commit, commit_descriptors))
            .try_collect()?;

        let rx_item = {
            let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = skim::prelude::unbounded();
            for i in items {
                tx_item.send(Arc::new(i))?;
            }
            rx_item
        };

        match Skim::run_with(&options, Some(rx_item)) {
            Some(result) => {
                if result.is_abort {
                    return Ok(None);
                }
                let selected = result
                    .selected_items
                    .first()
                    .and_then(|item| (*item).as_any().downcast_ref::<CommitSkimItem>());
                Ok(selected.map(|c| c.oid))
            }
            None => Ok(None),
        }
    }
}