mod app;
mod quiet_stderr;
mod render;
mod session;
pub use app::{
AnalysisSettings, AnalysisView, App, DISPOSITIONS, Effect, Focus, HitAreas, InputPane,
KEY_HELP, OPTION_COUNT, Phase, Screen, disposition_label,
};
pub use render::{draw, selected_text, unit_detail};
pub use session::{
AnalysisInput, BrowserSource, CurrentEu4Source, Inspection, Session, analyze_input,
};
use std::io::{self, IsTerminal, Write};
use std::sync::Arc;
use std::time::Duration;
use crossterm::cursor;
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
const TICK: Duration = Duration::from_millis(100);
pub fn run() -> io::Result<i32> {
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
eprintln!(
"error: bare `foch` opens an interactive terminal browser and needs a TTY; run `foch --help` for subcommands"
);
return Ok(2);
}
let _quiet_stderr = quiet_stderr::QuietStderr::new();
let mut session = Session::new(Arc::new(CurrentEu4Source));
enable_raw_mode()?;
let _guard = TerminalGuard;
let mut stdout = io::stdout();
execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
cursor::Hide
)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
loop {
let drawn = terminal.draw(|frame| draw(frame, session.app()))?;
if let Some(text) = session.take_selection(drawn.buffer)
&& let Err(error) = copy_to_clipboard(&text, terminal.backend_mut())
{
session.effect_failed(error);
}
if event::poll(TICK)? {
match event::read()? {
Event::Key(key) if !session.handle_key(key) => return Ok(0),
Event::Mouse(mouse) => session.handle_mouse(mouse),
Event::Resize(..) => terminal.clear()?,
_ => {}
}
}
if let Some(effect) = session.take_effect()
&& let Err(error) = carry_out(effect, terminal.backend_mut())
{
session.effect_failed(error);
}
session.pump(Duration::ZERO);
}
}
fn carry_out(effect: Effect, out: &mut impl Write) -> Result<(), String> {
match effect {
Effect::Copy(text) => copy_to_clipboard(&text, out),
Effect::OpenWorkshop(targets) => crate::cli::handler::input::open_workshop_pages(&targets)
.map(|_| ())
.map_err(|error| format!("could not open Steam: {error}")),
Effect::MouseCapture(true) => {
execute!(out, EnableMouseCapture).map_err(|error| error.to_string())
}
Effect::MouseCapture(false) => {
execute!(out, DisableMouseCapture).map_err(|error| error.to_string())
}
}
}
fn copy_to_clipboard(text: &str, out: &mut impl Write) -> Result<(), String> {
use crossterm::clipboard::CopyToClipboard;
match execute!(out, CopyToClipboard::to_clipboard_from(text)) {
Ok(()) => Ok(()),
Err(_) if cfg!(windows) => copy_with_clip_exe(text),
Err(error) => Err(format!("could not copy: {error}")),
}
}
fn copy_with_clip_exe(text: &str) -> Result<(), String> {
use std::process::{Command, Stdio};
let mut child = Command::new("clip")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| format!("could not copy: {error}"))?;
let bytes = std::iter::once(0xFEFF_u16)
.chain(text.encode_utf16())
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
let written = match child.stdin.take() {
Some(mut stdin) => stdin.write_all(&bytes),
None => Ok(()),
};
let waited = child.wait();
written
.and(waited)
.map(|_| ())
.map_err(|error| format!("could not copy: {error}"))
}
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let mut stdout = io::stdout();
let _ = execute!(
stdout,
DisableMouseCapture,
cursor::Show,
LeaveAlternateScreen
);
}
}