Skip to main content

ito_core/viewer/
mod.rs

1//! Proposal viewer support.
2
3use crate::errors::CoreResult;
4
5/// Artifact collection helpers for proposal viewing.
6pub mod collector;
7
8/// Bat-based terminal viewer backend.
9pub mod bat;
10
11/// Glow-based terminal viewer backend.
12pub mod glow;
13
14/// HTML browser viewer backend (pandoc + system browser).
15pub mod html;
16
17/// Viewer registry and lookup helpers.
18pub mod registry;
19
20/// Shared utilities for viewer backends.
21pub(crate) mod util;
22
23/// Tmux + Neovim popup viewer backend.
24pub mod tmux_nvim;
25
26pub use bat::BatViewer;
27pub use collector::collect_proposal_artifacts;
28pub use glow::GlowViewer;
29pub use html::HtmlViewer;
30pub use registry::ViewerRegistry;
31pub use tmux_nvim::TmuxNvimViewer;
32
33/// A pluggable backend that can render collected proposal artifacts.
34pub trait ViewerBackend {
35    /// Stable CLI/backend identifier.
36    fn name(&self) -> &str;
37
38    /// Human-readable summary shown in prompts and help.
39    fn description(&self) -> &str;
40
41    /// Whether the viewer can run in the current environment.
42    fn is_available(&self) -> bool;
43
44    /// Detailed hint shown when the viewer is unavailable.
45    ///
46    /// Returns `None` if `is_available()` is true or no specific guidance exists.
47    fn availability_hint(&self) -> Option<String> {
48        None
49    }
50
51    /// Open or render the provided proposal content.
52    fn open(&self, content: &str) -> CoreResult<()>;
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    use crate::errors::CoreResult;
60    #[cfg(unix)]
61    use std::sync::mpsc;
62    #[cfg(unix)]
63    use std::time::Duration;
64
65    struct DummyViewer;
66
67    impl ViewerBackend for DummyViewer {
68        fn name(&self) -> &str {
69            "dummy"
70        }
71
72        fn description(&self) -> &str {
73            "Dummy viewer for tests"
74        }
75
76        fn is_available(&self) -> bool {
77            true
78        }
79
80        fn open(&self, _content: &str) -> CoreResult<()> {
81            Ok(())
82        }
83    }
84
85    #[test]
86    fn viewer_backend_trait_exposes_required_methods() {
87        let viewer = DummyViewer;
88        assert_eq!(viewer.name(), "dummy");
89        assert_eq!(viewer.description(), "Dummy viewer for tests");
90        assert!(viewer.is_available());
91        viewer.open("hello").unwrap();
92    }
93
94    #[test]
95    fn viewer_registry_filters_and_finds_available_viewers() {
96        let registry =
97            ViewerRegistry::new(vec![Box::new(DummyViewer), Box::new(UnavailableViewer)]);
98
99        let available = registry.available_viewers();
100        assert_eq!(available.len(), 1);
101        assert_eq!(available[0].name(), "dummy");
102        assert!(registry.find_by_name("dummy").is_some());
103        assert!(registry.find_by_name("missing").is_none());
104    }
105
106    #[test]
107    fn viewer_registry_hides_tmux_when_disabled() {
108        let registry = ViewerRegistry::with_tmux_enabled(
109            vec![Box::new(TmuxNvimViewer), Box::new(DummyViewer)],
110            false,
111        );
112
113        let available = registry.available_viewers();
114        assert_eq!(available.len(), 1);
115        assert_eq!(available[0].name(), "dummy");
116        assert!(!registry.is_enabled("tmux-nvim"));
117        assert!(registry.find_by_name("tmux-nvim").is_some());
118    }
119
120    #[test]
121    fn concrete_viewers_report_expected_names() {
122        assert_eq!(BatViewer.name(), "bat");
123        assert_eq!(GlowViewer.name(), "glow");
124        assert_eq!(HtmlViewer.name(), "html");
125        assert_eq!(TmuxNvimViewer.name(), "tmux-nvim");
126    }
127
128    #[test]
129    fn default_registry_includes_html_viewer() {
130        let registry = ViewerRegistry::for_proposals(true);
131        assert!(
132            registry.find_by_name("html").is_some(),
133            "html viewer should be registered in the default proposal registry"
134        );
135    }
136
137    #[cfg(unix)]
138    #[test]
139    fn run_with_stdin_closes_pipe_after_write() {
140        let (tx, rx) = mpsc::channel();
141
142        std::thread::spawn(move || {
143            let result = crate::viewer::util::run_with_stdin("sh", &["-c", "cat >/dev/null"], "hi");
144            tx.send(result).unwrap();
145        });
146
147        let result = rx
148            .recv_timeout(Duration::from_secs(2))
149            .expect("run_with_stdin should finish after writing EOF");
150        assert!(result.is_ok(), "{result:?}");
151    }
152
153    struct UnavailableViewer;
154
155    impl ViewerBackend for UnavailableViewer {
156        fn name(&self) -> &str {
157            "unavailable"
158        }
159
160        fn description(&self) -> &str {
161            "Unavailable viewer for tests"
162        }
163
164        fn is_available(&self) -> bool {
165            false
166        }
167
168        fn open(&self, _content: &str) -> CoreResult<()> {
169            Ok(())
170        }
171    }
172}