bern-target-api 0.1.0

Interface between the Bern compiler and independent UI target modules
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
//! The interface every UI target module implements.
//!
//! A target describes itself and its capabilities, checks the shared IR,
//! generates code and resources, and returns a build plan that the build
//! driver executes. The compiler core only talks to this interface; it never
//! imports a concrete target, and disabling one target leaves the others
//! buildable.

pub mod brand_assets;
pub mod glue;
pub mod icon;
pub mod icon_set;
pub mod names;
pub mod skill;
pub mod style;
pub mod svg;
pub mod texts;

mod check;
mod plan;
mod report;

pub use check::check_common;
pub use plan::{
    BuildPlan, BuildStep, Run, SimulatedSystem, archive, dist_dir, files_below,
    package_architecture,
};
pub use report::{FeatureReport, feature_report};

use bern_ir::{Application, IR_VERSION, SourceLocation};
use std::fmt;
use std::ops::RangeInclusive;
use std::path::PathBuf;

/// Where a target's colors come from.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorSource {
    /// The UI framework's own light/dark system and standard styles.
    Native,
    /// Colors generated by the Bern theme algorithm.
    BernTheme,
}

/// How a target can present `Window` declarations.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WindowCapabilities {
    pub native: bool,
    pub virtual_windows: bool,
    /// Draggable, resizable windows: native windows, or floating panels.
    pub floating: bool,
}

/// The media backend and video presenter a target assembles for `Video`.
/// The two are versioned separately from the target and from each other.
#[derive(Clone, Debug)]
pub struct MediaSupport {
    /// Versions of the media contract both parts implement.
    pub contract_versions: RangeInclusive<u32>,
    /// Media backend that loads and plays, for example `avfoundation`.
    pub backend: &'static str,
    pub backend_version: &'static str,
    /// Presenter that attaches players to views, for example `avplayerview`.
    pub presenter: &'static str,
    pub presenter_version: &'static str,
    /// Players keep playing while no view is attached.
    pub background_playback: bool,
    /// Several views may show one player at the same time.
    pub multi_view: bool,
}

#[derive(Clone, Debug)]
pub struct TargetDescriptor {
    /// Stable target id, for example `swiftui-macos`.
    pub id: &'static str,
    /// UI technology, for example `swiftui`.
    pub ui: &'static str,
    pub platforms: &'static [&'static str],
    pub version: &'static str,
    /// IR versions this target consumes.
    pub ir_versions: RangeInclusive<u32>,
    pub color_source: ColorSource,
    /// Built-in components this target can realize.
    pub components: &'static [&'static str],
    pub windows: WindowCapabilities,
    /// Entry channels the target's host can assemble besides the UI.
    pub channels: &'static [&'static str],
    /// Media support for `Video` and application players, if any.
    pub media: Option<MediaSupport>,
    /// Style overrides the target can show; none by default.
    pub styles: &'static [StyleSupport],
    /// Motion properties the target shows (`appear`, `disappear`,
    /// `reflow`, `hero`); the others change without motion.
    pub motion: &'static [&'static str],
    /// Whether windows honor `chrome`, `immersive` and `idle_after`.
    pub window_chrome: bool,
    /// Whether `Text(mark:)` draws its highlight; otherwise the text shows
    /// plain.
    pub text_marks: bool,
    /// Whether items of `Sidebar`, `Tabs` and `Toolbar` may come and go
    /// with `if`, `if let`, `match` and `ForEach`.
    pub dynamic_items: bool,
    /// Further parts of the IR the target builds, named in [`feature`];
    /// using one a target does not list fails the build or, where a plain
    /// form is shown instead, warns.
    pub features: &'static [&'static str],
}

/// Names of IR features a target lists in [`TargetDescriptor::features`].
pub mod feature {
    /// `Button` with a child that it shows instead of its label.
    pub const BUTTON_CONTENT: &str = "button_content";
    /// `reveal: hover`: content shown while the pointer is over its
    /// container; without it the content always shows.
    pub const REVEAL: &str = "reveal";
    /// `Search` in a `Toolbar`.
    pub const TOOLBAR_SEARCH: &str = "toolbar_search";
    /// `Search` with children: results shown below the field.
    pub const SEARCH_RESULTS: &str = "search_results";
    /// Actions that change a session draft: `draft.panel.set(true)`.
    pub const SESSION_ACTIONS: &str = "session_actions";
    /// `Switch(style: button)`, and a `Switch` in a `Toolbar`.
    pub const TOGGLE_BUTTON: &str = "toggle_button";
    /// `Window(role: dialog)` shown modally over the primary window;
    /// without it a dialog is a window of its own.
    pub const DIALOG_WINDOWS: &str = "dialog_windows";
    /// Lengths bound to expressions, such as `padding: if window.idle { 0 }
    /// else { 24 }`, sizes of `Column`, `Row` and `Stack`, and the
    /// `corner` of a `Video`.
    pub const BOUND_LENGTHS: &str = "bound_lengths";
    /// `ScrollView(anchor:)`: the item of `position` at a fraction of the
    /// view without snapping, as near as the content reaches.
    pub const SCROLL_ANCHOR: &str = "scroll_anchor";
    /// `ScrollView(browsing:)`: a flag that holds while the person scrolls
    /// by hand.
    pub const SCROLL_BROWSING: &str = "scroll_browsing";
    /// `blur` bound to state, changing with `animate`.
    pub const BOUND_BLUR: &str = "bound_blur";
    /// `Text(bold:)`: a text bold while a condition holds.
    pub const TEXT_BOLD: &str = "text_bold";
    /// `appearance: dark` or `light` on a part of the interface; without it
    /// the part follows the window's appearance.
    pub const APPEARANCE: &str = "appearance";
    /// Keyboard `shortcut`s of buttons, menu items, switches and search
    /// fields; without them the controls work from the pointer only.
    pub const SHORTCUTS: &str = "shortcuts";
    /// Shortcuts that follow state, such as `shortcut: app.keys.open` and
    /// `Shortcut(key: app.keys.back)`: the runtime reads the text each time
    /// it changes; without it a shortcut is a literal.
    pub const BOUND_SHORTCUTS: &str = "bound_shortcuts";
    /// `ShortcutInput`: a button that takes the next key pressed, with its
    /// modifiers, as a shortcut.
    pub const SHORTCUT_INPUT: &str = "shortcut_input";
    /// `MenuBar` in the `WindowSet`: the application's menus in the system
    /// menu bar. A target whose platform has no menu bar for applications
    /// lists it as well: there the menus show nowhere, `window.menu_bar`
    /// is false, and nothing is left to warn about.
    pub const MENU_BAR: &str = "menu_bar";
    /// `StatusItem` in the `WindowSet`: the application's item in the
    /// system's status area, with its menu, and the application running on
    /// after its last window closes. A target whose platform has no status
    /// area for applications lists it as well: there the item shows nowhere
    /// and nothing is left to warn about.
    pub const STATUS_ITEM: &str = "status_item";
    /// `WindowSet(accent:, appearance:)` chosen at run time; without it the
    /// windows keep the system's accent and appearance.
    pub const RUNTIME_THEME: &str = "runtime_theme";
    pub const RUNTIME_PALETTE: &str = "runtime_palette";
    /// `Image(source:)` from a `MediaSource`: a file or an http(s) URL.
    pub const RUNTIME_IMAGES: &str = "runtime_images";
    /// `Shape(fill_from:)`, the average color of a picture, and `fade`.
    pub const PICTURE_FILLS: &str = "picture_fills";
    /// `Image(fit: cover)` without `width`, `height` or `aspect`: the
    /// picture covers the room its container gives, cropped.
    pub const COVER_IMAGES: &str = "cover_images";
    /// `pick:` on `Button`, `IconButton` and `MenuItem`: the system's open
    /// panel asks for a file before the action runs.
    pub const FILE_PICK: &str = "file_pick";
    /// `Table`: rows of data in columns, with a selection of the rows'
    /// keys, a row menu, a primary action and sorting by a heading.
    pub const TABLE: &str = "table";
    /// `TextInput(current:)`: a field showing engine state, each edit
    /// running its action, the user's text held while being edited.
    pub const CONTROLLED_TEXT: &str = "controlled_text";
    /// `Watch`: a command runs when the element is mounted and whenever
    /// the value it follows changes.
    pub const WATCH: &str = "watch";
    /// `Switch(login_item: true)`: whether the system opens the
    /// application at login, shown where it can, left out elsewhere.
    pub const LOGIN_SWITCH: &str = "login_switch";
    /// `paste:` on `Button`, `IconButton`, `MenuItem` and `CommandItem`:
    /// the clipboard's text goes into a draft before the action runs.
    pub const PASTE: &str = "paste";
    /// `ScrollView(snap:, position:)`: scrolling comes to rest on an item,
    /// and a session field follows and sets the item it stands at.
    pub const SCROLL_SNAP: &str = "scroll_snap";
    /// `Rail`: elements placed at fractions of a strip's length.
    pub const RAILS: &str = "rails";
    /// `PopoverButton`: a button that shows its children in a popover.
    pub const POPOVER_BUTTON: &str = "popover_button";
    /// `text { large_title: heavy }` in `style!`: weights of text styles.
    pub const TEXT_WEIGHTS: &str = "text_weights";
    /// `Text(lines:)` and `Text(line_height:)`: text limited to a number of
    /// lines, or with lines of a given height.
    pub const TEXT_LINES: &str = "text_lines";
    /// `Image(corner:)`: an image with rounded corners.
    pub const IMAGE_CORNERS: &str = "image_corners";
    /// `Sidebar(width:)`: a sidebar of a fixed width; without it the
    /// sidebar keeps the platform's width.
    pub const SIDEBAR_WIDTH: &str = "sidebar_width";
    /// `Switch(checked:)`, `Picker(selected:)`, `Slider(current:)` and `PopoverButton(open:)`:
    /// controls showing engine state and reporting changes as commands.
    pub const CONTROLLED_VALUES: &str = "controlled_values";
    /// `Search(style: palette)` and its `SearchItem`s: a toolbar search
    /// field that grows into a results panel.
    pub const SEARCH_PALETTE: &str = "search_palette";
    /// `Image(rendering: template)`: the image's alpha takes the text color.
    pub const IMAGE_TEMPLATE: &str = "image_template";
    /// `Image(aspect:)`, `Grid(row_gap:)` and `Slider(label_hidden:)`.
    pub const ADAPTIVE_IMAGES: &str = "adaptive_images";
    /// `ScaleBox`, `Positioned` and `Marquee`: a fixed coordinate scene
    /// scaled to its room.
    pub const SCALED_SCENE: &str = "scaled_scene";
    /// `Accordion`: columns of which one is open at a time.
    pub const ACCORDION_LAYOUT: &str = "accordion_layout";
    /// `ControlGroup` and capsule-shaped system controls.
    pub const CAPSULE_CONTROLS: &str = "capsule_controls";
    /// `Modal`, `Flow`, `Timer`, `PickerOption` and `Stack(enabled:)`.
    pub const MODAL_FLOW_TIMER: &str = "modal_flow_timer";
    /// `SidebarItem(badge:)`: a live count beside a page.
    pub const PAGE_BADGES: &str = "page_badges";
    /// `Window(idle_enabled:)`: idle detection switched on and off by
    /// state. Without it a target keeps detecting idleness all the time.
    pub const IDLE_CONTROL: &str = "idle_control";
    /// `Button(width: fill)`: a system button stretched across its
    /// container, as a phone's main action.
    pub const FILL_BUTTONS: &str = "fill_buttons";
    /// `Grid(cell_ratio:, cell_extra:)`: rows as tall as the column width
    /// gives, whatever their content.
    pub const GRID_CELLS: &str = "grid_cells";
    /// A `Search` standing in the page, not in a `Toolbar` or `Sidebar`:
    /// the platform's search field with its results below it.
    pub const PAGE_SEARCH: &str = "page_search";
    /// `Section(style: plain)`: rows on the page itself, as a heading that
    /// scrolls with its form; `Section(padding_x:, padding_y:)` its rows'
    /// insets and `Form(gap:)` the room between its sections.
    pub const FORM_LAYOUT: &str = "form_layout";
    /// `Modal(cover: compact)`: the modal covers the whole window where it
    /// is compact.
    pub const MODAL_COVER: &str = "modal_cover";
    /// `Window(landscape:)`: a phone turns the window to landscape while
    /// it holds, and back to portrait. A target whose windows never turn
    /// (a television, a desktop window) lists it as well: there the
    /// property does not apply, and nothing is left to warn about.
    pub const WINDOW_LANDSCAPE: &str = "window_landscape";
    /// `window.short`: the window is shorter than the compact breakpoint.
    pub const WINDOW_SHORT: &str = "window_short";
    /// `TabAccessory`: a bar kept with the tabs over every page.
    pub const TAB_ACCESSORY: &str = "tab_accessory";
    /// Transitions growing along one axis from a fixed edge.
    pub const EDGE_GROW: &str = "edge_grow";
    /// An entrance after a delay, including the first mounted frame.
    pub const DELAYED_APPEAR: &str = "delayed_appear";
    /// A `Spacer` in a `Toolbar`, parting leading items from trailing ones,
    /// and `Text` in a toolbar.
    pub const TOOLBAR_LEADING: &str = "toolbar_leading";
    /// `padding_x` and `padding_y`: different padding across and along.
    pub const AXIS_PADDING: &str = "axis_padding";
    /// `Menu(style: icon)`: a menu shown as a round icon button.
    pub const ICON_MENUS: &str = "icon_menus";
    /// `Label(display: icon)`: an icon with a nonvisual accessible title.
    pub const ICON_LABELS: &str = "icon_labels";
    /// `Tab(display: icon)`: a tab showing only its icon, its label the
    /// accessible name.
    pub const ICON_TABS: &str = "icon_tabs";
    /// `Row(align: baseline)`.
    pub const ROW_BASELINE: &str = "row_baseline";
    /// `SidebarHeader`: content pinned above a sidebar's list.
    pub const SIDEBAR_HEADER: &str = "sidebar_header";
    /// `Detail(titled: false)`: a detail column that leaves its title bar
    /// without the destination's name, for pages that show their own.
    pub const UNTITLED_DETAIL: &str = "untitled_detail";
    /// The `medium` and `large` layout sizes and `window.compact` and its
    /// siblings in expressions.
    pub const SIZE_CLASSES: &str = "size_classes";
    /// Plural branches, dates and durations in translated texts, and enum
    /// labels that show their variant's fields.
    pub const TEXT_FORMATS: &str = "text_formats";
}

/// Whether the application writes numbers, dates or durations in its
/// texts, or shows enum fields in labels: what [`feature::TEXT_FORMATS`]
/// names.
pub fn uses_text_formats(app: &Application) -> bool {
    use bern_protocol::format::{Piece, parse};
    let formatted = app.texts.iter().any(|text| {
        text.translations.values().any(|translation| {
            parse(translation).is_ok_and(|pieces| {
                pieces
                    .iter()
                    .any(|piece| !matches!(piece, Piece::Text(_) | Piece::Value(_)))
            })
        })
    });
    let labels_show_fields = app.types.enums.iter().any(|def| {
        def.variants.iter().any(|variant| {
            variant
                .label
                .as_ref()
                .is_some_and(|label| !label.params.is_empty())
        })
    });
    formatted || labels_show_fields
}

/// The style effects a target shows for one control kind: outside any
/// state, and in each state.
#[derive(Clone, Copy, Debug)]
pub struct StyleSupport {
    pub component: &'static str,
    pub base: &'static [&'static str],
    pub states: &'static [(bern_ir::style::StyleState, &'static [&'static str])],
}

impl StyleSupport {
    fn shows(&self, state: Option<bern_ir::style::StyleState>, effect: &str) -> bool {
        match state {
            None => self.base.contains(&effect),
            Some(state) => self
                .states
                .iter()
                .any(|(supported, effects)| *supported == state && effects.contains(&effect)),
        }
    }
}

impl TargetDescriptor {
    pub fn supports_ir(&self) -> bool {
        self.ir_versions.contains(&IR_VERSION)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Severity {
    Error,
    Warning,
    /// Information a report must show, such as an exemption.
    Note,
}

/// A diagnostic pointing back to the developer's declaration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
    pub severity: Severity,
    pub code: Option<&'static str>,
    pub message: String,
    pub location: Option<SourceLocation>,
}

impl Diagnostic {
    pub fn error(message: impl Into<String>) -> Self {
        Self {
            severity: Severity::Error,
            code: None,
            message: message.into(),
            location: None,
        }
    }

    pub fn warning(message: impl Into<String>) -> Self {
        Self {
            severity: Severity::Warning,
            ..Self::error(message)
        }
    }

    pub fn note(message: impl Into<String>) -> Self {
        Self {
            severity: Severity::Note,
            ..Self::error(message)
        }
    }

    pub fn code(mut self, code: &'static str) -> Self {
        self.code = Some(code);
        self
    }

    pub fn at(mut self, location: SourceLocation) -> Self {
        self.location = Some(location);
        self
    }

    pub fn is_error(&self) -> bool {
        self.severity == Severity::Error
    }
}

impl fmt::Display for Diagnostic {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let level = match self.severity {
            Severity::Error => "error",
            Severity::Warning => "warning",
            Severity::Note => "note",
        };
        match self.code {
            Some(code) => write!(f, "{level}[{code}]: {}", self.message)?,
            None => write!(f, "{level}: {}", self.message)?,
        }
        if let Some(location) = &self.location {
            write!(
                f,
                "\n  --> {}:{}:{}",
                location.file, location.line, location.column
            )?;
        }
        Ok(())
    }
}

/// Target settings from `[targets.<name>]` in `Bern.toml`.
#[derive(Clone, Debug, Default)]
pub struct TargetConfig {
    pub name: String,
    pub platform: String,
    pub ui: Option<String>,
    /// Web build mode: `app` (engine in the page) or `ui` (separate engine).
    pub web_mode: Option<String>,
    /// Browser extension entry and optional build command (receives the output directory).
    pub web_extension: Option<WebExtension>,
    pub channels: Vec<String>,
    /// Windows front ends to build, from [`WINDOWS_FRONTENDS`]; both unless
    /// the target names fewer. Empty for other platforms.
    pub frontends: Vec<String>,
    /// Override the target's web document capability. False removes all
    /// WebView dependencies and compiles SVGs into static pictures.
    pub webview: Option<bool>,
    /// Assemble the portable JavaScript host (off unless requested).
    pub javascript: bool,
    /// Explicit Android host capabilities; absent on other platforms.
    pub android: Option<AndroidOptions>,
}

/// Android capabilities needed by an application's Rust engine, which
/// cannot be inferred from its UI. Multicast also enables network access.
#[derive(Clone, Debug, Default, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AndroidOptions {
    pub network: bool,
    pub multicast: bool,
}

impl TargetConfig {
    pub fn has_webview(&self) -> bool {
        self.platform == "browser"
            || (self.platform != "tvos"
                && self.webview.unwrap_or(matches!(
                    self.platform.as_str(),
                    "macos" | "ios" | "android" | "windows" | "linux" | "browser"
                )))
    }

    pub fn javascript_backend(&self) -> Option<&'static str> {
        self.javascript.then(|| {
            if self.has_webview() {
                if self.platform == "browser" {
                    "browser"
                } else {
                    "webview"
                }
            } else {
                "boa"
            }
        })
    }
}

/// An application-owned browser adapter. Its `index.js` exports a setup function.
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WebExtension {
    pub path: String,
    #[serde(default)]
    pub build: Vec<String>,
}

/// The Windows front ends: WinUI 3 on the Windows App Runtime, and the
/// system XAML through XAML Islands, which every Windows 10 and 11 has.
pub const WINDOWS_FRONTENDS: [&str; 2] = ["winui3", "xaml-islands"];

/// Where the Bern crates come from: the source of the project's own `bern`
/// dependency. Generated crates take every Bern crate from it, so the
/// application and the code generated around it share one copy of each.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Framework {
    /// A checkout whose `crates` directory holds every Bern crate.
    Path(PathBuf),
    /// A Git repository, at the reference the project names, if any:
    /// `("branch", …)`, `("tag", …)` or `("rev", …)`.
    Git {
        url: String,
        reference: Option<(String, String)>,
    },
    /// A release on crates.io, with the project's version requirement.
    Registry(String),
}

impl Framework {
    /// The dependency on the Bern crate `name`, as the inside of an inline
    /// table, for example `path = "/src/bern/crates/bern-ffi"`.
    pub fn dependency(&self, name: &str) -> String {
        match self {
            Framework::Path(crates) => {
                let path = crates.join(name).display().to_string();
                format!("path = {}", toml_string(&path.replace('\\', "/")))
            }
            Framework::Git { url, reference } => {
                let mut out = format!("git = {}", toml_string(url));
                if let Some((kind, value)) = reference {
                    out.push_str(&format!(", {kind} = {}", toml_string(value)));
                }
                out
            }
            Framework::Registry(version) => format!("version = {}", toml_string(version)),
        }
    }
}

/// A TOML basic string.
pub fn toml_string(text: &str) -> String {
    let mut out = String::from("\"");
    for c in text.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            c if c.is_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

/// Paths and packages a target needs to plan its build.
#[derive(Clone, Debug)]
pub struct BuildContext {
    /// Directory containing `Bern.toml`.
    pub project_dir: PathBuf,
    /// Cargo workspace root.
    pub workspace_dir: PathBuf,
    /// `target/bern/<target-name>/`, owned by this target.
    pub output_dir: PathBuf,
    pub target: TargetConfig,
    pub protocol_package: String,
    pub protocol_path: PathBuf,
    /// Rust path of the protocol module, for example `showcase_protocol::showcase`.
    pub protocol_module: String,
    pub engine_package: String,
    pub engine_path: PathBuf,
    /// Rust path of the engine type, which implements `Default`.
    pub engine_type: String,
    /// The application's modules, in catalog order.
    pub modules: Vec<ModuleBuild>,
    /// Where generated crates take the Bern crates from.
    pub framework: Framework,
    pub release: bool,
}

/// The crates of one application module, for the generated glue.
#[derive(Clone, Debug)]
pub struct ModuleBuild {
    pub id: String,
    pub protocol_package: String,
    pub protocol_path: PathBuf,
    /// Rust path of the module's protocol module, e.g. `downloads_protocol::downloads`.
    pub protocol_module: String,
    pub engine_package: String,
    pub engine_path: PathBuf,
    /// Rust path of the module engine type, which implements `Default`.
    pub engine_type: String,
    /// Whether the module can be switched off at run time.
    pub optional: bool,
    /// Whether an optional module runs before the application saved a choice.
    pub enabled: bool,
    pub depends: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GeneratedFile {
    /// Path relative to the output directory.
    pub path: PathBuf,
    pub contents: Vec<u8>,
}

impl GeneratedFile {
    pub fn text(path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
        Self {
            path: path.into(),
            contents: contents.into().into_bytes(),
        }
    }
}

/// A UI target module.
pub trait Target: Send + Sync {
    fn descriptor(&self) -> TargetDescriptor;

    /// Operating systems that can build this target, as
    /// `std::env::consts::OS` names them; empty when any can.
    fn build_hosts(&self) -> &'static [&'static str] {
        &[]
    }

    /// Report constructs this target cannot realize, with locations.
    fn check(&self, app: &Application, config: &TargetConfig) -> Vec<Diagnostic>;

    /// Generate sources and resources into the output directory.
    fn generate(
        &self,
        app: &Application,
        context: &BuildContext,
    ) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>>;

    /// Steps that turn generated files into the final product.
    fn plan(&self, app: &Application, context: &BuildContext)
    -> Result<BuildPlan, Vec<Diagnostic>>;

    /// Steps that turn a release build, which `plan` has made, into the
    /// packages people install, in [`dist_dir`].
    fn package(
        &self,
        _app: &Application,
        _context: &BuildContext,
        _signing: &Signing,
    ) -> Result<BuildPlan, Vec<Diagnostic>> {
        Err(vec![Diagnostic::error(format!(
            "target `{}` cannot make packages yet",
            self.descriptor().id
        ))])
    }
}

/// How packages are signed, from the environment of `bern package`.
/// Certificates stay with the person or the CI service that builds; none
/// of this goes into `Bern.toml`.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Signing {
    /// `BERN_MACOS_SIGNING_IDENTITY`: a signing identity in the keychain,
    /// for distribution a "Developer ID Application" one.
    pub macos_identity: Option<String>,
    /// `BERN_MACOS_NOTARY_PROFILE`: a keychain profile made with
    /// `xcrun notarytool store-credentials`.
    pub macos_notary_profile: Option<String>,
    /// `BERN_WINDOWS_CERTIFICATE`: the SHA-1 thumbprint of a code signing
    /// certificate in the user's store, or a `.pfx` file.
    pub windows_certificate: Option<String>,
    /// `BERN_WINDOWS_CERTIFICATE_PASSWORD`: the password of the `.pfx`.
    pub windows_certificate_password: Option<String>,
    /// `BERN_ANDROID_KEYSTORE`: the keystore holding the upload or release
    /// key. Gradle reads it and the variables below from the environment;
    /// the passwords never go into a file.
    pub android_keystore: Option<String>,
    /// `BERN_ANDROID_KEYSTORE_PASSWORD`.
    pub android_keystore_password: Option<String>,
    /// `BERN_ANDROID_KEY_ALIAS`: the key in the keystore.
    pub android_key_alias: Option<String>,
}

impl Signing {
    pub fn from_env() -> Signing {
        let var = |name: &str| {
            std::env::var(name)
                .ok()
                .filter(|value| !value.trim().is_empty())
        };
        Signing {
            macos_identity: var("BERN_MACOS_SIGNING_IDENTITY"),
            macos_notary_profile: var("BERN_MACOS_NOTARY_PROFILE"),
            windows_certificate: var("BERN_WINDOWS_CERTIFICATE"),
            windows_certificate_password: var("BERN_WINDOWS_CERTIFICATE_PASSWORD"),
            android_keystore: var("BERN_ANDROID_KEYSTORE"),
            android_keystore_password: var("BERN_ANDROID_KEYSTORE_PASSWORD"),
            android_key_alias: var("BERN_ANDROID_KEY_ALIAS"),
        }
    }
}

/// Explicit registry of target modules, assembled by the build host.
#[derive(Default)]
pub struct Registry {
    targets: Vec<Box<dyn Target>>,
}

/// Default UI technology per platform.
pub fn default_ui(platform: &str) -> Option<&'static str> {
    match platform {
        "macos" | "ios" | "tvos" => Some("swiftui"),
        "linux" => Some("gtk"),
        "windows" => Some("winui3"),
        "android" => Some("compose"),
        "browser" => Some("web"),
        _ => None,
    }
}

impl Registry {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn register(&mut self, target: Box<dyn Target>) {
        self.targets.push(target);
    }

    pub fn descriptors(&self) -> Vec<TargetDescriptor> {
        self.targets
            .iter()
            .map(|target| target.descriptor())
            .collect()
    }

    /// Resolve the target for a configuration. A missing default backend is
    /// an error; there is never a silent switch to another UI technology.
    pub fn resolve(&self, config: &TargetConfig) -> Result<&dyn Target, Diagnostic> {
        let ui = match &config.ui {
            Some(ui) => ui.clone(),
            None => default_ui(&config.platform)
                .ok_or_else(|| {
                    Diagnostic::error(format!(
                        "target `{}`: unknown platform `{}`",
                        config.name, config.platform
                    ))
                })?
                .to_string(),
        };
        self.targets
            .iter()
            .map(AsRef::as_ref)
            .find(|target| {
                let descriptor = target.descriptor();
                descriptor.ui == ui && descriptor.platforms.contains(&config.platform.as_str())
            })
            .ok_or_else(|| {
                Diagnostic::error(format!(
                    "target `{}`: the `{ui} + {}` backend is not available in this build of Bern",
                    config.name, config.platform
                ))
            })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct Fake;

    impl Target for Fake {
        fn descriptor(&self) -> TargetDescriptor {
            TargetDescriptor {
                styles: &[],
                motion: &[],
                window_chrome: false,
                text_marks: false,
                dynamic_items: false,
                features: &[],
                id: "fake-macos",
                ui: "swiftui",
                platforms: &["macos"],
                version: "0",
                ir_versions: IR_VERSION..=IR_VERSION,
                color_source: ColorSource::Native,
                components: &[],
                windows: WindowCapabilities {
                    native: true,
                    virtual_windows: false,
                    floating: true,
                },
                channels: &[],
                media: None,
            }
        }
        fn check(&self, _: &Application, _: &TargetConfig) -> Vec<Diagnostic> {
            vec![]
        }
        fn generate(
            &self,
            _: &Application,
            _: &BuildContext,
        ) -> Result<Vec<GeneratedFile>, Vec<Diagnostic>> {
            Ok(vec![])
        }
        fn plan(&self, _: &Application, _: &BuildContext) -> Result<BuildPlan, Vec<Diagnostic>> {
            Ok(BuildPlan::default())
        }
    }

    fn config(platform: &str, ui: Option<&str>) -> TargetConfig {
        TargetConfig {
            name: "t".into(),
            platform: platform.into(),
            ui: ui.map(String::from),
            web_mode: None,
            web_extension: None,
            webview: None,
            javascript: false,
            android: None,
            channels: vec![],
            frontends: vec![],
        }
    }

    #[test]
    fn defaults_resolve_by_platform_and_missing_backends_fail() {
        let mut registry = Registry::new();
        registry.register(Box::new(Fake));
        assert_eq!(
            registry
                .resolve(&config("macos", None))
                .unwrap()
                .descriptor()
                .id,
            "fake-macos"
        );
        let error = registry.resolve(&config("linux", None)).err().unwrap();
        assert!(error.message.contains("gtk + linux"), "{}", error.message);
        assert!(registry.resolve(&config("macos", Some("flutter"))).is_err());
        assert!(registry.resolve(&config("amiga", None)).is_err());
    }

    #[test]
    fn generated_crates_name_bern_crates_like_the_project() {
        let path = Framework::Path(PathBuf::from("/src/Bern/crates"));
        assert_eq!(
            path.dependency("bern-ffi"),
            r#"path = "/src/Bern/crates/bern-ffi""#
        );
        let git = Framework::Git {
            url: "https://github.com/AimesSoft/Bern".into(),
            reference: Some(("branch".into(), "master".into())),
        };
        assert_eq!(
            git.dependency("bern-ffi"),
            r#"git = "https://github.com/AimesSoft/Bern", branch = "master""#
        );
        assert_eq!(
            Framework::Registry("0.1".into()).dependency("bern-ffi"),
            r#"version = "0.1""#
        );
        assert_eq!(toml_string(r#"C:\a "b""#), r#""C:\\a \"b\"""#);
    }
}

pub mod script;