use crate::copy_mode::VisualMode;
use crate::scrollback_metadata::ScrollbackMark;
pub(super) fn render_fps_overlay(
ctx: &egui::Context,
show_fps: bool,
fps_value: f64,
frame_time_ms: f64,
) {
if !show_fps {
return;
}
egui::Area::new(egui::Id::new("fps_overlay"))
.anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
.inner_margin(egui::Margin::same(8))
.corner_radius(4.0)
.show(ui, |ui| {
ui.style_mut().visuals.override_text_color =
Some(egui::Color32::from_rgb(0, 255, 0));
ui.label(
egui::RichText::new(format!(
"FPS: {:.1}\nFrame: {:.2}ms",
fps_value, frame_time_ms
))
.monospace()
.size(14.0),
);
});
});
}
pub(super) fn render_resize_overlay(
ctx: &egui::Context,
resize_overlay_visible: bool,
dimensions: Option<(u32, u32, usize, usize)>,
) {
if !resize_overlay_visible {
return;
}
let Some((width_px, height_px, cols, rows)) = dimensions else {
return;
};
egui::Area::new(egui::Id::new("resize_overlay"))
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 220))
.inner_margin(egui::Margin::same(16))
.corner_radius(8.0)
.show(ui, |ui| {
ui.style_mut().visuals.override_text_color =
Some(egui::Color32::from_rgb(255, 255, 255));
ui.label(
egui::RichText::new(format!(
"{}×{}\n{}×{} px",
cols, rows, width_px, height_px
))
.monospace()
.size(24.0),
);
});
});
}
pub(super) fn render_toast_overlay(ctx: &egui::Context, message: Option<&str>) {
let Some(message) = message else {
return;
};
egui::Area::new(egui::Id::new("toast_notification"))
.anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, 60.0))
.order(egui::Order::Foreground)
.show(ctx, |ui| {
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
.inner_margin(egui::Margin::symmetric(20, 12))
.corner_radius(8.0)
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(80, 80, 80)))
.show(ui, |ui| {
ui.style_mut().visuals.override_text_color =
Some(egui::Color32::from_rgb(255, 255, 255));
ui.label(egui::RichText::new(message).size(16.0));
});
});
}
pub(super) fn render_scrollbar_mark_tooltip(ctx: &egui::Context, mark: Option<&ScrollbackMark>) {
let Some(mark) = mark else {
return;
};
let mut lines = Vec::new();
if let Some(ref cmd) = mark.command {
let truncated = if cmd.len() > 50 {
format!("{}...", &cmd[..47])
} else {
cmd.clone()
};
lines.push(format!("Command: {}", truncated));
}
if let Some(start_time) = mark.start_time {
use chrono::{DateTime, Local, Utc};
let dt = DateTime::<Utc>::from_timestamp_millis(start_time as i64)
.expect("window_state: start_time millis out of valid timestamp range");
let local: DateTime<Local> = dt.into();
lines.push(format!("Time: {}", local.format("%H:%M:%S")));
}
if let Some(duration_ms) = mark.duration_ms {
if duration_ms < 1000 {
lines.push(format!("Duration: {}ms", duration_ms));
} else if duration_ms < 60000 {
lines.push(format!("Duration: {:.1}s", duration_ms as f64 / 1000.0));
} else {
let mins = duration_ms / 60000;
let secs = (duration_ms % 60000) / 1000;
lines.push(format!("Duration: {}m {}s", mins, secs));
}
}
if let Some(exit_code) = mark.exit_code {
lines.push(format!("Exit: {}", exit_code));
}
let tooltip_text = lines.join("\n");
let mouse_pos = ctx.pointer_hover_pos().unwrap_or(egui::pos2(100.0, 100.0));
let tooltip_x = (mouse_pos.x - 180.0).max(10.0);
let tooltip_y = (mouse_pos.y - 20.0).max(10.0);
egui::Area::new(egui::Id::new("scrollbar_mark_tooltip"))
.order(egui::Order::Tooltip)
.fixed_pos(egui::pos2(tooltip_x, tooltip_y))
.show(ctx, |ui| {
ui.set_min_width(150.0);
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
.inner_margin(egui::Margin::same(8))
.corner_radius(4.0)
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(80, 80, 80)))
.show(ui, |ui| {
ui.set_min_width(140.0);
ui.style_mut().visuals.override_text_color =
Some(egui::Color32::from_rgb(220, 220, 220));
ui.label(egui::RichText::new(&tooltip_text).monospace().size(12.0));
});
});
}
pub(super) fn render_copy_mode_status_bar(
ctx: &egui::Context,
active: bool,
show_status: bool,
is_searching: bool,
visual_mode: VisualMode,
mode_text_str: &str,
status: &str,
) {
if !active || !show_status {
return;
}
let color = if is_searching {
egui::Color32::from_rgb(255, 165, 0)
} else {
match visual_mode {
VisualMode::None => egui::Color32::from_rgb(100, 200, 100),
VisualMode::Char | VisualMode::Line | VisualMode::Block => {
egui::Color32::from_rgb(100, 150, 255)
}
}
};
egui::Area::new(egui::Id::new("copy_mode_status_bar"))
.anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(0.0, 0.0))
.order(egui::Order::Foreground)
.show(ctx, |ui| {
let available_width = ui.available_width();
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 230))
.inner_margin(egui::Margin::symmetric(12, 6))
.show(ui, |ui| {
ui.set_min_width(available_width);
ui.horizontal(|ui| {
ui.label(
egui::RichText::new(mode_text_str)
.monospace()
.size(13.0)
.color(color)
.strong(),
);
ui.separator();
ui.label(
egui::RichText::new(status)
.monospace()
.size(12.0)
.color(egui::Color32::from_rgb(200, 200, 200)),
);
});
});
});
}
pub(super) fn render_trigger_prompt_dialog(
ctx: &egui::Context,
trigger_state: &mut crate::app::window_state::TriggerState,
) {
if trigger_state.pending_trigger_actions.is_empty() {
trigger_state.trigger_prompt_dialog_open = false;
trigger_state.trigger_prompt_activated_frame = None;
return;
}
if !trigger_state.trigger_prompt_dialog_open {
trigger_state.trigger_prompt_dialog_open = true;
trigger_state.trigger_prompt_activated_frame = Some(ctx.cumulative_frame_nr());
}
let activated_frame = trigger_state.trigger_prompt_activated_frame.unwrap_or(0);
let current_frame = ctx.cumulative_frame_nr();
let trigger_name = trigger_state.pending_trigger_actions[0]
.trigger_name
.clone();
let description = trigger_state.pending_trigger_actions[0].description.clone();
let pending_count = trigger_state.pending_trigger_actions.len();
let mut approved = false;
let mut always_approve = false;
let mut denied = false;
egui::Window::new("Trigger Action Confirmation")
.id(egui::Id::new("trigger_prompt_dialog"))
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
.show(ctx, |ui| {
ui.set_min_width(380.0);
ui.set_max_width(500.0);
ui.add_space(4.0);
ui.label(
egui::RichText::new("Trigger Action Requires Confirmation")
.strong()
.size(15.0),
);
ui.add_space(8.0);
ui.label(format!("Trigger: {}", trigger_name));
ui.add_space(4.0);
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 40))
.inner_margin(egui::Margin::same(8))
.corner_radius(4.0)
.show(ui, |ui| {
ui.label(egui::RichText::new(&description).monospace());
});
ui.add_space(4.0);
ui.label(
egui::RichText::new(
"A trigger matched terminal output and wants to run this action.",
)
.weak()
.small(),
);
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui
.button(egui::RichText::new("Deny").color(egui::Color32::from_rgb(220, 60, 60)))
.clicked()
&& current_frame > activated_frame
{
denied = true;
}
ui.add_space(4.0);
if ui.button("Allow Once").clicked() && current_frame > activated_frame {
approved = true;
}
ui.add_space(4.0);
if ui
.button(
egui::RichText::new("Always Allow")
.color(egui::Color32::from_rgb(80, 180, 80)),
)
.clicked()
&& current_frame > activated_frame
{
always_approve = true;
approved = true;
}
});
if pending_count > 1 {
ui.add_space(4.0);
ui.label(
egui::RichText::new(format!("({} more pending actions)", pending_count - 1))
.weak()
.small(),
);
}
});
if denied || approved {
let pending = trigger_state.pending_trigger_actions.remove(0);
if approved {
if always_approve {
trigger_state
.always_allow_trigger_ids
.insert(pending.trigger_id);
}
trigger_state.approved_pending_actions.push(pending.action);
}
if trigger_state.pending_trigger_actions.is_empty() {
trigger_state.trigger_prompt_dialog_open = false;
trigger_state.trigger_prompt_activated_frame = None;
} else {
trigger_state.trigger_prompt_activated_frame = Some(ctx.cumulative_frame_nr());
}
}
}
pub(super) fn render_pane_identify_overlay(
ctx: &egui::Context,
pane_bounds: &[(usize, crate::pane::PaneBounds)],
) {
for (index, bounds) in pane_bounds {
let center_x = bounds.x + bounds.width / 2.0;
let center_y = bounds.y + bounds.height / 2.0;
egui::Area::new(egui::Id::new(format!("pane_identify_{}", index)))
.fixed_pos(egui::pos2(center_x - 30.0, center_y - 30.0))
.order(egui::Order::Foreground)
.interactable(false)
.show(ctx, |ui| {
egui::Frame::NONE
.fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
.inner_margin(egui::Margin::symmetric(16, 8))
.corner_radius(8.0)
.stroke(egui::Stroke::new(
2.0,
egui::Color32::from_rgb(100, 200, 255),
))
.show(ui, |ui| {
ui.label(
egui::RichText::new(format!("Pane {}", index))
.monospace()
.size(28.0)
.color(egui::Color32::from_rgb(100, 200, 255)),
);
});
});
}
}