1use crate::{Result, config::{TerminalConfig, TerminalLayoutSettings}};
2use crossterm::{
3 cursor, event::{DisableMouseCapture, EnableMouseCapture}, execute, terminal::{ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}
4};
5use log::{debug, error, warn};
6use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend};
7use serde::Deserialize;
8use std::{io::{self, BufRead, Read, Write}, thread::sleep, time::Duration};
9
10#[allow(dead_code)]
11pub struct Tui<W>
12where
13W: Write,
14{
15 pub terminal: ratatui::Terminal<CrosstermBackend<W>>,
16 layout: Option<TerminalLayoutSettings>,
17 pub area: Rect,
18 pub sleep : u64
19 }
21
22impl<W> Tui<W>
25where
26W: Write,
27{
28 pub fn new_with_writer(writer: W, config: TerminalConfig) -> Result<Self> {
31 let mut backend = CrosstermBackend::new(writer);
32 let mut options = TerminalOptions::default();
33
34 let (width, height) = Self::full_size().unwrap_or_default();
35 let area = if let Some(ref layout) = config.layout {
36 let request = layout.percentage.get_max(height, layout.max).min(height);
37
38 let cursor_y= Self::get_cursor_y().unwrap_or_else(|e| {
39 warn!("Failed to read cursor: {e}");
40 height });
42 let initial_height = height
43 .saturating_sub(cursor_y);
44
45 let scroll = request.saturating_sub(initial_height);
46 debug!("TUI dimensions: {width}, {height}. Cursor: {cursor_y}.", );
47
48 let cursor_y = match Self::scroll_up(&mut backend, scroll) {
50 Ok(_) => {
51 cursor_y.saturating_sub(scroll) }
53 Err(_) => {
54 cursor_y
55 }
56 };
57 let available_height = height
58 .saturating_sub(cursor_y);
59
60 debug!("TUI quantities: min: {}, initial: {initial_height}, requested: {request}, available: {available_height}, requested scroll: {scroll}", layout.min);
61
62 if available_height < layout.min {
63 error!("Failed to allocate minimum height, falling back to fullscreen");
64 Rect::new(0, 0, width, height)
65 } else {
66 let area = Rect::new(
67 0,
68 cursor_y,
69 width,
70 available_height.min(request),
71 );
72
73 options.viewport = Viewport::Fixed(area.clone());
75
76 area
77 }
78 } else {
79 Rect::new(0, 0, width, height)
80 };
81
82 debug!("TUI area: {area}");
83
84 let terminal = Terminal::with_options(backend, options)?;
85 Ok(Self {
86 terminal,
87 layout: config.layout,
88 area,
89 sleep: if config.sleep == 0 { 100 } else { config.sleep as u64 }
90 })
91 }
92
93
94
95 pub fn enter(&mut self) -> Result<()> {
96 let fullscreen = self.is_fullscreen();
97 let backend = self.terminal.backend_mut();
98 enable_raw_mode()?;
99 execute!(backend, EnableMouseCapture)?;
100
101 if fullscreen {
102 self.enter_alternate()?;
103 }
104 Ok(())
105 }
106
107 pub fn enter_alternate(&mut self) -> Result<()> {
108 let backend = self.terminal.backend_mut();
109 execute!(backend, EnterAlternateScreen)?;
110 execute!(
111 backend,
112 crossterm::terminal::Clear(ClearType::All)
113 )?;
114 self.terminal.clear()?;
115 debug!("Entered alternate screen");
116 Ok(())
117 }
118
119 pub fn enter_execute(&mut self) {
120 self.exit();
121 sleep(Duration::from_millis(self.sleep)); debug!("state: {:?}", crossterm::terminal::is_raw_mode_enabled());
123
124 }
126
127 pub fn return_execute(&mut self) -> Result<()>
128 {
129 self.enter()?;
130 if !self.is_fullscreen() {
131 let _ = self.enter_alternate();
133 }
134
135 sleep(Duration::from_millis(self.sleep));
136
137 if let Err(e) = execute!(
138 self.terminal.backend_mut(),
139 crossterm::terminal::Clear(ClearType::All)
140 ) {
141 warn!("Failed to leave alternate screen: {:?}", e);
142 }
143
144 if self.is_fullscreen() {
145 if let Some((width, height)) = Self::full_size() {
146 self.terminal.resize(Rect::new(0, 0, width, height))?;
147 } else {
148 error!("Failed to get terminal size")
149 }
150 } else {
151 self.terminal.resize(self.area)?;
152 }
153
154 Ok(())
155 }
156
157 pub fn exit(&mut self) {
158 let backend = self.terminal.backend_mut();
159
160 if let Err(e) = execute!(backend, cursor::MoveTo(0, self.area.y)) {
162 warn!("Failed to move cursor: {:?}", e);
163 }
164 if let Err(e) = execute!(
173 backend,
174 crossterm::terminal::Clear(ClearType::FromCursorDown)
175 ) {
176 warn!("Failed to clear screen: {:?}", e);
177 }
178
179 if let Err(e) = execute!(backend, LeaveAlternateScreen, DisableMouseCapture) {
180 warn!("Failed to leave alternate screen: {:?}", e);
181 }
182
183 if let Err(e) = self.terminal.show_cursor() {
184 warn!("Failed to show cursor: {:?}", e);
185 }
186
187 if let Err(e) = disable_raw_mode() {
188 warn!("Failed to disable raw mode: {:?}", e);
189 }
190
191 debug!("Terminal exited");
192 }
193
194 pub fn get_cursor_y() -> io::Result<u16> {
196 crossterm::cursor::position().map(|x| x.1)
197 }
198
199 pub fn get_cursor() -> io::Result<(u16, u16)> {
200 crossterm::cursor::position()
201 }
202
203 pub fn scroll_up(backend: &mut CrosstermBackend<W>, lines: u16) -> io::Result<u16> {
204 execute!(backend, crossterm::terminal::ScrollUp(lines))?;
205 Self::get_cursor_y() }
207 pub fn size() -> io::Result<(u16, u16)> {
208 crossterm::terminal::size()
209 }
210 pub fn full_size() -> Option<(u16, u16)> {
211 if let Ok((width, height)) = Self::size() {
212 Some((width, height))
213 } else {
214 error!("Failed to read terminal size");
215 None
216 }
217 }
218 pub fn is_fullscreen(&self) -> bool {
219 self.layout.is_none()
220 }
221 pub fn set_fullscreen(&mut self) {
222 self.layout = None;
223 }
224 pub fn layout(&self) -> &Option<TerminalLayoutSettings> {
225 &self.layout
226 }
227}
228
229impl Tui<Box<dyn Write + Send>> {
230 pub fn new(config: TerminalConfig) -> Result<Self> {
231 let writer = config.stream.to_stream();
232 let tui = Self::new_with_writer(writer, config)?;
233 Ok(tui)
234 }
235}
236
237impl<W> Drop for Tui<W>
238where
239W: Write,
240{
241 fn drop(&mut self) {
242 self.exit();
243 }
244}
245
246#[derive(Debug, Clone, Deserialize, Default)]
249pub enum IoStream {
250 Stdout,
251 #[default]
252 BufferedStderr,
253}
254
255impl IoStream {
256 pub fn to_stream(&self) -> Box<dyn std::io::Write + Send> {
257 match self {
258 IoStream::Stdout => Box::new(io::stdout()),
259 IoStream::BufferedStderr => Box::new(io::LineWriter::new(io::stderr())),
260 }
261 }
262}
263
264pub fn stdin_reader() -> Option<io::Stdin> {
267 if atty::is(atty::Stream::Stdin) {
268 error!("stdin is a TTY: picker requires piped input.");
269 return None
270 }
271
272 Some(io::stdin())
273}
274
275pub fn read_to_chunks<R: Read>(reader: R, delim: char) -> std::io::Split<std::io::BufReader<R>> {
276 io::BufReader::new(reader).split(delim as u8)
277}
278
279pub fn map_chunks<const INVALID_FAIL: bool>(iter: impl Iterator<Item = std::io::Result<Vec<u8>>>, mut f: impl FnMut(String) -> Result<()>)
282{
283 for (i, chunk_result) in iter.enumerate() {
284 if i == u32::MAX as usize {
285 warn!("Reached maximum segment limit, stopping input read");
286 break;
287 }
288
289 let chunk = match chunk_result {
290 Ok(bytes) => bytes,
291 Err(e) => {
292 error!("Error reading from stdin: {e}");
293 break;
294 }
295 };
296
297 match String::from_utf8(chunk) {
298 Ok(s) => {
299 debug!("Read: {s}");
300 if f(s).is_err() {
301 break;
302 }
303 }
304 Err(e) => {
305 error!("Invalid UTF-8 in stdin at byte {}: {}", e.utf8_error().valid_up_to(), e);
306 if INVALID_FAIL {
308 break
309 } else {
310 continue
311 }
312 }
313 }
314 }
315}
316
317pub fn map_reader_lines<const INVALID_FAIL: bool>(reader: impl Read, mut f: impl FnMut(String) -> Result<()>) {
318 let buf_reader = io::BufReader::new(reader);
319
320 for (i, line) in buf_reader.lines().enumerate() {
321 if i == u32::MAX as usize {
322 eprintln!("Reached maximum line limit, stopping input read");
323 break;
324 }
325 match line {
326 Ok(l) => {
327 if f(l).is_err() {
328 break;
329 }
330 }
331 Err(e) => {
332 eprintln!("Error reading line: {}", e);
333 if INVALID_FAIL {
334 break
335 } else {
336 continue
337 }
338 }
339 }
340 }
341}