scm_record/
helpers.rs

1//! Helper functions for rendering UI components.
2
3use std::{collections::VecDeque, time::Duration};
4
5use crate::{Event, RecordError, RecordInput, TerminalKind};
6
7/// Generate a one-line description of a binary file change.
8pub fn make_binary_description(hash: &str, num_bytes: u64) -> String {
9    format!("{} ({} bytes)", hash, num_bytes)
10}
11
12/// Reads input events from the terminal using `crossterm`.
13///
14/// Its default implementation of `edit_commit_message` returns the provided
15/// message unchanged.
16pub struct CrosstermInput;
17
18impl RecordInput for CrosstermInput {
19    fn terminal_kind(&self) -> TerminalKind {
20        TerminalKind::Crossterm
21    }
22
23    fn next_events(&mut self) -> Result<Vec<Event>, RecordError> {
24        // Ensure we block for at least one event.
25        let first_event = crossterm::event::read().map_err(RecordError::ReadInput)?;
26        let mut events = vec![first_event.into()];
27        // Some events, like scrolling, are generated more quickly than
28        // we can render the UI. In those cases, batch up all available
29        // events and process them before the next render.
30        while crossterm::event::poll(Duration::ZERO).map_err(RecordError::ReadInput)? {
31            let event = crossterm::event::read().map_err(RecordError::ReadInput)?;
32            events.push(event.into());
33        }
34        Ok(events)
35    }
36
37    fn edit_commit_message(&mut self, message: &str) -> Result<String, RecordError> {
38        Ok(message.to_owned())
39    }
40}
41
42/// Reads events from the provided sequence of events.
43pub struct TestingInput {
44    /// The width of the virtual terminal in columns.
45    pub width: usize,
46
47    /// The height of the virtual terminal in columns.
48    pub height: usize,
49
50    /// The sequence of events to emit.
51    pub events: Box<dyn Iterator<Item = Event>>,
52
53    /// Commit messages to use when the commit editor is opened.
54    pub commit_messages: VecDeque<String>,
55}
56
57impl TestingInput {
58    /// Helper function to construct a `TestingInput`.
59    pub fn new(
60        width: usize,
61        height: usize,
62        events: impl IntoIterator<Item = Event> + 'static,
63    ) -> Self {
64        Self {
65            width,
66            height,
67            events: Box::new(events.into_iter()),
68            commit_messages: Default::default(),
69        }
70    }
71}
72
73impl RecordInput for TestingInput {
74    fn terminal_kind(&self) -> TerminalKind {
75        let Self {
76            width,
77            height,
78            events: _,
79            commit_messages: _,
80        } = self;
81        TerminalKind::Testing {
82            width: *width,
83            height: *height,
84        }
85    }
86
87    fn next_events(&mut self) -> Result<Vec<Event>, RecordError> {
88        Ok(vec![self.events.next().unwrap_or(Event::None)])
89    }
90
91    fn edit_commit_message(&mut self, _message: &str) -> Result<String, RecordError> {
92        self.commit_messages
93            .pop_front()
94            .ok_or_else(|| RecordError::Other("No more commit messages available".to_string()))
95    }
96}