use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Duration;
use ahk_cli::{json, report};
use ahk::dialect::{self, Dialect};
use ahk::parse_with;
use ahk::runtime::Runtime;
use ahk::surface;
use ahk_box::LabDesktop;
fn pid_file() -> PathBuf {
let base = match std::env::var_os("XDG_CACHE_HOME") {
Some(base) => PathBuf::from(base),
None => match std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
Some(home) => PathBuf::from(home).join(".cache"),
None => std::env::temp_dir(),
},
};
base.join("ahk").join("run.pid")
}
fn write_pid_file() {
let path = pid_file();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&path, format!("{}\n", std::process::id()));
}
fn clear_pid_file() {
let _ = std::fs::remove_file(pid_file());
}
const STACK_BYTES: usize = 128 * 1024 * 1024;
const STACK_BUDGET: usize = 64 * 1024 * 1024;
fn main() -> std::process::ExitCode {
let dialect = the_dialect();
if std::env::args()
.skip(1)
.any(|argument| argument == "--surface")
{
let behaviour = std::env::args()
.skip(1)
.any(|argument| argument == "--behavior" || argument == "--behaviour");
if std::env::args()
.skip(1)
.any(|argument| argument == "--json")
{
print_surface_json(dialect, behaviour);
} else {
print_surface(dialect, behaviour);
}
return std::process::ExitCode::from(0);
}
let mut source = String::new();
if let Err(error) = std::io::stdin().read_to_string(&mut source) {
eprintln!("cannot read the script: {error}");
return std::process::ExitCode::from(2);
}
let timeout = std::env::args()
.skip(1)
.find_map(|argument| {
argument
.strip_prefix("--timeout=")
.and_then(|value| value.parse::<f64>().ok())
})
.unwrap_or(60.0);
let source = match resolve_includes(&source, Path::new("."), &mut Vec::new()) {
Ok(source) => source,
Err(error) => {
let report = report::the_failure_of_the_load(&error, 0, 0);
println!("{report}");
return std::process::ExitCode::from(2);
}
};
let statements = match parse_with(&source, dialect) {
Ok(statements) => statements,
Err(error) => {
let report = report::the_failure_of_the_load(&error.message, error.line, error.column);
println!("{report}");
return std::process::ExitCode::from(2);
}
};
write_pid_file();
let _guard = PidGuard;
let handle = std::thread::Builder::new()
.stack_size(STACK_BYTES)
.spawn(move || {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let x11 = ahk_box::desktop_kind().eq_ignore_ascii_case("x11")
&& ahk_box::x11_available();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let mut x11_desktop = ahk_box::x11::X11Desktop::new();
let mut lab_desktop = LabDesktop::new();
lab_desktop.ensure_session_env();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let desktop: &mut dyn ahk::desktop::Desktop = if x11 {
&mut x11_desktop
} else {
&mut lab_desktop
};
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
let desktop: &mut dyn ahk::desktop::Desktop = &mut lab_desktop;
let report = Runtime::new(desktop, Duration::from_secs_f64(timeout))
.with_stack_budget(STACK_BUDGET)
.with_source(&source)
.with_dialect(dialect)
.run(&statements);
let mut notes = report.notes.clone();
notes.extend(lab_desktop.notes().iter().cloned());
let payload = report::the_report(&report, ¬es);
(payload, report.ok)
})
.expect("the thread of the run starts");
let (payload, ok) = match handle.join() {
Ok(pair) => pair,
Err(_) => {
eprintln!("the run of the script died");
return std::process::ExitCode::from(1);
}
};
println!("{payload}");
std::process::ExitCode::from(if ok { 0 } else { 1 })
}
fn the_dialect() -> &'static dyn Dialect {
let asked = std::env::args().skip(1).find_map(|argument| {
argument
.strip_prefix("--dialect=")
.map(|value| value.to_lowercase())
});
match asked.as_deref() {
Some("v1") => dialect::v1(),
Some("v2.1") | Some("v2_1") => dialect::v2_1(),
Some("ks") => dialect::ks(),
Some("ks20") | Some("ks2.0") => dialect::ks_20(),
Some("v2") | None => dialect::v2(),
Some(other) => {
eprintln!(
"the dialect {other} is not one this tree speaks: the tree speaks v1, v2, v2.1, ks and ks20."
);
std::process::exit(2);
}
}
}
fn print_surface(dialect: &'static dyn Dialect, behaviour: bool) {
let entries = surface::entries_of(dialect);
let mut out = String::new();
for entry in &entries {
out.push_str(&entry.line());
if behaviour {
if let Some(answer) = ahk::behavior::behaviour_of(dialect, &entry.full_name()) {
out.push_str("\tbehaviour=");
out.push_str(&answer);
}
}
out.push('\n');
}
out.push_str(&surface::summary_of(&entries, dialect).text());
out.push('\n');
print!("{out}");
}
fn print_surface_json(dialect: &'static dyn Dialect, behaviour: bool) {
let entries = surface::entries_of(dialect);
let summary = surface::summary_of(&entries, dialect);
let mut report = json::Object::new(64 * 1024);
report.text("dialect", summary.dialect);
report.number("total", summary.total as i64);
report.number("values", summary.values as i64);
report.number("refusals", summary.refusals as i64);
report.number("pending", summary.pending as i64);
report.field("entries", |out| {
out.push('[');
for (index, entry) in entries.iter().enumerate() {
if index > 0 {
out.push(',');
}
let mut row = json::Object::new(96);
row.text("name", entry.name);
row.text("kind", entry.kind.as_str());
row.text(
"state",
match entry.state {
surface::State::Value => "value",
surface::State::Refusal(_) => "refusal",
surface::State::Pending(_) => "pending",
},
);
if !entry.class.is_empty() {
row.text("class", entry.class);
}
if let surface::State::Refusal(reason) | surface::State::Pending(reason) = entry.state {
row.text("reason", reason);
}
if behaviour {
if let Some(answer) = ahk::behavior::behaviour_of(dialect, &entry.full_name()) {
row.text("behavior", &answer);
}
}
out.push_str(&row.finish());
}
out.push(']');
});
println!("{}", report.finish());
}
fn resolve_includes(source: &str, base: &Path, seen: &mut Vec<PathBuf>) -> Result<String, String> {
resolve_includes_at(source, base, seen, 0)
}
const MAX_INCLUDE_DEPTH: usize = 64;
fn resolve_includes_at(
source: &str,
base: &Path,
seen: &mut Vec<PathBuf>,
depth: usize,
) -> Result<String, String> {
if depth > MAX_INCLUDE_DEPTH {
return Err(format!(
"the includes nest more than {MAX_INCLUDE_DEPTH} deep: a file includes itself, or a group of them includes one another."
));
}
let mut out = String::with_capacity(source.len());
for line in source.lines() {
let Some((again, argument)) = the_include_of(line) else {
out.push_str(line);
out.push('\n');
continue;
};
let path = argument
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.unwrap_or(argument)
.trim();
if path.is_empty() {
return Err("an include needs a path.".to_string());
}
let path = base.join(path);
let path = match path.canonicalize() {
Ok(path) => path,
Err(_) => return Err(format!("the include \"{path:?}\" cannot be found.")),
};
if !again && seen.contains(&path) {
out.push('\n');
continue;
}
let text = std::fs::read_to_string(&path)
.map_err(|error| format!("the include \"{path:?}\" cannot be read: {error}"))?;
let directory = path.parent().unwrap_or(Path::new(".")).to_path_buf();
if !seen.contains(&path) {
seen.push(path);
}
out.push_str(&resolve_includes_at(&text, &directory, seen, depth + 1)?);
out.push('\n');
}
Ok(out)
}
fn the_include_of(line: &str) -> Option<(bool, &str)> {
let trimmed = line.trim();
let rest = trimmed.strip_prefix('#')?;
let end = rest
.find(|character: char| character.is_whitespace())
.unwrap_or(rest.len());
let (name, argument) = rest.split_at(end);
let again = if name.eq_ignore_ascii_case("include") {
false
} else if name.eq_ignore_ascii_case("includeagain") {
true
} else {
return None;
};
Some((again, argument.trim()))
}
struct PidGuard;
impl Drop for PidGuard {
fn drop(&mut self) {
clear_pid_file();
}
}