1use crate::errors::CoreResult;
4
5pub mod collector;
7
8pub mod bat;
10
11pub mod glow;
13
14pub mod html;
16
17pub mod registry;
19
20pub(crate) mod util;
22
23pub 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
33pub trait ViewerBackend {
35 fn name(&self) -> &str;
37
38 fn description(&self) -> &str;
40
41 fn is_available(&self) -> bool;
43
44 fn availability_hint(&self) -> Option<String> {
48 None
49 }
50
51 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}