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, Serialize};
8use std::{io::{self, Write}, thread::sleep, time::Duration};
9
10pub struct Tui<W>
11where
12W: Write,
13{
14 pub terminal: ratatui::Terminal<CrosstermBackend<W>>,
15 layout: Option<TerminalLayoutSettings>,
16 pub area: Rect,
17 pub sleep : u64
18}
19
20impl<W> Tui<W>
21where
22W: Write,
23{
24 pub fn new_with_writer(writer: W, config: TerminalConfig) -> Result<Self> {
27 let mut backend = CrosstermBackend::new(writer);
28 let mut options = TerminalOptions::default();
29
30 let (width, height) = Self::full_size().unwrap_or_default();
31 let area = if let Some(ref layout) = config.layout {
32 let request = layout.percentage.get_max(height, layout.max).min(height);
33
34 let cursor_y= Self::get_cursor_y().unwrap_or_else(|e| {
35 warn!("Failed to read cursor: {e}");
36 height });
38 let initial_height = height
39 .saturating_sub(cursor_y);
40
41 let scroll = request.saturating_sub(initial_height);
42 debug!("TUI dimensions: {width}, {height}. Cursor: {cursor_y}.", );
43
44 let cursor_y = match Self::scroll_up(&mut backend, scroll) {
46 Ok(_) => {
47 cursor_y.saturating_sub(scroll) }
49 Err(_) => {
50 cursor_y
51 }
52 };
53 let available_height = height
54 .saturating_sub(cursor_y);
55
56 debug!("TUI quantities: min: {}, initial: {initial_height}, requested: {request}, available: {available_height}, requested scroll: {scroll}", layout.min);
57
58 if available_height < layout.min {
59 error!("Failed to allocate minimum height, falling back to fullscreen");
60 Rect::new(0, 0, width, height)
61 } else {
62 let area = Rect::new(
63 0,
64 cursor_y,
65 width,
66 available_height.min(request),
67 );
68
69 options.viewport = Viewport::Fixed(area);
71
72 area
73 }
74 } else {
75 Rect::new(0, 0, width, height)
76 };
77
78 debug!("TUI area: {area}");
79
80 let terminal = Terminal::with_options(backend, options)?;
81 Ok(Self {
82 terminal,
83 layout: config.layout,
84 area,
85 sleep: if config.sleep == 0 { 100 } else { config.sleep as u64 }
86 })
87 }
88
89
90
91 pub fn enter(&mut self) -> Result<()> {
92 let fullscreen = self.is_fullscreen();
93 let backend = self.terminal.backend_mut();
94 enable_raw_mode()?;
95 execute!(backend, EnableMouseCapture)?;
96
97 if fullscreen {
98 self.enter_alternate()?;
99 }
100 Ok(())
101 }
102
103 pub fn enter_alternate(&mut self) -> Result<()> {
104 let backend = self.terminal.backend_mut();
105 execute!(backend, EnterAlternateScreen)?;
106 execute!(
107 backend,
108 crossterm::terminal::Clear(ClearType::All)
109 )?;
110 self.terminal.clear()?;
111 debug!("Entered alternate screen");
112 Ok(())
113 }
114
115 pub fn enter_execute(&mut self) {
116 self.exit();
117 sleep(Duration::from_millis(self.sleep)); debug!("state: {:?}", crossterm::terminal::is_raw_mode_enabled());
119
120 }
122
123 pub fn resize(&mut self, area: Rect) {
124 if let Err(e) = self.terminal.resize(area) {
125 error!("Failed to resize TUI to {area}: {e}")
126 }
127 self.area = area
128 }
129
130 pub fn redraw(&mut self) {
131 if let Err(e) = self.terminal.resize(self.area) {
132 error!("Failed to resize TUI to {}: {e}", self.area)
133 }
134 }
135
136 pub fn return_execute(&mut self) -> Result<()>
137 {
138 self.enter()?;
139 if !self.is_fullscreen() {
140 let _ = self.enter_alternate();
142 }
143
144 sleep(Duration::from_millis(self.sleep));
145
146 if let Err(e) = execute!(
147 self.terminal.backend_mut(),
148 crossterm::terminal::Clear(ClearType::All)
149 ) {
150 warn!("Failed to leave alternate screen: {:?}", e);
151 }
152
153 if self.is_fullscreen() {
154 if let Some((width, height)) = Self::full_size() {
155 self.resize(Rect::new(0, 0, width, height));
156 } else {
157 error!("Failed to get terminal size")
158 }
159 } else {
160 self.resize(self.area);
161 }
162
163 Ok(())
164 }
165
166 pub fn exit(&mut self) {
167 let backend = self.terminal.backend_mut();
168
169 if let Err(e) = execute!(backend, cursor::MoveTo(0, self.area.y)) {
171 warn!("Failed to move cursor: {:?}", e);
172 }
173 if let Err(e) = execute!(
182 backend,
183 crossterm::terminal::Clear(ClearType::FromCursorDown)
184 ) {
185 warn!("Failed to clear screen: {:?}", e);
186 }
187
188 if let Err(e) = execute!(backend, LeaveAlternateScreen, DisableMouseCapture) {
189 warn!("Failed to leave alternate screen: {:?}", e);
190 }
191
192 if let Err(e) = self.terminal.show_cursor() {
193 warn!("Failed to show cursor: {:?}", e);
194 }
195
196 if let Err(e) = disable_raw_mode() {
197 warn!("Failed to disable raw mode: {:?}", e);
198 }
199
200 debug!("Terminal exited");
201 }
202
203 pub fn get_cursor_y() -> io::Result<u16> {
205 crossterm::cursor::position().map(|x| x.1)
206 }
207
208 pub fn get_cursor() -> io::Result<(u16, u16)> {
209 crossterm::cursor::position()
210 }
211
212 pub fn scroll_up(backend: &mut CrosstermBackend<W>, lines: u16) -> io::Result<u16> {
213 execute!(backend, crossterm::terminal::ScrollUp(lines))?;
214 Self::get_cursor_y() }
216 pub fn size() -> io::Result<(u16, u16)> {
217 crossterm::terminal::size()
218 }
219 pub fn full_size() -> Option<(u16, u16)> {
220 if let Ok((width, height)) = Self::size() {
221 Some((width, height))
222 } else {
223 error!("Failed to read terminal size");
224 None
225 }
226 }
227 pub fn is_fullscreen(&self) -> bool {
228 self.layout.is_none()
229 }
230 pub fn set_fullscreen(&mut self) {
231 self.layout = None;
232 }
233 pub fn layout(&self) -> &Option<TerminalLayoutSettings> {
234 &self.layout
235 }
236}
237
238impl Tui<Box<dyn Write + Send>> {
239 pub fn new(config: TerminalConfig) -> Result<Self> {
240 let writer = config.stream.to_stream();
241 let tui = Self::new_with_writer(writer, config)?;
242 Ok(tui)
243 }
244}
245
246impl<W> Drop for Tui<W>
247where
248W: Write,
249{
250 fn drop(&mut self) {
251 self.exit();
252 }
253}
254
255#[derive(Debug, Clone, Deserialize, Default, Serialize, PartialEq)]
258pub enum IoStream {
259 Stdout,
260 #[default]
261 BufferedStderr,
262}
263
264impl IoStream {
265 pub fn to_stream(&self) -> Box<dyn std::io::Write + Send> {
266 match self {
267 IoStream::Stdout => Box::new(io::stdout()),
268 IoStream::BufferedStderr => Box::new(io::LineWriter::new(io::stderr())),
269 }
270 }
271}