use std::path::Path;
use std::time::{Duration, Instant};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
};
use crate::sdk::{WhyBlocked, ZinitClient};
pub fn run(socket_path: &Path) -> Result<(), String> {
run_tui(socket_path).map_err(|e| e.to_string())
}
#[derive(Clone, Debug)]
struct ServiceDisplayInfo {
name: String,
state: String,
pid: u32,
exit_code: Option<i32>,
error: Option<String>,
}
struct App {
client: ZinitClient,
services: Vec<ServiceDisplayInfo>,
selected: ListState,
logs: Vec<String>,
log_scroll: usize,
show_help: bool,
show_detail: bool,
show_why: bool,
why_info: Option<WhyBlocked>,
status_message: Option<(String, Instant)>,
focus: Focus,
following_logs: bool,
}
#[derive(PartialEq, Clone, Copy)]
enum Focus {
Services,
Logs,
Deps,
}
impl App {
fn new(client: ZinitClient) -> Self {
let mut selected = ListState::default();
selected.select(Some(0));
Self {
client,
services: Vec::new(),
selected,
logs: Vec::new(),
log_scroll: 0,
show_help: false,
show_detail: false,
show_why: false,
why_info: None,
status_message: None,
focus: Focus::Services,
following_logs: true,
}
}
fn set_status(&mut self, msg: impl Into<String>) {
self.status_message = Some((msg.into(), Instant::now()));
}
fn selected_service(&self) -> Option<&ServiceDisplayInfo> {
self.selected.selected().and_then(|i| self.services.get(i))
}
fn next_service(&mut self) {
if self.services.is_empty() {
return;
}
let i = match self.selected.selected() {
Some(i) => {
if i >= self.services.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.selected.select(Some(i));
self.following_logs = true;
}
fn previous_service(&mut self) {
if self.services.is_empty() {
return;
}
let i = match self.selected.selected() {
Some(i) => {
if i == 0 {
self.services.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.selected.select(Some(i));
self.following_logs = true;
}
fn scroll_logs_up(&mut self) {
if self.log_scroll > 0 {
self.log_scroll -= 1;
self.following_logs = false;
}
}
fn scroll_logs_down(&mut self, visible_height: usize) {
let max_scroll = self.logs.len().saturating_sub(visible_height);
if self.log_scroll < max_scroll {
self.log_scroll += 1;
}
if self.log_scroll >= max_scroll {
self.following_logs = true;
}
}
fn scroll_logs_to_end(&mut self) {
self.following_logs = true;
}
fn cycle_focus(&mut self) {
self.focus = match self.focus {
Focus::Services => Focus::Deps,
Focus::Deps => Focus::Logs,
Focus::Logs => Focus::Services,
};
}
fn fetch_services(&mut self) {
let mut services = Vec::new();
if let Ok(names) = self.client.list() {
for name in names {
if let Ok(status) = self.client.status(&name) {
services.push(ServiceDisplayInfo {
name: status.name,
state: format!("{:?}", status.state).to_lowercase(),
pid: status.pid,
exit_code: status.exit_code,
error: status.error,
});
} else {
services.push(ServiceDisplayInfo {
name,
state: "unknown".to_string(),
pid: 0,
exit_code: None,
error: None,
});
}
}
}
services.sort_by(|a, b| a.name.cmp(&b.name));
self.services = services;
}
fn fetch_logs(&mut self) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
if let Ok(logs) = self.client.logs(&name, Some(200)) {
self.logs = logs;
}
}
}
fn fetch_why(&mut self) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
match self.client.why(&name) {
Ok(why) => {
self.why_info = Some(why);
self.show_why = true;
}
Err(e) => {
self.set_status(format!("Error: {}", e));
}
}
}
}
fn start_service(&mut self) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
match self.client.start(&name) {
Ok(_) => self.set_status(format!("Started: {}", name)),
Err(e) => self.set_status(format!("Error: {}", e)),
}
self.fetch_services();
}
}
fn stop_service(&mut self) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
match self.client.stop(&name) {
Ok(_) => self.set_status(format!("Stopped: {}", name)),
Err(e) => self.set_status(format!("Error: {}", e)),
}
self.fetch_services();
}
}
fn restart_service(&mut self) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
match self.client.restart(&name) {
Ok(_) => self.set_status(format!("Restarted: {}", name)),
Err(e) => self.set_status(format!("Error: {}", e)),
}
self.fetch_services();
}
}
fn remove_service(&mut self) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
match self.client.remove(&name) {
Ok(_) => self.set_status(format!("Removed: {}", name)),
Err(e) => self.set_status(format!("Error: {}", e)),
}
self.fetch_services();
if let Some(selected) = self.selected.selected()
&& selected >= self.services.len()
&& !self.services.is_empty()
{
self.selected.select(Some(self.services.len() - 1));
}
}
}
fn kill_service(&mut self, signal: &str) {
if let Some(service) = self.selected_service() {
let name = service.name.clone();
match self.client.kill(&name, Some(signal)) {
Ok(_) => self.set_status(format!("Sent {} to {}", signal, name)),
Err(e) => self.set_status(format!("Error: {}", e)),
}
self.fetch_services();
}
}
}
fn state_color(state: &str) -> Color {
match state {
"running" => Color::Green,
"starting" | "blocked" => Color::Yellow,
"stopping" => Color::Magenta,
"exited" => Color::Cyan,
"failed" => Color::Red,
"inactive" => Color::DarkGray,
_ => Color::DarkGray,
}
}
fn state_symbol(state: &str) -> &'static str {
match state {
"inactive" => "[-]",
"blocked" => "[?]",
"starting" => "[>]",
"running" => "[+]",
"stopping" | "exited" => "[.]",
"failed" => "[X]",
_ => "[?]",
}
}
fn draw(frame: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
.split(frame.area());
let title = Paragraph::new(Line::from(vec![
Span::styled(
"zinit ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled("TUI", Style::default().fg(Color::White)),
Span::raw(" | "),
Span::styled("?", Style::default().fg(Color::Yellow)),
Span::raw(" Help "),
Span::styled("w", Style::default().fg(Color::Yellow)),
Span::raw(" Why "),
Span::styled("Q", Style::default().fg(Color::Yellow)),
Span::raw(" Quit"),
]))
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
);
frame.render_widget(title, chunks[0]);
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(30),
Constraint::Percentage(25),
Constraint::Percentage(45),
])
.split(chunks[1]);
draw_services(frame, app, main_chunks[0]);
draw_dependencies(frame, app, main_chunks[1]);
draw_logs(frame, app, main_chunks[2]);
let status_text = if let Some((msg, time)) = &app.status_message {
if time.elapsed() < Duration::from_secs(5) {
msg.clone()
} else {
get_default_status(app)
}
} else {
get_default_status(app)
};
let status = Paragraph::new(status_text)
.style(Style::default().fg(Color::White))
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
);
frame.render_widget(status, chunks[2]);
if app.show_help {
draw_help_popup(frame);
}
if app.show_detail
&& let Some(service) = app.selected_service()
{
draw_detail_popup(frame, service);
}
if app.show_why
&& let Some(why) = &app.why_info
{
draw_why_popup(frame, why);
}
}
fn get_default_status(app: &App) -> String {
let service_info = app
.selected_service()
.map(|s| {
let pid_str = if s.pid > 0 {
format!(" PID:{}", s.pid)
} else {
String::new()
};
format!(
"{} {} {}{}",
state_symbol(&s.state),
s.name,
s.state,
pid_str
)
})
.unwrap_or_else(|| "No service selected".to_string());
let focus_indicator = match app.focus {
Focus::Services => "[Services]",
Focus::Deps => "[Deps]",
Focus::Logs => "[Logs]",
};
format!("{} | {} | Tab: cycle focus", service_info, focus_indicator)
}
fn draw_services(frame: &mut Frame, app: &App, area: Rect) {
let items: Vec<ListItem> = app
.services
.iter()
.map(|service| {
let state_style = Style::default().fg(state_color(&service.state));
let content = Line::from(vec![
Span::styled(format!("{} ", state_symbol(&service.state)), state_style),
Span::styled(&service.name, Style::default().fg(Color::White)),
]);
ListItem::new(content)
})
.collect();
let border_color = if app.focus == Focus::Services {
Color::Cyan
} else {
Color::DarkGray
};
let services_list = List::new(items)
.block(
Block::default()
.title(format!(" Services ({}) ", app.services.len()))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color)),
)
.highlight_style(
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("> ");
frame.render_stateful_widget(services_list, area, &mut app.selected.clone());
}
fn draw_dependencies(frame: &mut Frame, app: &App, area: Rect) {
let border_color = if app.focus == Focus::Deps {
Color::Cyan
} else {
Color::DarkGray
};
let info_lines: Vec<Line> = if let Some(service) = app.selected_service() {
let mut lines = vec![Line::from(vec![
Span::styled("State: ", Style::default().fg(Color::Yellow)),
Span::styled(
&service.state,
Style::default().fg(state_color(&service.state)),
),
])];
if service.pid > 0 {
lines.push(Line::from(vec![
Span::styled("PID: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{}", service.pid),
Style::default().fg(Color::White),
),
]));
}
if let Some(code) = service.exit_code {
lines.push(Line::from(vec![
Span::styled("Exit code: ", Style::default().fg(Color::Yellow)),
Span::styled(format!("{}", code), Style::default().fg(Color::Cyan)),
]));
}
if let Some(ref err) = service.error {
lines.push(Line::from(vec![
Span::styled("Error: ", Style::default().fg(Color::Red)),
Span::styled(err, Style::default().fg(Color::White)),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Press 'w' to see blocking info",
Style::default().fg(Color::DarkGray),
)));
lines
} else {
vec![Line::from(Span::styled(
"No service selected",
Style::default().fg(Color::DarkGray),
))]
};
let info_widget = Paragraph::new(info_lines)
.block(
Block::default()
.title(" Service Info ")
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color)),
)
.wrap(Wrap { trim: false });
frame.render_widget(info_widget, area);
}
fn draw_logs(frame: &mut Frame, app: &App, area: Rect) {
let border_color = if app.focus == Focus::Logs {
Color::Cyan
} else {
Color::DarkGray
};
let service_name = app
.selected_service()
.map(|s| s.name.clone())
.unwrap_or_else(|| "none".to_string());
let visible_height = (area.height as usize).saturating_sub(2);
let total_logs = app.logs.len();
let scroll_offset = if app.following_logs {
total_logs.saturating_sub(visible_height)
} else {
app.log_scroll
.min(total_logs.saturating_sub(visible_height))
};
let visible_logs: Vec<Line> = app
.logs
.iter()
.skip(scroll_offset)
.take(visible_height)
.map(|line| {
let style = if line.contains("[ERR]")
|| line.contains("error")
|| line.contains("Error")
|| line.contains("ERROR")
{
Style::default().fg(Color::Red)
} else if line.contains("warn") || line.contains("Warn") || line.contains("WARN") {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Gray)
};
Line::from(Span::styled(line.clone(), style))
})
.collect();
let follow_indicator = if app.following_logs {
" [following]"
} else {
""
};
let scroll_info = format!(
" {}/{} ",
scroll_offset + visible_height.min(total_logs),
total_logs
);
let logs_widget = Paragraph::new(visible_logs)
.block(
Block::default()
.title(format!(" Logs: {}{} ", service_name, follow_indicator))
.title_bottom(Line::from(scroll_info).centered())
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color)),
)
.wrap(Wrap { trim: false });
frame.render_widget(logs_widget, area);
}
fn draw_help_popup(frame: &mut Frame) {
let area = frame.area();
let popup_area = Rect {
x: area.width / 6,
y: area.height / 6,
width: area.width * 2 / 3,
height: area.height * 2 / 3,
};
frame.render_widget(Clear, popup_area);
let help_text = vec![
Line::from(Span::styled(
"Keyboard Shortcuts",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![Span::styled(
"Navigation:",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled(" Up/k ", Style::default().fg(Color::Green)),
Span::raw("Move up in service list"),
]),
Line::from(vec![
Span::styled(" Down/j ", Style::default().fg(Color::Green)),
Span::raw("Move down in service list"),
]),
Line::from(vec![
Span::styled(" Tab ", Style::default().fg(Color::Green)),
Span::raw("Cycle focus: Services -> Deps -> Logs"),
]),
Line::from(vec![
Span::styled(" PgUp/PgDn ", Style::default().fg(Color::Green)),
Span::raw("Scroll logs"),
]),
Line::from(vec![
Span::styled(" g/G ", Style::default().fg(Color::Green)),
Span::raw("Go to top/bottom of logs"),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Actions:",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled(" s ", Style::default().fg(Color::Green)),
Span::raw("Start selected service"),
]),
Line::from(vec![
Span::styled(" S ", Style::default().fg(Color::Green)),
Span::raw("Stop selected service"),
]),
Line::from(vec![
Span::styled(" r ", Style::default().fg(Color::Green)),
Span::raw("Restart selected service"),
]),
Line::from(vec![
Span::styled(" K ", Style::default().fg(Color::Green)),
Span::raw("Kill with SIGKILL"),
]),
Line::from(vec![
Span::styled(" d ", Style::default().fg(Color::Green)),
Span::raw("Remove service"),
]),
Line::from(vec![
Span::styled(" R ", Style::default().fg(Color::Green)),
Span::raw("Refresh service list"),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Info:",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::styled(" w ", Style::default().fg(Color::Green)),
Span::raw("Show why service is blocked"),
]),
Line::from(vec![
Span::styled(" Enter ", Style::default().fg(Color::Green)),
Span::raw("Show service details"),
]),
Line::from(vec![
Span::styled(" ? ", Style::default().fg(Color::Green)),
Span::raw("Toggle this help"),
]),
Line::from(vec![
Span::styled(" Q/Esc ", Style::default().fg(Color::Green)),
Span::raw("Quit"),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Dependency Types: ",
Style::default().fg(Color::Yellow),
)]),
Line::from(vec![
Span::styled(" R", Style::default().fg(Color::Cyan)),
Span::raw("=requires "),
Span::styled("W", Style::default().fg(Color::Cyan)),
Span::raw("=wants "),
Span::styled("A", Style::default().fg(Color::Cyan)),
Span::raw("=after "),
Span::styled("C", Style::default().fg(Color::Cyan)),
Span::raw("=conflicts"),
]),
Line::from(""),
Line::from(Span::styled(
"Press any key to close",
Style::default().fg(Color::DarkGray),
)),
];
let help = Paragraph::new(help_text)
.block(
Block::default()
.title(" Help ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.style(Style::default().bg(Color::Black));
frame.render_widget(help, popup_area);
}
fn draw_detail_popup(frame: &mut Frame, service: &ServiceDisplayInfo) {
let area = frame.area();
let popup_area = Rect {
x: area.width / 6,
y: area.height / 6,
width: area.width * 2 / 3,
height: area.height * 2 / 3,
};
frame.render_widget(Clear, popup_area);
let state_col = state_color(&service.state);
let mut detail_text = vec![
Line::from(Span::styled(
format!(" {} ", service.name),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![Span::styled(
"Status",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]),
Line::from(vec![
Span::raw(" State: "),
Span::styled(
format!("{} {}", state_symbol(&service.state), &service.state),
Style::default().fg(state_col).add_modifier(Modifier::BOLD),
),
]),
];
if service.pid > 0 {
detail_text.push(Line::from(vec![
Span::raw(" PID: "),
Span::styled(service.pid.to_string(), Style::default().fg(Color::White)),
]));
}
if let Some(code) = service.exit_code {
detail_text.push(Line::from(vec![
Span::raw(" Exit: "),
Span::styled(format!("{}", code), Style::default().fg(Color::Cyan)),
]));
}
if let Some(ref err) = service.error {
detail_text.push(Line::from(""));
detail_text.push(Line::from(vec![Span::styled(
"Error",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)]));
detail_text.push(Line::from(vec![
Span::raw(" "),
Span::styled(err, Style::default().fg(Color::Red)),
]));
}
detail_text.push(Line::from(""));
detail_text.push(Line::from(Span::styled(
"Press Esc to close",
Style::default().fg(Color::DarkGray),
)));
let detail = Paragraph::new(detail_text)
.block(
Block::default()
.title(" Service Details ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
)
.style(Style::default().bg(Color::Black));
frame.render_widget(detail, popup_area);
}
fn draw_why_popup(frame: &mut Frame, why: &WhyBlocked) {
let area = frame.area();
let popup_area = Rect {
x: area.width / 8,
y: area.height / 8,
width: area.width * 3 / 4,
height: area.height * 3 / 4,
};
frame.render_widget(Clear, popup_area);
let mut lines = vec![Line::from(Span::styled(
"Why Blocked?",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))];
if !why.blocked {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Service is not blocked",
Style::default().fg(Color::Green),
)));
} else {
lines.push(Line::from(""));
for line in why.ascii.lines() {
lines.push(Line::from(Span::styled(
line.to_string(),
Style::default().fg(Color::White),
)));
}
if !why.waiting_on.is_empty() {
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("Waiting on: ", Style::default().fg(Color::Yellow)),
Span::styled(why.waiting_on.join(", "), Style::default().fg(Color::White)),
]));
}
if !why.conflicts_with.is_empty() {
lines.push(Line::from(vec![
Span::styled("Conflicts with: ", Style::default().fg(Color::Red)),
Span::styled(
why.conflicts_with.join(", "),
Style::default().fg(Color::White),
),
]));
}
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Press any key to close",
Style::default().fg(Color::DarkGray),
)));
let why_widget = Paragraph::new(lines)
.block(
Block::default()
.title(" Why Blocked? ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow)),
)
.style(Style::default().bg(Color::Black))
.wrap(Wrap { trim: false });
frame.render_widget(why_widget, popup_area);
}
fn run_tui(socket_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let client = ZinitClient::connect(socket_path)?;
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(client);
app.fetch_services();
app.fetch_logs();
let refresh_interval = Duration::from_secs(2);
let mut last_refresh = Instant::now();
let mut needs_redraw = true;
loop {
if needs_redraw {
terminal.draw(|f| draw(f, &app))?;
needs_redraw = false;
}
let timeout = Duration::from_millis(200);
if event::poll(timeout)?
&& let Event::Key(key) = event::read()?
{
if app.show_help {
app.show_help = false;
needs_redraw = true;
continue;
}
if app.show_why {
app.show_why = false;
needs_redraw = true;
continue;
}
if app.show_detail {
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
app.show_detail = false;
}
needs_redraw = true;
continue;
}
match (key.code, key.modifiers) {
(KeyCode::Char('Q'), _) | (KeyCode::Esc, _) => break,
(KeyCode::Char('c'), KeyModifiers::CONTROL) => break,
(KeyCode::Char('?'), _) => app.show_help = true,
(KeyCode::Char('w'), _) => app.fetch_why(),
(KeyCode::Enter, _) => {
if app.focus == Focus::Services {
app.show_detail = true;
}
}
(KeyCode::Up, _) | (KeyCode::Char('k'), KeyModifiers::NONE) => {
if app.focus == Focus::Services {
app.previous_service();
app.fetch_logs();
} else if app.focus == Focus::Logs {
app.scroll_logs_up();
}
}
(KeyCode::Down, _) | (KeyCode::Char('j'), KeyModifiers::NONE) => {
if app.focus == Focus::Services {
app.next_service();
app.fetch_logs();
} else if app.focus == Focus::Logs {
let visible_height = terminal.size()?.height as usize;
app.scroll_logs_down(visible_height);
}
}
(KeyCode::Tab, _) => app.cycle_focus(),
(KeyCode::PageUp, _) => {
for _ in 0..10 {
app.scroll_logs_up();
}
}
(KeyCode::PageDown, _) => {
let visible_height = terminal.size()?.height as usize;
for _ in 0..10 {
app.scroll_logs_down(visible_height);
}
}
(KeyCode::Char('g'), _) => {
app.log_scroll = 0;
app.following_logs = false;
}
(KeyCode::Char('G'), _) => {
app.scroll_logs_to_end();
}
(KeyCode::Char('s'), KeyModifiers::NONE) => {
app.start_service();
}
(KeyCode::Char('S'), KeyModifiers::SHIFT) => {
app.stop_service();
}
(KeyCode::Char('r'), KeyModifiers::NONE) => {
app.restart_service();
}
(KeyCode::Char('d'), _) => {
app.remove_service();
}
(KeyCode::Char('K'), KeyModifiers::SHIFT) => {
app.kill_service("SIGKILL");
}
(KeyCode::Char('R'), KeyModifiers::SHIFT) => {
app.fetch_services();
app.fetch_logs();
app.set_status("Refreshed");
}
_ => {}
}
needs_redraw = true;
}
if last_refresh.elapsed() >= refresh_interval {
last_refresh = Instant::now();
app.fetch_services();
app.fetch_logs();
needs_redraw = true;
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}