1#![doc = include_str!("../tests/testing_harness.rs")]
12mod http;
15mod machine;
16
17pub(crate) use http::send as http_send;
18
19pub use machine::{MockPaneLaunch, MockProcess, MockWatch, MockWebSocket, ProcessScript};
20
21use alloc::collections::{BTreeMap, BTreeSet};
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24use core::cell::RefCell;
25
26use standard_plugin_manifest::{GrantDenied, Grants};
27
28use crate::api::Target;
29use crate::api::account::{AccountState, Pane};
30use crate::api::capabilities::Capabilities;
31use crate::api::view::Theme;
32use crate::error::{Error, Result};
33use crate::geometry::{Geometry, Rect};
34use crate::surface::{HEADER_LEN, Model, RegionLayout};
35use crate::ui_runtime::{Event, Power, UiPlugin, UiRuntime};
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum CallKind {
40 Wait,
42 Send,
44 Async(u64),
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct MockCall {
52 pub method: String,
53 pub payload: String,
54 pub target: Target,
55 pub kind: CallKind,
56 pub timeout_ms: Option<u32>,
57 pub answered: bool,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct MockCommit {
64 pub seq: u32,
66 pub slot: u8,
67 pub dirty: Vec<Rect>,
69 pub full: bool,
71 pub geometry: Geometry,
72 pub model: Model,
73 pub region: Vec<u8>,
75}
76
77impl MockCommit {
78 pub fn slot_bytes(&self) -> &[u8] {
80 let layout = RegionLayout::new(self.model, self.geometry);
81 let start = (HEADER_LEN + u32::from(self.slot) * layout.slot_len) as usize;
82 &self.region[start..start + layout.slot_len as usize]
83 }
84}
85
86#[derive(Clone, Debug)]
87struct MockSurface {
88 model: Model,
89 geometry: Geometry,
90 region: Option<(usize, usize)>,
92 pending: Option<(usize, usize)>,
93 committed_once: bool,
94 commits: Vec<MockCommit>,
95}
96
97#[derive(Clone, Debug)]
100pub struct MockHost {
101 plugin_id: String,
102 granted: Vec<String>,
103 grants: Grants,
104 pub config: String,
105 pub values: BTreeMap<String, String>,
106 pub secrets: BTreeMap<String, String>,
107 pub account: AccountState,
108 pub driving: bool,
109 pub theme: Theme,
110 pub capabilities: Capabilities,
111 pub focused_pane: Option<String>,
112 pub claimed_elsewhere: BTreeSet<String>,
114 pub claims: BTreeSet<String>,
116 pub emitted: Vec<(String, String, Target)>,
118 pub published: Vec<(String, String)>,
120 pub live_deleted: Vec<String>,
122 pub listening: BTreeSet<String>,
124 pub call_answers: BTreeMap<String, String>,
127 pub calls: Vec<MockCall>,
129 pub denials: Vec<String>,
131 pub panes: Vec<Pane>,
132 pub frame_request: Option<u64>,
136 pub opened: Vec<String>,
138 pub opened_urls: Vec<String>,
140 pub focused_panes: Vec<String>,
142 pub now_ms: u64,
145 pub url_gesture_ms: Option<u64>,
148 pub closed: Vec<String>,
150 pub gesture: bool,
153 pub machine_id: Option<String>,
156 pub account_id: Option<String>,
158 pub lease_epoch: Option<u64>,
161 pub release: Option<crate::daemon::Release>,
163 pub instance_id: String,
165 pub environment: Vec<(String, String)>,
167 pub wall_ms: u64,
169 pub utc_offset_minutes: i32,
171 pub time_zone: Option<String>,
173 pub health: Option<(crate::health::Health, String)>,
175 pub tints: BTreeMap<String, u32>,
178 pub requested: BTreeMap<String, (u32, u32)>,
181 pub labels: BTreeMap<String, String>,
183 pub carets: BTreeMap<String, (u32, u32)>,
185 machine_surfaces: BTreeSet<String>,
187 project_surfaces: BTreeSet<String>,
189 surfaces: BTreeMap<String, MockSurface>,
190 machine: machine::Machine,
192 http: http::MockHttp,
194}
195
196impl MockHost {
197 pub fn new(plugin_id: &str) -> Self {
199 Self {
200 plugin_id: plugin_id.to_string(),
201 granted: Vec::new(),
202 grants: Grants::new(plugin_id, []),
203 config: String::new(),
204 values: BTreeMap::new(),
205 secrets: BTreeMap::new(),
206 account: AccountState::default(),
207 driving: false,
208 theme: Theme::default(),
209 capabilities: Capabilities {
210 graphics: false,
211 kitty: false,
212 frame_rate: 60,
213 cell_px: (0, 0),
214 pixel_scale: 1,
215 },
216 focused_pane: None,
217 claimed_elsewhere: BTreeSet::new(),
218 claims: BTreeSet::new(),
219 emitted: Vec::new(),
220 published: Vec::new(),
221 live_deleted: Vec::new(),
222 listening: BTreeSet::new(),
223 call_answers: BTreeMap::new(),
224 calls: Vec::new(),
225 denials: Vec::new(),
226 panes: Vec::new(),
227 frame_request: None,
228 opened: Vec::new(),
229 focused_panes: Vec::new(),
230 opened_urls: Vec::new(),
231 now_ms: 0,
232 url_gesture_ms: None,
233 closed: Vec::new(),
234 gesture: false,
235 machine_id: None,
236 account_id: None,
237 lease_epoch: None,
238 release: None,
239 instance_id: "test-viewer".to_string(),
240 environment: Vec::new(),
241 wall_ms: 0,
242 utc_offset_minutes: 0,
243 time_zone: None,
244 health: None,
245 tints: BTreeMap::new(),
246 requested: BTreeMap::new(),
247 labels: BTreeMap::new(),
248 carets: BTreeMap::new(),
249 machine_surfaces: BTreeSet::new(),
250 project_surfaces: BTreeSet::new(),
251 surfaces: BTreeMap::new(),
252 machine: machine::Machine::default(),
253 http: http::MockHttp::default(),
254 }
255 }
256
257 pub fn clock(mut self, wall_ms: u64, utc_offset_minutes: i32, time_zone: Option<&str>) -> Self {
259 self.wall_ms = wall_ms;
260 self.utc_offset_minutes = utc_offset_minutes;
261 self.time_zone = time_zone.map(str::to_string);
262 self
263 }
264
265 pub fn singleton(mut self, epoch: u64) -> Self {
267 self.lease_epoch = Some(epoch);
268 self
269 }
270
271 pub fn machine(mut self, machine_id: &str) -> Self {
273 self.machine_id = Some(machine_id.to_string());
274 self
275 }
276
277 pub fn env(mut self, name: &str, value: &str) -> Self {
279 self.environment.retain(|(key, _)| key != name);
280 self.environment.push((name.to_string(), value.to_string()));
281 self.environment.sort();
282 self
283 }
284
285 pub fn grant(mut self, grants: &[&str]) -> Self {
287 self.granted
288 .extend(grants.iter().map(|grant| grant.to_string()));
289 self.grants = Grants::new(&self.plugin_id, self.granted.iter().cloned());
290 self
291 }
292
293 pub fn surface(mut self, id: &str, model: Model) -> Self {
295 self.surfaces.insert(
296 id.to_string(),
297 MockSurface {
298 model,
299 geometry: Geometry::default(),
300 region: None,
301 pending: None,
302 committed_once: false,
303 commits: Vec::new(),
304 },
305 );
306 self
307 }
308
309 pub fn machine_surface(mut self, id: &str, model: Model) -> Self {
312 self.machine_surfaces.insert(id.to_string());
313 self.surface(id, model)
314 }
315
316 pub fn project_surface(mut self, id: &str, model: Model) -> Self {
319 self.project_surfaces.insert(id.to_string());
320 self.surface(id, model)
321 }
322
323 pub fn driving(mut self, driving: bool) -> Self {
324 self.driving = driving;
325 self
326 }
327
328 pub fn set_geometry(&mut self, id: &str, geometry: Geometry) -> Option<Event> {
331 if !self.surfaces.contains_key(id)
332 && let Some((base, _pane)) = id.split_once('@')
333 && let Some(template) = self.surfaces.get(base)
334 {
335 let mut instance = template.clone();
337 instance.geometry = Geometry::default();
338 instance.region = None;
339 instance.pending = None;
340 instance.committed_once = false;
341 instance.commits.clear();
342 self.surfaces.insert(id.to_string(), instance);
343 }
344 let surface = self.surfaces.get_mut(id)?;
345 if surface.geometry == geometry {
346 return None;
347 }
348 surface.geometry = geometry;
349 Some(Event::Resize {
350 surface: id.to_string(),
351 geometry,
352 })
353 }
354
355 pub fn set_model(&mut self, id: &str, model: Model) -> Option<Event> {
358 let surface = self.surfaces.get_mut(id)?;
359 if surface.model == model {
360 return None;
361 }
362 surface.model = model;
363 surface.region = None;
364 surface.pending = None;
365 surface.committed_once = false;
366 Some(Event::Resize {
367 surface: id.to_string(),
368 geometry: surface.geometry,
369 })
370 }
371
372 pub fn commits(&self, id: &str) -> Vec<MockCommit> {
374 self.surfaces
375 .get(id)
376 .map(|surface| surface.commits.clone())
377 .unwrap_or_default()
378 }
379
380 fn answer(&self, method: &str) -> Result<String> {
382 self.call_answers
383 .get(method)
384 .cloned()
385 .ok_or_else(|| Error::Invalid(alloc::format!("unknown method {method:?}")))
386 }
387
388 fn take_async_answers(&mut self) -> Vec<Event> {
391 let mut events = Vec::new();
392 for index in 0..self.calls.len() {
393 let CallKind::Async(id) = self.calls[index].kind else {
394 continue;
395 };
396 if self.calls[index].answered {
397 continue;
398 }
399 self.calls[index].answered = true;
400 let result = self.answer(&self.calls[index].method).map(crate::Json);
401 events.push(Event::CallResult {
402 id: crate::api::calls::CallId(id),
403 result,
404 });
405 }
406 events
407 }
408
409 fn deny(&mut self, denial: GrantDenied) -> Error {
410 self.denials.push(denial.grant.clone());
411 Error::GrantDenied {
412 grant: denial.grant,
413 }
414 }
415
416 fn check(&mut self, grant: &str) -> Result<()> {
417 self.grants.check(grant).map_err(|denial| self.deny(denial))
418 }
419
420 fn check_namespaced(&mut self, kind: &str, subject: &str) -> Result<()> {
421 self.grants
422 .check_namespaced(kind, subject)
423 .map_err(|denial| self.deny(denial))
424 }
425}
426
427std::thread_local! {
428 static MOCK: RefCell<Option<MockHost>> = const { RefCell::new(None) };
429}
430
431pub fn install(host: MockHost) {
434 MOCK.with(|mock| *mock.borrow_mut() = Some(host));
435}
436
437pub fn with_host<R>(f: impl FnOnce(&mut MockHost) -> R) -> R {
439 MOCK.with(|mock| {
440 let mut mock = mock.borrow_mut();
441 f(mock.get_or_insert_with(|| MockHost::new("test-plugin")))
442 })
443}
444
445pub struct Harness<P: UiPlugin> {
447 runtime: UiRuntime<P>,
448}
449
450impl<P: UiPlugin> Harness<P> {
451 pub fn activate(host: MockHost, config: &str) -> Self {
453 let mut host = host;
454 host.config = config.to_string();
455 install(host);
456 let runtime = UiRuntime::new();
457 runtime.activate(config.to_string());
458 Self { runtime }
459 }
460
461 pub fn resize(&mut self, surface: &str, geometry: Geometry) {
463 if let Some(event) = with_host(|host| host.set_geometry(surface, geometry)) {
464 self.runtime.event(event);
465 }
466 }
467
468 pub fn frame(&mut self, now_ms: u64, interval_ms: u32, power: Power) -> Option<u64> {
471 with_host(|host| {
472 host.frame_request = None;
473 host.now_ms = now_ms;
474 });
475 self.runtime.frame(now_ms, interval_ms, power)
476 }
477
478 pub fn event(&mut self, event: Event) {
479 let gesture = matches!(
480 &event,
481 Event::Key(_)
482 | Event::Paste { .. }
483 | Event::Command(_)
484 | Event::Pointer(crate::Pointer {
485 kind: crate::PointerKind::Down,
486 ..
487 })
488 );
489 with_host(|host| {
490 host.gesture = gesture;
491 if gesture {
492 host.url_gesture_ms = Some(host.now_ms);
493 }
494 });
495 self.runtime.event(event);
496 with_host(|host| host.gesture = false);
497 }
498
499 pub fn show(&mut self, surface: &str, visible: bool) {
501 self.event(Event::Visibility {
502 surface: surface.to_string(),
503 visible,
504 });
505 }
506
507 pub fn set_capabilities(&mut self, capabilities: Capabilities) {
509 with_host(|host| host.capabilities = capabilities);
510 self.event(Event::CapabilitiesChanged);
511 }
512
513 pub fn set_theme(&mut self, theme: Theme) {
515 with_host(|host| host.theme = theme);
516 self.event(Event::ThemeChanged);
517 }
518
519 pub fn set_model(&mut self, surface: &str, model: Model) {
522 if let Some(event) = with_host(|host| host.set_model(surface, model)) {
523 self.runtime.event(event);
524 }
525 }
526
527 pub fn key(&mut self, surface: &str, code: crate::KeyCode) {
529 self.event(Event::Key(crate::Key::press(surface, code)));
530 }
531
532 pub fn paste(&mut self, surface: &str, text: &str) {
534 self.event(Event::Paste {
535 surface: surface.to_string(),
536 text: text.to_string(),
537 });
538 }
539
540 pub fn pointer(&mut self, surface: &str, kind: crate::PointerKind, x: u32, y: u32) {
542 let cell = with_host(|host| {
543 host.surfaces
544 .get(surface)
545 .map_or((1, 1), |surface| match surface.model {
546 Model::Cells => (1, 1),
547 Model::Pixels => (
548 surface.geometry.cell_px_w.max(1),
549 surface.geometry.cell_px_h.max(1),
550 ),
551 })
552 });
553 self.event(Event::Pointer(crate::Pointer {
554 surface: surface.to_string(),
555 x,
556 y,
557 col: x / cell.0,
558 row: y / cell.1,
559 button: if matches!(kind, crate::PointerKind::Move) {
560 crate::Button::None
561 } else {
562 crate::Button::Left
563 },
564 kind,
565 modifiers: crate::Modifiers::NONE,
566 }));
567 }
568
569 pub fn command(&mut self, id: &str) {
571 self.event(Event::Command(id.to_string()));
572 }
573
574 pub fn answer_calls(&mut self) -> usize {
578 let events = with_host(MockHost::take_async_answers);
579 let count = events.len();
580 for event in events {
581 self.event(event);
582 }
583 count
584 }
585
586 pub fn take_frame_request(&mut self) -> Option<u64> {
588 with_host(|host| host.frame_request.take())
589 }
590
591 pub fn plugin<R>(&self, f: impl FnOnce(&mut P) -> R) -> R {
593 self.runtime
594 .with_plugin(f)
595 .unwrap_or_else(|| panic!("the plugin is not active"))
596 }
597
598 pub fn commits(&self, surface: &str) -> Vec<MockCommit> {
599 with_host(|host| host.commits(surface))
600 }
601
602 pub fn host<R>(&self, f: impl FnOnce(&mut MockHost) -> R) -> R {
603 with_host(f)
604 }
605
606 pub fn deactivate(self) {
607 self.runtime.deactivate();
608 }
609}
610
611pub struct DaemonHarness<P: crate::daemon::DaemonPlugin> {
614 runtime: crate::daemon::runtime::DaemonRuntime<P>,
615}
616
617impl<P: crate::daemon::DaemonPlugin> DaemonHarness<P> {
618 pub fn activate(host: MockHost, config: &str) -> Self {
620 let mut host = host;
621 host.config = config.to_string();
622 install(host);
623 let runtime = crate::daemon::runtime::DaemonRuntime::new();
624 runtime.activate(config.to_string());
625 Self { runtime }
626 }
627
628 pub fn drive(&mut self, now_ms: u64) -> Option<u64> {
632 loop {
633 let pending = with_host(|host| host.machine.pending.pop_front());
634 let Some(event) = pending else {
635 break;
636 };
637 self.runtime.event(event);
638 }
639 self.runtime.drive(now_ms)
640 }
641
642 pub fn output(&mut self, pid: u32, stderr: bool, bytes: &[u8]) {
645 with_host(|host| host.output(pid, stderr, bytes));
646 }
647
648 pub fn exit(&mut self, pid: u32, status: i32) {
650 with_host(|host| host.exited(pid, status));
651 }
652
653 pub fn file_changed(&mut self, path: &str) -> usize {
657 with_host(|host| host.changed(path))
658 }
659
660 pub fn plugin<R>(&self, f: impl FnOnce(&P) -> R) -> R {
662 self.runtime
663 .with_plugin(f)
664 .unwrap_or_else(|| panic!("the plugin is not active"))
665 }
666
667 pub fn event(&mut self, event: Event) {
669 self.runtime.event(event);
670 }
671
672 pub fn interest(&mut self, surfaces: &[&str]) {
678 let payload = serde_json::json!({ "surfaces": surfaces }).to_string();
679 self.runtime.event(Event::Plugin {
680 name: crate::daemon::INTEREST_EVENT.to_string(),
681 payload: crate::Json(payload),
682 });
683 }
684
685 pub fn call(&mut self, method: &str, request: &str) -> Result<String> {
687 self.call_from(method, request, crate::daemon::Caller::default())
688 }
689
690 pub fn call_from(
692 &mut self,
693 method: &str,
694 request: &str,
695 caller: crate::daemon::Caller,
696 ) -> Result<String> {
697 self.runtime
698 .handle_call(method, request.to_string(), caller)
699 }
700
701 pub fn host<R>(&self, f: impl FnOnce(&mut MockHost) -> R) -> R {
702 with_host(f)
703 }
704
705 pub fn deactivate(self) {
706 self.runtime.deactivate();
707 }
708}
709
710pub(crate) mod host_calls {
712 use super::*;
713
714 fn check_key_name(key: &str) -> Result<()> {
719 let name = key.split_once('.').map_or("", |(_, name)| name);
720 let allowed =
721 |c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | ':' | '/' | '-');
722 let first = name.chars().next();
723 if first.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_') && name.chars().all(allowed)
724 {
725 Ok(())
726 } else {
727 Err(Error::Invalid(alloc::format!(
728 "{key:?}: a key is <namespace>.<name>, the name of letters, digits and _ . : / -"
729 )))
730 }
731 }
732
733 pub(crate) fn values_get(key: &str) -> Result<Option<String>> {
734 with_host(|host| {
735 host.check_namespaced("values.read", key)?;
736 Ok(host.values.get(key).cloned())
737 })
738 }
739
740 pub(crate) fn values_set(key: &str, value: &str) -> Result<()> {
741 with_host(|host| {
742 host.check_namespaced("values.write", key)?;
743 check_key_name(key)?;
744 host.values.insert(key.to_string(), value.to_string());
745 Ok(())
746 })
747 }
748
749 pub(crate) fn values_delete(key: &str) -> Result<()> {
750 with_host(|host| {
751 host.check_namespaced("values.write", key)?;
752 host.values.remove(key);
753 Ok(())
754 })
755 }
756
757 pub(crate) fn values_keys(prefix: &str) -> Result<Vec<String>> {
758 with_host(|host| {
759 host.check_namespaced("values.read", prefix)?;
760 Ok(host
761 .values
762 .keys()
763 .filter(|key| key.starts_with(prefix))
764 .cloned()
765 .collect())
766 })
767 }
768
769 pub(crate) fn values_watch(prefix: &str) -> Result<()> {
770 with_host(|host| host.check_namespaced("values.read", prefix))
771 }
772
773 pub(crate) fn live_publish(key: &str, payload: &str) -> Result<()> {
774 with_host(|host| {
775 host.check_namespaced("live.publish", key)?;
776 check_key_name(key)?;
777 host.published.push((key.to_string(), payload.to_string()));
778 Ok(())
779 })
780 }
781
782 pub(crate) fn live_delete(key: &str) -> Result<()> {
783 with_host(|host| {
784 host.check_namespaced("live.publish", key)?;
785 host.live_deleted.push(key.to_string());
786 Ok(())
787 })
788 }
789
790 pub(crate) fn live_subscribe(prefix: &str) -> Result<()> {
791 with_host(|host| host.check_namespaced("live.subscribe", prefix))
792 }
793
794 pub(crate) fn live_unsubscribe(prefix: &str) -> Result<()> {
795 with_host(|host| host.check_namespaced("live.subscribe", prefix))
796 }
797
798 pub(crate) fn events_emit(name: &str, payload: &str, to: &Target) -> Result<()> {
799 with_host(|host| {
800 if name.starts_with("system.") {
801 return Err(host.deny(GrantDenied {
802 grant: alloc::format!("events.emit:{name}"),
803 }));
804 }
805 host.check_namespaced("events.emit", name)?;
806 host.emitted
807 .push((name.to_string(), payload.to_string(), to.clone()));
808 Ok(())
809 })
810 }
811
812 pub(crate) fn events_on(pattern: &str) -> Result<()> {
813 with_host(|host| {
814 host.check_namespaced("events.on", pattern)?;
815 host.listening.insert(pattern.to_string());
816 Ok(())
817 })
818 }
819
820 pub(crate) fn events_off(pattern: &str) -> Result<()> {
821 with_host(|host| {
822 host.check_namespaced("events.on", pattern)?;
823 host.listening.remove(pattern);
824 Ok(())
825 })
826 }
827
828 fn record_call(
829 host: &mut MockHost,
830 method: &str,
831 payload: &str,
832 to: &Target,
833 kind: CallKind,
834 timeout_ms: Option<u32>,
835 ) -> Result<()> {
836 let own = alloc::format!("call:{}", host.grants.plugin_id());
837 host.check(&own)?;
838 host.calls.push(MockCall {
839 method: method.to_string(),
840 payload: payload.to_string(),
841 target: to.clone(),
842 kind,
843 timeout_ms,
844 answered: false,
845 });
846 Ok(())
847 }
848
849 pub(crate) fn call(
850 method: &str,
851 payload: &str,
852 to: &Target,
853 timeout_ms: Option<u32>,
854 ) -> Result<String> {
855 with_host(|host| {
856 record_call(host, method, payload, to, CallKind::Wait, timeout_ms)?;
857 host.answer(method)
858 })
859 }
860
861 pub(crate) fn call_send(method: &str, payload: &str, to: &Target) -> Result<()> {
862 with_host(|host| record_call(host, method, payload, to, CallKind::Send, None))
863 }
864
865 pub(crate) fn call_async(
866 method: &str,
867 payload: &str,
868 to: &Target,
869 timeout_ms: Option<u32>,
870 ) -> Result<u64> {
871 with_host(|host| {
872 let id = host
873 .calls
874 .iter()
875 .filter(|call| matches!(call.kind, CallKind::Async(_)))
876 .count() as u64
877 + 1;
878 record_call(host, method, payload, to, CallKind::Async(id), timeout_ms)?;
879 Ok(id)
880 })
881 }
882
883 pub(crate) fn config() -> String {
884 with_host(|host| host.config.clone())
885 }
886
887 pub(crate) fn health_set(health: crate::health::Health, message: &str) {
888 with_host(|host| host.health = Some((health, message.to_string())));
889 }
890
891 pub(crate) fn secret(name: &str) -> Result<Option<String>> {
892 with_host(|host| {
893 host.check(&alloc::format!("secret:{name}"))?;
894 Ok(host.secrets.get(name).cloned())
895 })
896 }
897
898 pub(crate) fn account_state() -> Result<AccountState> {
899 with_host(|host| {
900 host.check("account.read")?;
901 Ok(host.account.clone())
902 })
903 }
904
905 pub(crate) fn account_watch() -> Result<()> {
906 with_host(|host| host.check("account.read"))
907 }
908
909 pub(crate) fn claim(key: &str, _ttl_ms: u32) -> Result<bool> {
910 with_host(|host| {
911 if host.claimed_elsewhere.contains(key) {
912 return Ok(false);
913 }
914 host.claims.insert(key.to_string());
915 Ok(true)
916 })
917 }
918
919 pub(crate) fn release(key: &str) -> Result<()> {
920 with_host(|host| {
921 host.claims.remove(key);
922 Ok(())
923 })
924 }
925
926 fn unknown(id: &str) -> Error {
927 Error::Invalid(alloc::format!("no surface {id:?} in the manifest"))
928 }
929
930 pub(crate) fn surface_layout(id: &str) -> Result<RegionLayout> {
931 with_host(|host| {
932 let surface = host.surfaces.get(id).ok_or_else(|| unknown(id))?;
933 Ok(RegionLayout::new(surface.model, surface.geometry))
934 })
935 }
936
937 pub(crate) fn surface_attach(id: &str, region: &[u8]) -> Result<()> {
938 with_host(|host| {
939 let surface = host.surfaces.get_mut(id).ok_or_else(|| unknown(id))?;
940 let layout = RegionLayout::new(surface.model, surface.geometry);
941 if surface.geometry.is_empty() {
942 return Err(Error::Invalid(alloc::format!(
943 "surface {id:?} has no size yet"
944 )));
945 }
946 if region.len() < layout.len as usize {
947 return Err(Error::Invalid("region too small".into()));
948 }
949 let word = |index: usize| {
950 u32::from_le_bytes([
951 region[index * 4],
952 region[index * 4 + 1],
953 region[index * 4 + 2],
954 region[index * 4 + 3],
955 ])
956 };
957 let g = surface.geometry;
958 let expected = [
959 match surface.model {
960 Model::Cells => 0,
961 Model::Pixels => 1,
962 },
963 g.cols,
964 g.rows,
965 g.px_w,
966 g.px_h,
967 g.cell_px_w,
968 g.cell_px_h,
969 ];
970 if (1..8).any(|index| word(index) != expected[index - 1]) {
971 return Err(Error::Invalid(
972 "region header does not describe the current layout".into(),
973 ));
974 }
975 let bound = (region.as_ptr() as usize, region.len());
976 if surface.region.is_none() {
977 surface.region = Some(bound);
978 } else {
979 surface.pending = Some(bound);
980 }
981 Ok(())
982 })
983 }
984
985 pub(crate) fn surface_commit(id: &str, slot: u8, dirty: &[Rect]) -> Result<()> {
986 with_host(|host| {
987 let surface = host.surfaces.get_mut(id).ok_or_else(|| unknown(id))?;
988 if surface.region.is_none() && surface.pending.is_none() {
989 return Err(Error::Invalid(alloc::format!(
990 "surface {id:?} has no region attached"
991 )));
992 }
993 if slot > 1 {
994 return Err(Error::Invalid("slot must be 0 or 1".into()));
995 }
996 let full = surface.pending.is_some() || !surface.committed_once;
997 if let Some(pending) = surface.pending.take() {
998 surface.region = Some(pending);
999 }
1000 let Some((address, len)) = surface.region else {
1001 return Err(Error::Invalid("no region".into()));
1002 };
1003 let bytes = unsafe { core::slice::from_raw_parts(address as *const u8, len) };
1008 let seq = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1009 surface.committed_once = true;
1010 surface.commits.push(MockCommit {
1011 seq,
1012 slot,
1013 dirty: dirty.to_vec(),
1014 full,
1015 geometry: surface.geometry,
1016 model: surface.model,
1017 region: bytes.to_vec(),
1018 });
1019 Ok(())
1020 })
1021 }
1022
1023 pub(crate) fn surface_detach(id: &str) -> Result<()> {
1024 with_host(|host| {
1025 let surface = host.surfaces.get_mut(id).ok_or_else(|| unknown(id))?;
1026 surface.region = None;
1027 surface.pending = None;
1028 Ok(())
1029 })
1030 }
1031
1032 pub(crate) fn is_driving() -> bool {
1033 with_host(|host| host.driving)
1034 }
1035
1036 pub(crate) fn theme() -> Theme {
1037 with_host(|host| host.theme)
1038 }
1039
1040 pub(crate) fn capabilities() -> Capabilities {
1041 with_host(|host| host.capabilities)
1042 }
1043
1044 pub(crate) fn focused_pane() -> Option<String> {
1045 with_host(|host| host.focused_pane.clone())
1046 }
1047
1048 fn instance(surface: &str) -> Option<(&str, &str)> {
1050 surface
1051 .split_once('@')
1052 .filter(|(_, owner)| !owner.is_empty())
1053 }
1054
1055 pub(crate) fn surface_machine(surface: &str) -> Option<String> {
1056 let (template, machine) = instance(surface)?;
1057 with_host(|host| host.machine_surfaces.contains(template)).then(|| machine.to_string())
1058 }
1059
1060 pub(crate) fn surface_project(surface: &str) -> Option<String> {
1061 let (template, project) = instance(surface)?;
1062 with_host(|host| host.project_surfaces.contains(template)).then(|| project.to_string())
1063 }
1064
1065 pub(crate) fn surface_tint(surface: &str) -> Option<u32> {
1066 if let Some(owner) = surface_machine(surface).or_else(|| surface_project(surface)) {
1067 return with_host(|host| host.tints.get(&owner).copied());
1068 }
1069 let pane = surface_pane(surface)?;
1070 with_host(|host| {
1071 let pane = host
1072 .account
1073 .panes
1074 .iter()
1075 .find(|known| known.id == pane.id)?;
1076 pane.project
1077 .as_ref()
1078 .and_then(|project| host.tints.get(project))
1079 .or_else(|| host.tints.get(&pane.machine))
1080 .copied()
1081 })
1082 }
1083
1084 pub(crate) fn identity_tint(id: &str) -> Option<u32> {
1085 with_host(|host| host.tints.get(id).copied())
1086 }
1087
1088 pub(crate) fn surface_request_size(id: &str, cols: u32, rows: u32) -> Result<()> {
1089 with_host(|host| {
1090 let known = host.surfaces.contains_key(id)
1091 || instance(id).is_some_and(|(template, _)| host.surfaces.contains_key(template));
1092 if !known {
1093 return Err(unknown(id));
1094 }
1095 host.requested
1096 .insert(id.to_string(), (cols.min(1024), rows.min(512)));
1097 Ok(())
1098 })
1099 }
1100
1101 pub(crate) fn surface_set_label(id: &str, label: Option<&str>) -> Result<()> {
1102 with_host(|host| {
1103 let known = host.surfaces.contains_key(id)
1104 || instance(id).is_some_and(|(template, _)| host.surfaces.contains_key(template));
1105 if !known {
1106 return Err(unknown(id));
1107 }
1108 match label {
1109 Some(label)
1110 if label.chars().count() > 48 || label.chars().any(char::is_control) =>
1111 {
1112 return Err(crate::Error::Invalid(
1113 "a label is at most 48 characters with no control characters".into(),
1114 ));
1115 }
1116 Some(label) => {
1117 host.labels.insert(id.to_string(), label.to_string());
1118 }
1119 None => {
1120 host.labels.remove(id);
1121 }
1122 }
1123 Ok(())
1124 })
1125 }
1126
1127 pub(crate) fn surface_set_caret(id: &str, caret: Option<(u32, u32)>) -> Result<()> {
1128 with_host(|host| {
1129 let known = host.surfaces.contains_key(id)
1130 || instance(id).is_some_and(|(template, _)| host.surfaces.contains_key(template));
1131 if !known {
1132 return Err(unknown(id));
1133 }
1134 match caret {
1135 Some(caret) => {
1136 host.carets.insert(id.to_string(), caret);
1137 }
1138 None => {
1139 host.carets.remove(id);
1140 }
1141 }
1142 Ok(())
1143 })
1144 }
1145
1146 pub(crate) fn surface_pane(surface: &str) -> Option<crate::api::view::PaneRef> {
1147 let (template, pane) = instance(surface)?;
1148 if with_host(|host| {
1149 host.machine_surfaces.contains(template) || host.project_surfaces.contains(template)
1150 }) {
1151 return None;
1152 }
1153 let id = pane.to_string();
1154 let generation = with_host(|host| {
1155 host.account
1156 .panes
1157 .iter()
1158 .find(|pane| pane.id == id)
1159 .map_or(0, |pane| pane.generation)
1160 });
1161 Some(crate::api::view::PaneRef { id, generation })
1162 }
1163
1164 pub(crate) fn machine_id() -> Option<String> {
1165 with_host(|host| host.machine_id.clone())
1166 }
1167
1168 pub(crate) fn instance_id() -> String {
1169 with_host(|host| host.instance_id.clone())
1170 }
1171
1172 pub(crate) fn wall_ms() -> u64 {
1173 with_host(|host| host.wall_ms)
1174 }
1175
1176 pub(crate) fn utc_offset_minutes() -> i32 {
1177 with_host(|host| host.utc_offset_minutes)
1178 }
1179
1180 pub(crate) fn time_zone() -> Option<String> {
1181 with_host(|host| host.time_zone.clone())
1182 }
1183
1184 pub(crate) fn daemon_machine_id() -> String {
1185 with_host(|host| host.machine_id.clone().unwrap_or_default())
1186 }
1187
1188 pub(crate) fn daemon_account_id() -> Option<String> {
1189 with_host(|host| host.account_id.clone())
1190 }
1191
1192 pub(crate) fn daemon_lease_epoch() -> Option<u64> {
1193 with_host(|host| host.lease_epoch)
1194 }
1195
1196 pub(crate) fn daemon_release() -> Option<crate::daemon::Release> {
1197 with_host(|host| host.release.clone())
1198 }
1199
1200 pub(crate) fn daemon_environment() -> Vec<(String, String)> {
1201 with_host(|host| host.environment.clone())
1202 }
1203
1204 pub(crate) fn request_frame() {
1205 wake_at(0);
1206 }
1207
1208 pub(crate) fn wake_at(at_ms: u64) {
1209 with_host(|host| {
1210 host.frame_request = Some(host.frame_request.map_or(at_ms, |at| at.min(at_ms)));
1211 });
1212 }
1213
1214 pub(crate) fn url_open(url: &str) -> Result<()> {
1215 use standard_plugin_manifest::url::{URL_OPEN_WINDOW_MS, https_host};
1216 with_host(|host| {
1217 let domain = https_host(url).map_err(Error::Invalid)?;
1218 if !host.grants.allows_url_open(&domain) {
1219 return Err(host.deny(GrantDenied {
1220 grant: alloc::format!("url.open:{domain}"),
1221 }));
1222 }
1223 let now = host.now_ms;
1224 if !host
1225 .url_gesture_ms
1226 .is_some_and(|at| host.gesture || now.saturating_sub(at) <= URL_OPEN_WINDOW_MS)
1227 {
1228 return Err(Error::Invalid(
1229 "url.open needs user input within the last second".into(),
1230 ));
1231 }
1232 host.url_gesture_ms = None;
1233 host.opened_urls.push(url.to_string());
1234 Ok(())
1235 })
1236 }
1237
1238 pub(crate) fn surface_open(id: &str) -> Result<()> {
1239 with_host(|host| {
1240 if !host.surfaces.contains_key(id) {
1241 return Err(unknown(id));
1242 }
1243 if !host.gesture {
1244 return Err(Error::Invalid(
1245 "surface.open needs a user gesture or a command".into(),
1246 ));
1247 }
1248 host.opened.push(id.to_string());
1249 Ok(())
1250 })
1251 }
1252
1253 pub(crate) fn focus_pane(pane: &str) -> Result<()> {
1254 with_host(|host| {
1255 if !host.gesture {
1256 return Err(Error::Invalid(
1257 "view.focus-pane needs a user gesture or a command".into(),
1258 ));
1259 }
1260 host.focused_panes.push(pane.to_string());
1261 Ok(())
1262 })
1263 }
1264
1265 pub(crate) fn surface_close(id: &str) -> Result<()> {
1266 with_host(|host| {
1267 host.closed.push(id.to_string());
1268 Ok(())
1269 })
1270 }
1271
1272 pub(crate) fn process_spawn(program: &str, args: &[String], cwd: Option<&str>) -> Result<u32> {
1273 let mut command = crate::daemon::process::Command::new(program);
1274 command.args = args.to_vec();
1275 command.cwd = cwd.map(ToString::to_string);
1276 with_host(|host| host.spawn(&command))
1277 }
1278
1279 pub(crate) fn process_run(command: &crate::daemon::process::Command) -> Result<u32> {
1280 with_host(|host| host.spawn(command))
1281 }
1282
1283 pub(crate) fn process_write(pid: u32, bytes: &[u8]) -> Result<()> {
1284 with_host(|host| host.write(pid, bytes))
1285 }
1286
1287 pub(crate) fn process_close_stdin(pid: u32) -> Result<()> {
1288 with_host(|host| host.close_stdin(pid))
1289 }
1290
1291 pub(crate) fn process_try_wait(pid: u32) -> Result<Option<i32>> {
1292 with_host(|host| host.try_wait(pid))
1293 }
1294
1295 pub(crate) fn process_wait(pid: u32) -> Result<i32> {
1296 with_host(|host| host.wait(pid))
1297 }
1298
1299 pub(crate) fn process_kill(pid: u32) -> Result<()> {
1300 with_host(|host| host.kill(pid))
1301 }
1302
1303 pub(crate) fn watch(path: &str, options: &crate::daemon::watch::Options) -> Result<u32> {
1304 with_host(|host| host.watch(path, options.recursive, &options.exclude))
1305 }
1306
1307 pub(crate) fn unwatch(handle: u32) -> Result<()> {
1308 with_host(|host| host.unwatch(handle))
1309 }
1310
1311 pub(crate) fn websocket_open(url: &str, headers: &[(String, String)]) -> Result<u32> {
1312 with_host(|host| host.websocket_open(url, headers))
1313 }
1314
1315 pub(crate) fn websocket_send(socket: u32, text: &str) -> Result<()> {
1316 with_host(|host| host.websocket_send(socket, text))
1317 }
1318
1319 pub(crate) fn websocket_close(socket: u32) {
1320 with_host(|host| host.websocket_close(socket));
1321 }
1322
1323 pub(crate) fn panes() -> Result<Vec<Pane>> {
1324 with_host(|host| {
1325 host.check("panes.read")?;
1326 Ok(host.panes.clone())
1327 })
1328 }
1329
1330 pub(crate) fn pane_create(
1331 cwd: &str,
1332 command: Option<&str>,
1333 env: &[(String, String)],
1334 ) -> Result<(String, u64)> {
1335 with_host(|host| {
1336 host.check("panes.write")?;
1337 host.create_pane(cwd, command, env, None)
1338 })
1339 }
1340
1341 pub(crate) fn pane_create_with(
1342 cwd: &str,
1343 command: Option<&str>,
1344 env: &[(String, String)],
1345 title: Option<&str>,
1346 ) -> Result<(String, u64)> {
1347 with_host(|host| {
1348 host.check("panes.write")?;
1349 host.create_pane(cwd, command, env, title)
1350 })
1351 }
1352
1353 pub(crate) fn pane_input(_pane: &str, _bytes: &[u8]) -> Result<()> {
1354 with_host(|host| host.check("panes.write"))
1355 }
1356
1357 pub(crate) fn pane_close(_pane: &str) -> Result<()> {
1358 with_host(|host| host.check("panes.write"))
1359 }
1360
1361 pub(crate) fn panes_subscribe(_on: bool) -> Result<()> {
1362 with_host(|host| host.check("panes.read"))
1363 }
1364
1365 pub(crate) fn pane_wait(_pane: &str, _timeout_ms: u32) -> Result<bool> {
1366 with_host(|host| {
1367 host.check("panes.read")?;
1368 Ok(true)
1369 })
1370 }
1371}