use super::*;
use crate::ai::{assess, Risk};
use crate::procs;
pub(super) struct DangerConfirm {
targets: Vec<PaneId>,
bytes: Vec<u8>,
line: String,
reasons: Vec<String>,
}
impl App {
fn danger_reason(&self, pane: &Pane) -> Option<String> {
match pane.danger_manual {
Some(true) => return Some("marked".into()),
Some(false) => return None,
None => {}
}
let cfg = &self.config.danger;
if !cfg.enabled {
return None;
}
let shell = pane.session.pid();
let fg = procs::foreground(shell).unwrap_or(shell);
let args = procs::cmdline(fg);
if let Some(target) = procs::ssh_target(&args) {
if cfg
.hosts
.iter()
.any(|p| procs::glob(p, &target.host) || procs::glob(p, &target.destination))
{
return Some(format!("ssh {}", target.host));
}
}
if cfg.root && (procs::euid(fg) == Some(0) || procs::euid(shell) == Some(0)) {
return Some("root".into());
}
if !cfg.paths.is_empty() {
if let Some(cwd) = pane.session.cwd() {
if cfg.paths.iter().any(|p| procs::path_glob(p, &cwd)) {
let home = std::env::var("HOME").unwrap_or_default();
let shown = cwd.to_string_lossy().into_owned();
let shown = match shown.strip_prefix(&home) {
Some(rest) if !home.is_empty() => format!("~{rest}"),
_ => shown,
};
return Some(shown);
}
}
}
None
}
pub(super) fn refresh_danger(&mut self) {
let mut changed = false;
for i in 0..self.panes.len() {
let reason = self.danger_reason(&self.panes[i]);
if self.panes[i].danger != reason {
self.panes[i].danger = reason;
changed = true;
}
}
if changed {
self.request_redraw();
}
}
pub(super) fn toggle_danger(&mut self) {
let Some(pane) = self.pane_mut(self.focused) else {
return;
};
let on = pane.danger.is_none();
pane.danger_manual = Some(on);
self.refresh_danger();
}
pub(super) fn tab_in_danger(&self, tab: &Tab) -> bool {
tab.root
.panes()
.iter()
.any(|id| self.pane(*id).is_some_and(|p| p.danger.is_some()))
}
pub(super) fn danger_hold(&mut self, targets: &[PaneId], key: &Key, bytes: &[u8]) -> bool {
let threshold = match self.config.danger.confirm.as_str() {
"off" => return false,
"caution" => Risk::Caution,
_ => Risk::Dangerous,
};
if !matches!(key, Key::Named(NamedKey::Enter)) {
return false;
}
let mods = self.mods;
if mods.control_key() || mods.alt_key() || mods.super_key() {
return false;
}
for &id in targets {
let Some(pane) = self.pane(id).filter(|p| p.danger.is_some()) else {
continue;
};
let line = {
let term = pane.session.term.lock();
if term.mode().contains(TermMode::ALT_SCREEN) {
continue;
}
crate::danger::command_line(&*term)
};
let (risk, reasons) = assess(&line);
if risk >= threshold {
self.danger_confirm = Some(DangerConfirm {
targets: targets.to_vec(),
bytes: bytes.to_vec(),
line,
reasons,
});
self.request_redraw();
return true;
}
}
false
}
pub(super) fn danger_confirm_open(&self) -> bool {
self.danger_confirm.is_some()
}
pub(super) fn danger_confirm_key(&mut self, event: &KeyEvent) {
if event.state != ElementState::Pressed || event.repeat {
return;
}
let Some(confirm) = self.danger_confirm.take() else {
return;
};
if matches!(event.logical_key, Key::Named(NamedKey::Enter)) {
for id in &confirm.targets {
if let Some(pane) = self.pane(*id) {
pane.session.term.lock().scroll_display(Scroll::Bottom);
pane.session.write(confirm.bytes.clone());
}
}
}
self.request_redraw();
}
fn danger_color(&self) -> [u8; 3] {
frame::hex_to_rgb(self.palette.ansi[1])
}
pub(super) fn danger_overlays(&self, pane: &Pane, rect: Rect, overlays: &mut Vec<Overlay>) {
if pane.danger.is_none() {
return;
}
let color = self.danger_color();
let tint = self.config.danger.tint.clamp(0.0, 0.3);
if tint > 0.0 {
overlays.push(Overlay {
rect,
color,
alpha: tint,
});
}
let scale = self
.gpu
.as_ref()
.map_or(1.0, |g| g.window.scale_factor() as f32);
let w = (2.0 * scale).round().max(1.0);
for r in [
Rect { h: w, ..rect },
Rect {
y: rect.y + rect.h - w,
h: w,
..rect
},
Rect { w, ..rect },
Rect {
x: rect.x + rect.w - w,
w,
..rect
},
] {
overlays.push(Overlay {
rect: r,
color,
alpha: 1.0,
});
}
}
pub(super) fn draw_danger_badge(&self, pane: &Pane, frame: &mut Frame) -> usize {
let Some(reason) = &pane.danger else { return 0 };
let short: String = reason.chars().take(30).collect();
let text = format!(" ⚠ {short} ");
let width = text.chars().count();
if frame.rows == 0 || frame.cols < width + 2 {
return 0;
}
let fg = frame::hex_to_rgb(self.palette.bg);
frame.put(
frame.rows - 1,
frame.cols - width,
&text,
fg,
self.danger_color(),
);
width
}
pub(super) fn draw_danger_confirm(&self, frame: &mut Frame) {
let Some(c) = &self.danger_confirm else {
return;
};
let fg = frame::hex_to_rgb(self.palette.fg);
let bg = frame::hex_to_rgb(self.palette.bg);
let red = self.danger_color();
let cols = frame.cols;
let width = cols.saturating_sub(4).max(1);
let mut line: String = c.line.chars().take(width * 2).collect();
if c.line.chars().count() > width * 2 {
line.pop();
line.push('…');
}
let chars: Vec<char> = line.chars().collect();
let rows: Vec<String> = chars.chunks(width).map(|w| w.iter().collect()).collect();
let height = 2 + rows.len() + 1;
if frame.rows < height {
return;
}
let reason = self
.pane(*c.targets.first().unwrap_or(&self.focused))
.and_then(|p| p.danger.clone())
.unwrap_or_default();
let mut row = frame.rows - height;
frame.fill(row, 0, bg, red);
frame.put(
row,
1,
&format!("⚠ Dangerous pane ({reason}). Run this?"),
bg,
red,
);
row += 1;
for r in &rows {
frame.fill(row, 0, fg, bg);
frame.put(row, 2, r, fg, bg);
row += 1;
}
frame.fill(row, 0, red, bg);
frame.put(row, 2, &c.reasons.join(", "), red, bg);
row += 1;
frame.fill(row, 0, fg, bg);
frame.put(row, 1, "Enter run it · any other key cancel", fg, bg);
frame.cursor = None;
}
}