#![doc = include_str!("../tests/testing_harness.rs")]
mod http;
mod machine;
pub(crate) use http::send as http_send;
pub use machine::{MockPaneLaunch, MockProcess, MockWatch, MockWebSocket, ProcessScript};
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cell::RefCell;
use standard_plugin_manifest::{GrantDenied, Grants};
use crate::api::Target;
use crate::api::account::{AccountState, Pane};
use crate::api::capabilities::Capabilities;
use crate::api::view::Theme;
use crate::error::{Error, Result};
use crate::geometry::{Geometry, Rect};
use crate::surface::{HEADER_LEN, Model, RegionLayout};
use crate::ui_runtime::{Event, Power, UiPlugin, UiRuntime};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CallKind {
Wait,
Send,
Async(u64),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MockCall {
pub method: String,
pub payload: String,
pub target: Target,
pub kind: CallKind,
pub timeout_ms: Option<u32>,
pub answered: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MockCommit {
pub seq: u32,
pub slot: u8,
pub dirty: Vec<Rect>,
pub full: bool,
pub geometry: Geometry,
pub model: Model,
pub region: Vec<u8>,
}
impl MockCommit {
pub fn slot_bytes(&self) -> &[u8] {
let layout = RegionLayout::new(self.model, self.geometry);
let start = (HEADER_LEN + u32::from(self.slot) * layout.slot_len) as usize;
&self.region[start..start + layout.slot_len as usize]
}
}
#[derive(Clone, Debug)]
struct MockSurface {
model: Model,
geometry: Geometry,
region: Option<(usize, usize)>,
pending: Option<(usize, usize)>,
committed_once: bool,
commits: Vec<MockCommit>,
}
#[derive(Clone, Debug)]
pub struct MockHost {
plugin_id: String,
granted: Vec<String>,
grants: Grants,
pub config: String,
pub values: BTreeMap<String, String>,
pub secrets: BTreeMap<String, String>,
pub account: AccountState,
pub driving: bool,
pub theme: Theme,
pub capabilities: Capabilities,
pub focused_pane: Option<String>,
pub claimed_elsewhere: BTreeSet<String>,
pub claims: BTreeSet<String>,
pub emitted: Vec<(String, String, Target)>,
pub published: Vec<(String, String)>,
pub live_deleted: Vec<String>,
pub listening: BTreeSet<String>,
pub call_answers: BTreeMap<String, String>,
pub calls: Vec<MockCall>,
pub denials: Vec<String>,
pub panes: Vec<Pane>,
pub frame_request: Option<u64>,
pub opened: Vec<String>,
pub opened_urls: Vec<String>,
pub focused_panes: Vec<String>,
pub now_ms: u64,
pub url_gesture_ms: Option<u64>,
pub closed: Vec<String>,
pub gesture: bool,
pub machine_id: Option<String>,
pub account_id: Option<String>,
pub lease_epoch: Option<u64>,
pub release: Option<crate::daemon::Release>,
pub instance_id: String,
pub environment: Vec<(String, String)>,
pub wall_ms: u64,
pub utc_offset_minutes: i32,
pub time_zone: Option<String>,
pub health: Option<(crate::health::Health, String)>,
pub tints: BTreeMap<String, u32>,
pub requested: BTreeMap<String, (u32, u32)>,
pub labels: BTreeMap<String, String>,
pub carets: BTreeMap<String, (u32, u32)>,
machine_surfaces: BTreeSet<String>,
project_surfaces: BTreeSet<String>,
surfaces: BTreeMap<String, MockSurface>,
machine: machine::Machine,
http: http::MockHttp,
}
impl MockHost {
pub fn new(plugin_id: &str) -> Self {
Self {
plugin_id: plugin_id.to_string(),
granted: Vec::new(),
grants: Grants::new(plugin_id, []),
config: String::new(),
values: BTreeMap::new(),
secrets: BTreeMap::new(),
account: AccountState::default(),
driving: false,
theme: Theme::default(),
capabilities: Capabilities {
graphics: false,
kitty: false,
frame_rate: 60,
cell_px: (0, 0),
pixel_scale: 1,
},
focused_pane: None,
claimed_elsewhere: BTreeSet::new(),
claims: BTreeSet::new(),
emitted: Vec::new(),
published: Vec::new(),
live_deleted: Vec::new(),
listening: BTreeSet::new(),
call_answers: BTreeMap::new(),
calls: Vec::new(),
denials: Vec::new(),
panes: Vec::new(),
frame_request: None,
opened: Vec::new(),
focused_panes: Vec::new(),
opened_urls: Vec::new(),
now_ms: 0,
url_gesture_ms: None,
closed: Vec::new(),
gesture: false,
machine_id: None,
account_id: None,
lease_epoch: None,
release: None,
instance_id: "test-viewer".to_string(),
environment: Vec::new(),
wall_ms: 0,
utc_offset_minutes: 0,
time_zone: None,
health: None,
tints: BTreeMap::new(),
requested: BTreeMap::new(),
labels: BTreeMap::new(),
carets: BTreeMap::new(),
machine_surfaces: BTreeSet::new(),
project_surfaces: BTreeSet::new(),
surfaces: BTreeMap::new(),
machine: machine::Machine::default(),
http: http::MockHttp::default(),
}
}
pub fn clock(mut self, wall_ms: u64, utc_offset_minutes: i32, time_zone: Option<&str>) -> Self {
self.wall_ms = wall_ms;
self.utc_offset_minutes = utc_offset_minutes;
self.time_zone = time_zone.map(str::to_string);
self
}
pub fn singleton(mut self, epoch: u64) -> Self {
self.lease_epoch = Some(epoch);
self
}
pub fn machine(mut self, machine_id: &str) -> Self {
self.machine_id = Some(machine_id.to_string());
self
}
pub fn env(mut self, name: &str, value: &str) -> Self {
self.environment.retain(|(key, _)| key != name);
self.environment.push((name.to_string(), value.to_string()));
self.environment.sort();
self
}
pub fn grant(mut self, grants: &[&str]) -> Self {
self.granted
.extend(grants.iter().map(|grant| grant.to_string()));
self.grants = Grants::new(&self.plugin_id, self.granted.iter().cloned());
self
}
pub fn surface(mut self, id: &str, model: Model) -> Self {
self.surfaces.insert(
id.to_string(),
MockSurface {
model,
geometry: Geometry::default(),
region: None,
pending: None,
committed_once: false,
commits: Vec::new(),
},
);
self
}
pub fn machine_surface(mut self, id: &str, model: Model) -> Self {
self.machine_surfaces.insert(id.to_string());
self.surface(id, model)
}
pub fn project_surface(mut self, id: &str, model: Model) -> Self {
self.project_surfaces.insert(id.to_string());
self.surface(id, model)
}
pub fn driving(mut self, driving: bool) -> Self {
self.driving = driving;
self
}
pub fn set_geometry(&mut self, id: &str, geometry: Geometry) -> Option<Event> {
if !self.surfaces.contains_key(id)
&& let Some((base, _pane)) = id.split_once('@')
&& let Some(template) = self.surfaces.get(base)
{
let mut instance = template.clone();
instance.geometry = Geometry::default();
instance.region = None;
instance.pending = None;
instance.committed_once = false;
instance.commits.clear();
self.surfaces.insert(id.to_string(), instance);
}
let surface = self.surfaces.get_mut(id)?;
if surface.geometry == geometry {
return None;
}
surface.geometry = geometry;
Some(Event::Resize {
surface: id.to_string(),
geometry,
})
}
pub fn set_model(&mut self, id: &str, model: Model) -> Option<Event> {
let surface = self.surfaces.get_mut(id)?;
if surface.model == model {
return None;
}
surface.model = model;
surface.region = None;
surface.pending = None;
surface.committed_once = false;
Some(Event::Resize {
surface: id.to_string(),
geometry: surface.geometry,
})
}
pub fn commits(&self, id: &str) -> Vec<MockCommit> {
self.surfaces
.get(id)
.map(|surface| surface.commits.clone())
.unwrap_or_default()
}
fn answer(&self, method: &str) -> Result<String> {
self.call_answers
.get(method)
.cloned()
.ok_or_else(|| Error::Invalid(alloc::format!("unknown method {method:?}")))
}
fn take_async_answers(&mut self) -> Vec<Event> {
let mut events = Vec::new();
for index in 0..self.calls.len() {
let CallKind::Async(id) = self.calls[index].kind else {
continue;
};
if self.calls[index].answered {
continue;
}
self.calls[index].answered = true;
let result = self.answer(&self.calls[index].method).map(crate::Json);
events.push(Event::CallResult {
id: crate::api::calls::CallId(id),
result,
});
}
events
}
fn deny(&mut self, denial: GrantDenied) -> Error {
self.denials.push(denial.grant.clone());
Error::GrantDenied {
grant: denial.grant,
}
}
fn check(&mut self, grant: &str) -> Result<()> {
self.grants.check(grant).map_err(|denial| self.deny(denial))
}
fn check_namespaced(&mut self, kind: &str, subject: &str) -> Result<()> {
self.grants
.check_namespaced(kind, subject)
.map_err(|denial| self.deny(denial))
}
}
std::thread_local! {
static MOCK: RefCell<Option<MockHost>> = const { RefCell::new(None) };
}
pub fn install(host: MockHost) {
MOCK.with(|mock| *mock.borrow_mut() = Some(host));
}
pub fn with_host<R>(f: impl FnOnce(&mut MockHost) -> R) -> R {
MOCK.with(|mock| {
let mut mock = mock.borrow_mut();
f(mock.get_or_insert_with(|| MockHost::new("test-plugin")))
})
}
pub struct Harness<P: UiPlugin> {
runtime: UiRuntime<P>,
}
impl<P: UiPlugin> Harness<P> {
pub fn activate(host: MockHost, config: &str) -> Self {
let mut host = host;
host.config = config.to_string();
install(host);
let runtime = UiRuntime::new();
runtime.activate(config.to_string());
Self { runtime }
}
pub fn resize(&mut self, surface: &str, geometry: Geometry) {
if let Some(event) = with_host(|host| host.set_geometry(surface, geometry)) {
self.runtime.event(event);
}
}
pub fn frame(&mut self, now_ms: u64, interval_ms: u32, power: Power) -> Option<u64> {
with_host(|host| {
host.frame_request = None;
host.now_ms = now_ms;
});
self.runtime.frame(now_ms, interval_ms, power)
}
pub fn event(&mut self, event: Event) {
let gesture = matches!(
&event,
Event::Key(_)
| Event::Paste { .. }
| Event::Command(_)
| Event::Pointer(crate::Pointer {
kind: crate::PointerKind::Down,
..
})
);
with_host(|host| {
host.gesture = gesture;
if gesture {
host.url_gesture_ms = Some(host.now_ms);
}
});
self.runtime.event(event);
with_host(|host| host.gesture = false);
}
pub fn show(&mut self, surface: &str, visible: bool) {
self.event(Event::Visibility {
surface: surface.to_string(),
visible,
});
}
pub fn set_capabilities(&mut self, capabilities: Capabilities) {
with_host(|host| host.capabilities = capabilities);
self.event(Event::CapabilitiesChanged);
}
pub fn set_theme(&mut self, theme: Theme) {
with_host(|host| host.theme = theme);
self.event(Event::ThemeChanged);
}
pub fn set_model(&mut self, surface: &str, model: Model) {
if let Some(event) = with_host(|host| host.set_model(surface, model)) {
self.runtime.event(event);
}
}
pub fn key(&mut self, surface: &str, code: crate::KeyCode) {
self.event(Event::Key(crate::Key::press(surface, code)));
}
pub fn paste(&mut self, surface: &str, text: &str) {
self.event(Event::Paste {
surface: surface.to_string(),
text: text.to_string(),
});
}
pub fn pointer(&mut self, surface: &str, kind: crate::PointerKind, x: u32, y: u32) {
let cell = with_host(|host| {
host.surfaces
.get(surface)
.map_or((1, 1), |surface| match surface.model {
Model::Cells => (1, 1),
Model::Pixels => (
surface.geometry.cell_px_w.max(1),
surface.geometry.cell_px_h.max(1),
),
})
});
self.event(Event::Pointer(crate::Pointer {
surface: surface.to_string(),
x,
y,
col: x / cell.0,
row: y / cell.1,
button: if matches!(kind, crate::PointerKind::Move) {
crate::Button::None
} else {
crate::Button::Left
},
kind,
modifiers: crate::Modifiers::NONE,
}));
}
pub fn command(&mut self, id: &str) {
self.event(Event::Command(id.to_string()));
}
pub fn answer_calls(&mut self) -> usize {
let events = with_host(MockHost::take_async_answers);
let count = events.len();
for event in events {
self.event(event);
}
count
}
pub fn take_frame_request(&mut self) -> Option<u64> {
with_host(|host| host.frame_request.take())
}
pub fn plugin<R>(&self, f: impl FnOnce(&mut P) -> R) -> R {
self.runtime
.with_plugin(f)
.unwrap_or_else(|| panic!("the plugin is not active"))
}
pub fn commits(&self, surface: &str) -> Vec<MockCommit> {
with_host(|host| host.commits(surface))
}
pub fn host<R>(&self, f: impl FnOnce(&mut MockHost) -> R) -> R {
with_host(f)
}
pub fn deactivate(self) {
self.runtime.deactivate();
}
}
pub struct DaemonHarness<P: crate::daemon::DaemonPlugin> {
runtime: crate::daemon::runtime::DaemonRuntime<P>,
}
impl<P: crate::daemon::DaemonPlugin> DaemonHarness<P> {
pub fn activate(host: MockHost, config: &str) -> Self {
let mut host = host;
host.config = config.to_string();
install(host);
let runtime = crate::daemon::runtime::DaemonRuntime::new();
runtime.activate(config.to_string());
Self { runtime }
}
pub fn drive(&mut self, now_ms: u64) -> Option<u64> {
loop {
let pending = with_host(|host| host.machine.pending.pop_front());
let Some(event) = pending else {
break;
};
self.runtime.event(event);
}
self.runtime.drive(now_ms)
}
pub fn output(&mut self, pid: u32, stderr: bool, bytes: &[u8]) {
with_host(|host| host.output(pid, stderr, bytes));
}
pub fn exit(&mut self, pid: u32, status: i32) {
with_host(|host| host.exited(pid, status));
}
pub fn file_changed(&mut self, path: &str) -> usize {
with_host(|host| host.changed(path))
}
pub fn plugin<R>(&self, f: impl FnOnce(&P) -> R) -> R {
self.runtime
.with_plugin(f)
.unwrap_or_else(|| panic!("the plugin is not active"))
}
pub fn event(&mut self, event: Event) {
self.runtime.event(event);
}
pub fn interest(&mut self, surfaces: &[&str]) {
let payload = serde_json::json!({ "surfaces": surfaces }).to_string();
self.runtime.event(Event::Plugin {
name: crate::daemon::INTEREST_EVENT.to_string(),
payload: crate::Json(payload),
});
}
pub fn call(&mut self, method: &str, request: &str) -> Result<String> {
self.call_from(method, request, crate::daemon::Caller::default())
}
pub fn call_from(
&mut self,
method: &str,
request: &str,
caller: crate::daemon::Caller,
) -> Result<String> {
self.runtime
.handle_call(method, request.to_string(), caller)
}
pub fn host<R>(&self, f: impl FnOnce(&mut MockHost) -> R) -> R {
with_host(f)
}
pub fn deactivate(self) {
self.runtime.deactivate();
}
}
pub(crate) mod host_calls {
use super::*;
fn check_key_name(key: &str) -> Result<()> {
let name = key.split_once('.').map_or("", |(_, name)| name);
let allowed =
|c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '/' | '-');
let first = name.chars().next();
if first.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') && name.chars().all(allowed)
{
Ok(())
} else {
Err(Error::Invalid(alloc::format!(
"{key:?}: a key is <namespace>.<name>, the name of letters, digits and _ . : / -"
)))
}
}
pub(crate) fn values_get(key: &str) -> Result<Option<String>> {
with_host(|host| {
host.check_namespaced("values.read", key)?;
Ok(host.values.get(key).cloned())
})
}
pub(crate) fn values_set(key: &str, value: &str) -> Result<()> {
with_host(|host| {
host.check_namespaced("values.write", key)?;
check_key_name(key)?;
host.values.insert(key.to_string(), value.to_string());
Ok(())
})
}
pub(crate) fn values_delete(key: &str) -> Result<()> {
with_host(|host| {
host.check_namespaced("values.write", key)?;
host.values.remove(key);
Ok(())
})
}
pub(crate) fn values_keys(prefix: &str) -> Result<Vec<String>> {
with_host(|host| {
host.check_namespaced("values.read", prefix)?;
Ok(host
.values
.keys()
.filter(|key| key.starts_with(prefix))
.cloned()
.collect())
})
}
pub(crate) fn values_watch(prefix: &str) -> Result<()> {
with_host(|host| host.check_namespaced("values.read", prefix))
}
pub(crate) fn live_publish(key: &str, payload: &str) -> Result<()> {
with_host(|host| {
host.check_namespaced("live.publish", key)?;
check_key_name(key)?;
host.published.push((key.to_string(), payload.to_string()));
Ok(())
})
}
pub(crate) fn live_delete(key: &str) -> Result<()> {
with_host(|host| {
host.check_namespaced("live.publish", key)?;
host.live_deleted.push(key.to_string());
Ok(())
})
}
pub(crate) fn live_subscribe(prefix: &str) -> Result<()> {
with_host(|host| host.check_namespaced("live.subscribe", prefix))
}
pub(crate) fn live_unsubscribe(prefix: &str) -> Result<()> {
with_host(|host| host.check_namespaced("live.subscribe", prefix))
}
pub(crate) fn events_emit(name: &str, payload: &str, to: &Target) -> Result<()> {
with_host(|host| {
if name.starts_with("system.") {
return Err(host.deny(GrantDenied {
grant: alloc::format!("events.emit:{name}"),
}));
}
host.check_namespaced("events.emit", name)?;
host.emitted
.push((name.to_string(), payload.to_string(), to.clone()));
Ok(())
})
}
pub(crate) fn events_on(pattern: &str) -> Result<()> {
with_host(|host| {
host.check_namespaced("events.on", pattern)?;
host.listening.insert(pattern.to_string());
Ok(())
})
}
pub(crate) fn events_off(pattern: &str) -> Result<()> {
with_host(|host| {
host.check_namespaced("events.on", pattern)?;
host.listening.remove(pattern);
Ok(())
})
}
fn record_call(
host: &mut MockHost,
method: &str,
payload: &str,
to: &Target,
kind: CallKind,
timeout_ms: Option<u32>,
) -> Result<()> {
let own = alloc::format!("call:{}", host.grants.plugin_id());
host.check(&own)?;
host.calls.push(MockCall {
method: method.to_string(),
payload: payload.to_string(),
target: to.clone(),
kind,
timeout_ms,
answered: false,
});
Ok(())
}
pub(crate) fn call(
method: &str,
payload: &str,
to: &Target,
timeout_ms: Option<u32>,
) -> Result<String> {
with_host(|host| {
record_call(host, method, payload, to, CallKind::Wait, timeout_ms)?;
host.answer(method)
})
}
pub(crate) fn call_send(method: &str, payload: &str, to: &Target) -> Result<()> {
with_host(|host| record_call(host, method, payload, to, CallKind::Send, None))
}
pub(crate) fn call_async(
method: &str,
payload: &str,
to: &Target,
timeout_ms: Option<u32>,
) -> Result<u64> {
with_host(|host| {
let id = host
.calls
.iter()
.filter(|call| matches!(call.kind, CallKind::Async(_)))
.count() as u64
+ 1;
record_call(host, method, payload, to, CallKind::Async(id), timeout_ms)?;
Ok(id)
})
}
pub(crate) fn config() -> String {
with_host(|host| host.config.clone())
}
pub(crate) fn health_set(health: crate::health::Health, message: &str) {
with_host(|host| host.health = Some((health, message.to_string())));
}
pub(crate) fn secret(name: &str) -> Result<Option<String>> {
with_host(|host| {
host.check(&alloc::format!("secret:{name}"))?;
Ok(host.secrets.get(name).cloned())
})
}
pub(crate) fn account_state() -> Result<AccountState> {
with_host(|host| {
host.check("account.read")?;
Ok(host.account.clone())
})
}
pub(crate) fn account_watch() -> Result<()> {
with_host(|host| host.check("account.read"))
}
pub(crate) fn claim(key: &str, _ttl_ms: u32) -> Result<bool> {
with_host(|host| {
if host.claimed_elsewhere.contains(key) {
return Ok(false);
}
host.claims.insert(key.to_string());
Ok(true)
})
}
pub(crate) fn release(key: &str) -> Result<()> {
with_host(|host| {
host.claims.remove(key);
Ok(())
})
}
fn unknown(id: &str) -> Error {
Error::Invalid(alloc::format!("no surface {id:?} in the manifest"))
}
pub(crate) fn surface_layout(id: &str) -> Result<RegionLayout> {
with_host(|host| {
let surface = host.surfaces.get(id).ok_or_else(|| unknown(id))?;
Ok(RegionLayout::new(surface.model, surface.geometry))
})
}
pub(crate) fn surface_attach(id: &str, region: &[u8]) -> Result<()> {
with_host(|host| {
let surface = host.surfaces.get_mut(id).ok_or_else(|| unknown(id))?;
let layout = RegionLayout::new(surface.model, surface.geometry);
if surface.geometry.is_empty() {
return Err(Error::Invalid(alloc::format!(
"surface {id:?} has no size yet"
)));
}
if region.len() < layout.len as usize {
return Err(Error::Invalid("region too small".into()));
}
let word = |index: usize| {
u32::from_le_bytes([
region[index * 4],
region[index * 4 + 1],
region[index * 4 + 2],
region[index * 4 + 3],
])
};
let g = surface.geometry;
let expected = [
match surface.model {
Model::Cells => 0,
Model::Pixels => 1,
},
g.cols,
g.rows,
g.px_w,
g.px_h,
g.cell_px_w,
g.cell_px_h,
];
if (1..8).any(|index| word(index) != expected[index - 1]) {
return Err(Error::Invalid(
"region header does not describe the current layout".into(),
));
}
let bound = (region.as_ptr() as usize, region.len());
if surface.region.is_none() {
surface.region = Some(bound);
} else {
surface.pending = Some(bound);
}
Ok(())
})
}
pub(crate) fn surface_commit(id: &str, slot: u8, dirty: &[Rect]) -> Result<()> {
with_host(|host| {
let surface = host.surfaces.get_mut(id).ok_or_else(|| unknown(id))?;
if surface.region.is_none() && surface.pending.is_none() {
return Err(Error::Invalid(alloc::format!(
"surface {id:?} has no region attached"
)));
}
if slot > 1 {
return Err(Error::Invalid("slot must be 0 or 1".into()));
}
let full = surface.pending.is_some() || !surface.committed_once;
if let Some(pending) = surface.pending.take() {
surface.region = Some(pending);
}
let Some((address, len)) = surface.region else {
return Err(Error::Invalid("no region".into()));
};
let bytes = unsafe { core::slice::from_raw_parts(address as *const u8, len) };
let seq = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
surface.committed_once = true;
surface.commits.push(MockCommit {
seq,
slot,
dirty: dirty.to_vec(),
full,
geometry: surface.geometry,
model: surface.model,
region: bytes.to_vec(),
});
Ok(())
})
}
pub(crate) fn surface_detach(id: &str) -> Result<()> {
with_host(|host| {
let surface = host.surfaces.get_mut(id).ok_or_else(|| unknown(id))?;
surface.region = None;
surface.pending = None;
Ok(())
})
}
pub(crate) fn is_driving() -> bool {
with_host(|host| host.driving)
}
pub(crate) fn theme() -> Theme {
with_host(|host| host.theme)
}
pub(crate) fn capabilities() -> Capabilities {
with_host(|host| host.capabilities)
}
pub(crate) fn focused_pane() -> Option<String> {
with_host(|host| host.focused_pane.clone())
}
fn instance(surface: &str) -> Option<(&str, &str)> {
surface
.split_once('@')
.filter(|(_, owner)| !owner.is_empty())
}
pub(crate) fn surface_machine(surface: &str) -> Option<String> {
let (template, machine) = instance(surface)?;
with_host(|host| host.machine_surfaces.contains(template)).then(|| machine.to_string())
}
pub(crate) fn surface_project(surface: &str) -> Option<String> {
let (template, project) = instance(surface)?;
with_host(|host| host.project_surfaces.contains(template)).then(|| project.to_string())
}
pub(crate) fn surface_tint(surface: &str) -> Option<u32> {
if let Some(owner) = surface_machine(surface).or_else(|| surface_project(surface)) {
return with_host(|host| host.tints.get(&owner).copied());
}
let pane = surface_pane(surface)?;
with_host(|host| {
let pane = host
.account
.panes
.iter()
.find(|known| known.id == pane.id)?;
pane.project
.as_ref()
.and_then(|project| host.tints.get(project))
.or_else(|| host.tints.get(&pane.machine))
.copied()
})
}
pub(crate) fn identity_tint(id: &str) -> Option<u32> {
with_host(|host| host.tints.get(id).copied())
}
pub(crate) fn surface_request_size(id: &str, cols: u32, rows: u32) -> Result<()> {
with_host(|host| {
let known = host.surfaces.contains_key(id)
|| instance(id).is_some_and(|(template, _)| host.surfaces.contains_key(template));
if !known {
return Err(unknown(id));
}
host.requested
.insert(id.to_string(), (cols.min(1024), rows.min(512)));
Ok(())
})
}
pub(crate) fn surface_set_label(id: &str, label: Option<&str>) -> Result<()> {
with_host(|host| {
let known = host.surfaces.contains_key(id)
|| instance(id).is_some_and(|(template, _)| host.surfaces.contains_key(template));
if !known {
return Err(unknown(id));
}
match label {
Some(label)
if label.chars().count() > 48 || label.chars().any(char::is_control) =>
{
return Err(crate::Error::Invalid(
"a label is at most 48 characters with no control characters".into(),
));
}
Some(label) => {
host.labels.insert(id.to_string(), label.to_string());
}
None => {
host.labels.remove(id);
}
}
Ok(())
})
}
pub(crate) fn surface_set_caret(id: &str, caret: Option<(u32, u32)>) -> Result<()> {
with_host(|host| {
let known = host.surfaces.contains_key(id)
|| instance(id).is_some_and(|(template, _)| host.surfaces.contains_key(template));
if !known {
return Err(unknown(id));
}
match caret {
Some(caret) => {
host.carets.insert(id.to_string(), caret);
}
None => {
host.carets.remove(id);
}
}
Ok(())
})
}
pub(crate) fn surface_pane(surface: &str) -> Option<crate::api::view::PaneRef> {
let (template, pane) = instance(surface)?;
if with_host(|host| {
host.machine_surfaces.contains(template) || host.project_surfaces.contains(template)
}) {
return None;
}
let id = pane.to_string();
let generation = with_host(|host| {
host.account
.panes
.iter()
.find(|pane| pane.id == id)
.map_or(0, |pane| pane.generation)
});
Some(crate::api::view::PaneRef { id, generation })
}
pub(crate) fn machine_id() -> Option<String> {
with_host(|host| host.machine_id.clone())
}
pub(crate) fn instance_id() -> String {
with_host(|host| host.instance_id.clone())
}
pub(crate) fn wall_ms() -> u64 {
with_host(|host| host.wall_ms)
}
pub(crate) fn utc_offset_minutes() -> i32 {
with_host(|host| host.utc_offset_minutes)
}
pub(crate) fn time_zone() -> Option<String> {
with_host(|host| host.time_zone.clone())
}
pub(crate) fn daemon_machine_id() -> String {
with_host(|host| host.machine_id.clone().unwrap_or_default())
}
pub(crate) fn daemon_account_id() -> Option<String> {
with_host(|host| host.account_id.clone())
}
pub(crate) fn daemon_lease_epoch() -> Option<u64> {
with_host(|host| host.lease_epoch)
}
pub(crate) fn daemon_release() -> Option<crate::daemon::Release> {
with_host(|host| host.release.clone())
}
pub(crate) fn daemon_environment() -> Vec<(String, String)> {
with_host(|host| host.environment.clone())
}
pub(crate) fn request_frame() {
wake_at(0);
}
pub(crate) fn wake_at(at_ms: u64) {
with_host(|host| {
host.frame_request = Some(host.frame_request.map_or(at_ms, |at| at.min(at_ms)));
});
}
pub(crate) fn url_open(url: &str) -> Result<()> {
use standard_plugin_manifest::url::{URL_OPEN_WINDOW_MS, https_host};
with_host(|host| {
let domain = https_host(url).map_err(Error::Invalid)?;
if !host.grants.allows_url_open(&domain) {
return Err(host.deny(GrantDenied {
grant: alloc::format!("url.open:{domain}"),
}));
}
let now = host.now_ms;
if !host
.url_gesture_ms
.is_some_and(|at| host.gesture || now.saturating_sub(at) <= URL_OPEN_WINDOW_MS)
{
return Err(Error::Invalid(
"url.open needs user input within the last second".into(),
));
}
host.url_gesture_ms = None;
host.opened_urls.push(url.to_string());
Ok(())
})
}
pub(crate) fn surface_open(id: &str) -> Result<()> {
with_host(|host| {
if !host.surfaces.contains_key(id) {
return Err(unknown(id));
}
if !host.gesture {
return Err(Error::Invalid(
"surface.open needs a user gesture or a command".into(),
));
}
host.opened.push(id.to_string());
Ok(())
})
}
pub(crate) fn focus_pane(pane: &str) -> Result<()> {
with_host(|host| {
if !host.gesture {
return Err(Error::Invalid(
"view.focus-pane needs a user gesture or a command".into(),
));
}
host.focused_panes.push(pane.to_string());
Ok(())
})
}
pub(crate) fn surface_close(id: &str) -> Result<()> {
with_host(|host| {
host.closed.push(id.to_string());
Ok(())
})
}
pub(crate) fn process_spawn(program: &str, args: &[String], cwd: Option<&str>) -> Result<u32> {
let mut command = crate::daemon::process::Command::new(program);
command.args = args.to_vec();
command.cwd = cwd.map(ToString::to_string);
with_host(|host| host.spawn(&command))
}
pub(crate) fn process_run(command: &crate::daemon::process::Command) -> Result<u32> {
with_host(|host| host.spawn(command))
}
pub(crate) fn process_write(pid: u32, bytes: &[u8]) -> Result<()> {
with_host(|host| host.write(pid, bytes))
}
pub(crate) fn process_close_stdin(pid: u32) -> Result<()> {
with_host(|host| host.close_stdin(pid))
}
pub(crate) fn process_try_wait(pid: u32) -> Result<Option<i32>> {
with_host(|host| host.try_wait(pid))
}
pub(crate) fn process_wait(pid: u32) -> Result<i32> {
with_host(|host| host.wait(pid))
}
pub(crate) fn process_kill(pid: u32) -> Result<()> {
with_host(|host| host.kill(pid))
}
pub(crate) fn watch(path: &str, options: &crate::daemon::watch::Options) -> Result<u32> {
with_host(|host| host.watch(path, options.recursive, &options.exclude))
}
pub(crate) fn unwatch(handle: u32) -> Result<()> {
with_host(|host| host.unwatch(handle))
}
pub(crate) fn websocket_open(url: &str, headers: &[(String, String)]) -> Result<u32> {
with_host(|host| host.websocket_open(url, headers))
}
pub(crate) fn websocket_send(socket: u32, text: &str) -> Result<()> {
with_host(|host| host.websocket_send(socket, text))
}
pub(crate) fn websocket_close(socket: u32) {
with_host(|host| host.websocket_close(socket));
}
pub(crate) fn panes() -> Result<Vec<Pane>> {
with_host(|host| {
host.check("panes.read")?;
Ok(host.panes.clone())
})
}
pub(crate) fn pane_create(
cwd: &str,
command: Option<&str>,
env: &[(String, String)],
) -> Result<(String, u64)> {
with_host(|host| {
host.check("panes.write")?;
host.create_pane(cwd, command, env, None)
})
}
pub(crate) fn pane_create_with(
cwd: &str,
command: Option<&str>,
env: &[(String, String)],
title: Option<&str>,
) -> Result<(String, u64)> {
with_host(|host| {
host.check("panes.write")?;
host.create_pane(cwd, command, env, title)
})
}
pub(crate) fn pane_input(_pane: &str, _bytes: &[u8]) -> Result<()> {
with_host(|host| host.check("panes.write"))
}
pub(crate) fn pane_close(_pane: &str) -> Result<()> {
with_host(|host| host.check("panes.write"))
}
pub(crate) fn panes_subscribe(_on: bool) -> Result<()> {
with_host(|host| host.check("panes.read"))
}
pub(crate) fn pane_wait(_pane: &str, _timeout_ms: u32) -> Result<bool> {
with_host(|host| {
host.check("panes.read")?;
Ok(true)
})
}
}