use std::any::Any;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crossterm::event::KeyEvent;
use foch::game::eu4::base::snapshot::BaseBuildObserver;
use foch::input::{Config, CurrentEu4Input, inspect_current_eu4_input};
use foch::merge::{CancellationToken, MergeError, MergeProgress, ProgressObserver, analyze_merge};
use super::app::{AnalysisView, App, AppCommand, Effect, Phase};
use crate::cli::arg::MergeArgs;
use crate::cli::handler::merge::{PreparedMerge, prepare_merge};
#[derive(Clone, Debug)]
pub enum AnalysisInput {
Current(Box<CurrentEu4Input>),
Path { path: PathBuf, config: Config },
}
pub struct Inspection {
pub input: CurrentEu4Input,
pub analysis: Option<AnalysisInput>,
pub game_base_available: bool,
}
pub trait BrowserSource: Send + Sync {
fn inspect(&self) -> Inspection;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CurrentEu4Source;
impl BrowserSource for CurrentEu4Source {
fn inspect(&self) -> Inspection {
let input = inspect_current_eu4_input();
let analysis = input
.can_select_mods(false)
.then(|| AnalysisInput::Current(Box::new(input.clone())));
let game_base_available = input_base_ready(&input);
Inspection {
input,
analysis,
game_base_available,
}
}
}
fn input_base_ready(input: &CurrentEu4Input) -> bool {
input.can_select_mods(true)
}
enum WorkerMessage {
Inspected {
generation: u64,
inspection: Box<Inspection>,
},
Progress {
generation: u64,
progress: MergeProgress,
},
Analyzed {
generation: u64,
result: Result<Box<AnalysisView>, String>,
},
Built {
generation: u64,
result: Result<Vec<String>, String>,
},
BuildStage {
generation: u64,
name: String,
done: bool,
},
}
struct ChannelProgress {
generation: u64,
tx: Sender<WorkerMessage>,
}
impl ProgressObserver for ChannelProgress {
fn update(&self, progress: MergeProgress) {
let _ = self.tx.send(WorkerMessage::Progress {
generation: self.generation,
progress,
});
}
}
pub struct Session {
app: App,
source: Arc<dyn BrowserSource>,
tx: Sender<WorkerMessage>,
rx: Receiver<WorkerMessage>,
generation: u64,
pending: Option<AnalysisInput>,
cancellation: Option<CancellationToken>,
busy: bool,
}
impl Session {
pub fn new(source: Arc<dyn BrowserSource>) -> Self {
let (tx, rx) = mpsc::channel();
let mut session = Self {
app: App::default(),
source,
tx,
rx,
generation: 0,
pending: None,
cancellation: None,
busy: false,
};
session.start_inspection(false);
session
}
pub fn app(&self) -> &App {
&self.app
}
pub fn is_busy(&self) -> bool {
self.busy
}
pub fn handle_key(&mut self, key: KeyEvent) -> bool {
match self.app.handle_key(key) {
Some(AppCommand::Quit) => {
self.cancel_running();
return false;
}
Some(AppCommand::Analyze) => self.start_analysis(),
Some(AppCommand::BuildBaseData) => self.start_build(),
Some(AppCommand::Refresh) => {
let analyzed = matches!(self.app.phase, Phase::Reviewed(_) | Phase::Failed(_));
self.start_inspection(analyzed);
}
None => {}
}
true
}
pub fn take_effect(&mut self) -> Option<Effect> {
self.app.effect.take()
}
pub fn take_selection(&mut self, buffer: &ratatui::buffer::Buffer) -> Option<String> {
if !std::mem::take(&mut self.app.copy_selection) {
return None;
}
let text =
super::render::selected_text(self.app.selection?, self.app.selection_bounds, buffer);
self.app.notice = Some(format!(
"Copied {} selected characters.",
text.chars().count()
));
Some(text)
}
pub fn effect_failed(&mut self, error: String) {
self.app.notice = None;
self.app.refusal = Some(error);
}
pub fn handle_mouse(&mut self, mouse: crossterm::event::MouseEvent) {
self.app.handle_mouse(mouse);
}
pub fn pump(&mut self, timeout: Duration) -> bool {
let mut changed = false;
let mut next = self.rx.recv_timeout(timeout).ok();
while let Some(message) = next {
changed |= self.apply(message);
next = self.rx.try_recv().ok();
}
changed
}
pub fn wait_idle(&mut self) {
while self.busy {
self.pump(Duration::from_millis(50));
}
}
fn apply(&mut self, message: WorkerMessage) -> bool {
match message {
WorkerMessage::Inspected {
generation,
inspection,
} if generation == self.generation => {
let Inspection {
input,
analysis,
game_base_available,
} = *inspection;
self.busy = false;
self.app
.inspected(input, analysis.is_some(), game_base_available);
if let Some(AnalysisInput::Path { path, .. }) = &analysis {
self.app.source_path = Some(path.clone());
}
self.pending = analysis;
if self.app.take_analyze_after_inspection() {
self.start_analysis();
}
true
}
WorkerMessage::Progress {
generation,
progress,
} if generation == self.generation => {
self.app.progress(progress);
true
}
WorkerMessage::BuildStage {
generation,
name,
done,
} if generation == self.generation => {
self.app.build_stage(name, done);
true
}
WorkerMessage::Built { generation, result } if generation == self.generation => {
self.busy = false;
self.app.built(result);
self.start_inspection(false);
true
}
WorkerMessage::Analyzed { generation, result } if generation == self.generation => {
self.busy = false;
self.cancellation = None;
self.app.analyzed(result.map(|view| *view));
true
}
_ => false,
}
}
fn start_inspection(&mut self, analyze_after: bool) {
self.cancel_running();
self.generation += 1;
self.pending = None;
self.busy = true;
self.app.inspecting(analyze_after);
let generation = self.generation;
let source = Arc::clone(&self.source);
let tx = self.tx.clone();
std::thread::spawn(move || {
let message = match catch_unwind(AssertUnwindSafe(|| source.inspect())) {
Ok(inspection) => WorkerMessage::Inspected {
generation,
inspection: Box::new(inspection),
},
Err(panic) => WorkerMessage::Analyzed {
generation,
result: Err(format!(
"input inspection panicked: {}",
panic_text(&*panic)
)),
},
};
let _ = tx.send(message);
});
}
fn start_build(&mut self) {
let Some(args) = self.app.data_build_args() else {
return;
};
self.cancel_running();
self.generation += 1;
self.busy = true;
self.app.building();
let generation = self.generation;
let tx = self.tx.clone();
let stages = self.tx.clone();
spawn_with_merge_stack(move || {
let observer = BaseBuildObserver::silent(&args.game_name).with_stage_listener(
Box::new(move |name: &str, done: bool| {
let _ = stages.send(WorkerMessage::BuildStage {
generation,
name: name.to_string(),
done,
});
}),
);
let result = catch_unwind(AssertUnwindSafe(|| {
let config =
foch::input::load_config_read_only().map_err(|error| error.to_string())?;
crate::cli::handler::data::run_data_build(&args, &config, observer)
.map_err(|error| error.to_string())
}))
.unwrap_or_else(|panic| {
Err(format!("base data build panicked: {}", panic_text(&*panic)))
});
let _ = tx.send(WorkerMessage::Built { generation, result });
});
}
fn start_analysis(&mut self) {
let Some(input) = self.pending.take() else {
return;
};
let out_dir = unused_analysis_target();
let merge_args = self.app.merge_args(out_dir);
self.generation += 1;
self.busy = true;
self.app.analyzing();
let generation = self.generation;
let cancellation = CancellationToken::new();
self.cancellation = Some(cancellation.clone());
let tx = self.tx.clone();
spawn_with_merge_stack(move || {
let progress = ChannelProgress {
generation,
tx: tx.clone(),
};
let result = catch_unwind(AssertUnwindSafe(|| {
analyze_input(&merge_args, input, &progress, &cancellation)
}))
.unwrap_or_else(|panic| Err(format!("analysis panicked: {}", panic_text(&*panic))))
.map(Box::new);
let _ = tx.send(WorkerMessage::Analyzed { generation, result });
});
}
fn cancel_running(&mut self) {
if let Some(cancellation) = self.cancellation.take() {
cancellation.cancel();
}
}
}
impl Drop for Session {
fn drop(&mut self) {
self.cancel_running();
}
}
fn spawn_with_merge_stack(work: impl FnOnce() + Send + 'static) {
std::thread::Builder::new()
.name("foch-tui-analysis".to_string())
.stack_size(64 * 1024 * 1024)
.spawn(work)
.expect("spawn analysis thread");
}
pub fn analyze_input(
merge_args: &MergeArgs,
input: AnalysisInput,
progress: &dyn ProgressObserver,
cancellation: &CancellationToken,
) -> Result<AnalysisView, String> {
let (config, current) = match input {
AnalysisInput::Current(current) => (Config::default(), Some(*current)),
AnalysisInput::Path { config, .. } => (config, None),
};
let PreparedMerge { request, options } =
prepare_merge(merge_args, config, current).map_err(|error| error.to_string())?;
let analyzed =
analyze_merge(request, options, progress, cancellation).map_err(|error| match error {
MergeError::Cancelled => "analysis cancelled".to_string(),
other => other.to_string(),
})?;
debug_assert!(
!merge_args.out.exists(),
"analysis must not create its target"
);
Ok(AnalysisView::from_analyzed(&analyzed))
}
fn unused_analysis_target() -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_nanos());
std::env::temp_dir().join(format!("foch-tui-analysis-{}-{nanos}", std::process::id()))
}
fn panic_text(panic: &(dyn Any + Send)) -> String {
panic
.downcast_ref::<&str>()
.map(|text| (*text).to_string())
.or_else(|| panic.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string())
}