Skip to main content

bern_protocol/
session.rs

1//! Framework-provided session and window commands.
2//!
3//! Standard controls are driven through these commands. They are executed
4//! by the UI session that owns the target control, so external entries must
5//! name a live `session_id`; without one the host answers
6//! `requires_ui_session`.
7
8use crate::{
9    CommandPolicy, CommandSpec, EnumDef, ExternalNames, FieldDef, OperationKind, ParamSpec,
10    RecordDef, Retry, Scope, Type, TypeTable, VariantDef,
11};
12
13/// Identifies one framework session command.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum SessionCommand {
16    Inspect,
17    Activate,
18    Edit,
19    Submit,
20    SetBool,
21    SetNumber,
22    Select,
23    Back,
24    WindowOpen,
25    WindowClose,
26    WindowActivate,
27    WindowConfirmClose,
28    WindowList,
29    WindowMinimize,
30    WindowRestore,
31    WindowMaximize,
32    WindowMove,
33    WindowResize,
34    Language,
35    SetLanguage,
36    Focus,
37    SetFile,
38    TraceStart,
39    TraceStop,
40    TraceReport,
41}
42
43impl SessionCommand {
44    /// Frontends address commands by position: new commands go last.
45    pub const ALL: [SessionCommand; 25] = [
46        SessionCommand::Inspect,
47        SessionCommand::Activate,
48        SessionCommand::Edit,
49        SessionCommand::Submit,
50        SessionCommand::SetBool,
51        SessionCommand::SetNumber,
52        SessionCommand::Select,
53        SessionCommand::Back,
54        SessionCommand::WindowOpen,
55        SessionCommand::WindowClose,
56        SessionCommand::WindowActivate,
57        SessionCommand::WindowConfirmClose,
58        SessionCommand::WindowList,
59        SessionCommand::WindowMinimize,
60        SessionCommand::WindowRestore,
61        SessionCommand::WindowMaximize,
62        SessionCommand::WindowMove,
63        SessionCommand::WindowResize,
64        SessionCommand::Language,
65        SessionCommand::SetLanguage,
66        SessionCommand::Focus,
67        SessionCommand::SetFile,
68        SessionCommand::TraceStart,
69        SessionCommand::TraceStop,
70        SessionCommand::TraceReport,
71    ];
72
73    pub fn id(self) -> &'static str {
74        match self {
75            SessionCommand::Inspect => "ui.inspect",
76            SessionCommand::Activate => "ui.activate",
77            SessionCommand::Edit => "ui.edit",
78            SessionCommand::Submit => "ui.submit",
79            SessionCommand::SetBool => "ui.set_bool",
80            SessionCommand::SetNumber => "ui.set_number",
81            SessionCommand::Select => "ui.select",
82            SessionCommand::Back => "ui.back",
83            SessionCommand::WindowOpen => "window.open",
84            SessionCommand::WindowClose => "window.close",
85            SessionCommand::WindowActivate => "window.activate",
86            SessionCommand::WindowConfirmClose => "window.confirm_close",
87            SessionCommand::WindowList => "window.list",
88            SessionCommand::WindowMinimize => "window.minimize",
89            SessionCommand::WindowRestore => "window.restore",
90            SessionCommand::WindowMaximize => "window.maximize",
91            SessionCommand::WindowMove => "window.move",
92            SessionCommand::WindowResize => "window.resize",
93            SessionCommand::Language => "ui.language",
94            SessionCommand::SetLanguage => "ui.set_language",
95            SessionCommand::Focus => "ui.focus",
96            SessionCommand::SetFile => "ui.set_file",
97            SessionCommand::TraceStart => "trace.start",
98            SessionCommand::TraceStop => "trace.stop",
99            SessionCommand::TraceReport => "trace.report",
100        }
101    }
102
103    pub fn from_id(id: &str) -> Option<Self> {
104        Self::ALL.into_iter().find(|command| command.id() == id)
105    }
106
107    /// Commands of the performance trace, which the session runtime
108    /// answers itself; the frontend only records.
109    pub fn is_trace(self) -> bool {
110        matches!(
111            self,
112            SessionCommand::TraceStart | SessionCommand::TraceStop | SessionCommand::TraceReport
113        )
114    }
115
116    /// Commands that act on a window's presentation. The target performs
117    /// them and the session runtime answers with the window's facts.
118    pub fn is_presentation(self) -> bool {
119        matches!(
120            self,
121            SessionCommand::WindowMinimize
122                | SessionCommand::WindowRestore
123                | SessionCommand::WindowMaximize
124                | SessionCommand::WindowMove
125                | SessionCommand::WindowResize
126        )
127    }
128}
129
130fn param(name: &str, ty: Type, doc: &str) -> ParamSpec {
131    ParamSpec {
132        name: name.into(),
133        ty,
134        doc: doc.into(),
135    }
136}
137
138fn spec(
139    command: SessionCommand,
140    kind: OperationKind,
141    doc: &str,
142    params: Vec<ParamSpec>,
143    result: Type,
144    retry: Retry,
145) -> CommandSpec {
146    let (namespace, name) = command.id().split_once('.').expect("namespaced id");
147    CommandSpec {
148        id: command.id().into(),
149        name: name.into(),
150        doc: doc.into(),
151        kind,
152        scope: Scope::Session,
153        params,
154        result,
155        errors: vec![
156            "requires_ui_session".into(),
157            "target_not_found".into(),
158            "target_unavailable".into(),
159            "target_blocked".into(),
160            "stale_window".into(),
161            "stale_confirmation".into(),
162        ],
163        policy: CommandPolicy {
164            retry,
165            ..CommandPolicy::default()
166        },
167        external: ExternalNames::derive(namespace, name),
168        reads: vec![],
169        on: None,
170    }
171}
172
173fn with_errors(mut spec: CommandSpec, errors: &[&str]) -> CommandSpec {
174    spec.errors
175        .extend(errors.iter().map(|error| (*error).to_string()));
176    spec
177}
178
179fn named(name: &str) -> Type {
180    Type::Record { name: name.into() }
181}
182
183fn distribution() -> Type {
184    named("TraceDistribution")
185}
186
187fn segment() -> Type {
188    named("TraceSegment")
189}
190
191fn trace_record(name: &str, fields: &[(&str, Type, &str)]) -> RecordDef {
192    RecordDef {
193        name: name.into(),
194        fields: fields
195            .iter()
196            .map(|(name, ty, doc)| FieldDef {
197                name: (*name).into(),
198                ty: ty.clone(),
199                doc: (*doc).into(),
200                content: false,
201            })
202            .collect(),
203    }
204}
205
206/// Type table and command specs for the framework session catalog.
207pub fn session_catalog() -> (TypeTable, Vec<CommandSpec>) {
208    let control = Type::Record {
209        name: "ControlInfo".into(),
210    };
211    let field = |name: &str, ty: Type, doc: &str| FieldDef {
212        name: name.into(),
213        ty,
214        doc: doc.into(),
215        content: false,
216    };
217    let unit_enum = |name: &str, variants: &[&str]| EnumDef {
218        name: name.into(),
219        variants: variants
220            .iter()
221            .map(|variant| VariantDef {
222                name: (*variant).into(),
223                fields: vec![],
224                label: None,
225            })
226            .collect(),
227    };
228    let decision = || Type::Enum {
229        name: "CloseDecision".into(),
230    };
231    let close_result = Type::Record {
232        name: "CloseResult".into(),
233    };
234    let named_enum = |name: &str| Type::Enum { name: name.into() };
235    let window_info = Type::Record {
236        name: "WindowInfo".into(),
237    };
238    let types = TypeTable {
239        enums: vec![
240            unit_enum(
241                "CloseOutcome",
242                &["Closed", "Rejected", "ConfirmationRequired"],
243            ),
244            unit_enum("CloseDecision", &["Save", "Discard", "Cancel"]),
245            unit_enum("WindowPresentation", &["Native", "Virtual"]),
246            unit_enum("WindowLifecycle", &["Opening", "Open", "Closing"]),
247            unit_enum("WindowVisibility", &["Visible", "Minimized", "Hidden"]),
248            unit_enum("WindowSize", &["Normal", "Maximized", "FullScreen"]),
249            unit_enum(
250                "WindowAction",
251                &[
252                    "Activate", "Close", "Minimize", "Restore", "Maximize", "Move", "Resize",
253                ],
254            ),
255            unit_enum("WindowChangeSource", &["Command", "System"]),
256        ],
257        records: vec![
258            RecordDef {
259                name: "ControlInfo".into(),
260                fields: vec![
261                    field("target", Type::Text, "Stable control identity"),
262                    field("window", Type::Text, "Owning window instance"),
263                    field("role", Type::Text, "Semantic role"),
264                    field("label", Type::Text, "Resolved accessible name"),
265                    field("enabled", Type::Bool, "Whether the control can be operated"),
266                    field(
267                        "value",
268                        Type::optional(Type::Text),
269                        "Current value, when the control has one",
270                    ),
271                    field(
272                        "reason",
273                        Type::optional(Type::Text),
274                        "Why the control cannot be operated: `disabled` or `blocked_by_modal`",
275                    ),
276                ],
277            },
278            RecordDef {
279                name: "CloseResult".into(),
280                fields: vec![
281                    field(
282                        "outcome",
283                        Type::Enum {
284                            name: "CloseOutcome".into(),
285                        },
286                        "Closed, rejected, or waiting for a confirmation decision",
287                    ),
288                    field("window", Type::Text, "Window instance"),
289                    field(
290                        "confirmation",
291                        Type::optional(Type::Text),
292                        "Confirmation to decide with window.confirm_close",
293                    ),
294                    field(
295                        "draft_revision",
296                        Type::Int,
297                        "Draft revision the confirmation is bound to",
298                    ),
299                    field(
300                        "decisions",
301                        Type::list(decision()),
302                        "Decisions the confirmation accepts",
303                    ),
304                    field(
305                        "message",
306                        Type::optional(Type::Text),
307                        "The declared confirmation message, as shown in the dialog",
308                    ),
309                    field(
310                        "reason",
311                        Type::optional(Type::Text),
312                        "Why closing was rejected",
313                    ),
314                ],
315            },
316            RecordDef {
317                name: "WindowFrame".into(),
318                fields: vec![
319                    field(
320                        "x",
321                        Type::Float,
322                        "Left edge in points from the main display's left edge",
323                    ),
324                    field(
325                        "y",
326                        Type::Float,
327                        "Top edge in points below the main display's top edge",
328                    ),
329                    field("width", Type::Float, "Outer width in points"),
330                    field("height", Type::Float, "Outer height in points"),
331                ],
332            },
333            trace_record(
334                "TraceStarted",
335                &[
336                    ("rules", Type::Text, "The rules in their normal spelling"),
337                    (
338                        "file",
339                        Type::optional(Type::Text),
340                        "Where the timeline goes, when it is written to a file",
341                    ),
342                ],
343            ),
344            trace_record(
345                "TraceStopped",
346                &[
347                    (
348                        "files",
349                        Type::list(Type::Text),
350                        "The timeline files written, for ui.perfetto.dev",
351                    ),
352                    ("events", Type::Int, "Records read"),
353                    (
354                        "dropped",
355                        Type::Int,
356                        "Records lost because a ring was full; raise `buffer:`",
357                    ),
358                    (
359                        "timeline",
360                        Type::optional(Type::Text),
361                        "The timeline itself, when asked for",
362                    ),
363                ],
364            ),
365            trace_record(
366                "TraceDistribution",
367                &[
368                    ("count", Type::Int, "Samples in all"),
369                    ("p50", Type::Int, "Median of the recent samples"),
370                    ("p95", Type::Int, "95th percentile of the recent samples"),
371                    ("max", Type::Int, "Largest sample"),
372                ],
373            ),
374            trace_record(
375                "TraceCommand",
376                &[
377                    ("id", Type::Text, "Command id"),
378                    ("entry", Type::Text, "ui, cli, json, mcp or test"),
379                    (
380                        "total_us",
381                        distribution(),
382                        "From dispatch to on screen, or to the engine's answer",
383                    ),
384                    ("queue_us", distribution(), "Waiting for the engine"),
385                    ("exec_us", distribution(), "Running the command"),
386                    (
387                        "publish_us",
388                        distribution(),
389                        "Snapshot, comparison and encoding of the state",
390                    ),
391                    (
392                        "apply_us",
393                        distribution(),
394                        "Decoding and writing the state on the UI thread",
395                    ),
396                    ("bytes", distribution(), "State bytes sent to the UI"),
397                ],
398            ),
399            trace_record(
400                "TraceQuery",
401                &[
402                    ("id", Type::Text, "Query id"),
403                    ("cancelled", Type::Int, "Answers no one waited for any more"),
404                    ("queue_us", distribution(), "Waiting for the engine"),
405                    (
406                        "provider_us",
407                        distribution(),
408                        "From the provider's start to its answer",
409                    ),
410                    (
411                        "round_trip_us",
412                        distribution(),
413                        "From the request to the UI showing the answer",
414                    ),
415                ],
416            ),
417            trace_record(
418                "TraceField",
419                &[
420                    ("name", Type::Text, "State field"),
421                    (
422                        "sent",
423                        Type::Int,
424                        "Times it changed, and was sent to a UI if one runs",
425                    ),
426                    ("bytes_max", Type::Int, "Largest encoding sent"),
427                    (
428                        "items_max",
429                        Type::optional(Type::Int),
430                        "Most items, for a list",
431                    ),
432                    ("snapshot_us", distribution(), "Taking it from the engine"),
433                    (
434                        "compare_us",
435                        distribution(),
436                        "Comparing it with the last state",
437                    ),
438                    ("encode_us", distribution(), "Encoding it for the UI"),
439                    (
440                        "apply_us",
441                        distribution(),
442                        "Decoding and writing it on the UI thread",
443                    ),
444                ],
445            ),
446            trace_record(
447                "TraceSegment",
448                &[
449                    ("name", Type::Text, "Part of the work"),
450                    ("us", Type::Int, "Microseconds"),
451                ],
452            ),
453            trace_record(
454                "TraceInteraction",
455                &[
456                    (
457                        "control",
458                        Type::optional(Type::Text),
459                        "The control operated; absent for commands from other entries",
460                    ),
461                    ("entry", Type::Text, "Where it came from"),
462                    ("commands", Type::list(Type::Text), "Commands it ran"),
463                    ("at_ms", Type::Int, "Milliseconds after tracing started"),
464                    (
465                        "total_us",
466                        Type::Int,
467                        "From the action to the frame showing its result",
468                    ),
469                    (
470                        "revision",
471                        Type::optional(Type::Int),
472                        "The state revision it produced",
473                    ),
474                    ("segments", Type::list(segment()), "Where the time went"),
475                ],
476            ),
477            trace_record(
478                "TraceFrames",
479                &[
480                    ("spacing_us", distribution(), "Time between frame ticks"),
481                    ("long", Type::Int, "Frames over the long-frame threshold"),
482                ],
483            ),
484            trace_record(
485                "TraceFrame",
486                &[
487                    ("at_ms", Type::Int, "Milliseconds after tracing started"),
488                    ("us", Type::Int, "Time since the frame before"),
489                    (
490                        "refresh_us",
491                        Type::Int,
492                        "The display's refresh interval, 0 when unknown",
493                    ),
494                    ("bern_us", Type::Int, "Bern's own work on the UI thread"),
495                    (
496                        "outside_us",
497                        Type::Int,
498                        "The rest: generated views and the platform's layout and drawing",
499                    ),
500                    (
501                        "segments",
502                        Type::list(segment()),
503                        "Bern's longest work on the UI thread",
504                    ),
505                    (
506                        "engine",
507                        Type::list(segment()),
508                        "What the engine did meanwhile",
509                    ),
510                ],
511            ),
512            trace_record(
513                "TraceSpan",
514                &[
515                    ("name", Type::Text, "Span name"),
516                    ("us", distribution(), "Durations"),
517                ],
518            ),
519            trace_record(
520                "TraceReport",
521                &[
522                    ("running", Type::Bool, "Whether tracing is still on"),
523                    ("window_ms", Type::Int, "Milliseconds the statistics cover"),
524                    ("events", Type::Int, "Records read"),
525                    ("dropped", Type::Int, "Records lost because a ring was full"),
526                    (
527                        "commands",
528                        Type::list(named("TraceCommand")),
529                        "Per command, slowest first",
530                    ),
531                    ("queries", Type::list(named("TraceQuery")), "Per query"),
532                    (
533                        "fields",
534                        Type::list(named("TraceField")),
535                        "State fields sent, largest first",
536                    ),
537                    (
538                        "interactions",
539                        Type::list(named("TraceInteraction")),
540                        "The slowest interactions and commands",
541                    ),
542                    ("frames", named("TraceFrames"), "Frame ticks"),
543                    (
544                        "long_frames",
545                        Type::list(named("TraceFrame")),
546                        "The latest long frames",
547                    ),
548                    ("spans", Type::list(named("TraceSpan")), "Spans by name"),
549                ],
550            ),
551            RecordDef {
552                name: "WindowInfo".into(),
553                fields: vec![
554                    field("window", Type::Text, "Window instance"),
555                    field("kind", Type::Text, "Window kind id"),
556                    field(
557                        "presentation",
558                        named_enum("WindowPresentation"),
559                        "Native system window or virtual window",
560                    ),
561                    field(
562                        "lifecycle",
563                        named_enum("WindowLifecycle"),
564                        "Opening until the platform reports it; Closing while a close confirmation waits",
565                    ),
566                    field(
567                        "visibility",
568                        named_enum("WindowVisibility"),
569                        "Visible, minimized or hidden, as the platform reports it",
570                    ),
571                    field(
572                        "size",
573                        named_enum("WindowSize"),
574                        "Normal, maximized or full screen",
575                    ),
576                    field(
577                        "frame",
578                        Type::optional(Type::Record {
579                            name: "WindowFrame".into(),
580                        }),
581                        "Outer frame; absent while opening",
582                    ),
583                    field(
584                        "focused",
585                        Type::Bool,
586                        "Whether the window has keyboard focus",
587                    ),
588                    field(
589                        "actions",
590                        Type::list(named_enum("WindowAction")),
591                        "Window actions available now",
592                    ),
593                    field(
594                        "version",
595                        Type::Int,
596                        "Bumped by every change of this instance's presentation",
597                    ),
598                    field(
599                        "changed_by",
600                        Type::optional(named_enum("WindowChangeSource")),
601                        "Whether the latest change came from a command or from the user and system",
602                    ),
603                ],
604            },
605        ],
606    };
607    let target = || param("target", Type::Text, "Control identity from ui.inspect");
608    let window = || param("window", Type::Text, "Window instance id");
609    let commands = vec![
610        spec(
611            SessionCommand::Inspect,
612            OperationKind::Query,
613            "List the mounted controls of the session",
614            vec![],
615            Type::list(control),
616            Retry::Idempotent,
617        ),
618        spec(
619            SessionCommand::Activate,
620            OperationKind::Command,
621            "Activate a button or link through its real control binding",
622            vec![target()],
623            Type::Unit,
624            Retry::Never,
625        ),
626        spec(
627            SessionCommand::Edit,
628            OperationKind::Command,
629            "Replace the draft text of a text input",
630            vec![target(), param("value", Type::Text, "New text")],
631            Type::Unit,
632            Retry::Idempotent,
633        ),
634        spec(
635            SessionCommand::Submit,
636            OperationKind::Command,
637            "Submit a text input",
638            vec![target()],
639            Type::Unit,
640            Retry::Never,
641        ),
642        spec(
643            SessionCommand::SetBool,
644            OperationKind::Command,
645            "Set a switch or checkbox",
646            vec![target(), param("value", Type::Bool, "New state")],
647            Type::Unit,
648            Retry::Idempotent,
649        ),
650        spec(
651            SessionCommand::SetNumber,
652            OperationKind::Command,
653            "Set and commit a slider",
654            vec![target(), param("value", Type::Float, "New value")],
655            Type::Unit,
656            Retry::Never,
657        ),
658        spec(
659            SessionCommand::Select,
660            OperationKind::Command,
661            "Select an option of a picker, sidebar or tab view by variant name",
662            vec![target(), param("option", Type::Text, "Option name")],
663            Type::Unit,
664            Retry::Idempotent,
665        ),
666        spec(
667            SessionCommand::Back,
668            OperationKind::Command,
669            "Go back one step in a navigation container",
670            vec![target()],
671            Type::Unit,
672            Retry::Never,
673        ),
674        spec(
675            SessionCommand::Focus,
676            OperationKind::Command,
677            "Give a control the keyboard focus, such as a search field, whose results then show",
678            vec![target()],
679            Type::Unit,
680            Retry::Idempotent,
681        ),
682        with_errors(
683            spec(
684                SessionCommand::SetFile,
685                OperationKind::Command,
686                "Give a file to a control that asks for one or takes dropped files, as if the user chose or dropped it; its action then runs",
687                vec![
688                    target(),
689                    param(
690                        "url",
691                        Type::Text,
692                        "A `file://` or http(s) URL; it must be of a kind the control accepts",
693                    ),
694                ],
695                Type::Unit,
696                Retry::Never,
697            ),
698            &["invalid_url", "unsupported_file"],
699        ),
700        spec(
701            SessionCommand::Language,
702            OperationKind::Query,
703            "The session's interface language tag",
704            vec![],
705            Type::Text,
706            Retry::Idempotent,
707        ),
708        with_errors(
709            spec(
710                SessionCommand::SetLanguage,
711                OperationKind::Command,
712                "Switch the interface language; every localized text updates while drafts, navigation and windows stay",
713                vec![param(
714                    "language",
715                    Type::Text,
716                    "A supported language tag, such as `en`",
717                )],
718                Type::Unit,
719                Retry::Idempotent,
720            ),
721            &["unsupported_language"],
722        ),
723        with_errors(
724            spec(
725                SessionCommand::TraceStart,
726                OperationKind::Command,
727                "Start the performance trace with rules such as `engine,ui,slow:16ms` or `command:library.*`; a running trace ends first",
728                vec![param(
729                    "rules",
730                    Type::Text,
731                    "Categories (engine, ui, frame, span, motion, style, all), filters (command:, query:, control:, field:, feature:), slow:, long:, file:, quiet, buffer:",
732                )],
733                named("TraceStarted"),
734                Retry::Idempotent,
735            ),
736            &["invalid_rules"],
737        ),
738        spec(
739            SessionCommand::TraceStop,
740            OperationKind::Command,
741            "Stop the performance trace and write its timeline",
742            vec![param(
743                "timeline",
744                Type::optional(Type::Bool),
745                "Also return the timeline itself, for an app whose files are on another device",
746            )],
747            named("TraceStopped"),
748            Retry::Idempotent,
749        ),
750        spec(
751            SessionCommand::TraceReport,
752            OperationKind::Query,
753            "What the performance trace measured: p50, p95 and maximum per command, the slowest interactions and the long frames",
754            vec![
755                param(
756                    "top",
757                    Type::optional(Type::Int),
758                    "How many of the slowest interactions to list (10)",
759                ),
760                param(
761                    "reset",
762                    Type::optional(Type::Bool),
763                    "Start the statistics over after this report",
764                ),
765            ],
766            named("TraceReport"),
767            Retry::Idempotent,
768        ),
769        spec(
770            SessionCommand::WindowOpen,
771            OperationKind::Command,
772            "Open a window kind or activate its existing instance",
773            vec![param("kind", Type::Text, "Window kind id")],
774            Type::Text,
775            Retry::Idempotent,
776        ),
777        spec(
778            SessionCommand::WindowClose,
779            OperationKind::Command,
780            "Request closing a window instance; its declared close check may ask for a confirmation",
781            vec![window()],
782            close_result.clone(),
783            Retry::Idempotent,
784        ),
785        spec(
786            SessionCommand::WindowActivate,
787            OperationKind::Command,
788            "Bring a window instance to the front",
789            vec![window()],
790            Type::Unit,
791            Retry::Idempotent,
792        ),
793        spec(
794            SessionCommand::WindowConfirmClose,
795            OperationKind::Command,
796            "Decide a pending close confirmation, as its dialog would",
797            vec![
798                window(),
799                param(
800                    "confirmation",
801                    Type::Text,
802                    "Confirmation id from window.close",
803                ),
804                param("decision", decision(), "Save, Discard or Cancel"),
805            ],
806            close_result,
807            Retry::Never,
808        ),
809        spec(
810            SessionCommand::WindowList,
811            OperationKind::Query,
812            "List the open window instances with their actual presentation and available actions",
813            vec![],
814            Type::list(window_info.clone()),
815            Retry::Idempotent,
816        ),
817        spec(
818            SessionCommand::WindowMinimize,
819            OperationKind::Command,
820            "Minimize a window instance; its drafts and navigation stay",
821            vec![window()],
822            window_info.clone(),
823            Retry::Idempotent,
824        ),
825        spec(
826            SessionCommand::WindowRestore,
827            OperationKind::Command,
828            "Undo one step: bring a minimized window back, or leave full screen or the maximized size",
829            vec![window()],
830            window_info.clone(),
831            Retry::Never,
832        ),
833        spec(
834            SessionCommand::WindowMaximize,
835            OperationKind::Command,
836            "Grow a window instance to fill its display, as the platform zooms windows",
837            vec![window()],
838            window_info.clone(),
839            Retry::Idempotent,
840        ),
841        with_errors(
842            spec(
843                SessionCommand::WindowMove,
844                OperationKind::Command,
845                "Move a window instance so its top-left corner is at x, y; its title bar must stay on a display",
846                vec![
847                    window(),
848                    param("x", Type::Float, "Left edge in points"),
849                    param("y", Type::Float, "Top edge in points"),
850                ],
851                window_info.clone(),
852                Retry::Idempotent,
853            ),
854            &["out_of_range"],
855        ),
856        with_errors(
857            spec(
858                SessionCommand::WindowResize,
859                OperationKind::Command,
860                "Resize a window instance, keeping its top-left corner; the size must respect the window's minimum",
861                vec![
862                    window(),
863                    param("width", Type::Float, "Outer width in points"),
864                    param("height", Type::Float, "Outer height in points"),
865                ],
866                window_info,
867                Retry::Idempotent,
868            ),
869            &["out_of_range"],
870        ),
871    ];
872    (types, commands)
873}
874
875#[cfg(test)]
876mod tests {
877    use super::*;
878    use crate::descriptor::check_external_names;
879
880    #[test]
881    fn session_catalog_is_consistent() {
882        let (types, commands) = session_catalog();
883        types.validate().unwrap();
884        check_external_names(commands.iter()).unwrap();
885        assert_eq!(commands.len(), SessionCommand::ALL.len());
886        for command in SessionCommand::ALL {
887            assert_eq!(SessionCommand::from_id(command.id()), Some(command));
888            assert!(commands.iter().any(|spec| spec.id == command.id()));
889        }
890    }
891}