map_gui/tools/
command.rs
1use std::collections::VecDeque;
2use std::time::Duration;
3
4use instant::Instant;
5use subprocess::{Communicator, Popen};
6
7use widgetry::tools::PopupMsg;
8use widgetry::{Color, EventCtx, GfxCtx, Line, Panel, State, Text, Transition, UpdateType};
9
10use crate::AppLike;
11
12pub struct RunCommand<A: AppLike> {
15 p: Popen,
16 comm: Option<Communicator>,
18 panel: Panel,
19 lines: VecDeque<String>,
20 max_capacity: usize,
21 started: Instant,
22 last_drawn: Instant,
23 show_success_popup: bool,
24 on_load: Option<Box<dyn FnOnce(&mut EventCtx, &mut A, bool, Vec<String>) -> Transition<A>>>,
27}
28
29impl<A: AppLike + 'static> RunCommand<A> {
30 pub fn new_state(
31 ctx: &mut EventCtx,
32 show_success_popup: bool,
33 args: Vec<String>,
34 on_load: Box<dyn FnOnce(&mut EventCtx, &mut A, bool, Vec<String>) -> Transition<A>>,
35 ) -> Box<dyn State<A>> {
36 info!("RunCommand: {}", args.join(" "));
37 match subprocess::Popen::create(
38 &args,
39 subprocess::PopenConfig {
40 stdout: subprocess::Redirection::Pipe,
41 stderr: subprocess::Redirection::Merge,
42 ..Default::default()
43 },
44 ) {
45 Ok(mut p) => {
46 let comm = Some(
47 p.communicate_start(None)
48 .limit_time(Duration::from_millis(0)),
49 );
50 let panel = ctx.make_loading_screen(Text::from("Starting command..."));
51 let max_capacity =
52 (0.8 * ctx.canvas.window_height / ctx.default_line_height()) as usize;
53 Box::new(RunCommand {
54 p,
55 comm,
56 panel,
57 lines: VecDeque::new(),
58 max_capacity,
59 started: Instant::now(),
60 last_drawn: Instant::now(),
61 show_success_popup,
62 on_load: Some(on_load),
63 })
64 }
65 Err(err) => PopupMsg::new_state(
66 ctx,
67 "Error",
68 vec![format!("Couldn't start command: {}", err)],
69 ),
70 }
71 }
72
73 fn read_output(&mut self) {
74 let mut new_lines = Vec::new();
75 let (stdout, stderr) = match self.comm.as_mut().unwrap().read() {
76 Ok(pair) => pair,
77 Err(err) => err.capture,
79 };
80 assert!(stderr.is_none());
81 if let Some(bytes) = stdout {
82 if let Ok(string) = String::from_utf8(bytes) {
83 if !string.is_empty() {
84 for line in string.split('\n') {
85 new_lines.push(line.to_string());
86 }
87 }
88 }
89 }
90 for line in new_lines {
91 if self.lines.len() == self.max_capacity {
92 self.lines.pop_front();
93 }
94 if line.contains('\r') {
95 let parts = line.split('\r').collect::<Vec<_>>();
100 if parts[0].is_empty() {
101 self.lines.pop_back();
102 self.lines.push_back(parts[1].to_string());
103 } else {
104 println!("> {}", parts[0]);
105 self.lines.push_back(parts[0].to_string());
106 }
107 } else {
108 println!("> {}", line);
109 self.lines.push_back(line);
110 }
111 }
112 }
113}
114
115impl<A: AppLike + 'static> State<A> for RunCommand<A> {
116 fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Transition<A> {
117 ctx.request_update(UpdateType::Game);
118 if ctx.input.nonblocking_is_update_event().is_none() {
119 return Transition::Keep;
120 }
121
122 self.read_output();
123
124 if abstutil::elapsed_seconds(self.last_drawn) > 0.1 {
126 let mut txt = Text::from(
127 Line(format!(
128 "Running command... {} so far",
129 geom::Duration::realtime_elapsed(self.started)
130 ))
131 .small_heading(),
132 );
133 for line in &self.lines {
134 if line.len() < 300 {
139 txt.add_line(line);
140 }
141 }
142 self.panel = ctx.make_loading_screen(txt);
143 self.last_drawn = Instant::now();
144 }
145
146 if let Some(status) = self.p.poll() {
147 let comm = self.comm.take().unwrap();
149 self.comm = Some(comm.limit_time(Duration::from_secs(10)));
150 self.read_output();
151 if self.lines.back().map(|x| x.is_empty()).unwrap_or(false) {
153 self.lines.pop_back();
154 }
155
156 let success = status.success();
157 let mut lines: Vec<String> = self.lines.drain(..).collect();
158 if !success {
159 lines.push(format!("Command failed: {:?}", status));
160 }
161 let mut transitions = vec![
162 Transition::Pop,
163 (self.on_load.take().unwrap())(ctx, app, success, lines.clone()),
164 ];
165 if !success || self.show_success_popup {
166 transitions.push(Transition::Push(PopupMsg::new_state(
167 ctx,
168 if success { "Success!" } else { "Failure!" },
169 lines,
170 )));
171 }
172 return Transition::Multi(transitions);
173 }
174
175 Transition::Keep
176 }
177
178 fn draw(&self, g: &mut GfxCtx, _: &A) {
179 g.clear(Color::BLACK);
180 self.panel.draw(g);
181 }
182}