mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
8061813f32
Consensus is that explicit imports make it easier to understand the example code. This commit removes the prelude import from all examples and replaces it with the necessary imports, and expands other glob imports (widget::*, Constraint::*, KeyCode::*, etc.) everywhere else. Prelude glob imports not in examples are not covered by this PR. See https://github.com/ratatui-org/ratatui/issues/1150 for more details.
652 lines
20 KiB
Rust
652 lines
20 KiB
Rust
//! # [Ratatui] Constraint explorer example
|
|
//!
|
|
//! The latest version of this example is available in the [examples] folder in the repository.
|
|
//!
|
|
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
|
//! repository. This means that you may not be able to compile with the latest release version on
|
|
//! crates.io, or the one that you have installed locally.
|
|
//!
|
|
//! See the [examples readme] for more information on finding examples that match the version of the
|
|
//! library you are using.
|
|
//!
|
|
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
|
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
|
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
|
|
|
use std::io::{self, stdout};
|
|
|
|
use color_eyre::{config::HookBuilder, Result};
|
|
use itertools::Itertools;
|
|
use ratatui::{
|
|
backend::{Backend, CrosstermBackend},
|
|
buffer::Buffer,
|
|
crossterm::{
|
|
event::{self, Event, KeyCode, KeyEventKind},
|
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
ExecutableCommand,
|
|
},
|
|
layout::{
|
|
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
|
Flex, Layout, Rect,
|
|
},
|
|
style::{
|
|
palette::tailwind::{BLUE, SKY, SLATE, STONE},
|
|
Color, Style, Stylize,
|
|
},
|
|
symbols::{self, line},
|
|
terminal::Terminal,
|
|
text::{Line, Span, Text},
|
|
widgets::{Block, Paragraph, Widget, Wrap},
|
|
};
|
|
use strum::{Display, EnumIter, FromRepr};
|
|
|
|
#[derive(Default)]
|
|
struct App {
|
|
mode: AppMode,
|
|
spacing: u16,
|
|
constraints: Vec<Constraint>,
|
|
selected_index: usize,
|
|
value: u16,
|
|
}
|
|
|
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
enum AppMode {
|
|
#[default]
|
|
Running,
|
|
Quit,
|
|
}
|
|
|
|
/// A variant of [`Constraint`] that can be rendered as a tab.
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
|
|
enum ConstraintName {
|
|
#[default]
|
|
Length,
|
|
Percentage,
|
|
Ratio,
|
|
Min,
|
|
Max,
|
|
Fill,
|
|
}
|
|
|
|
/// A widget that renders a [`Constraint`] as a block. E.g.:
|
|
/// ```plain
|
|
/// ┌──────────────┐
|
|
/// │ Length(16) │
|
|
/// │ 16px │
|
|
/// └──────────────┘
|
|
/// ```
|
|
struct ConstraintBlock {
|
|
constraint: Constraint,
|
|
legend: bool,
|
|
selected: bool,
|
|
}
|
|
|
|
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
|
|
///
|
|
/// ```plain
|
|
/// ┌ ┐
|
|
/// 8 px
|
|
/// └ ┘
|
|
/// ```
|
|
struct SpacerBlock;
|
|
|
|
fn main() -> Result<()> {
|
|
init_error_hooks()?;
|
|
let terminal = init_terminal()?;
|
|
App::default().run(terminal)?;
|
|
restore_terminal()?;
|
|
Ok(())
|
|
}
|
|
|
|
// App behaviour
|
|
impl App {
|
|
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
|
self.insert_test_defaults();
|
|
|
|
while self.is_running() {
|
|
self.draw(&mut terminal)?;
|
|
self.handle_events()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// TODO remove these - these are just for testing
|
|
fn insert_test_defaults(&mut self) {
|
|
self.constraints = vec![
|
|
Constraint::Length(20),
|
|
Constraint::Length(20),
|
|
Constraint::Length(20),
|
|
];
|
|
self.value = 20;
|
|
}
|
|
|
|
fn is_running(&self) -> bool {
|
|
self.mode == AppMode::Running
|
|
}
|
|
|
|
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
|
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_events(&mut self) -> Result<()> {
|
|
match event::read()? {
|
|
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
|
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
|
|
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
|
|
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
|
|
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
|
|
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
|
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
|
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
|
|
KeyCode::Char('+') => self.increment_spacing(),
|
|
KeyCode::Char('-') => self.decrement_spacing(),
|
|
KeyCode::Char('x') => self.delete_block(),
|
|
KeyCode::Char('a') => self.insert_block(),
|
|
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
|
|
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
|
|
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
|
|
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
|
|
_ => {}
|
|
},
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn increment_value(&mut self) {
|
|
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
|
return;
|
|
};
|
|
match constraint {
|
|
Constraint::Length(v)
|
|
| Constraint::Min(v)
|
|
| Constraint::Max(v)
|
|
| Constraint::Fill(v)
|
|
| Constraint::Percentage(v) => *v = v.saturating_add(1),
|
|
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
|
|
};
|
|
}
|
|
|
|
fn decrement_value(&mut self) {
|
|
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
|
return;
|
|
};
|
|
match constraint {
|
|
Constraint::Length(v)
|
|
| Constraint::Min(v)
|
|
| Constraint::Max(v)
|
|
| Constraint::Fill(v)
|
|
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
|
|
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
|
|
};
|
|
}
|
|
|
|
/// select the next block with wrap around
|
|
fn next_block(&mut self) {
|
|
if self.constraints.is_empty() {
|
|
return;
|
|
}
|
|
let len = self.constraints.len();
|
|
self.selected_index = (self.selected_index + 1) % len;
|
|
}
|
|
|
|
/// select the previous block with wrap around
|
|
fn prev_block(&mut self) {
|
|
if self.constraints.is_empty() {
|
|
return;
|
|
}
|
|
let len = self.constraints.len();
|
|
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
|
|
}
|
|
|
|
/// delete the selected block
|
|
fn delete_block(&mut self) {
|
|
if self.constraints.is_empty() {
|
|
return;
|
|
}
|
|
self.constraints.remove(self.selected_index);
|
|
self.selected_index = self.selected_index.saturating_sub(1);
|
|
}
|
|
|
|
/// insert a block after the selected block
|
|
fn insert_block(&mut self) {
|
|
let index = self
|
|
.selected_index
|
|
.saturating_add(1)
|
|
.min(self.constraints.len());
|
|
let constraint = Constraint::Length(self.value);
|
|
self.constraints.insert(index, constraint);
|
|
self.selected_index = index;
|
|
}
|
|
|
|
fn increment_spacing(&mut self) {
|
|
self.spacing = self.spacing.saturating_add(1);
|
|
}
|
|
|
|
fn decrement_spacing(&mut self) {
|
|
self.spacing = self.spacing.saturating_sub(1);
|
|
}
|
|
|
|
fn exit(&mut self) {
|
|
self.mode = AppMode::Quit;
|
|
}
|
|
|
|
fn swap_constraint(&mut self, name: ConstraintName) {
|
|
if self.constraints.is_empty() {
|
|
return;
|
|
}
|
|
let constraint = match name {
|
|
ConstraintName::Length => Length(self.value),
|
|
ConstraintName::Percentage => Percentage(self.value),
|
|
ConstraintName::Min => Min(self.value),
|
|
ConstraintName::Max => Max(self.value),
|
|
ConstraintName::Fill => Fill(self.value),
|
|
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
|
|
};
|
|
self.constraints[self.selected_index] = constraint;
|
|
}
|
|
}
|
|
|
|
impl From<Constraint> for ConstraintName {
|
|
fn from(constraint: Constraint) -> Self {
|
|
match constraint {
|
|
Length(_) => Self::Length,
|
|
Percentage(_) => Self::Percentage,
|
|
Ratio(_, _) => Self::Ratio,
|
|
Min(_) => Self::Min,
|
|
Max(_) => Self::Max,
|
|
Fill(_) => Self::Fill,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Widget for &App {
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
|
|
Layout::vertical([
|
|
Length(2), // header
|
|
Length(2), // instructions
|
|
Length(1), // swap key legend
|
|
Length(1), // gap
|
|
Fill(1), // blocks
|
|
])
|
|
.areas(area);
|
|
|
|
App::header().render(header_area, buf);
|
|
App::instructions().render(instructions_area, buf);
|
|
App::swap_legend().render(swap_legend_area, buf);
|
|
self.render_layout_blocks(blocks_area, buf);
|
|
}
|
|
}
|
|
|
|
// App rendering
|
|
impl App {
|
|
const HEADER_COLOR: Color = SLATE.c200;
|
|
const TEXT_COLOR: Color = SLATE.c400;
|
|
const AXIS_COLOR: Color = SLATE.c500;
|
|
|
|
fn header() -> impl Widget {
|
|
let text = "Constraint Explorer";
|
|
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
|
}
|
|
|
|
fn instructions() -> impl Widget {
|
|
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
|
|
Paragraph::new(text)
|
|
.fg(Self::TEXT_COLOR)
|
|
.centered()
|
|
.wrap(Wrap { trim: false })
|
|
}
|
|
|
|
fn swap_legend() -> impl Widget {
|
|
#[allow(unstable_name_collisions)]
|
|
Paragraph::new(
|
|
Line::from(
|
|
[
|
|
ConstraintName::Min,
|
|
ConstraintName::Max,
|
|
ConstraintName::Length,
|
|
ConstraintName::Percentage,
|
|
ConstraintName::Ratio,
|
|
ConstraintName::Fill,
|
|
]
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, name)| {
|
|
format!(" {i}: {name} ", i = i + 1)
|
|
.fg(SLATE.c200)
|
|
.bg(name.color())
|
|
})
|
|
.intersperse(Span::from(" "))
|
|
.collect_vec(),
|
|
)
|
|
.centered(),
|
|
)
|
|
.wrap(Wrap { trim: false })
|
|
}
|
|
|
|
/// A bar like `<----- 80 px (gap: 2 px) ----->`
|
|
///
|
|
/// Only shows the gap when spacing is not zero
|
|
fn axis(&self, width: u16) -> impl Widget {
|
|
let label = if self.spacing != 0 {
|
|
format!("{} px (gap: {} px)", width, self.spacing)
|
|
} else {
|
|
format!("{width} px")
|
|
};
|
|
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
|
|
let width_bar = format!("<{label:-^bar_width$}>");
|
|
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
|
|
}
|
|
|
|
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
|
|
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
|
|
.spacing(1)
|
|
.areas(area);
|
|
|
|
self.render_user_constraints_legend(user_constraints, buf);
|
|
|
|
let [start, center, end, space_around, space_between] =
|
|
Layout::vertical([Length(7); 5]).areas(area);
|
|
|
|
self.render_layout_block(Flex::Start, start, buf);
|
|
self.render_layout_block(Flex::Center, center, buf);
|
|
self.render_layout_block(Flex::End, end, buf);
|
|
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
|
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
|
}
|
|
|
|
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
|
let blocks = Layout::horizontal(
|
|
self.constraints
|
|
.iter()
|
|
.map(|_| Constraint::Fill(1))
|
|
.collect_vec(),
|
|
)
|
|
.split(area);
|
|
|
|
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
|
let selected = self.selected_index == i;
|
|
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
|
|
}
|
|
}
|
|
|
|
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
|
|
let [label_area, axis_area, blocks_area] =
|
|
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
|
|
|
|
if label_area.height > 0 {
|
|
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
|
}
|
|
|
|
self.axis(area.width).render(axis_area, buf);
|
|
|
|
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
|
.flex(flex)
|
|
.spacing(self.spacing)
|
|
.split_with_spacers(blocks_area);
|
|
|
|
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
|
let selected = self.selected_index == i;
|
|
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
|
|
}
|
|
|
|
for area in spacers.iter() {
|
|
SpacerBlock.render(*area, buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Widget for ConstraintBlock {
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
match area.height {
|
|
1 => self.render_1px(area, buf),
|
|
2 => self.render_2px(area, buf),
|
|
_ => self.render_4px(area, buf),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ConstraintBlock {
|
|
const TEXT_COLOR: Color = SLATE.c200;
|
|
|
|
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
|
Self {
|
|
constraint,
|
|
legend,
|
|
selected,
|
|
}
|
|
}
|
|
|
|
fn label(&self, width: u16) -> String {
|
|
let long_width = format!("{width} px");
|
|
let short_width = format!("{width}");
|
|
// border takes up 2 columns
|
|
let available_space = width.saturating_sub(2) as usize;
|
|
let width_label = if long_width.len() < available_space {
|
|
long_width
|
|
} else if short_width.len() < available_space {
|
|
short_width
|
|
} else {
|
|
String::new()
|
|
};
|
|
format!("{}\n{}", self.constraint, width_label)
|
|
}
|
|
|
|
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
|
|
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
|
let main_color = ConstraintName::from(self.constraint).color();
|
|
let selected_color = if self.selected {
|
|
lighter_color
|
|
} else {
|
|
main_color
|
|
};
|
|
Block::new()
|
|
.fg(Self::TEXT_COLOR)
|
|
.bg(selected_color)
|
|
.render(area, buf);
|
|
}
|
|
|
|
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
|
|
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
|
let main_color = ConstraintName::from(self.constraint).color();
|
|
let selected_color = if self.selected {
|
|
lighter_color
|
|
} else {
|
|
main_color
|
|
};
|
|
Block::bordered()
|
|
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
|
.border_style(Style::reset().fg(selected_color).reversed())
|
|
.render(area, buf);
|
|
}
|
|
|
|
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
|
|
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
|
let main_color = ConstraintName::from(self.constraint).color();
|
|
let selected_color = if self.selected {
|
|
lighter_color
|
|
} else {
|
|
main_color
|
|
};
|
|
let color = if self.legend {
|
|
selected_color
|
|
} else {
|
|
main_color
|
|
};
|
|
let label = self.label(area.width);
|
|
let block = Block::bordered()
|
|
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
|
.border_style(Style::reset().fg(color).reversed())
|
|
.fg(Self::TEXT_COLOR)
|
|
.bg(color);
|
|
Paragraph::new(label)
|
|
.centered()
|
|
.fg(Self::TEXT_COLOR)
|
|
.bg(color)
|
|
.block(block)
|
|
.render(area, buf);
|
|
|
|
if !self.legend {
|
|
let border_color = if self.selected {
|
|
lighter_color
|
|
} else {
|
|
main_color
|
|
};
|
|
if let Some(last_row) = area.rows().last() {
|
|
buf.set_style(last_row, border_color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Widget for SpacerBlock {
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
match area.height {
|
|
1 => (),
|
|
2 => Self::render_2px(area, buf),
|
|
3 => Self::render_3px(area, buf),
|
|
_ => Self::render_4px(area, buf),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SpacerBlock {
|
|
const TEXT_COLOR: Color = SLATE.c500;
|
|
const BORDER_COLOR: Color = SLATE.c600;
|
|
|
|
/// A block with a corner borders
|
|
fn block() -> impl Widget {
|
|
let corners_only = symbols::border::Set {
|
|
top_left: line::NORMAL.top_left,
|
|
top_right: line::NORMAL.top_right,
|
|
bottom_left: line::NORMAL.bottom_left,
|
|
bottom_right: line::NORMAL.bottom_right,
|
|
vertical_left: " ",
|
|
vertical_right: " ",
|
|
horizontal_top: " ",
|
|
horizontal_bottom: " ",
|
|
};
|
|
Block::bordered()
|
|
.border_set(corners_only)
|
|
.border_style(Self::BORDER_COLOR)
|
|
}
|
|
|
|
/// A vertical line used if there is not enough space to render the block
|
|
fn line() -> impl Widget {
|
|
Paragraph::new(Text::from(vec![
|
|
Line::from(""),
|
|
Line::from("│"),
|
|
Line::from("│"),
|
|
Line::from(""),
|
|
]))
|
|
.style(Self::BORDER_COLOR)
|
|
}
|
|
|
|
/// A label that says "Spacer" if there is enough space
|
|
fn spacer_label(width: u16) -> impl Widget {
|
|
let label = if width >= 6 { "Spacer" } else { "" };
|
|
label.fg(Self::TEXT_COLOR).into_centered_line()
|
|
}
|
|
|
|
/// A label that says "8 px" if there is enough space
|
|
fn label(width: u16) -> impl Widget {
|
|
let long_label = format!("{width} px");
|
|
let short_label = format!("{width}");
|
|
let label = if long_label.len() < width as usize {
|
|
long_label
|
|
} else if short_label.len() < width as usize {
|
|
short_label
|
|
} else {
|
|
String::new()
|
|
};
|
|
Line::styled(label, Self::TEXT_COLOR).centered()
|
|
}
|
|
|
|
fn render_2px(area: Rect, buf: &mut Buffer) {
|
|
if area.width > 1 {
|
|
Self::block().render(area, buf);
|
|
} else {
|
|
Self::line().render(area, buf);
|
|
}
|
|
}
|
|
|
|
fn render_3px(area: Rect, buf: &mut Buffer) {
|
|
if area.width > 1 {
|
|
Self::block().render(area, buf);
|
|
} else {
|
|
Self::line().render(area, buf);
|
|
}
|
|
|
|
let row = area.rows().nth(1).unwrap_or_default();
|
|
Self::spacer_label(area.width).render(row, buf);
|
|
}
|
|
|
|
fn render_4px(area: Rect, buf: &mut Buffer) {
|
|
if area.width > 1 {
|
|
Self::block().render(area, buf);
|
|
} else {
|
|
Self::line().render(area, buf);
|
|
}
|
|
|
|
let row = area.rows().nth(1).unwrap_or_default();
|
|
Self::spacer_label(area.width).render(row, buf);
|
|
|
|
let row = area.rows().nth(2).unwrap_or_default();
|
|
Self::label(area.width).render(row, buf);
|
|
}
|
|
}
|
|
|
|
impl ConstraintName {
|
|
const fn color(self) -> Color {
|
|
match self {
|
|
Self::Length => SLATE.c700,
|
|
Self::Percentage => SLATE.c800,
|
|
Self::Ratio => SLATE.c900,
|
|
Self::Fill => SLATE.c950,
|
|
Self::Min => BLUE.c800,
|
|
Self::Max => BLUE.c900,
|
|
}
|
|
}
|
|
|
|
const fn lighter_color(self) -> Color {
|
|
match self {
|
|
Self::Length => STONE.c500,
|
|
Self::Percentage => STONE.c600,
|
|
Self::Ratio => STONE.c700,
|
|
Self::Fill => STONE.c800,
|
|
Self::Min => SKY.c600,
|
|
Self::Max => SKY.c700,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn init_error_hooks() -> Result<()> {
|
|
let (panic, error) = HookBuilder::default().into_hooks();
|
|
let panic = panic.into_panic_hook();
|
|
let error = error.into_eyre_hook();
|
|
color_eyre::eyre::set_hook(Box::new(move |e| {
|
|
let _ = restore_terminal();
|
|
error(e)
|
|
}))?;
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
let _ = restore_terminal();
|
|
panic(info);
|
|
}));
|
|
Ok(())
|
|
}
|
|
|
|
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
|
enable_raw_mode()?;
|
|
stdout().execute(EnterAlternateScreen)?;
|
|
let backend = CrosstermBackend::new(stdout());
|
|
let terminal = Terminal::new(backend)?;
|
|
Ok(terminal)
|
|
}
|
|
|
|
fn restore_terminal() -> Result<()> {
|
|
disable_raw_mode()?;
|
|
stdout().execute(LeaveAlternateScreen)?;
|
|
Ok(())
|
|
}
|