mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 08:52:26 +00:00
Merge main into fix/script-console-report-text
This commit is contained in:
@@ -208,7 +208,7 @@ impl Console {
|
||||
|
||||
/// `c` as the console shows it: a control character as its Control Picture, a line or paragraph
|
||||
/// separator as U+2424, anything else as it is.
|
||||
fn shown(c: char) -> char {
|
||||
pub(crate) fn shown(c: char) -> char {
|
||||
match c {
|
||||
'\u{0}'..='\u{1f}' => char::from_u32(0x2400 + c as u32).unwrap_or('\u{fffd}'),
|
||||
'\u{7f}' => '\u{2421}',
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
//! `alert(message[, title, errorIcon])`, `confirm(message[, noAsDflt, title])` and
|
||||
//! `prompt(message, preset[, title])`: the dialogs a script opens while it runs.
|
||||
//!
|
||||
//! A message and a preset arrive as the string `String(value)` gives, a title the same way unless
|
||||
//! it is left out or `undefined` ([`DEFAULT_TITLE`]), and `errorIcon` and `noAsDflt` as the
|
||||
//! boolean `Boolean(value)` gives. `alert` returns `undefined`, `confirm` `true` for Yes and
|
||||
//! `false` for No, and `prompt` the text in its field for OK and `null` for Cancel.
|
||||
//!
|
||||
//! In the app the run waits at the call for the person's answer, and spends no fuel while it
|
||||
//! waits; every dialog also has Stop, which ends the run and puts everything back. A run with
|
||||
//! nobody to ask takes its answers, in order, from `script.run`'s `answers`; a call with no
|
||||
//! answer left, or with the next answer for another kind of dialog, ends the run. The report's
|
||||
//! `dialogs` lists each dialog and its answer.
|
||||
//!
|
||||
//! The values here are VectorCraft's own limits: [`MAX_MESSAGE_BYTES`], [`MAX_TITLE_BYTES`],
|
||||
//! [`MAX_PROMPT_BYTES`], [`MAX_ANSWERS`], [`MAX_ANSWERS_BYTES`], [`MAX_RECORDS`] and
|
||||
//! [`MAX_RECORD_BYTES`].
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_js::{Arg, Data, HostError, HostFunction, Realm, RunError};
|
||||
|
||||
use super::{Host, console};
|
||||
|
||||
/// The most bytes (UTF-8) of a dialog's message.
|
||||
pub const MAX_MESSAGE_BYTES: usize = 16 << 10;
|
||||
/// The most bytes (UTF-8) of a dialog's title.
|
||||
pub const MAX_TITLE_BYTES: usize = 256;
|
||||
/// The most bytes (UTF-8) of a prompt's preset and of the text it returns.
|
||||
pub const MAX_PROMPT_BYTES: usize = 16 << 10;
|
||||
/// The most answers `script.run` takes.
|
||||
pub const MAX_ANSWERS: usize = 10_000;
|
||||
/// The most bytes of `answers`, as JSON.
|
||||
pub const MAX_ANSWERS_BYTES: usize = 1 << 20;
|
||||
/// The most dialogs a report lists.
|
||||
pub const MAX_RECORDS: usize = 10_000;
|
||||
/// The most bytes of the dialogs a report lists, as JSON.
|
||||
pub const MAX_RECORD_BYTES: usize = 1 << 20;
|
||||
/// A dialog's title when the script gives none, in English; the app translates it when it shows it.
|
||||
pub const DEFAULT_TITLE: &str = "Script Alert";
|
||||
|
||||
/// Which dialog a script opened.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum DialogKind {
|
||||
Alert,
|
||||
Confirm,
|
||||
Prompt,
|
||||
}
|
||||
|
||||
impl DialogKind {
|
||||
/// The function's name, which is also the `kind` of an answer and of a report's dialog.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Alert => "alert",
|
||||
Self::Confirm => "confirm",
|
||||
Self::Prompt => "prompt",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A dialog a script opened, which waits for its answer.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ScriptDialog {
|
||||
pub kind: DialogKind,
|
||||
pub message: String,
|
||||
/// `None`: the script gave no title ([`DEFAULT_TITLE`]).
|
||||
pub title: Option<String>,
|
||||
/// `alert`: show the error icon.
|
||||
pub error_icon: bool,
|
||||
/// `confirm`: No is the default button.
|
||||
pub no_as_default: bool,
|
||||
/// `prompt`: the text its field starts with.
|
||||
pub preset: String,
|
||||
}
|
||||
|
||||
/// The answer to a [`ScriptDialog`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DialogAnswer {
|
||||
/// OK, for `alert`.
|
||||
Ok,
|
||||
/// Yes (`true`) or No, for `confirm`.
|
||||
Confirm(bool),
|
||||
/// OK with the field's text, or Cancel (`None`), for `prompt`.
|
||||
Prompt(Option<String>),
|
||||
}
|
||||
|
||||
impl DialogAnswer {
|
||||
pub fn kind(&self) -> DialogKind {
|
||||
match self {
|
||||
Self::Ok => DialogKind::Alert,
|
||||
Self::Confirm(_) => DialogKind::Confirm,
|
||||
Self::Prompt(_) => DialogKind::Prompt,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the function returns.
|
||||
fn data(&self) -> Data {
|
||||
match self {
|
||||
Self::Ok => Data::Undefined,
|
||||
Self::Confirm(b) => Data::Bool(*b),
|
||||
Self::Prompt(Some(t)) => Data::String(t.clone()),
|
||||
Self::Prompt(None) => Data::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// The answer as a report gives it: `"ok"`, a boolean, a string or `null`.
|
||||
fn json(&self) -> Value {
|
||||
match self {
|
||||
Self::Ok => json!("ok"),
|
||||
Self::Confirm(b) => json!(b),
|
||||
Self::Prompt(t) => json!(t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `text` as a dialog shows it. With `lines`, a line feed, a carriage return or the two together
|
||||
/// is a line break; every other control character, and with `lines` false every line break too,
|
||||
/// shows as its Unicode Control Picture, as the Script Console shows it.
|
||||
pub fn shown_text(text: &str, lines: bool) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
match c {
|
||||
'\r' if lines => {
|
||||
chars.next_if_eq(&'\n');
|
||||
out.push('\n');
|
||||
}
|
||||
'\n' if lines => out.push('\n'),
|
||||
c => out.push(console::shown(c)),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The longest start of `text` that is at most `max` bytes, cut at a character boundary.
|
||||
pub fn cut_to(text: &str, max: usize) -> &str {
|
||||
if text.len() <= max {
|
||||
return text;
|
||||
}
|
||||
let mut end = max;
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
text.get(..end).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `script.run`'s `answers`: a list of `{kind: "alert", value: "ok"}`, `{kind: "confirm", value:
|
||||
/// true | false}` and `{kind: "prompt", value: text | null}`. An empty object, which the parameter
|
||||
/// took before dialogs existed, is an empty list.
|
||||
pub(crate) fn parse_answers(v: Option<&Value>) -> Result<Vec<DialogAnswer>, String> {
|
||||
let list = match v {
|
||||
None => return Ok(Vec::new()),
|
||||
Some(Value::Object(o)) if o.is_empty() => return Ok(Vec::new()),
|
||||
Some(Value::Array(a)) => a,
|
||||
Some(_) => return Err("`answers` must be a list of answers".into()),
|
||||
};
|
||||
if list.len() > MAX_ANSWERS {
|
||||
return Err(format!("`answers` has more than {MAX_ANSWERS} answers"));
|
||||
}
|
||||
if serde_json::to_vec(list).map_or(true, |b| b.len() > MAX_ANSWERS_BYTES) {
|
||||
return Err("`answers` is larger than 1 MiB".into());
|
||||
}
|
||||
list.iter().enumerate().map(|(i, a)| answer(a).map_err(|e| format!("`answers[{i}]` {e}"))).collect()
|
||||
}
|
||||
|
||||
fn answer(a: &Value) -> Result<DialogAnswer, &'static str> {
|
||||
let Some(o) = a.as_object() else { return Err("must be an object with `kind` and `value`") };
|
||||
if o.len() != 2 || !o.contains_key("value") {
|
||||
return Err("must have exactly `kind` and `value`");
|
||||
}
|
||||
match (o.get("kind").and_then(Value::as_str), &o["value"]) {
|
||||
(Some("alert"), Value::String(s)) if s == "ok" => Ok(DialogAnswer::Ok),
|
||||
(Some("alert"), _) => Err("is an alert's answer, whose `value` must be \"ok\""),
|
||||
(Some("confirm"), Value::Bool(b)) => Ok(DialogAnswer::Confirm(*b)),
|
||||
(Some("confirm"), _) => Err("is a confirm's answer, whose `value` must be true or false"),
|
||||
(Some("prompt"), Value::Null) => Ok(DialogAnswer::Prompt(None)),
|
||||
(Some("prompt"), Value::String(s)) if s.len() <= MAX_PROMPT_BYTES => Ok(DialogAnswer::Prompt(Some(s.clone()))),
|
||||
(Some("prompt"), Value::String(_)) => Err("is longer than 16 KiB"),
|
||||
(Some("prompt"), _) => Err("is a prompt's answer, whose `value` must be a string or null"),
|
||||
_ => Err("must have `kind` alert, confirm or prompt"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialogs of one run: the answers left for a run with nobody to ask, the dialog that waits
|
||||
/// for the person's answer, and the list the report gives.
|
||||
pub(crate) struct Dialogs {
|
||||
/// Answers come from the person, not from `answers`.
|
||||
framed: bool,
|
||||
/// The realm's [`Realm::wait_flag`].
|
||||
can_wait: Rc<Cell<bool>>,
|
||||
answers: RefCell<VecDeque<DialogAnswer>>,
|
||||
waiting: RefCell<Option<ScriptDialog>>,
|
||||
/// The error that ended the run when an answer could not reach the script.
|
||||
failed: RefCell<Option<RunError>>,
|
||||
records: RefCell<Vec<Value>>,
|
||||
record_bytes: Cell<usize>,
|
||||
count: Cell<u64>,
|
||||
truncated: Cell<bool>,
|
||||
}
|
||||
|
||||
impl Dialogs {
|
||||
pub(crate) fn new(framed: bool, realm: &Realm, answers: Vec<DialogAnswer>) -> Self {
|
||||
Self {
|
||||
framed,
|
||||
can_wait: realm.wait_flag(),
|
||||
answers: RefCell::new(answers.into()),
|
||||
waiting: RefCell::default(),
|
||||
failed: RefCell::default(),
|
||||
records: RefCell::default(),
|
||||
record_bytes: Cell::new(0),
|
||||
count: Cell::new(0),
|
||||
truncated: Cell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog that waits for its answer.
|
||||
pub(crate) fn waiting(&self) -> Option<ScriptDialog> {
|
||||
self.waiting.borrow().clone()
|
||||
}
|
||||
|
||||
pub(crate) fn is_waiting(&self) -> bool {
|
||||
self.waiting.borrow().is_some()
|
||||
}
|
||||
|
||||
/// The error that ended the run when its answer could not reach the script.
|
||||
pub(crate) fn take_failure(&self) -> Option<RunError> {
|
||||
self.failed.borrow_mut().take()
|
||||
}
|
||||
|
||||
/// How many of `answers` the run did not use.
|
||||
pub(crate) fn unused(&self) -> usize {
|
||||
self.answers.borrow().len()
|
||||
}
|
||||
|
||||
/// Gives the waiting dialog `answer`, which must be for its kind and, for a prompt, at most
|
||||
/// [`MAX_PROMPT_BYTES`]. → false when it is not, or nothing waits.
|
||||
pub(crate) fn answer(&self, realm: &mut Realm, answer: DialogAnswer) -> bool {
|
||||
let fits = match &answer {
|
||||
DialogAnswer::Prompt(Some(t)) => t.len() <= MAX_PROMPT_BYTES,
|
||||
_ => true,
|
||||
};
|
||||
let mut waiting = self.waiting.borrow_mut();
|
||||
let Some(d) = waiting.as_ref().filter(|d| d.kind == answer.kind() && fits) else { return false };
|
||||
self.record(d, answer.json());
|
||||
*waiting = None;
|
||||
if let Err(e) = realm.answer(answer.data()) {
|
||||
*self.failed.borrow_mut() = Some(e);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The run ends while a dialog waits: the report lists it with the answer `"stop"`.
|
||||
pub(crate) fn stopped(&self) {
|
||||
if let Some(d) = self.waiting.borrow_mut().take() {
|
||||
self.record(&d, json!("stop"));
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&self, d: &ScriptDialog, answer: Value) {
|
||||
self.count.set(self.count.get().saturating_add(1));
|
||||
let mut r = Map::new();
|
||||
r.insert("kind".into(), json!(d.kind.id()));
|
||||
r.insert("message".into(), json!(d.message));
|
||||
r.insert("title".into(), json!(d.title.as_deref().unwrap_or(DEFAULT_TITLE)));
|
||||
match d.kind {
|
||||
DialogKind::Alert => r.insert("errorIcon".into(), json!(d.error_icon)),
|
||||
DialogKind::Confirm => r.insert("noAsDefault".into(), json!(d.no_as_default)),
|
||||
DialogKind::Prompt => r.insert("preset".into(), json!(d.preset)),
|
||||
};
|
||||
r.insert("answer".into(), answer);
|
||||
let r = Value::Object(r);
|
||||
let bytes = serde_json::to_vec(&r).map_or(usize::MAX, |b| b.len());
|
||||
let mut records = self.records.borrow_mut();
|
||||
if self.truncated.get() || records.len() >= MAX_RECORDS || self.record_bytes.get().saturating_add(bytes) > MAX_RECORD_BYTES {
|
||||
self.truncated.set(true);
|
||||
return;
|
||||
}
|
||||
self.record_bytes.set(self.record_bytes.get() + bytes);
|
||||
records.push(r);
|
||||
}
|
||||
|
||||
/// Adds `dialogs`, `dialogCount` and `dialogsTruncated` to a report.
|
||||
pub(crate) fn report(&self, report: &mut Map<String, Value>) {
|
||||
report.insert("dialogs".into(), Value::Array(self.records.borrow().clone()));
|
||||
report.insert("dialogCount".into(), json!(self.count.get()));
|
||||
report.insert("dialogsTruncated".into(), json!(self.truncated.get()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds `alert`, `confirm` and `prompt` to the realm.
|
||||
pub(super) fn install(realm: &mut Realm, host: &Rc<Host>) -> Result<(), RunError> {
|
||||
use Arg::{Bool, Text, TextOrUndefined};
|
||||
let call = |kind: DialogKind| -> HostFunction {
|
||||
let h = host.clone();
|
||||
Rc::new(move |args| open(&h, kind, &args))
|
||||
};
|
||||
realm.define_host_function("alert", vec![Text, TextOrUndefined, Bool], call(DialogKind::Alert))?;
|
||||
realm.define_host_function("confirm", vec![Text, Bool, TextOrUndefined], call(DialogKind::Confirm))?;
|
||||
realm.define_host_function("prompt", vec![Text, Text, TextOrUndefined], call(DialogKind::Prompt))
|
||||
}
|
||||
|
||||
fn text(args: &[Data], i: usize) -> Option<String> {
|
||||
match args.get(i) {
|
||||
Some(Data::String(s)) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn flag(args: &[Data], i: usize) -> bool {
|
||||
matches!(args.get(i), Some(Data::Bool(true)))
|
||||
}
|
||||
|
||||
/// A call of `alert`, `confirm` or `prompt`.
|
||||
fn open(host: &Host, kind: DialogKind, args: &[Data]) -> Result<Data, HostError> {
|
||||
let name = kind.id();
|
||||
host.charge_call(args.iter().map(super::data_size).sum())?;
|
||||
let Some(message) = text(args, 0) else { return Err(HostError::Type(format!("{name}: give a message"))) };
|
||||
let (title, error_icon, no_as_default, preset) = match kind {
|
||||
DialogKind::Alert => (text(args, 1), flag(args, 2), false, String::new()),
|
||||
DialogKind::Confirm => (text(args, 2), false, flag(args, 1), String::new()),
|
||||
DialogKind::Prompt => {
|
||||
let Some(preset) = text(args, 1) else { return Err(HostError::Type(format!("{name}: give the text the field starts with"))) };
|
||||
(text(args, 2), false, false, preset)
|
||||
}
|
||||
};
|
||||
if message.len() > MAX_MESSAGE_BYTES {
|
||||
return Err(HostError::Range(format!("{name}: the message is longer than 16 KiB")));
|
||||
}
|
||||
if title.as_ref().is_some_and(|t| t.len() > MAX_TITLE_BYTES) {
|
||||
return Err(HostError::Range(format!("{name}: the title is longer than 256 bytes")));
|
||||
}
|
||||
if preset.len() > MAX_PROMPT_BYTES {
|
||||
return Err(HostError::Range(format!("{name}: the text the field starts with is longer than 16 KiB")));
|
||||
}
|
||||
let dialog = ScriptDialog { kind, message, title, error_icon, no_as_default, preset };
|
||||
let d = &host.dialogs;
|
||||
if d.framed {
|
||||
if !d.can_wait.get() {
|
||||
return Err(HostError::Error(format!(
|
||||
"{name}: a dialog cannot open inside a callback of a built-in function, a getter, a conversion or a file $.evalFile runs; call it from the script's own code"
|
||||
)));
|
||||
}
|
||||
*d.waiting.borrow_mut() = Some(dialog);
|
||||
return Err(HostError::Wait);
|
||||
}
|
||||
let next = d.answers.borrow_mut().pop_front();
|
||||
match next {
|
||||
Some(a) if a.kind() == kind => {
|
||||
d.record(&dialog, a.json());
|
||||
Ok(a.data())
|
||||
}
|
||||
Some(a) => {
|
||||
d.record(&dialog, json!("missing"));
|
||||
Err(HostError::Abort(format!("{name}: the next answer in `answers` is for {}, not {name}", a.kind().id())))
|
||||
}
|
||||
None => {
|
||||
d.record(&dialog, json!("missing"));
|
||||
Err(HostError::Abort(format!("{name}: `answers` has no answer left for this dialog")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,16 @@
|
||||
//! there too. The session's console gets a start line, the run's output, its uncaught error, an end
|
||||
//! line and a report line; the run's report holds its own output and error, its time, its host
|
||||
//! calls by kind and its slowest commands.
|
||||
//!
|
||||
//! `alert`, `confirm` and `prompt` open dialogs ([`dialogs`]): in the app the run waits for the
|
||||
//! person's answer, and a run with nobody to ask takes its answers from `script.run`'s `answers`.
|
||||
|
||||
pub mod console;
|
||||
pub mod dialogs;
|
||||
pub mod include;
|
||||
|
||||
pub use dialogs::{DialogAnswer, DialogKind, ScriptDialog};
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
@@ -88,7 +94,7 @@ const MAX_BRIDGE_BYTES: u64 = 4 << 30;
|
||||
/// The version `vc.api.version` gives.
|
||||
pub const API_VERSION: &str = "1.0";
|
||||
/// The features `vc.api.has` knows.
|
||||
pub const API_FEATURES: &[&str] = &["run", "document", "commands", "log", "args", "console"];
|
||||
pub const API_FEATURES: &[&str] = &["run", "document", "commands", "log", "args", "console", "dialogs"];
|
||||
/// The most commands a run's report lists in `slowestCommands`.
|
||||
pub const SLOWEST_COMMANDS: usize = 5;
|
||||
|
||||
@@ -147,6 +153,8 @@ pub(crate) struct Request {
|
||||
pub source: String,
|
||||
pub args: Map<String, Value>,
|
||||
pub fuel: u64,
|
||||
/// The answers to the dialogs of a run with nobody to ask, in order.
|
||||
pub answers: Vec<DialogAnswer>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -486,6 +494,8 @@ struct Host {
|
||||
commands: RefCell<HashMap<&'static str, (u64, u128, u128)>>,
|
||||
/// The time the run's slices took, in nanoseconds.
|
||||
run_nanos: Cell<u128>,
|
||||
/// `alert`, `confirm` and `prompt`: their answers and the list the report gives.
|
||||
dialogs: dialogs::Dialogs,
|
||||
/// The files the run reads: its includes and `$.evalFile`'s.
|
||||
files: RefCell<include::Files>,
|
||||
/// Where the lines of the script's expanded source came from.
|
||||
@@ -1067,7 +1077,7 @@ fn kind_id(kind: &RunErrorKind) -> &'static str {
|
||||
|
||||
/// `message`, with what to do about a failure that comes from a script written for another program.
|
||||
fn explained(kind: &RunErrorKind, message: &str) -> String {
|
||||
const HOST_OBJECTS: &[&str] = &["app", "alert", "confirm", "prompt", "File", "Folder", "Window", "BridgeTalk", "activeDocument", "documents"];
|
||||
const HOST_OBJECTS: &[&str] = &["app", "File", "Folder", "Window", "BridgeTalk", "activeDocument", "documents"];
|
||||
let names_host_object =
|
||||
message.strip_prefix("ReferenceError: ").is_some_and(|m| HOST_OBJECTS.iter().any(|o| m.split_whitespace().next() == Some(o)));
|
||||
if *kind == RunErrorKind::Thrown && names_host_object {
|
||||
@@ -1169,6 +1179,23 @@ impl Session {
|
||||
!self.script_stop.is_set() && self.script_key.as_ref().and_then(|k| with_job(k, |j| j.host.waiting.get())).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The dialog (`alert`, `confirm` or `prompt`) the running script waits at for the person's
|
||||
/// answer. A run asked to stop no longer waits: the next [`Session::script_pump`] ends it.
|
||||
pub fn script_dialog(&self) -> Option<ScriptDialog> {
|
||||
if self.script_stop.is_set() {
|
||||
return None;
|
||||
}
|
||||
self.script_key.as_ref().and_then(|k| with_job(k, |j| j.host.dialogs.waiting())).flatten()
|
||||
}
|
||||
|
||||
/// Gives the dialog the running script waits at its answer; the next [`Session::script_pump`]
|
||||
/// goes on from the call. → false when nothing waits, the answer is for another kind of
|
||||
/// dialog, or a prompt's text is longer than [`dialogs::MAX_PROMPT_BYTES`].
|
||||
pub fn script_answer(&mut self, answer: DialogAnswer) -> bool {
|
||||
let Some(k) = self.script_key.as_ref().filter(|_| !self.script_stop.is_set()) else { return false };
|
||||
with_job(k, |j| j.host.dialogs.answer(&mut j.realm, answer)).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The name of the running script.
|
||||
pub fn script_name(&self) -> Option<String> {
|
||||
self.script_key.as_ref().and_then(|k| with_job(k, |j| j.name.clone()))
|
||||
@@ -1285,6 +1312,7 @@ impl Session {
|
||||
counts: Counts::default(),
|
||||
commands: RefCell::default(),
|
||||
run_nanos: Cell::new(0),
|
||||
dialogs: dialogs::Dialogs::new(self.script_frames, &realm, request.answers),
|
||||
files: RefCell::new(files),
|
||||
script_map: RefCell::default(),
|
||||
eval_maps: RefCell::default(),
|
||||
@@ -1301,6 +1329,7 @@ impl Session {
|
||||
realm
|
||||
.define_host_object("vc", vc_object(&host, args))
|
||||
.and_then(|()| realm.define_host_object("$", dollar_object(&host)))
|
||||
.and_then(|()| dialogs::install(&mut realm, &host))
|
||||
.and_then(|()| realm.start(&expanded.source))
|
||||
.map_err(|e| located(&host, realm.error_source(), &e))
|
||||
}
|
||||
@@ -1329,11 +1358,20 @@ impl Session {
|
||||
self.script_gone(key, console::Ending::Internal);
|
||||
return Some(Err(EngineError::Other(JOB_GONE.to_string())));
|
||||
};
|
||||
if job.host.waiting.get() && !self.script_stop.is_set() {
|
||||
if let Some(e) = job.host.dialogs.take_failure() {
|
||||
return Some(self.script_failed(job, &e));
|
||||
}
|
||||
if (job.host.waiting.get() || job.host.dialogs.is_waiting()) && !self.script_stop.is_set() {
|
||||
JOBS.with(|jobs| jobs.borrow_mut().insert(key.id, job));
|
||||
self.script_key = Some(key);
|
||||
return None;
|
||||
}
|
||||
// A stop while a dialog waits ends the run there; the interpreter cannot go on from the call.
|
||||
if job.realm.waits() {
|
||||
let kind = LimitKind::Interrupted;
|
||||
let e = RunError { kind: RunErrorKind::Limit(kind), message: format!("limit reached: {kind:?}"), line: 0, column: 0 };
|
||||
return Some(self.script_failed(job, &e));
|
||||
}
|
||||
if vectorcraft_doc::cancel::check().is_err() {
|
||||
self.script_stop.set();
|
||||
}
|
||||
@@ -1350,6 +1388,12 @@ impl Session {
|
||||
self.script_key = Some(key);
|
||||
None
|
||||
}
|
||||
Ok(Ok(Progress::Done(_))) if job.host.dialogs.unused() > 0 => {
|
||||
let n = job.host.dialogs.unused();
|
||||
let kind = LimitKind::Host;
|
||||
let message = format!("`answers` has {n} answer{} the script did not use", if n == 1 { "" } else { "s" });
|
||||
Some(self.script_failed(job, &RunError { kind: RunErrorKind::Limit(kind), message, line: 0, column: 0 }))
|
||||
}
|
||||
Ok(Ok(Progress::Done(_))) => {
|
||||
let r = self.script_commit(job);
|
||||
self.trim_histories();
|
||||
@@ -1359,6 +1403,7 @@ impl Session {
|
||||
}
|
||||
|
||||
fn script_failed(&mut self, job: Job, e: &RunError) -> Result<Value> {
|
||||
job.host.dialogs.stopped();
|
||||
let fuel_left = job.realm.fuel_left();
|
||||
let (e, file) = located(&job.host, job.realm.error_source(), e);
|
||||
let mut report = failure_report(&job.name, &e, file.as_deref(), &job.host, fuel_left, job.realm.limits().fuel.saturating_sub(fuel_left));
|
||||
@@ -1455,7 +1500,7 @@ impl Session {
|
||||
if let Some(result) = self.script_pump() {
|
||||
return result;
|
||||
}
|
||||
if self.script_waiting() {
|
||||
if self.script_waiting() || self.script_dialog().is_some() {
|
||||
self.script_stop.set();
|
||||
}
|
||||
}
|
||||
@@ -1521,8 +1566,8 @@ fn line_to(s: &mut Session, host: &Host, entry: console::Entry, cut: bool) {
|
||||
|
||||
/// Ends the console lines of `host`'s run: the line `$.write` left unended, then for a failed run
|
||||
/// (`report["ok"]` false) its error as `name:line:column: message`, then an end line and a report
|
||||
/// line in the session's console. Adds the run's time, host calls by kind, slowest commands and
|
||||
/// console to `report`.
|
||||
/// line in the session's console. Adds the run's time, host calls by kind, slowest commands,
|
||||
/// console and dialogs to `report`.
|
||||
fn close_run(s: &mut Session, host: &Host, report: &mut Value, ending: console::Ending) {
|
||||
let mut pending = std::mem::take(&mut *host.pending.borrow_mut());
|
||||
if !pending.is_empty() {
|
||||
@@ -1556,6 +1601,7 @@ fn close_run(s: &mut Session, host: &Host, report: &mut Value, ending: console::
|
||||
o.insert("hostCallCounts".into(), host.counts.json());
|
||||
o.insert("slowestCommands".into(), Value::Array(slow_json));
|
||||
o.insert("console".into(), host.console.borrow().report());
|
||||
host.dialogs.report(o);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user