use std::any::Any;
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::Frame;
use super::chrome::ModalChrome;
use super::types::{Modal, ModalKind, ModalOutcome, ModalRenderCtx};
use crate::app::App;
use crate::terminal::Capabilities;
use crate::ui::{
build_cap_lines, theme_downgrade_lines, CapSummary, ModalButton, ModalResponse,
PROSE_CONTENT_WIDTH,
};
const ADJUST_BUTTON: usize = 0;
struct ThemeDowngrade {
configured: String,
substituted: &'static str,
}
pub struct TerminalCapabilitiesModal {
summary: CapSummary,
fingerprint: String,
downgrade: Option<ThemeDowngrade>,
buttons: Vec<ModalButton>,
chrome: ModalChrome,
}
impl TerminalCapabilitiesModal {
pub fn from_capabilities(caps: &Capabilities, seen: &[String]) -> Option<Self> {
let fingerprint = caps.fingerprint();
if seen.iter().any(|s| s == &fingerprint) {
return None;
}
Some(Self {
summary: CapSummary::from_caps(caps),
fingerprint,
downgrade: None,
buttons: vec![ModalButton::new("Adjust settings")],
chrome: ModalChrome::new(ModalKind::Normal, true)
.with_max_content_width(PROSE_CONTENT_WIDTH),
})
}
pub fn with_theme_downgrade(mut self, configured: String, substituted: &'static str) -> Self {
self.downgrade = Some(ThemeDowngrade {
configured,
substituted,
});
self.buttons.clear();
self
}
fn record_outcome(&self, adjust: bool) -> ModalOutcome {
let fp = self.fingerprint.clone();
ModalOutcome::CloseAnd(Box::new(move |app| {
if !app.config.editor.seen_terminal_fingerprints.contains(&fp) {
app.config.editor.seen_terminal_fingerprints.push(fp);
app.save_config_with_flash("failed to persist terminal capabilities notice");
}
if adjust {
app.open_welcome_modal();
}
}))
}
fn resolve(&self, response: ModalResponse) -> ModalOutcome {
match response {
ModalResponse::Continue => ModalOutcome::Continue,
ModalResponse::ButtonPressed(ADJUST_BUTTON) => self.record_outcome(true),
ModalResponse::Cancelled | ModalResponse::ButtonPressed(_) => {
self.record_outcome(false)
}
}
}
}
impl Modal for TerminalCapabilitiesModal {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &ModalRenderCtx<'_>) {
let mut body: Vec<Line<'static>> = Vec::new();
body.push(Line::raw(
"It looks like you're using a new terminal application. edamame has \
detected the following capabilities for this terminal:",
));
body.push(Line::raw(""));
body.extend(build_cap_lines(&self.summary.rows, ctx.theme));
if !self.summary.all_ok() {
body.push(Line::raw(""));
body.push(Line::styled(
"Items marked ✗ will be disabled or degraded automatically.",
Style::default().fg(ctx.theme.palette.warning),
));
}
if let Some(d) = &self.downgrade {
body.push(Line::raw(""));
body.extend(theme_downgrade_lines(
&d.configured,
d.substituted,
ctx.theme,
));
}
if self.downgrade.is_none() {
body.push(Line::raw(""));
body.push(Line::raw(
"Adjust settings re-opens the setup screen so theme, images, and diagrams \
can be matched to this terminal.",
));
}
self.chrome.render(
frame,
area,
ctx,
"Terminal capabilities",
&body,
&self.buttons,
);
}
fn handle_key(
&mut self,
key: KeyEvent,
_app: &mut App,
_doc_height: usize,
_doc_width: usize,
) -> ModalOutcome {
let response = self.chrome.on_key(&key, self.buttons.len());
self.resolve(response)
}
fn handle_wheel(&mut self, delta: i32) {
self.chrome.on_wheel(delta);
}
fn handle_click(&mut self, col: u16, row: u16, _app: &mut App) -> ModalOutcome {
let response = self.chrome.on_click(col, row);
self.resolve(response)
}
fn kind(&self) -> ModalKind {
self.chrome.kind()
}
fn dismissable(&self) -> bool {
self.chrome.dismissable()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_folded_downgrade_drops_the_welcome_route() {
let modal = TerminalCapabilitiesModal::from_capabilities(&Capabilities::minimal(), &[])
.expect("unseen fingerprint yields a modal")
.with_theme_downgrade("Dracula".into(), "256 Dark");
assert!(modal.buttons.is_empty());
}
#[test]
fn adjust_button_index_matches_its_position() {
let modal = TerminalCapabilitiesModal::from_capabilities(&Capabilities::minimal(), &[])
.expect("unseen fingerprint yields a modal");
assert_eq!(modal.buttons[ADJUST_BUTTON].label, "Adjust settings");
assert_eq!(modal.buttons.len(), 1);
}
#[test]
fn seen_fingerprint_suppresses_the_notice() {
let caps = Capabilities::minimal();
let seen = vec![caps.fingerprint()];
assert!(TerminalCapabilitiesModal::from_capabilities(&caps, &seen).is_none());
}
}