use std::time::Duration;
use eframe::egui;
use hyprcorrect_core::runtime::{self, ReviewRequest};
const APP_ID: &str = "hyprcorrect-review";
const REFOCUS_DELAY_MS: u64 = 280;
const WINDOW_WIDTH: f32 = 560.0;
const MIN_WINDOW_HEIGHT: f32 = 240.0;
const MAX_WINDOW_HEIGHT: f32 = 900.0;
pub(crate) fn run() {
let request = match runtime::read_review_request() {
Ok(Some(req)) => req,
Ok(None) => return,
Err(e) => {
eprintln!("hyprcorrect: could not read review request: {e}");
return;
}
};
let estimated_height = estimate_window_height(&request);
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_app_id(APP_ID)
.with_title("hyprcorrect — Review")
.with_inner_size([WINDOW_WIDTH, estimated_height])
.with_min_inner_size([WINDOW_WIDTH, MIN_WINDOW_HEIGHT])
.with_resizable(true),
vsync: false,
..Default::default()
};
let _ = eframe::run_native(
"hyprcorrect — Review",
options,
Box::new(move |cc| {
crate::prefs::install_glyph_fonts(&cc.egui_ctx);
Ok(Box::new(ReviewApp::new(request)))
}),
);
}
struct ReviewApp {
request: ReviewRequest,
decision: Option<&'static str>,
}
impl ReviewApp {
fn new(request: ReviewRequest) -> Self {
Self {
request,
decision: None,
}
}
}
impl eframe::App for ReviewApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let (apply, cancel) = ctx.input(|i| {
(
i.key_pressed(egui::Key::Enter),
i.key_pressed(egui::Key::Escape),
)
});
if apply {
self.decision = Some("apply");
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} else if cancel {
self.decision = Some("cancel");
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
egui::TopBottomPanel::bottom("review_actions")
.resizable(false)
.show(ctx, |ui| {
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button("Cancel (Esc)").clicked() {
self.decision = Some("cancel");
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
let apply_label = egui::RichText::new("Apply (Enter)")
.color(egui::Color32::from_rgb(90, 200, 120));
if ui.button(apply_label).clicked() {
self.decision = Some("apply");
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.add_space(8.0);
});
egui::CentralPanel::default()
.frame(
egui::Frame::central_panel(&ctx.style())
.inner_margin(egui::Margin::symmetric(20, 18)),
)
.show(ctx, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
ui.heading("Review correction");
ui.add_space(12.0);
section_label(ui, "Original");
show_block(ui, &self.request.original, egui::Color32::from_gray(170));
ui.add_space(14.0);
section_label(ui, "Proposed");
show_block(ui, &self.request.corrected, egui::Color32::from_gray(230));
});
});
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
let decision = self.decision.unwrap_or("cancel");
let action = match decision {
"apply" => "review-apply",
_ => "review-cancel",
};
if let Err(e) = std::fs::write(runtime::action_path(), action) {
eprintln!("hyprcorrect: could not write review action: {e}");
return;
}
std::thread::sleep(Duration::from_millis(REFOCUS_DELAY_MS));
notify_daemon();
}
}
fn estimate_window_height(request: &ReviewRequest) -> f32 {
const CHARS_PER_LINE: usize = 65;
const LINE_HEIGHT: f32 = 22.0;
const CHROME: f32 = 200.0;
let lines = |s: &str| -> usize {
s.lines()
.map(|line| line.chars().count().max(1).div_ceil(CHARS_PER_LINE))
.sum::<usize>()
.max(1)
};
let total_lines = lines(&request.original) + lines(&request.corrected);
let body_height = total_lines as f32 * LINE_HEIGHT;
(CHROME + body_height).clamp(MIN_WINDOW_HEIGHT, MAX_WINDOW_HEIGHT)
}
fn section_label(ui: &mut egui::Ui, text: &str) {
ui.label(egui::RichText::new(text).strong().size(14.0));
ui.add_space(4.0);
}
fn show_block(ui: &mut egui::Ui, text: &str, color: egui::Color32) {
egui::Frame::new()
.fill(egui::Color32::from_gray(40))
.corner_radius(egui::CornerRadius::same(6))
.inner_margin(egui::Margin::symmetric(10, 8))
.show(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.label(egui::RichText::new(text).color(color).size(14.0));
});
}
fn notify_daemon() {
let Ok(Some(pid)) = runtime::read_daemon_pid() else {
return;
};
#[cfg(unix)]
{
let _ = std::process::Command::new("kill")
.args(["-USR1", &pid.to_string()])
.output();
}
#[cfg(not(unix))]
let _ = pid;
}