yarli_cli/dashboard/
copy_mode.rs1use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
7use crossterm::ExecutableCommand;
8use std::io::{self, Write};
9
10use ratatui::style::{Color, Modifier, Style};
11use ratatui::text::{Line, Span};
12
13pub struct CopyMode {
15 active: bool,
17 mouse_was_enabled: bool,
19}
20
21impl CopyMode {
22 pub fn new() -> Self {
24 Self {
25 active: false,
26 mouse_was_enabled: false,
27 }
28 }
29
30 pub fn is_active(&self) -> bool {
32 self.active
33 }
34
35 pub fn toggle<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
37 if self.active {
38 self.deactivate(writer)
39 } else {
40 self.activate(writer)
41 }
42 }
43
44 pub fn activate<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
46 if !self.active {
47 self.mouse_was_enabled = true;
48 writer.execute(DisableMouseCapture)?;
49 self.active = true;
50 }
51 Ok(())
52 }
53
54 pub fn deactivate<W: Write>(&mut self, writer: &mut W) -> io::Result<()> {
56 if self.active {
57 if self.mouse_was_enabled {
58 writer.execute(EnableMouseCapture)?;
59 }
60 self.active = false;
61 }
62 Ok(())
63 }
64
65 pub fn status_indicator(&self) -> Option<Span<'static>> {
67 if self.active {
68 Some(Span::styled(
69 "[COPY] ",
70 Style::default()
71 .fg(Color::Yellow)
72 .add_modifier(Modifier::BOLD),
73 ))
74 } else {
75 None
76 }
77 }
78
79 pub fn strip_borders(&self) -> bool {
81 self.active
82 }
83
84 pub fn banner_line(&self) -> Option<Line<'static>> {
86 if self.active {
87 Some(Line::from(vec![
88 Span::styled(
89 " COPY MODE ",
90 Style::default()
91 .fg(Color::Black)
92 .bg(Color::Yellow)
93 .add_modifier(Modifier::BOLD),
94 ),
95 Span::styled(
96 " Select text with your terminal. Press c to exit.",
97 Style::default().fg(Color::Yellow),
98 ),
99 ]))
100 } else {
101 None
102 }
103 }
104}
105
106impl Default for CopyMode {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn new_is_inactive() {
118 let cm = CopyMode::new();
119 assert!(!cm.is_active());
120 assert!(!cm.strip_borders());
121 }
122
123 #[test]
124 fn status_indicator_when_inactive() {
125 let cm = CopyMode::new();
126 assert!(cm.status_indicator().is_none());
127 }
128
129 #[test]
130 fn status_indicator_when_active() {
131 let mut cm = CopyMode::new();
132 cm.active = true;
133 let span = cm.status_indicator().unwrap();
134 assert!(span.content.contains("COPY"));
135 }
136
137 #[test]
138 fn strip_borders_follows_active() {
139 let mut cm = CopyMode::new();
140 assert!(!cm.strip_borders());
141 cm.active = true;
142 assert!(cm.strip_borders());
143 }
144
145 #[test]
146 fn banner_when_inactive() {
147 let cm = CopyMode::new();
148 assert!(cm.banner_line().is_none());
149 }
150
151 #[test]
152 fn banner_when_active() {
153 let mut cm = CopyMode::new();
154 cm.active = true;
155 let line = cm.banner_line().unwrap();
156 let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
157 assert!(text.contains("COPY MODE"));
158 assert!(text.contains("Press c to exit"));
159 }
160
161 #[test]
162 fn default_is_inactive() {
163 let cm = CopyMode::default();
164 assert!(!cm.is_active());
165 }
166
167 #[test]
168 fn activate_sets_active() {
169 let mut cm = CopyMode::new();
170 cm.active = true;
172 cm.mouse_was_enabled = true;
173 assert!(cm.is_active());
174 assert!(cm.strip_borders());
175 }
176
177 #[test]
178 fn deactivate_clears_active() {
179 let mut cm = CopyMode::new();
180 cm.active = true;
181 cm.mouse_was_enabled = true;
182 cm.active = false;
184 assert!(!cm.is_active());
185 assert!(!cm.strip_borders());
186 }
187}