foch_cli/tui/browser/
mod.rs1mod app;
6mod quiet_stderr;
7mod render;
8mod session;
9
10pub use app::{
11 AnalysisSettings, AnalysisView, App, DISPOSITIONS, Effect, Focus, HitAreas, InputPane,
12 KEY_HELP, OPTION_COUNT, Phase, Screen, disposition_label,
13};
14pub use render::{draw, selected_text, unit_detail};
15pub use session::{
16 AnalysisInput, BrowserSource, CurrentEu4Source, Inspection, Session, analyze_input,
17};
18
19use std::io::{self, IsTerminal, Write};
20use std::sync::Arc;
21use std::time::Duration;
22
23use crossterm::cursor;
24use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event};
25use crossterm::execute;
26use crossterm::terminal::{
27 EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
28};
29use ratatui::Terminal;
30use ratatui::backend::CrosstermBackend;
31
32const TICK: Duration = Duration::from_millis(100);
33
34pub fn run() -> io::Result<i32> {
36 if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
37 eprintln!(
38 "error: bare `foch` opens an interactive terminal browser and needs a TTY; run `foch --help` for subcommands"
39 );
40 return Ok(2);
41 }
42 let _quiet_stderr = quiet_stderr::QuietStderr::new();
44 let mut session = Session::new(Arc::new(CurrentEu4Source));
45
46 enable_raw_mode()?;
47 let _guard = TerminalGuard;
48 let mut stdout = io::stdout();
49 execute!(
50 stdout,
51 EnterAlternateScreen,
52 EnableMouseCapture,
53 cursor::Hide
54 )?;
55 let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
56
57 loop {
58 let drawn = terminal.draw(|frame| draw(frame, session.app()))?;
59 if let Some(text) = session.take_selection(drawn.buffer)
60 && let Err(error) = copy_to_clipboard(&text, terminal.backend_mut())
61 {
62 session.effect_failed(error);
63 }
64 if event::poll(TICK)? {
65 match event::read()? {
66 Event::Key(key) if !session.handle_key(key) => return Ok(0),
67 Event::Mouse(mouse) => session.handle_mouse(mouse),
68 Event::Resize(..) => terminal.clear()?,
69 _ => {}
70 }
71 }
72 if let Some(effect) = session.take_effect()
73 && let Err(error) = carry_out(effect, terminal.backend_mut())
74 {
75 session.effect_failed(error);
76 }
77 session.pump(Duration::ZERO);
78 }
79}
80
81fn carry_out(effect: Effect, out: &mut impl Write) -> Result<(), String> {
82 match effect {
83 Effect::Copy(text) => copy_to_clipboard(&text, out),
84 Effect::OpenWorkshop(targets) => crate::cli::handler::input::open_workshop_pages(&targets)
85 .map(|_| ())
86 .map_err(|error| format!("could not open Steam: {error}")),
87 Effect::MouseCapture(true) => {
88 execute!(out, EnableMouseCapture).map_err(|error| error.to_string())
89 }
90 Effect::MouseCapture(false) => {
91 execute!(out, DisableMouseCapture).map_err(|error| error.to_string())
92 }
93 }
94}
95
96fn copy_to_clipboard(text: &str, out: &mut impl Write) -> Result<(), String> {
99 use crossterm::clipboard::CopyToClipboard;
100 match execute!(out, CopyToClipboard::to_clipboard_from(text)) {
101 Ok(()) => Ok(()),
102 Err(_) if cfg!(windows) => copy_with_clip_exe(text),
103 Err(error) => Err(format!("could not copy: {error}")),
104 }
105}
106
107fn copy_with_clip_exe(text: &str) -> Result<(), String> {
108 use std::process::{Command, Stdio};
109 let mut child = Command::new("clip")
110 .stdin(Stdio::piped())
111 .stdout(Stdio::null())
112 .stderr(Stdio::null())
113 .spawn()
114 .map_err(|error| format!("could not copy: {error}"))?;
115 let bytes = std::iter::once(0xFEFF_u16)
117 .chain(text.encode_utf16())
118 .flat_map(u16::to_le_bytes)
119 .collect::<Vec<_>>();
120 let written = match child.stdin.take() {
121 Some(mut stdin) => stdin.write_all(&bytes),
122 None => Ok(()),
123 };
124 let waited = child.wait();
125 written
126 .and(waited)
127 .map(|_| ())
128 .map_err(|error| format!("could not copy: {error}"))
129}
130
131struct TerminalGuard;
132
133impl Drop for TerminalGuard {
134 fn drop(&mut self) {
135 let _ = disable_raw_mode();
136 let mut stdout = io::stdout();
137 let _ = execute!(
138 stdout,
139 DisableMouseCapture,
140 cursor::Show,
141 LeaveAlternateScreen
142 );
143 }
144}