use std::{iter::once, str::FromStr};
use anyhow::Result;
use chrono::Local;
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use rustic_core::{
repofile::{DeleteOption, SnapshotFile},
IndexedFull, ProgressBars, Repository, SnapshotGroup, SnapshotGroupCriterion, StringList,
};
use style::palette::tailwind;
use crate::{
commands::{
snapshots::{fill_table, snap_to_table},
tui::{
ls::{Snapshot, SnapshotResult},
tree::{Tree, TreeIterItem, TreeNode},
widgets::{
popup_input, popup_prompt, popup_table, popup_text, Draw, PopUpInput, PopUpPrompt,
PopUpTable, PopUpText, ProcessEvent, PromptResult, SelectTable, TextInputResult,
WithBlock,
},
},
},
filtering::SnapshotFilter,
};
enum CurrentScreen<'a, P, S> {
Snapshots,
ShowHelp(PopUpText),
SnapshotDetails(PopUpTable),
EnterLabel(PopUpInput),
EnterDescription(PopUpInput),
EnterAddTags(PopUpInput),
EnterSetTags(PopUpInput),
EnterRemoveTags(PopUpInput),
EnterFilter(PopUpInput),
PromptWrite(PopUpPrompt),
PromptExit(PopUpPrompt),
Dir(Snapshot<'a, P, S>),
}
#[derive(Clone, Copy, Default)]
struct SnapStatus {
marked: bool,
modified: bool,
to_forget: bool,
}
impl SnapStatus {
fn toggle_mark(&mut self) {
self.marked = !self.marked;
}
}
#[derive(Debug)]
enum View {
Filter,
All,
Marked,
Modified,
}
#[derive(PartialEq, Eq)]
enum SnapshotNode {
Group(SnapshotGroup),
Snap(usize),
}
const INFO_TEXT: &str =
"(Esc) quit | (F5) reload snapshots | (Enter) show contents | (v) toggle view | (i) show snapshot | (?) show all commands";
const HELP_TEXT: &str = r#"General Commands:
q, Esc : exit
F5 : re-read all snapshots from repository
Enter : show snapshot contents
v : toggle snapshot view [Filtered -> All -> Marked -> Modified]
V : modify filter to use
Ctrl-v : reset filter
i : show detailed snapshot information for selected snapshot
w : write modified snapshots and delete snapshots to-forget
? : show this help page
Commands for marking snapshot(s):
x : toggle marking for selected snapshot
X : toggle markings for all snapshots
Ctrl-x : clear all markings
Commands applied to marked snapshot(s) (selected if none marked):
f : toggle to-forget for snapshot(s)
Ctrl-f : clear to-forget for snapshot(s)
l : set label for snapshot(s)
Ctrl-l : remove label for snapshot(s)
d : set description for snapshot(s)
Ctrl-d : remove description for snapshot(s)
t : add tag(s) for snapshot(s)
Ctrl-t : remove all tags for snapshot(s)
s : set tag(s) for snapshot(s)
r : remove tag(s) for snapshot(s)
p : set delete protection for snapshot(s)
Ctrl-p : remove delete protection for snapshot(s)
"#;
pub(crate) struct Snapshots<'a, P, S> {
current_screen: CurrentScreen<'a, P, S>,
current_view: View,
table: WithBlock<SelectTable>,
repo: &'a Repository<P, S>,
snaps_status: Vec<SnapStatus>,
snapshots: Vec<SnapshotFile>,
original_snapshots: Vec<SnapshotFile>,
filtered_snapshots: Vec<usize>,
tree: Tree<SnapshotNode, usize>,
filter: SnapshotFilter,
default_filter: SnapshotFilter,
group_by: SnapshotGroupCriterion,
}
impl<'a, P: ProgressBars, S: IndexedFull> Snapshots<'a, P, S> {
pub fn new(
repo: &'a Repository<P, S>,
filter: SnapshotFilter,
group_by: SnapshotGroupCriterion,
) -> Result<Self> {
let header = [
"", " ID", "Time", "Host", "Label", "Tags", "Paths", "Files", "Dirs", "Size",
]
.into_iter()
.map(Text::from)
.collect();
let mut app = Self {
current_screen: CurrentScreen::Snapshots,
current_view: View::Filter,
table: WithBlock::new(SelectTable::new(header), Block::new()),
repo,
snaps_status: Vec::new(),
original_snapshots: Vec::new(),
snapshots: Vec::new(),
filtered_snapshots: Vec::new(),
tree: Tree::Leaf(0),
default_filter: filter.clone(),
filter,
group_by,
};
app.reread()?;
Ok(app)
}
fn selected_tree(&self) -> Option<TreeIterItem<'_, SnapshotNode, usize>> {
self.table
.widget
.selected()
.and_then(|selected| self.tree.iter_open().nth(selected))
}
fn selected_tree_mut(&mut self) -> Option<&mut Tree<SnapshotNode, usize>> {
self.table
.widget
.selected()
.and_then(|selected| self.tree.nth_mut(selected))
}
fn snap_idx(&self) -> Vec<usize> {
self.selected_tree()
.iter()
.flat_map(|item| item.tree.iter().map(|item| item.tree))
.filter_map(|tree| tree.leaf_data().copied())
.collect()
}
fn selected_snapshot(&self) -> Option<&SnapshotFile> {
self.selected_tree().map(|tree_info| match tree_info.tree {
Tree::Leaf(index)
| Tree::Node(TreeNode {
data: SnapshotNode::Snap(index),
..
}) => Some(&self.snapshots[*index]),
_ => None,
})?
}
pub fn has_mark(&self) -> bool {
self.snaps_status.iter().any(|s| s.marked)
}
pub fn has_modified(&self) -> bool {
self.snaps_status.iter().any(|s| s.modified)
}
pub fn toggle_view_mark(&mut self) {
match self.current_view {
View::Filter => self.current_view = View::All,
View::All => {
self.current_view = View::Marked;
if !self.has_mark() {
self.toggle_view_mark();
}
}
View::Marked => {
self.current_view = View::Modified;
if !self.has_modified() {
self.toggle_view_mark();
}
}
View::Modified => self.current_view = View::Filter,
}
}
pub fn toggle_view(&mut self) {
self.toggle_view_mark();
self.apply_view();
}
pub fn apply_view(&mut self) {
self.filtered_snapshots = self
.snapshots
.iter()
.enumerate()
.zip(self.snaps_status.iter())
.filter_map(|((i, sn), status)| {
match self.current_view {
View::All => true,
View::Filter => self.filter.matches(sn),
View::Marked => status.marked,
View::Modified => status.modified,
}
.then_some(i)
})
.collect();
self.create_tree();
}
pub fn create_tree(&mut self) {
let old_tree = self.selected_tree().map(|t| t.tree);
let mut result = Vec::new();
for (group, snaps) in &self
.filtered_snapshots
.iter()
.chunk_by(|i| SnapshotGroup::from_snapshot(&self.snapshots[**i], self.group_by))
{
let mut same_id_group = Vec::new();
for (_, s) in &snaps.into_iter().chunk_by(|i| self.snapshots[**i].tree) {
let leafs: Vec<_> = s.map(|i| Tree::leaf(*i)).collect();
let first = leafs[0].leaf_data().unwrap(); if leafs.len() == 1 {
same_id_group.push(Tree::leaf(*first));
} else {
same_id_group.push(Tree::node(SnapshotNode::Snap(*first), false, leafs));
}
}
result.push(Tree::node(SnapshotNode::Group(group), false, same_id_group));
}
let tree = Tree::node(SnapshotNode::Snap(0), true, result);
let len = tree.iter_open().count();
let selected = if len == 0 {
None
} else {
Some(
tree.iter()
.position(|info| Some(info.tree) == old_tree)
.unwrap_or(len - 1),
)
};
self.tree = tree;
self.update_table();
self.table.widget.select(selected);
}
fn table_row(&self, info: TreeIterItem<'_, SnapshotNode, usize>) -> Vec<Text<'static>> {
let (has_mark, has_not_mark, has_modified, has_to_forget) = info
.tree
.iter()
.filter_map(|item| item.leaf_data().copied())
.fold(
(false, false, false, false),
|(mut a, mut b, mut c, mut d), i| {
if self.snaps_status[i].marked {
a = true;
} else {
b = true;
}
if self.snaps_status[i].modified {
c = true;
}
if self.snaps_status[i].to_forget {
d = true;
}
(a, b, c, d)
},
);
let mark = match (has_mark, has_not_mark) {
(false, _) => " ",
(true, true) => "*",
(true, false) => "X",
};
let modified = if has_modified { "*" } else { " " };
let del = if has_to_forget { "🗑" } else { "" };
let mut collapse = " ".repeat(info.depth);
collapse.push_str(match info.tree {
Tree::Leaf(_) => "",
Tree::Node(TreeNode { open: false, .. }) => "\u{25b6} ", Tree::Node(TreeNode { open: true, .. }) => "\u{25bc} ", });
match info.tree {
Tree::Leaf(index)
| Tree::Node(TreeNode {
data: SnapshotNode::Snap(index),
..
}) => {
let snap = &self.snapshots[*index];
let symbols = match (
snap.delete == DeleteOption::NotSet,
snap.description.is_none(),
) {
(true, true) => "",
(true, false) => "🗎",
(false, true) => "🛡",
(false, false) => "🛡 🗎",
};
let count = info.tree.child_count();
once(&mark.to_string())
.chain(snap_to_table(snap, count).iter())
.cloned()
.enumerate()
.map(|(i, mut content)| {
if i == 1 {
content = format!("{collapse}{modified}{del}{content}{symbols}");
}
Text::from(content)
})
.collect()
}
Tree::Node(TreeNode {
data: SnapshotNode::Group(group),
..
}) => {
let host = group
.hostname
.as_ref()
.map(String::from)
.unwrap_or_default();
let label = group.label.as_ref().map(String::from).unwrap_or_default();
let paths = group
.paths
.as_ref()
.map_or_else(String::default, |p| p.formatln());
let tags = group
.tags
.as_ref()
.map_or_else(String::default, |t| t.formatln());
[
mark.to_string(),
format!("{collapse}{modified}{del}group"),
String::default(),
host,
label,
tags,
paths,
String::default(),
String::default(),
String::default(),
]
.into_iter()
.map(Text::from)
.collect()
}
}
}
pub fn update_table(&mut self) {
let max_tags = self
.filtered_snapshots
.iter()
.map(|&i| self.snapshots[i].tags.iter().count())
.max()
.unwrap_or(1);
let max_paths = self
.filtered_snapshots
.iter()
.map(|&i| self.snapshots[i].paths.iter().count())
.max()
.unwrap_or(1);
let height = max_tags.max(max_paths).max(1) + 1;
let rows = self
.tree
.iter_open()
.map(|tree| self.table_row(tree))
.collect();
self.table.widget.set_content(rows, height);
self.table.block = Block::new()
.borders(Borders::BOTTOM)
.title_bottom(format!(
"{:?} view: {}, total: {}, marked: {}, modified: {}, to forget: {}",
self.current_view,
self.filtered_snapshots.len(),
self.snapshots.len(),
self.count_marked_snaps(),
self.count_modified_snaps(),
self.count_forget_snaps()
))
.title_alignment(Alignment::Center);
}
pub fn toggle_mark(&mut self) {
for snap_idx in self.snap_idx() {
self.snaps_status[snap_idx].toggle_mark();
}
self.update_table();
}
pub fn toggle_mark_all(&mut self) {
for snap_idx in &self.filtered_snapshots {
self.snaps_status[*snap_idx].toggle_mark();
}
self.update_table();
}
pub fn clear_marks(&mut self) {
for status in self.snaps_status.iter_mut() {
status.marked = false;
}
self.update_table();
}
pub fn reset_filter(&mut self) {
self.filter = self.default_filter.clone();
self.apply_view();
}
pub fn collapse(&mut self) {
if let Some(tree) = self.selected_tree_mut() {
tree.close();
self.update_table();
}
}
pub fn extendable(&self) -> bool {
matches!(self.selected_tree(), Some(tree_info) if tree_info.tree.openable())
}
pub fn extend(&mut self) {
if let Some(tree) = self.selected_tree_mut() {
tree.open();
self.update_table();
}
}
pub fn snapshot_details(&self) -> PopUpTable {
let mut rows = Vec::new();
if let Some(snap) = self.selected_snapshot() {
fill_table(snap, |title, value| {
rows.push(vec![Text::from(title.to_string()), Text::from(value)]);
});
}
popup_table("snapshot details", rows)
}
pub fn dir(&self) -> Result<Option<Snapshot<'a, P, S>>> {
self.selected_snapshot().map_or(Ok(None), |snap| {
Some(Snapshot::new(self.repo, snap.clone())).transpose()
})
}
pub fn count_marked_snaps(&self) -> usize {
self.snaps_status.iter().filter(|s| s.marked).count()
}
pub fn count_modified_snaps(&self) -> usize {
self.snaps_status.iter().filter(|s| s.modified).count()
}
pub fn count_forget_snaps(&self) -> usize {
self.snaps_status.iter().filter(|s| s.to_forget).count()
}
pub fn process_marked_snaps(&mut self, mut process: impl FnMut(&mut SnapshotFile) -> bool) {
let has_mark = self.has_mark();
if !has_mark {
self.toggle_mark();
}
for ((snap, status), original_snap) in self
.snapshots
.iter_mut()
.zip(self.snaps_status.iter_mut())
.zip(self.original_snapshots.iter())
{
if status.marked && process(snap) {
status.modified =
serde_json::to_string(snap).ok() != serde_json::to_string(original_snap).ok();
}
}
if !has_mark {
self.toggle_mark();
}
self.update_table();
}
pub fn get_snap_entity(&mut self, f: impl Fn(&SnapshotFile) -> String) -> String {
let has_mark = self.has_mark();
if !has_mark {
self.toggle_mark();
}
let entity = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| status.marked.then_some(f(snap)))
.reduce(|entity, e| if entity == e { e } else { String::new() })
.unwrap_or_default();
if !has_mark {
self.toggle_mark();
}
entity
}
pub fn get_label(&mut self) -> String {
self.get_snap_entity(|snap| snap.label.clone())
}
pub fn get_tags(&mut self) -> String {
self.get_snap_entity(|snap| snap.tags.formatln())
}
pub fn get_description(&mut self) -> String {
self.get_snap_entity(|snap| snap.description.clone().unwrap_or_default())
}
pub fn get_filter(&self) -> Result<String> {
Ok(toml::to_string_pretty(&self.filter)?)
}
pub fn set_filter(&mut self, filter: String) {
if let Ok(filter) = toml::from_str::<SnapshotFilter>(&filter) {
self.filter = filter;
self.apply_view();
}
}
pub fn set_label(&mut self, label: String) {
self.process_marked_snaps(|snap| {
if snap.label == label {
return false;
}
snap.label.clone_from(&label);
true
});
}
pub fn clear_label(&mut self) {
self.set_label(String::new());
}
pub fn set_description(&mut self, desc: String) {
let desc = if desc.is_empty() { None } else { Some(desc) };
self.process_marked_snaps(|snap| {
if snap.description == desc {
return false;
}
snap.description.clone_from(&desc);
true
});
}
pub fn clear_description(&mut self) {
self.set_description(String::new());
}
pub fn add_tags(&mut self, tags: String) {
let tags = vec![StringList::from_str(&tags).unwrap()];
self.process_marked_snaps(|snap| snap.add_tags(tags.clone()));
}
pub fn set_tags(&mut self, tags: String) {
let tags = vec![StringList::from_str(&tags).unwrap()];
self.process_marked_snaps(|snap| snap.set_tags(tags.clone()));
}
pub fn remove_tags(&mut self, tags: String) {
let tags = vec![StringList::from_str(&tags).unwrap()];
self.process_marked_snaps(|snap| snap.remove_tags(&tags));
}
pub fn clear_tags(&mut self) {
let no_tags = vec![StringList::default()];
self.process_marked_snaps(|snap| snap.set_tags(no_tags.clone()));
}
pub fn set_delete_protection_to(&mut self, delete: DeleteOption) {
self.process_marked_snaps(|snap| {
if snap.delete == delete {
return false;
}
snap.delete = delete;
true
});
}
pub fn toggle_to_forget(&mut self) {
let has_mark = self.has_mark();
if !has_mark {
self.toggle_mark();
}
let now = Local::now();
for (snap, status) in self.snapshots.iter_mut().zip(self.snaps_status.iter_mut()) {
if status.marked {
if status.to_forget {
status.to_forget = false;
} else if !snap.must_keep(now) {
status.to_forget = true;
}
}
}
if !has_mark {
self.toggle_mark();
}
self.update_table();
}
pub fn clear_to_forget(&mut self) {
for status in self.snaps_status.iter_mut() {
status.to_forget = false;
}
self.update_table();
}
pub fn apply_input(&mut self, input: String) {
match self.current_screen {
CurrentScreen::EnterLabel(_) => self.set_label(input),
CurrentScreen::EnterDescription(_) => self.set_description(input),
CurrentScreen::EnterAddTags(_) => self.add_tags(input),
CurrentScreen::EnterSetTags(_) => self.set_tags(input),
CurrentScreen::EnterRemoveTags(_) => self.remove_tags(input),
CurrentScreen::EnterFilter(_) => self.set_filter(input),
_ => {}
}
}
pub fn set_delete_protection(&mut self) {
self.set_delete_protection_to(DeleteOption::Never);
}
pub fn clear_delete_protection(&mut self) {
self.set_delete_protection_to(DeleteOption::NotSet);
}
pub fn write(&mut self) -> Result<()> {
if !self.has_modified() && self.count_forget_snaps() == 0 {
return Ok(());
};
let save_snaps: Vec<_> = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| status.modified.then_some(snap))
.cloned()
.collect();
let old_snap_ids = save_snaps.iter().map(|sn| sn.id);
let snap_ids_to_forget = self
.snapshots
.iter()
.zip(self.snaps_status.iter())
.filter_map(|(snap, status)| status.to_forget.then_some(snap.id));
let delete_ids: Vec<_> = old_snap_ids.chain(snap_ids_to_forget).collect();
self.repo.save_snapshots(save_snaps)?;
self.repo.delete_snapshots(&delete_ids)?;
self.reread()
}
pub fn reread(&mut self) -> Result<()> {
self.snapshots = self.repo.get_all_snapshots()?;
self.snapshots
.sort_unstable_by(|sn1, sn2| sn1.cmp_group(self.group_by, sn2).then(sn1.cmp(sn2)));
self.snaps_status = vec![SnapStatus::default(); self.snapshots.len()];
self.original_snapshots.clone_from(&self.snapshots);
self.table.widget.select(None);
self.apply_view();
Ok(())
}
pub fn input(&mut self, event: Event) -> Result<bool> {
use KeyCode::*;
match &mut self.current_screen {
CurrentScreen::Snapshots => {
match event {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if key.modifiers == KeyModifiers::CONTROL {
match key.code {
Char('f') => self.clear_to_forget(),
Char('x') => self.clear_marks(),
Char('l') => self.clear_label(),
Char('d') => self.clear_description(),
Char('t') => self.clear_tags(),
Char('p') => self.clear_delete_protection(),
Char('v') => self.reset_filter(),
_ => {}
}
} else {
match key.code {
Esc | Char('q') => {
self.current_screen = CurrentScreen::PromptExit(popup_prompt(
"exit rustic",
"do you want to exit? (y/n)".into(),
));
}
Char('f') => self.toggle_to_forget(),
F(5) => self.reread()?,
Enter => {
if let Some(dir) = self.dir()? {
self.current_screen = CurrentScreen::Dir(dir);
}
}
Right => {
if self.extendable() {
self.extend();
} else if let Some(dir) = self.dir()? {
self.current_screen = CurrentScreen::Dir(dir);
}
}
Char('+') => {
if self.extendable() {
self.extend();
}
}
Left | Char('-') => self.collapse(),
Char('?') => {
self.current_screen = CurrentScreen::ShowHelp(popup_text(
"help",
HELP_TEXT.into(),
));
}
Char('x') => {
self.toggle_mark();
self.table.widget.next();
}
Char('X') => self.toggle_mark_all(),
Char('v') => self.toggle_view(),
Char('V') => {
self.current_screen = CurrentScreen::EnterFilter(popup_input(
"set filter (Ctrl-s to confirm)",
"enter filter in TOML format",
&self.get_filter()?,
15,
));
}
Char('i') => {
self.current_screen =
CurrentScreen::SnapshotDetails(self.snapshot_details());
}
Char('l') => {
self.current_screen = CurrentScreen::EnterLabel(popup_input(
"set label",
"enter label",
&self.get_label(),
1,
));
}
Char('d') => {
self.current_screen =
CurrentScreen::EnterDescription(popup_input(
"set description (Ctrl-s to confirm)",
"enter description",
&self.get_description(),
5,
));
}
Char('t') => {
self.current_screen = CurrentScreen::EnterAddTags(popup_input(
"add tags",
"enter tags",
"",
1,
));
}
Char('s') => {
self.current_screen = CurrentScreen::EnterSetTags(popup_input(
"set tags",
"enter tags",
&self.get_tags(),
1,
));
}
Char('r') => {
self.current_screen = CurrentScreen::EnterRemoveTags(
popup_input("remove tags", "enter tags", "", 1),
);
}
Char('p') => self.set_delete_protection(),
Char('w') => {
let msg = format!(
"Do you want to write {} modified and remove {} snapshots? (y/n)",
self.count_modified_snaps(),
self.count_forget_snaps()
);
self.current_screen = CurrentScreen::PromptWrite(popup_prompt(
"write snapshots",
msg.into(),
));
}
_ => self.table.input(event),
}
}
}
_ => {}
}
}
CurrentScreen::SnapshotDetails(_) | CurrentScreen::ShowHelp(_) => match event {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if matches!(
key.code,
Char('q') | Esc | Enter | Char(' ') | Char('i') | Char('?')
) {
self.current_screen = CurrentScreen::Snapshots;
}
}
_ => {}
},
CurrentScreen::EnterLabel(prompt)
| CurrentScreen::EnterDescription(prompt)
| CurrentScreen::EnterAddTags(prompt)
| CurrentScreen::EnterSetTags(prompt)
| CurrentScreen::EnterRemoveTags(prompt)
| CurrentScreen::EnterFilter(prompt) => match prompt.input(event) {
TextInputResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
TextInputResult::Input(input) => {
self.apply_input(input);
self.current_screen = CurrentScreen::Snapshots;
}
TextInputResult::None => {}
},
CurrentScreen::PromptWrite(prompt) => match prompt.input(event) {
PromptResult::Ok => {
self.write()?;
self.current_screen = CurrentScreen::Snapshots;
}
PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
PromptResult::None => {}
},
CurrentScreen::PromptExit(prompt) => match prompt.input(event) {
PromptResult::Ok => return Ok(true),
PromptResult::Cancel => self.current_screen = CurrentScreen::Snapshots,
PromptResult::None => {}
},
CurrentScreen::Dir(dir) => match dir.input(event)? {
SnapshotResult::Exit => return Ok(true),
SnapshotResult::Return => self.current_screen = CurrentScreen::Snapshots,
SnapshotResult::None => {}
},
}
Ok(false)
}
pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
if let CurrentScreen::Dir(dir) = &mut self.current_screen {
dir.draw(area, f);
return;
}
let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
self.table.draw(rects[0], f);
let buffer_bg = tailwind::SLATE.c950;
let row_fg = tailwind::SLATE.c200;
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(row_fg).bg(buffer_bg))
.centered();
f.render_widget(info_footer, rects[1]);
match &mut self.current_screen {
CurrentScreen::SnapshotDetails(popup) => popup.draw(area, f),
CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
CurrentScreen::EnterLabel(popup)
| CurrentScreen::EnterDescription(popup)
| CurrentScreen::EnterAddTags(popup)
| CurrentScreen::EnterSetTags(popup)
| CurrentScreen::EnterRemoveTags(popup)
| CurrentScreen::EnterFilter(popup) => popup.draw(area, f),
CurrentScreen::PromptWrite(popup) | CurrentScreen::PromptExit(popup) => {
popup.draw(area, f);
}
_ => {}
}
}
}