andiskaz/
screen.rs

1//! This module defines screen related utilities.
2
3mod buffer;
4
5use crate::{
6    color::{self, Color, Color2},
7    coord,
8    coord::{Coord, Vec2},
9    error::Error,
10    screen::buffer::ScreenBuffer,
11    stdio,
12    stdio::{restore_screen, save_screen, LockedStdout, Stdout},
13    string::{TermGrapheme, TermString},
14    style::Style,
15    terminal::Shared,
16    tile::{self, Tile},
17};
18use std::{
19    fmt::Write,
20    sync::atomic::{AtomicBool, Ordering::*},
21    time::Duration,
22};
23use tokio::{
24    io,
25    sync::{Mutex, MutexGuard, Notify},
26    task,
27    time,
28};
29
30/// Shared memory between terminal handle copies.
31#[derive(Debug)]
32pub(crate) struct ScreenData {
33    /// Minimum screen size.
34    min_size: Vec2,
35    /// Frame interval time.
36    frame_time: Duration,
37    /// Whether the terminal handle has been cleaned up (using
38    /// terminal.cleanup).
39    cleanedup: AtomicBool,
40    /// A lock to the standard output.
41    stdout: Stdout,
42    /// Buffer responsible for rendering the screen.
43    buffer: Mutex<ScreenBuffer>,
44    /// Notification handle of the screen.
45    notifier: Notify,
46}
47
48impl ScreenData {
49    /// Creates screen data from the given settings. If given actual size is
50    /// less than given minimum allowed size, the actual size is replaced by the
51    /// minimum size.
52    pub fn new(size: Vec2, min_size: Vec2, frame_time: Duration) -> Self {
53        let corrected_size = if size.x >= min_size.x && size.y >= min_size.y {
54            size
55        } else {
56            min_size
57        };
58        Self {
59            min_size,
60            frame_time,
61            cleanedup: AtomicBool::new(false),
62            stdout: Stdout::new(),
63            buffer: Mutex::new(ScreenBuffer::blank(corrected_size)),
64            notifier: Notify::new(),
65        }
66    }
67
68    /// Notifies all parties subscribed to the screen updates.
69    pub fn notify(&self) {
70        self.notifier.notify_waiters()
71    }
72
73    /// Subscribes to changes in the screen data such as disconnection.
74    async fn subscribe(&self) {
75        self.notifier.notified().await
76    }
77
78    /// Locks the screen data into an actual screen handle.
79    pub async fn lock<'this>(&'this self) -> Screen<'this> {
80        Screen::new(self).await
81    }
82
83    /// Initialization of the terminal, such as cleaning the screen.
84    pub async fn setup(&self) -> Result<(), Error> {
85        let mut buf = String::new();
86        save_screen(&mut buf)?;
87        write!(
88            buf,
89            "{}{}{}{}",
90            crossterm::style::SetBackgroundColor(
91                crossterm::style::Color::Black
92            ),
93            crossterm::style::SetForegroundColor(
94                crossterm::style::Color::White
95            ),
96            crossterm::cursor::Hide,
97            crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
98        )?;
99        self.stdout.write_and_flush(buf.as_bytes()).await?;
100        Ok(())
101    }
102
103    /// Asynchronous cleanup. It is preferred to call this before dropping.
104    pub async fn cleanup(&self) -> Result<(), Error> {
105        task::block_in_place(|| crossterm::terminal::disable_raw_mode())
106            .map_err(Error::from_crossterm)?;
107        let mut buf = String::new();
108        write!(buf, "{}", crossterm::cursor::Show)?;
109        restore_screen(&mut buf)?;
110        self.stdout.write_and_flush(buf.as_bytes()).await?;
111        self.cleanedup.store(true, Release);
112        Ok(())
113    }
114}
115
116impl Drop for ScreenData {
117    fn drop(&mut self) {
118        if !self.cleanedup.load(Relaxed) {
119            let _ = crossterm::terminal::disable_raw_mode();
120            let mut buf = String::new();
121            write!(buf, "{}", crossterm::cursor::Show)
122                .ok()
123                .and_then(|_| stdio::restore_screen(&mut buf).ok())
124                .map(|_| println!("{}", buf));
125        }
126    }
127}
128
129/// Panics given that a point in the screen is out of bounds. This is here so
130/// that the compiler can make other functions smaller.
131#[cold]
132#[inline(never)]
133fn out_of_bounds(point: Vec2, size: Vec2) -> ! {
134    panic!(
135        "Point x: {}, y: {} out of screen size x: {}, y: {}",
136        point.x, point.y, size.x, size.y
137    )
138}
139
140/// A locked screen terminal with exclusive access to it. With this struct, a
141/// locked screen handle, one can execute many operations without locking and
142/// unlocking.
143#[derive(Debug)]
144pub struct Screen<'terminal> {
145    /// Reference to the original screen.
146    data: &'terminal ScreenData,
147    /// Locked guard to the buffer.
148    buffer: MutexGuard<'terminal, ScreenBuffer>,
149}
150
151impl<'terminal> Screen<'terminal> {
152    /// Creates a locked screen from a reference to the unlocked screen handle,
153    /// and a locked guard to the buffer.
154    pub(crate) async fn new<'param>(
155        data: &'param ScreenData,
156    ) -> Screen<'terminal>
157    where
158        'param: 'terminal,
159    {
160        Self { data, buffer: data.buffer.lock().await }
161    }
162
163    /// Returns the current size of the screen.
164    pub fn size(&self) -> Vec2 {
165        self.buffer.size()
166    }
167
168    /// Returns whether the stored size is the actual size and the actual size
169    /// is valid.
170    pub fn valid_size(&self) -> bool {
171        self.buffer.valid
172    }
173
174    /// Returns the minimum size required for the screen.
175    pub fn min_size(&self) -> Vec2 {
176        self.data.min_size
177    }
178
179    /// Applies an update function to a [`Tile`]. An update function gets access
180    /// to a mutable reference of a [`Tile`], updates it, and then the screen
181    /// handles any changes made to it. A regular [`Tile`] can be used as an
182    /// updater, in which the case a simple replacement is made. This operation
183    /// is buffered.
184    pub fn set<T>(&mut self, point: Vec2, updater: T)
185    where
186        T: tile::Updater,
187    {
188        let index = self
189            .buffer
190            .make_index(point)
191            .unwrap_or_else(|| out_of_bounds(point, self.buffer.size()));
192        updater.update(&mut self.buffer.curr[index]);
193        if self.buffer.old[index] != self.buffer.curr[index] {
194            self.buffer.changed.insert(point);
195        } else {
196            self.buffer.changed.remove(&point);
197        }
198    }
199
200    /// Gets the attributes of a given [`Tile`], regardless of being flushed to
201    /// the screen yet or not.
202    pub fn get(&self, point: Vec2) -> &Tile {
203        let index = self
204            .buffer
205            .make_index(point)
206            .unwrap_or_else(|| out_of_bounds(point, self.buffer.size()));
207        &self.buffer.curr[index]
208    }
209
210    /// Sets every [`Tile`] into a whitespace grapheme with the given color.
211    pub fn clear(&mut self, background: Color) {
212        let size = self.buffer.size();
213        let tile = Tile {
214            colors: Color2 { background, ..Color2::default() },
215            grapheme: TermGrapheme::space(),
216        };
217
218        for y in 0 .. size.y {
219            for x in 0 .. size.x {
220                self.set(Vec2 { x, y }, tile.clone());
221            }
222        }
223    }
224
225    /// Prints a grapheme-encoded text (a [`TermString`]) using some style
226    /// options like ratio to the screen, color, margin and others. See
227    /// [`Style`].
228    pub fn styled_text<C>(
229        &mut self,
230        tstring: &TermString,
231        style: Style<C>,
232    ) -> Coord
233    where
234        C: color::Updater,
235    {
236        let mut len = tstring.count_graphemes();
237        let mut slice = tstring.index(..);
238        let screen_size = self.buffer.size();
239        let size = style.make_size(screen_size);
240
241        let mut cursor = Vec2 { x: 0, y: style.top_margin };
242        let mut is_inside = cursor.y - style.top_margin < size.y;
243
244        while len > 0 && is_inside {
245            is_inside = cursor.y - style.top_margin + 1 < size.y;
246            let width = coord::to_index(size.x);
247            let pos = self.find_break_pos(width, len, size, &slice, is_inside);
248
249            cursor.x = size.x - coord::from_index(pos);
250            cursor.x = cursor.x + style.left_margin - style.right_margin;
251            cursor.x = cursor.x * style.align_numer / style.align_denom;
252
253            slice = slice.index(.. pos);
254
255            self.write_styled_slice(&slice, &style, &mut cursor);
256
257            if pos != len && !is_inside {
258                self.set(cursor, |tile: &mut Tile| {
259                    let grapheme = TermGrapheme::new_lossy("…");
260                    let colors = style.colors.update(tile.colors);
261                    *tile = Tile { grapheme, colors };
262                });
263            }
264
265            cursor.y += 1;
266            len -= pos;
267        }
268        cursor.y
269    }
270
271    /// Finds the position where a line should break in a styled text.
272    fn find_break_pos(
273        &self,
274        width: usize,
275        total_graphemes: usize,
276        term_size: Vec2,
277        slice: &TermString,
278        is_inside: bool,
279    ) -> usize {
280        if width <= slice.len() {
281            let mut pos = slice
282                .index(.. coord::to_index(term_size.x))
283                .iter()
284                .rev()
285                .position(|grapheme| grapheme == TermGrapheme::space())
286                .map_or(width, |rev| total_graphemes - rev);
287            if !is_inside {
288                pos -= 1;
289            }
290            pos
291        } else {
292            total_graphemes
293        }
294    }
295
296    /// Writes a slice using the given style. It should fit in one line.
297    fn write_styled_slice<C>(
298        &mut self,
299        slice: &TermString,
300        style: &Style<C>,
301        cursor: &mut Vec2,
302    ) where
303        C: color::Updater,
304    {
305        for grapheme in slice {
306            self.set(*cursor, |tile: &mut Tile| {
307                tile.grapheme = grapheme;
308                tile.colors = style.colors.update(tile.colors);
309            });
310            cursor.x += 1;
311        }
312    }
313
314    /// Checks if the new size is valid. If valid, then it resizes the screen,
315    /// and sets the `guard` to `None`. Otherwise, stdout is locked, and the
316    /// locked stdout is put into `guard`.
317    pub(crate) async fn check_resize(
318        &mut self,
319        new_size: Vec2,
320        guard: &mut Option<LockedStdout<'terminal>>,
321    ) -> io::Result<()> {
322        let min_size = self.data.min_size;
323        if new_size.x < min_size.x || new_size.y < min_size.y {
324            if guard.is_none() {
325                self.buffer.valid = false;
326                let mut stdout = self.data.stdout.lock().await;
327                self.ask_resize(&mut stdout, min_size).await?;
328                *guard = Some(stdout);
329            }
330        } else {
331            let mut stdout = match guard.take() {
332                Some(stdout) => stdout,
333                None => self.data.stdout.lock().await,
334            };
335            self.buffer.valid = true;
336            self.resize(new_size, &mut stdout).await?;
337        }
338
339        Ok(())
340    }
341
342    /// Asks the user to resize the screen (manually).
343    async fn ask_resize(
344        &mut self,
345        stdout: &mut LockedStdout<'terminal>,
346        min_size: Vec2,
347    ) -> io::Result<()> {
348        let buf = format!(
349            "{}{}RESIZE {}x{}",
350            crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
351            crossterm::cursor::MoveTo(0, 0),
352            min_size.x,
353            min_size.y,
354        );
355
356        stdout.write_and_flush(buf.as_bytes()).await?;
357
358        Ok(())
359    }
360
361    /// Triggers the resize of the screen.
362    async fn resize(
363        &mut self,
364        new_size: Vec2,
365        stdout: &mut LockedStdout<'terminal>,
366    ) -> io::Result<()> {
367        let buf = format!(
368            "{}{}{}",
369            crossterm::style::SetForegroundColor(
370                crossterm::style::Color::White
371            ),
372            crossterm::style::SetBackgroundColor(
373                crossterm::style::Color::Black
374            ),
375            crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
376        );
377        stdout.write_and_flush(buf.as_bytes()).await?;
378        self.buffer.resize(new_size);
379
380        Ok(())
381    }
382
383    /// Renders the buffer into the screen using the referred terminal.
384    pub(crate) async fn render(
385        &mut self,
386        buf: &mut String,
387    ) -> Result<(), Error> {
388        let screen_size = self.buffer.size();
389        buf.clear();
390
391        let mut colors = Color2::default();
392        let mut cursor = Vec2 { x: 0, y: 0 };
393        self.render_init_term(buf, colors, cursor)?;
394
395        for &coord in self.buffer.changed.iter() {
396            self.render_tile(
397                buf,
398                &mut colors,
399                &mut cursor,
400                screen_size,
401                coord,
402            )?;
403        }
404
405        if let Some(mut stdout) = self.data.stdout.try_lock() {
406            stdout.write_and_flush(buf.as_bytes()).await?;
407        }
408
409        self.buffer.next_tick();
410
411        Ok(())
412    }
413
414    /// Initializes terminal state into the buffer before rendering.
415    fn render_init_term(
416        &self,
417        buf: &mut String,
418        colors: Color2,
419        cursor: Vec2,
420    ) -> Result<(), Error> {
421        write!(
422            buf,
423            "{}{}{}",
424            crossterm::style::SetForegroundColor(
425                colors.foreground.to_crossterm()
426            ),
427            crossterm::style::SetBackgroundColor(
428                colors.background.to_crossterm()
429            ),
430            crossterm::cursor::MoveTo(
431                coord::to_crossterm(cursor.x),
432                coord::to_crossterm(cursor.y)
433            ),
434        )?;
435
436        Ok(())
437    }
438
439    /// Renders a single tile in the given coordinate.
440    fn render_tile(
441        &self,
442        buf: &mut String,
443        colors: &mut Color2,
444        cursor: &mut Vec2,
445        screen_size: Vec2,
446        coord: Vec2,
447    ) -> Result<(), Error> {
448        if *cursor != coord {
449            write!(
450                buf,
451                "{}",
452                crossterm::cursor::MoveTo(
453                    coord::to_crossterm(coord.x),
454                    coord::to_crossterm(coord.y)
455                )
456            )?;
457        }
458        *cursor = coord;
459
460        let tile = self.get(*cursor);
461        if colors.background != tile.colors.background {
462            let color = tile.colors.background.to_crossterm();
463            write!(buf, "{}", crossterm::style::SetBackgroundColor(color))?;
464        }
465        if colors.foreground != tile.colors.foreground {
466            let color = tile.colors.foreground.to_crossterm();
467            write!(buf, "{}", crossterm::style::SetForegroundColor(color))?;
468        }
469        *colors = tile.colors;
470
471        write!(buf, "{}", tile.grapheme)?;
472
473        if cursor.x <= screen_size.x {
474            cursor.x += 1;
475        }
476
477        Ok(())
478    }
479}
480
481/// The renderer loop. Should be called only when setting up a terminal handler.
482/// Exits on error or when notified that it should exit.
483pub(crate) async fn renderer(shared: &Shared) -> Result<(), Error> {
484    let mut interval = time::interval(shared.screen().frame_time);
485    let mut buf = String::new();
486
487    loop {
488        {
489            let _guard = shared.service_guard().await?;
490            let mut screen = shared.screen().lock().await;
491            screen.render(&mut buf).await?;
492        }
493
494        tokio::select! {
495            _ = interval.tick() => (),
496            _ = shared.screen().subscribe() => break,
497        };
498    }
499
500    Ok(())
501}