use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::app::{self, AppTarget};
use crate::config::{self, Config, EffectiveSettings};
use crate::controller::{self, AppAction, DecisionContext, MacAppController};
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct RunOutput {
pub success: bool,
#[serde(default)]
pub should_fallback_direct: bool,
pub stdout: String,
pub stderr: String,
}
impl RunOutput {
pub fn emit(&self) {
if !self.stdout.is_empty() {
print!("{}", self.stdout);
}
if !self.stderr.is_empty() {
eprint!("{}", self.stderr);
}
}
}
pub fn run_app(app: &str, verbose: u8) -> RunOutput {
let target = match app::classify_app_target(app) {
Ok(target) => target,
Err(err) => {
return failure(format!("Invalid app target: {err}\n"));
}
};
let settings = config::EffectiveSettings {
launch_if_not_running: true,
..config::EffectiveSettings::default()
};
run_target(app, &target, &settings, verbose)
}
pub fn run_binding_from_path(name: &str, path: &Path, verbose: u8) -> RunOutput {
let config = match config::load_from(path) {
Ok(config) => config,
Err(err) => {
return failure(format!("Config error in {}:\n {err}\n", path.display()));
}
};
run_binding_with_config(name, path, &config, verbose)
}
pub fn run_binding_with_config(name: &str, path: &Path, config: &Config, verbose: u8) -> RunOutput {
let resolved = match config::resolve_binding(config, name, path) {
Ok(resolved) => resolved,
Err(err) => {
return failure(format!("{err}\n"));
}
};
run_target(
&resolved.name,
&resolved.target,
&resolved.settings,
verbose,
)
}
fn run_target(
label: &str,
target: &AppTarget,
settings: &EffectiveSettings,
verbose: u8,
) -> RunOutput {
controller::with_autorelease_pool(|| {
let controller = MacAppController::new();
let observation = match controller.observe_target(target) {
Ok(observation) => observation,
Err(err) => {
let should_fallback_direct =
matches!(err, controller::ControllerError::PermissionDenied { .. });
return RunOutput {
success: false,
should_fallback_direct,
stdout: String::new(),
stderr: format!("Failed to inspect {label}: {err}\n"),
};
}
};
let (action, context) = controller::decide_action_for_observation(observation, settings);
let stderr = render_decision(verbose, label, target, action, context);
let core = controller.execute_action_with_observation(target, action, observation);
let ensure = settings.open_window_when_empty
&& matches!(
action,
AppAction::Focus | AppAction::AlreadyFocused | AppAction::Cycle
);
match window_follow_up(&core, ensure) {
WindowFollowUp::None => match core {
Ok(()) => RunOutput {
success: true,
should_fallback_direct: false,
stdout: String::new(),
stderr,
},
Err(err) => RunOutput {
success: false,
should_fallback_direct: matches!(
err,
controller::ControllerError::PermissionDenied { .. }
),
stdout: String::new(),
stderr: format!("{stderr}Failed to {action:?} {label}: {err}\n"),
},
},
WindowFollowUp::EnsureWindow => {
match controller.ensure_window(target, observation) {
Ok(()) => RunOutput {
success: true,
should_fallback_direct: false,
stdout: String::new(),
stderr,
},
Err(err) => {
let note = if verbose > 0 {
format!("Focused {label} but could not open a new window: {err}\n")
} else {
String::new()
};
RunOutput {
success: true,
should_fallback_direct: false,
stdout: String::new(),
stderr: format!("{stderr}{note}"),
}
}
}
}
WindowFollowUp::OpenWindowInstead => {
match controller.open_new_window(target) {
Ok(()) => RunOutput {
success: true,
should_fallback_direct: false,
stdout: String::new(),
stderr,
},
Err(err) => RunOutput {
success: false,
should_fallback_direct: matches!(
err,
controller::ControllerError::PermissionDenied { .. }
),
stdout: String::new(),
stderr: format!("{stderr}Failed to open a new window for {label}: {err}\n"),
},
}
}
}
})
}
#[derive(Debug, PartialEq, Eq)]
enum WindowFollowUp {
None,
EnsureWindow,
OpenWindowInstead,
}
fn window_follow_up(
core: &Result<(), controller::ControllerError>,
ensure: bool,
) -> WindowFollowUp {
if !ensure {
return WindowFollowUp::None;
}
match core {
Ok(()) => WindowFollowUp::EnsureWindow,
Err(controller::ControllerError::NoWindows { .. }) => WindowFollowUp::OpenWindowInstead,
_ => WindowFollowUp::None,
}
}
fn failure(stderr: String) -> RunOutput {
RunOutput {
success: false,
should_fallback_direct: false,
stdout: String::new(),
stderr,
}
}
fn render_decision(
verbose: u8,
label: &str,
target: &AppTarget,
action: AppAction,
context: DecisionContext,
) -> String {
if verbose == 0 {
return String::new();
}
let mut output = format!(
"summon {label}: running={} frontmost={:?} launch={} cycle={} -> {action:?}\n",
context.is_running,
context.frontmost,
context.launch_when_missing,
context.cycle_when_focused
);
if verbose > 1 {
output.push_str(&format!(
" target={}\n",
controller::target_display(target)
));
}
output
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
fn finder_target() -> AppTarget {
AppTarget::BundleId("com.apple.finder".into())
}
#[test]
fn window_follow_up_disabled_is_none() {
assert_eq!(window_follow_up(&Ok(()), false), WindowFollowUp::None);
assert_eq!(
window_follow_up(
&Err(controller::ControllerError::NoWindows {
target: "Finder".into()
}),
false
),
WindowFollowUp::None
);
}
#[test]
fn window_follow_up_success_ensures_window() {
assert_eq!(
window_follow_up(&Ok(()), true),
WindowFollowUp::EnsureWindow
);
}
#[test]
fn window_follow_up_no_windows_opens_one_instead() {
assert_eq!(
window_follow_up(
&Err(controller::ControllerError::NoWindows {
target: "Finder".into()
}),
true
),
WindowFollowUp::OpenWindowInstead
);
}
#[test]
fn window_follow_up_other_errors_are_propagated() {
let focus_failed = Err(controller::ControllerError::FocusFailed {
target: "Finder".into(),
reason: "boom".into(),
});
assert_eq!(window_follow_up(&focus_failed, true), WindowFollowUp::None);
let no_cyclable = Err(controller::ControllerError::NoCyclableWindows {
target: "Finder".into(),
total_windows: 2,
rejected_windows: 2,
});
assert_eq!(window_follow_up(&no_cyclable, true), WindowFollowUp::None);
}
#[test]
fn window_follow_up_ensure_flag_drives_behavior() {
let ok = Ok(());
assert_eq!(window_follow_up(&ok, false), WindowFollowUp::None);
assert_eq!(window_follow_up(&ok, true), WindowFollowUp::EnsureWindow);
let _ = finder_target(); }
}