r/interpreter/builtins/
progress.rs1use indicatif::{ProgressBar, ProgressStyle};
17
18use super::CallArgs;
19use crate::interpreter::value::*;
20use crate::interpreter::BuiltinContext;
21use crate::interpreter::Interpreter;
22use minir_macros::interpreter_builtin;
23
24pub struct ProgressBarState {
28 bar: ProgressBar,
30 min: f64,
32 max: f64,
34 value: f64,
36}
37
38impl Interpreter {
43 pub(crate) fn add_progress_bar(&self, state: ProgressBarState) -> usize {
45 let mut bars = self.progress_bars.borrow_mut();
46 let id = bars.len();
47 bars.push(Some(state));
48 id
49 }
50
51 pub(crate) fn close_progress_bar(&self, id: usize) -> bool {
54 let mut bars = self.progress_bars.borrow_mut();
55 if let Some(slot) = bars.get_mut(id) {
56 if let Some(state) = slot.take() {
57 state.bar.finish_and_clear();
58 return true;
59 }
60 }
61 false
62 }
63}
64
65fn progress_bar_value(id: usize) -> RValue {
71 let mut rv = RVector::from(Vector::Integer(
72 vec![Some(i64::try_from(id).unwrap_or(0))].into(),
73 ));
74 rv.set_attr(
75 "class".to_string(),
76 RValue::vec(Vector::Character(
77 vec![Some("txtProgressBar".to_string())].into(),
78 )),
79 );
80 RValue::Vector(rv)
81}
82
83fn progress_bar_id(val: &RValue) -> Option<usize> {
85 val.as_vector()
86 .and_then(|v| v.as_integer_scalar())
87 .and_then(|i| usize::try_from(i).ok())
88}
89
90pub fn is_progress_bar(val: &RValue) -> bool {
92 match val {
93 RValue::Vector(rv) => rv
94 .class()
95 .map(|cls| cls.iter().any(|c| c == "txtProgressBar"))
96 .unwrap_or(false),
97 _ => false,
98 }
99}
100
101fn value_to_position(value: f64, min: f64, max: f64, total: u64) -> u64 {
103 if max <= min {
104 return 0;
105 }
106 let fraction = ((value - min) / (max - min)).clamp(0.0, 1.0);
107 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
109 let pos = (fraction * total as f64).round() as u64;
110 pos
111}
112
113fn style_for(style: i64) -> ProgressStyle {
115 match style {
116 1 => ProgressStyle::with_template(" |{bar:50}|")
117 .expect("valid progress template")
118 .progress_chars("= "),
119 2 => ProgressStyle::with_template(" {percent}%")
121 .expect("valid progress template")
122 .progress_chars("= "),
123 _ => ProgressStyle::with_template(" |{bar:50}| {percent}%")
125 .expect("valid progress template")
126 .progress_chars("= "),
127 }
128}
129
130#[interpreter_builtin(name = "txtProgressBar")]
144fn interp_txt_progress_bar(
145 args: &[RValue],
146 named: &[(String, RValue)],
147 context: &BuiltinContext,
148) -> Result<RValue, RError> {
149 let call_args = CallArgs::new(args, named);
150
151 let min = call_args
152 .value("min", 0)
153 .and_then(|v| v.as_vector()?.as_double_scalar())
154 .unwrap_or(0.0);
155 let max = call_args
156 .value("max", 1)
157 .and_then(|v| v.as_vector()?.as_double_scalar())
158 .unwrap_or(1.0);
159 let style = call_args.integer_or("style", 2, 3);
160
161 if max <= min {
162 return Err(RError::new(
163 RErrorKind::Argument,
164 format!("'max' ({max}) must be greater than 'min' ({min}) in txtProgressBar()"),
165 ));
166 }
167
168 let total = 1000u64; let bar = ProgressBar::new(total);
170 bar.set_style(style_for(style));
171 bar.set_position(0);
172
173 let state = ProgressBarState {
174 bar,
175 min,
176 max,
177 value: min,
178 };
179
180 let interp = context.interpreter();
181 let id = interp.add_progress_bar(state);
182 Ok(progress_bar_value(id))
183}
184
185#[interpreter_builtin(name = "setTxtProgressBar", min_args = 2)]
191fn interp_set_txt_progress_bar(
192 args: &[RValue],
193 named: &[(String, RValue)],
194 context: &BuiltinContext,
195) -> Result<RValue, RError> {
196 let call_args = CallArgs::new(args, named);
197
198 let pb_val = call_args
199 .value("pb", 0)
200 .ok_or_else(|| RError::new(RErrorKind::Argument, "argument 'pb' is missing".to_string()))?;
201 let id = progress_bar_id(pb_val).ok_or_else(|| {
202 RError::new(
203 RErrorKind::Argument,
204 "argument 'pb' is not a valid txtProgressBar".to_string(),
205 )
206 })?;
207
208 let value = call_args
209 .value("value", 1)
210 .and_then(|v| v.as_vector()?.as_double_scalar())
211 .ok_or_else(|| {
212 RError::new(
213 RErrorKind::Argument,
214 "argument 'value' is missing or not numeric".to_string(),
215 )
216 })?;
217
218 let interp = context.interpreter();
219 let mut bars = interp.progress_bars.borrow_mut();
220 if let Some(Some(state)) = bars.get_mut(id) {
221 state.value = value;
222 let total = state.bar.length().unwrap_or(1000);
223 let pos = value_to_position(value, state.min, state.max, total);
224 state.bar.set_position(pos);
225 } else {
226 return Err(RError::new(
227 RErrorKind::Argument,
228 format!("progress bar {id} has been closed or does not exist"),
229 ));
230 }
231
232 context.interpreter().set_invisible();
233 Ok(RValue::Null)
234}
235
236#[interpreter_builtin(name = "getTxtProgressBar", min_args = 1)]
241fn interp_get_txt_progress_bar(
242 args: &[RValue],
243 named: &[(String, RValue)],
244 context: &BuiltinContext,
245) -> Result<RValue, RError> {
246 let call_args = CallArgs::new(args, named);
247
248 let pb_val = call_args
249 .value("pb", 0)
250 .ok_or_else(|| RError::new(RErrorKind::Argument, "argument 'pb' is missing".to_string()))?;
251 let id = progress_bar_id(pb_val).ok_or_else(|| {
252 RError::new(
253 RErrorKind::Argument,
254 "argument 'pb' is not a valid txtProgressBar".to_string(),
255 )
256 })?;
257
258 let interp = context.interpreter();
259 let bars = interp.progress_bars.borrow();
260 if let Some(Some(state)) = bars.get(id) {
261 Ok(RValue::vec(Vector::Double(vec![Some(state.value)].into())))
262 } else {
263 Err(RError::new(
264 RErrorKind::Argument,
265 format!("progress bar {id} has been closed or does not exist"),
266 ))
267 }
268}
269
270