From 1dde90bcb9e6379c37d5dfb0329730cdf1c49b90 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 18 Oct 2026 06:37:46 -0400 Subject: [PATCH 001/798] touchid: fix the false positive on the enrolment check The check looked for 'not enrolled' and missed fprintd's actual wording, 'User X has no fingers enrolled', so it reported a finger as enrolled when none was. Match the negative explicitly instead. --- mseries/aurora-touchid/mseries-touchid-settings | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mseries/aurora-touchid/mseries-touchid-settings b/mseries/aurora-touchid/mseries-touchid-settings index 30d1ec768..a291e8c46 100755 --- a/mseries/aurora-touchid/mseries-touchid-settings +++ b/mseries/aurora-touchid/mseries-touchid-settings @@ -89,12 +89,9 @@ def probe(): code, out = run(["fprintd-list", os.environ.get("USER", "")]) reader = code == 0 and "No devices available" not in out - # fprintd-list prints one line per stored print; anything that names a finger - # and is not explicitly unenrolled counts as enrolled. - enrolled = reader and any( - "finger" in line.lower() and "not enrolled" not in line.lower() - for line in out.splitlines() - ) + # fprintd-list states the negative explicitly: "User X has no fingers + # enrolled". Anything else, with a reader present, means at least one print. + enrolled = reader and "no fingers enrolled" not in out.lower() return [ ("Kernel", on_sep_kernel, f"running {kernel}"), From f5fdeecca1fa55712c66aa76fa37fde4de55c76c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 11:26:15 -0400 Subject: [PATCH 002/798] CodexBar: show available Codex reset credits per account and when the first expires Co-Authored-By: Claude Sonnet 5.5 --- mseries/codexbar-accounts.py | 15 +- mseries/codexbar-wingpanel/Indicator.vala | 288 +++++++++++++----- mseries/codexbar-wingpanel/Presentation.vala | 49 +++ .../codexbar-wingpanel/test-presentation.vala | 39 +++ 4 files changed, 318 insertions(+), 73 deletions(-) diff --git a/mseries/codexbar-accounts.py b/mseries/codexbar-accounts.py index e77b17238..0e86e54ce 100644 --- a/mseries/codexbar-accounts.py +++ b/mseries/codexbar-accounts.py @@ -164,7 +164,20 @@ def usage_for(home: pathlib.Path, cli: str) -> dict[str, Any]: if isinstance(window, dict) and window.get("usedPercent") is not None: windows.append({"key": key, "label": label, "remaining": max(0.0, 100.0 - float(window["usedPercent"])), "resetsAt": window.get("resetsAt", "")}) - return {"windows": windows} + return {"windows": windows, "resetCredits": reset_credits(row)} + + +def reset_credits(row: dict[str, Any]) -> dict[str, Any] | None: + """Available rate-limit resets and when the first one expires, or None when the CLI does not report them.""" + summary = row.get("resetCredits") + if not isinstance(summary, dict) or not isinstance(summary.get("available"), int): + return None + expires = summary.get("nextExpiresAt") if isinstance(summary.get("nextExpiresAt"), str) else "" + if not expires: # older summaries carry only the count; fall back to the credit list + detail = (row.get("usage") or {}).get("codexResetCredits") or {} + dates = [c.get("expiresAt") for c in detail.get("credits", []) if isinstance(c, dict) and isinstance(c.get("expiresAt"), str)] + expires = min(dates) if dates else "" + return {"available": summary["available"], "nextExpiresAt": expires} def usage_report(cli: str) -> list[dict[str, Any]]: diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 47f8da449..2fa426602 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -6,7 +6,9 @@ using Wingpanel; namespace MSeries { public class CodexBarIndicator : Wingpanel.Indicator { - private Gtk.Label panel_label; + private Gtk.Box panel_segments; + private string panel_digest = ""; + private Json.Array? panel_entries = null; private delegate void ActionFunc (); private Gtk.Box panel_display; private Gtk.Box menu; @@ -53,19 +55,12 @@ namespace MSeries { construct { executable = GLib.Path.build_filename (Environment.get_home_dir (), ".local", "bin", "codexbar-linux"); - var panel_icon = new Gtk.Image.from_icon_name ("mseries-codexbar-openai-symbolic") { - pixel_size = 12 - }; - panel_icon.add_css_class ("symbolic"); - panel_label = new Gtk.Label ("—") { - ellipsize = Pango.EllipsizeMode.END, - max_width_chars = 52 - }; - panel_label.add_css_class ("caption"); - panel_label.add_css_class ("mseries-codexbar-bar"); - panel_display = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 5); - panel_display.append (panel_icon); - panel_display.append (panel_label); + // One dense segment per provider: company mark, % left of the binding limit, time to its reset. + panel_segments = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 9); + panel_segments.add_css_class ("mseries-codexbar-bar"); + panel_display = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + panel_display.append (panel_segments); + show_panel_placeholder (); panel_display.set_tooltip_text ("CodexBar · loading usage… · Middle-click to refresh"); var refresh_gesture = new Gtk.GestureClick () { button = 2 }; refresh_gesture.released.connect (() => refresh_usage ()); @@ -198,9 +193,13 @@ namespace MSeries { ".mseries-codexbar button.compact { padding: 1px 8px; min-height: 0; min-width: 0; } " + ".mseries-codexbar button.flat.compact { padding: 0 4px; } " + ".mseries-codexbar .mseries-switcher button { padding: 2px 6px; } " + - "label.mseries-codexbar-bar.level-low { color: @warning_color; } " + - "label.mseries-codexbar-bar.level-critical { color: @error_color; } " + - "label.mseries-codexbar-bar.stale { opacity: 0.55; }"); + ".mseries-codexbar-bar label { font-size: 8.5pt; font-feature-settings: \"tnum\"; } " + + ".mseries-codexbar-bar .seg-value { font-weight: 700; } " + + ".mseries-codexbar-bar .seg-reset { font-size: 7.5pt; opacity: 0.7; } " + + ".mseries-codexbar-bar .level-low { color: @warning_color; } " + + ".mseries-codexbar-bar .level-critical { color: @error_color; } " + + ".mseries-codexbar-bar .mark-claude { color: #d97757; } " + + ".mseries-codexbar-bar .stale { opacity: 0.55; }"); Gtk.StyleContext.add_provider_for_display (display, style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); } @@ -484,13 +483,16 @@ namespace MSeries { }); } - private async void run_saver (string[] command, string ok_heading, string failure_heading) { + private async void run_saver (string[] command, string ok_heading, string failure_heading, + bool report_output = false) { try { var process = new GLib.Subprocess.newv (command, SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); string output; yield process.communicate_utf8_async (null, null, out output, null); if (process.get_successful ()) { - tell (ok_heading, "It now appears under Accounts."); + tell (ok_heading, report_output && output.strip ().length > 0 + ? output.strip () : "It now appears under Accounts."); + if (report_output) refresh_usage (); } else { tell (failure_heading, output.strip ().length > 0 ? output.strip () : "The helper reported an error."); } @@ -504,6 +506,16 @@ namespace MSeries { refresh_claude.begin (); } + // Upstream only imports opencode.ai cookies on macOS; our helper signs in and saves them as a manual session. + private void add_opencode_account () { + ask_to_save ("Sign in to OpenCode Go", + "A sign-in window for opencode.ai opens. After you sign in, CodexBar saves that session and reads your OpenCode Go quota from it.", + "Sign in", () => { + run_saver.begin ({ "python3", helper_path ("codexbar-opencode-login.py"), "login" }, + "OpenCode Go signed in", "Couldn't sign in to OpenCode Go", true); + }); + } + private void add_claude_account () { ask_to_save ("Add a Claude account", "CodexBar opens your browser to sign in to Claude, then saves the login so you can switch between accounts from here. Any account that is already signed in is saved first.", @@ -628,6 +640,7 @@ namespace MSeries { detail = "%.0f%%".printf (windows.get_object_element (windows.get_length () - 1) .get_double_member_with_default ("remaining", 0)); } + detail = with_resets (detail, account.get_object_member ("resetCredits")); box.append (build_account_line (email, detail, "Make %s the active Codex account. Restart running Codex sessions to use it.".printf (email), () => run_accounts_helper ({ "switch", account_id }))); @@ -635,6 +648,33 @@ namespace MSeries { return box; } + /* "2 resets · 6%" for an account line: the reset count leads, quota follows. */ + private static string with_resets (string detail, Json.Object? credits) { + if (credits == null || !credits.has_member ("available")) { + return detail; + } + var count = Presentation.reset_count_text (credits.get_int_member_with_default ("available", -1)); + return count.length > 0 ? count + " · " + detail : detail; + } + + /* Seconds until the first reset credit expires; 0 when unknown. */ + private static int64 reset_expiry_seconds (Json.Object credits, int64 now_unix) { + var expires = credits.get_string_member_with_default ("nextExpiresAt", ""); + var when = expires.length > 0 ? new GLib.DateTime.from_iso8601 (expires, null) : null; + return when != null ? when.to_unix () - now_unix : 0; + } + + /* Available resets for the active account, with when the first one expires; red inside the last day. */ + private Gtk.Widget build_reset_credits (Json.Object credits, int64 now_unix) { + var available = credits.get_int_member_with_default ("available", -1); + var seconds = reset_expiry_seconds (credits, now_unix); + var label = subtle_label (Presentation.reset_credits_line (available, seconds)); + if (Presentation.reset_expiry_urgent (available, seconds)) { + label.add_css_class ("dot-bad"); + } + return label; + } + private static string local_digest (Json.Object? usage) { if (usage == null) { return ""; @@ -765,6 +805,9 @@ namespace MSeries { } refresh_header (now_unix); + // The panel's countdowns tick every poll; the popover is rebuilt only when its data changes. + panel_entries = entries; + update_panel (entries, now_unix); var digest = snapshot_digest (snapshot, now_unix); if (digest == last_digest) { return; @@ -772,7 +815,6 @@ namespace MSeries { last_digest = digest; render_quota (entries, now_unix); render_spend (snapshot); - update_panel (entries, now_unix); } private void refresh_header (int64 now_unix) { @@ -798,69 +840,164 @@ namespace MSeries { } } + private static int provider_rank (string provider) { + return provider == "codex" ? 0 : provider == "claude" ? 1 : provider == "opencodego" ? 2 : 3; + } + + private void show_panel_placeholder () { + panel_digest = ""; + clear (panel_segments); + panel_segments.append (new Gtk.Label ("—")); + } + + /* Fills one segment's texts from a quota window list; returns the level of the binding window. */ + private static QuotaLevel window_segment (Json.Array windows, int64 now_unix, out string value_text, + out string reset_text, StringBuilder tooltip) { + value_text = "—"; + reset_text = ""; + var level = QuotaLevel.UNKNOWN; + var binding = Presentation.binding_window (windows); + for (uint w = 0; w < windows.get_length (); w++) { + var window = windows.get_object_element (w); + var minutes = (int) window.get_int_member_with_default ("minutes", 0); + var short_label = Presentation.short_window_label ( + window.get_string_member_with_default ("label", "Quota"), minutes); + var node = window.get_member ("remaining"); + if (node == null || node.get_node_type () != Json.NodeType.VALUE) { + tooltip.append ("\n %s not reported yet".printf (short_label)); + continue; + } + var remaining = node.get_double (); + var reset = window.get_string_member_with_default ("resetText", ""); + tooltip.append ("\n %s %0.0f%% left · %s".printf (short_label, remaining, + reset.length > 0 ? reset.down () : "reset unknown")); + if ((int) w != binding) continue; + level = Presentation.level_for (remaining); + value_text = Presentation.quota_lead (short_label, minutes, "%0.0f%%".printf (remaining)); + var resets_at = new DateTime.from_iso8601 (window.get_string_member_with_default ("resetsAt", ""), null); + reset_text = resets_at != null ? Presentation.countdown_text (resets_at.to_unix () - now_unix) + : compact_reset (reset).replace (" ", ""); + } + return level; + } + private void update_panel (Json.Array entries, int64 now_unix) { - var parts = new string[0]; - var tags = new string[0]; + string[] providers = {}; + string[] values = {}; + string[] resets = {}; + QuotaLevel[] levels = {}; + bool[] stales = {}; var tooltip = new StringBuilder (); - var level = QuotaLevel.UNKNOWN; + var has_claude = false; + for (uint i = 0; i < entries.get_length (); i++) { var entry = entries.get_object_element (i); var provider = (entry.get_string_member ("provider") ?? "").down (); + has_claude = has_claude || provider == "claude"; var windows = entry.get_array_member ("windows"); - if (windows == null || windows.get_length () == 0) { - continue; - } var stale = QuotaEta.entry_stale (entry, snapshot_stale, now_unix); - var eta = QuotaEta.for_entry (entry, stale, now_unix); - tooltip.append ("%s%s\n".printf (tooltip.len > 0 ? "\n" : "", provider_title (provider))); - string remaining_text = "—"; - string lead_window = ""; - bool first_measured = true; - for (uint w = 0; w < windows.get_length (); w++) { - var window = windows.get_object_element (w); - var node = window.get_member ("remaining"); - var short_label = Presentation.short_window_label (window.get_string_member ("label") ?? "Quota", - (int) window.get_int_member ("minutes")); - if (node == null || node.get_node_type () == Json.NodeType.NULL) { - tooltip.append (" %s not reported yet\n".printf (short_label)); - continue; - } - var remaining = node.get_double (); - level = Presentation.worse (level, Presentation.level_for (remaining)); - if (first_measured) { - // The first measured window is the provider's session/general quota. - var minutes = (int) window.get_int_member ("minutes"); - remaining_text = Presentation.quota_lead (short_label, minutes, "%0.0f%%".printf (remaining)); - lead_window = minutes > 360 ? short_label : ""; - first_measured = false; + var section = new StringBuilder (provider_title (provider)); + string value_text = "—"; + string reset_text = ""; + var level = QuotaLevel.UNKNOWN; + if (windows != null && windows.get_length () > 0) { + level = window_segment (windows, now_unix, out value_text, out reset_text, section); + } else if (provider == "opencodego" && opencode_local != null) { + // No account quota without a web sign-in: show this machine's 7-day token count instead. + var periods = opencode_local.get_array_member ("periods"); + for (uint p = 0; periods != null && p < periods.get_length (); p++) { + var period = periods.get_object_element (p); + var label = period.get_string_member_with_default ("label", ""); + var tokens = period.get_int_member_with_default ("tokens", 0); + section.append ("\n %s".printf (Presentation.local_usage_line (label, + period.get_double_member_with_default ("costUSD", 0), + period.get_int_member_with_default ("requests", 0), tokens))); + if (label.contains ("7")) { + value_text = Presentation.panel_count_text (tokens); + reset_text = "7d tok"; + } } - tooltip.append (" %s %0.0f%% left · %s\n".printf (short_label, remaining, - (window.get_string_member ("resetText") ?? "").length > 0 - ? window.get_string_member ("resetText").down () : "reset unknown")); - } - if (provider.contains ("codex") || provider.contains ("claude")) { - tags += provider.contains ("codex") ? "CX" : "CL"; - parts += Presentation.bar_segment (remaining_text, eta.state, eta.compact, - Presentation.limiting_window ( - Presentation.short_window_label (eta.window_label, eta.window_minutes), lead_window)); + section.append ("\n Local estimate, not account quota"); + stale = false; + } else { + var error = entry.get_string_member_with_default ("error", ""); + section.append ("\n " + (error.length > 0 ? error : "Usage not reported yet")); } + providers += provider; + values += value_text; + resets += reset_text; + levels += level; + stales += stale; + tooltip.append ((tooltip.len > 0 ? "\n\n" : "") + section.str); } - var shown = new string[0]; - for (int i = 0; i < parts.length; i++) { - shown += (parts.length > 1 ? tags[i] + " " : "") + parts[i]; + // Claude quota comes from our claude-swap helper when upstream's snapshot has no Claude entry. + if (!has_claude && claude_data != null) { + var accounts = claude_data.get_array_member ("accounts"); + for (uint i = 0; accounts != null && i < accounts.get_length (); i++) { + var account = accounts.get_object_element (i); + var windows = account.get_array_member ("windows"); + if (!account.get_boolean_member_with_default ("active", false) || windows == null + || windows.get_length () == 0) continue; + var section = new StringBuilder ("Claude"); + string value_text; + string reset_text; + var level = window_segment (windows, now_unix, out value_text, out reset_text, section); + providers += "claude"; + values += value_text; + resets += reset_text; + levels += level; + stales += false; + tooltip.append ((tooltip.len > 0 ? "\n\n" : "") + section.str); + break; + } } - panel_label.set_text (shown.length > 0 ? string.joinv (" ", shown) : "—"); - panel_label.remove_css_class ("level-low"); - panel_label.remove_css_class ("level-critical"); - panel_label.remove_css_class ("stale"); - if (snapshot_stale) panel_label.add_css_class ("stale"); - if (level == QuotaLevel.LOW) panel_label.add_css_class ("level-low"); - if (level == QuotaLevel.CRITICAL) panel_label.add_css_class ("level-critical"); + panel_display.set_tooltip_text (tooltip.len > 0 - ? tooltip.str.strip () + (snapshot_stale ? "\n\nCached: CodexBar couldn't reach a provider." : "") + ? tooltip.str + (snapshot_stale ? "\n\nCached: CodexBar couldn't reach a provider." : "") + "\n\nMiddle-click to refresh" : "Usage unavailable"); + + var digest = new StringBuilder (); + for (int i = 0; i < providers.length; i++) { + digest.append ("%s|%s|%s|%d|%s;".printf (providers[i], values[i], resets[i], (int) levels[i], + stales[i].to_string ())); + } + if (digest.str == panel_digest) return; + panel_digest = digest.str; + + clear (panel_segments); + for (int rank = 0; rank <= 3; rank++) { + for (int i = 0; i < providers.length; i++) { + if (provider_rank (providers[i]) != rank) continue; + panel_segments.append (build_panel_segment (providers[i], values[i], resets[i], levels[i], stales[i])); + } + } + if (providers.length == 0) panel_segments.append (new Gtk.Label ("—")); + } + + private static Gtk.Widget build_panel_segment (string provider, string value_text, string reset_text, + QuotaLevel level, bool stale) { + var segment = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3); + if (stale) segment.add_css_class ("stale"); + var mark = build_provider_mark (provider, provider_title (provider)); + var icon = mark as Gtk.Image; + if (icon != null) { + icon.pixel_size = 12; + icon.valign = Gtk.Align.CENTER; + } + segment.append (mark); + var value = new Gtk.Label (value_text) { valign = Gtk.Align.BASELINE_CENTER }; + value.add_css_class ("seg-value"); + if (level == QuotaLevel.LOW) value.add_css_class ("level-low"); + if (level == QuotaLevel.CRITICAL) value.add_css_class ("level-critical"); + segment.append (value); + if (reset_text.length > 0) { + var reset = new Gtk.Label (reset_text) { valign = Gtk.Align.BASELINE_CENTER }; + reset.add_css_class ("seg-reset"); + segment.append (reset); + } + return segment; } // ---- Pages -------------------------------------------------------------------- @@ -928,8 +1065,16 @@ namespace MSeries { card.append (subtle_label ("Provider status · " + status)); } if (provider == "codex") { + var active_resets = entry.get_member ("resetCredits"); + if (active_resets != null && active_resets.get_node_type () == Json.NodeType.OBJECT) { + card.append (build_reset_credits (active_resets.get_object (), now_unix)); + } card.append (build_codex_accounts ()); } + if (provider == "opencodego") { + card.append (build_accounts_header ("Sign in to opencode.ai so CodexBar can read your OpenCode Go quota", + () => add_opencode_account ())); + } if (error.length > 0 || windows == null || windows.get_length () == 0) { var estimate = provider == "opencodego" ? opencode_local : null; var periods = estimate != null ? estimate.get_array_member ("periods") : null; @@ -958,7 +1103,7 @@ namespace MSeries { settings.clicked.connect (() => { popdown_from (settings); if (provider == "opencodego") { - try { AppInfo.launch_default_for_uri ("https://opencode.ai", null); } catch (Error e) { } + add_opencode_account (); } else { run_action ("--settings"); } @@ -1269,9 +1414,8 @@ namespace MSeries { private void show_unavailable (string icon, string headline, string message) { snapshot_busy = false; last_digest = ""; - panel_label.set_text ("—"); - panel_label.remove_css_class ("level-low"); - panel_label.remove_css_class ("level-critical"); + panel_entries = null; + show_panel_placeholder (); panel_display.set_tooltip_text (headline + " · " + message); subtitle_label.set_text ("Usage unavailable"); chip_label.set_visible (false); diff --git a/mseries/codexbar-wingpanel/Presentation.vala b/mseries/codexbar-wingpanel/Presentation.vala index 2085178b0..52f27eb93 100644 --- a/mseries/codexbar-wingpanel/Presentation.vala +++ b/mseries/codexbar-wingpanel/Presentation.vala @@ -52,6 +52,12 @@ namespace MSeries { return value.to_string (); } + /* count_text without the decimal once it adds nothing at panel density: "121M", "9.4M". */ + public static string panel_count_text (int64 value) { + if (value >= 100000000 && value < 1000000000) return "%.0fM".printf ((double) value / 1000000.0); + return count_text (value); + } + /* Label the bar number when its window isn't the short session limit, so "35%" is never ambiguous. */ public static string quota_lead (string short_label, int minutes, string remaining_text) { return minutes > 360 && short_label.length > 0 ? short_label + " " + remaining_text : remaining_text; @@ -70,6 +76,30 @@ namespace MSeries { return remaining_text; } + /* Compact time to a reset for the panel: "38m", "4h44m", "6d22h"; empty when unknown or already due. */ + public static string countdown_text (int64 seconds) { + if (seconds <= 0) return ""; + if (seconds < 3600) return "%dm".printf ((int) ((seconds + 59) / 60)); + if (seconds < 86400) return "%dh%02dm".printf ((int) (seconds / 3600), (int) (seconds % 3600 / 60)); + return "%dd%dh".printf ((int) (seconds / 86400), (int) (seconds % 86400 / 3600)); + } + + /* The window that will stop you first: lowest measured remaining; a tie keeps the earlier (shorter) one. */ + public static int binding_window (Json.Array windows) { + int best = -1; + double best_remaining = 0; + for (uint i = 0; i < windows.get_length (); i++) { + var node = windows.get_object_element (i).get_member ("remaining"); + if (node == null || node.get_node_type () != Json.NodeType.VALUE) continue; + var remaining = node.get_double (); + if (best < 0 || remaining < best_remaining) { + best = (int) i; + best_remaining = remaining; + } + } + return best; + } + /* One line of local OpenCode Go activity: an estimate from this machine's history, not account quota. */ public static string local_usage_line (string label, double cost_usd, int64 requests, int64 tokens) { return "%s · $%.2f · %s %s · %s tokens".printf (label, cost_usd, count_text (requests), @@ -82,5 +112,24 @@ namespace MSeries { var base_text = reset.length > 0 ? reset : "Reset time unknown"; return state == EtaState.DEPLETING ? base_text + " · " + eta_detail : base_text; } + + /* "2 resets" for an account line; empty when the count is unknown. */ + public static string reset_count_text (int64 available) { + if (available < 0) return ""; + return "%s %s".printf (available.to_string (), available == 1 ? "reset" : "resets"); + } + + /* "2 resets available · first expires in 3d4h"; the expiry is dropped when unknown or past. */ + public static string reset_credits_line (int64 available, int64 seconds_to_expiry) { + if (available < 0) return ""; + var text = "%s available".printf (reset_count_text (available)); + var countdown = countdown_text (seconds_to_expiry); + return available > 0 && countdown.length > 0 ? text + " · first expires in " + countdown : text; + } + + /* Within a day of the first reset expiring, use it or lose it. */ + public static bool reset_expiry_urgent (int64 available, int64 seconds_to_expiry) { + return available > 0 && seconds_to_expiry > 0 && seconds_to_expiry < 86400; + } } } diff --git a/mseries/codexbar-wingpanel/test-presentation.vala b/mseries/codexbar-wingpanel/test-presentation.vala index 72ff5d7f3..83a3c0196 100644 --- a/mseries/codexbar-wingpanel/test-presentation.vala +++ b/mseries/codexbar-wingpanel/test-presentation.vala @@ -23,6 +23,45 @@ int main (string[] args) { assert (MSeries.Presentation.short_window_label ("Monthly", 0) == "30D"); assert (MSeries.Presentation.short_window_label ("Session", 0) == "Session"); }); + GLib.Test.add_func ("/presentation/reset-credits", () => { + assert (MSeries.Presentation.reset_count_text (-1) == ""); + assert (MSeries.Presentation.reset_count_text (1) == "1 reset"); + assert (MSeries.Presentation.reset_count_text (3) == "3 resets"); + assert (MSeries.Presentation.reset_credits_line (0, 5000) == "0 resets available"); + assert (MSeries.Presentation.reset_credits_line (2, 0) == "2 resets available"); + assert (MSeries.Presentation.reset_credits_line (2, 3600) == "2 resets available · first expires in 1h00m"); + assert (MSeries.Presentation.reset_expiry_urgent (2, 3600)); + assert (!MSeries.Presentation.reset_expiry_urgent (2, 90000)); + assert (!MSeries.Presentation.reset_expiry_urgent (0, 3600)); + }); + GLib.Test.add_func ("/presentation/countdown", () => { + assert (MSeries.Presentation.countdown_text (0) == ""); + assert (MSeries.Presentation.countdown_text (-5) == ""); + assert (MSeries.Presentation.countdown_text (61) == "2m"); + assert (MSeries.Presentation.countdown_text (3600) == "1h00m"); + assert (MSeries.Presentation.countdown_text (4 * 3600 + 44 * 60 + 10) == "4h44m"); + assert (MSeries.Presentation.countdown_text (6 * 86400 + 22 * 3600 + 59 * 60) == "6d22h"); + }); + GLib.Test.add_func ("/presentation/panel-count", () => { + assert (MSeries.Presentation.panel_count_text (120813143) == "121M"); + assert (MSeries.Presentation.panel_count_text (9400000) == "9.4M"); + assert (MSeries.Presentation.panel_count_text (1813153516) == "1.8B"); + }); + GLib.Test.add_func ("/presentation/binding-window", () => { + var parser = new Json.Parser (); + try { + parser.load_from_data ("[{\"remaining\":79},{\"remaining\":null},{\"remaining\":12},{\"remaining\":12}]"); + } catch (Error e) { + assert_not_reached (); + } + assert (MSeries.Presentation.binding_window (parser.get_root ().get_array ()) == 2); + try { + parser.load_from_data ("[{\"remaining\":null},{}]"); + } catch (Error e) { + assert_not_reached (); + } + assert (MSeries.Presentation.binding_window (parser.get_root ().get_array ()) == -1); + }); GLib.Test.add_func ("/presentation/plan-label", () => { assert (MSeries.Presentation.plan_label ("prolite") == "Pro Lite"); assert (MSeries.Presentation.plan_label ("pro") == "Pro"); From f89de61da1bd32703037f8cae3f91d27b045653f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 11:42:12 -0400 Subject: [PATCH 003/798] CodexBar: offer to restart the ChatGPT app after switching Codex accounts The desktop app keeps the account it started with, so the auth.json swap alone left it on the old user. After a successful Switch, ask to restart the app. Co-Authored-By: Claude Sonnet 5.5 --- mseries/codexbar-accounts.py | 46 +++++++++++++++++++++++ mseries/codexbar-wingpanel/Indicator.vala | 23 ++++++++++++ 2 files changed, 69 insertions(+) diff --git a/mseries/codexbar-accounts.py b/mseries/codexbar-accounts.py index 0e86e54ce..108601403 100644 --- a/mseries/codexbar-accounts.py +++ b/mseries/codexbar-accounts.py @@ -8,6 +8,8 @@ codexbar-accounts.py save remember the signed-in account codexbar-accounts.py switch make a saved account the active one codexbar-accounts.py usage list plus each account's weekly quota + codexbar-accounts.py app-running exit 0 when the ChatGPT desktop app is open + codexbar-accounts.py restart-app close and reopen the ChatGPT desktop app Running Codex sessions keep the account they started with; restart them. """ @@ -18,9 +20,11 @@ import json import os import pathlib +import signal import subprocess import sys import tempfile +import time from typing import Any @@ -146,6 +150,42 @@ def switch(target: str) -> tuple[bool, str]: return True, f"Switched Codex to {wanted['email']}. Restart running Codex sessions to use it." +APP_BINARY = "/usr/lib/chatgpt/ChatGPT" + + +def app_pids() -> list[int]: + """Main process of the ChatGPT desktop app (children carry --type=).""" + pids = [] + for entry in pathlib.Path("/proc").iterdir(): + if not entry.name.isdigit(): + continue + try: + argv = (entry / "cmdline").read_bytes().split(b"\0") + except OSError: + continue + if argv and argv[0].decode(errors="replace") == APP_BINARY and not any(a.startswith(b"--type=") for a in argv): + pids.append(int(entry.name)) + return pids + + +def restart_app() -> tuple[bool, str]: + """The desktop app keeps the account it started with, so a switch needs a relaunch.""" + pids = app_pids() + for pid in pids: + try: + os.kill(pid, signal.SIGTERM) + except OSError: + pass + deadline = time.monotonic() + 10 + while app_pids() and time.monotonic() < deadline: + time.sleep(0.2) + if app_pids(): + return False, "ChatGPT did not close; quit it and open it again." + subprocess.Popen(["chatgpt"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, start_new_session=True) + return True, "ChatGPT restarted." if pids else "ChatGPT opened." + + def usage_for(home: pathlib.Path, cli: str) -> dict[str, Any]: environment = {**os.environ, "CODEX_HOME": str(home)} try: @@ -207,6 +247,12 @@ def main(arguments: list[str]) -> int: ok, message = switch(arguments[1]) print(json.dumps({"ok": ok, "message": message})) return 0 if ok else 1 + if command == "app-running": + return 0 if app_pids() else 1 + if command == "restart-app": + ok, message = restart_app() + print(message) + return 0 if ok else 1 if command == "usage": cli = str(pathlib.Path.home() / ".local/bin/codexbar") print(json.dumps({"accounts": usage_report(cli)})) diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 2fa426602..673ff54c6 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -606,12 +606,35 @@ namespace MSeries { run_action ("--refresh"); refresh_codex_accounts.begin (); read_snapshot.begin (); + if (arguments[0] == "switch" && process.get_successful ()) { + offer_chatgpt_restart.begin (); + } }); } catch (Error e) { subtitle_label.set_text ("Could not switch account: " + e.message); } } + /* The ChatGPT desktop app keeps the account it started with; only a relaunch picks up the new sign-in. */ + private async void offer_chatgpt_restart () { + try { + var check = new GLib.Subprocess.newv ({ "python3", helper_path ("codexbar-accounts.py"), "app-running" }, + SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE); + yield check.wait_async (null); + if (!check.get_successful ()) { + return; + } + } catch (Error e) { + return; + } + ask_to_save ("Restart ChatGPT?", + "The account is switched, but the open ChatGPT app keeps the account it started with. Restart it to use the new account; unsaved drafts in it may be lost.", + "Restart ChatGPT", () => { + run_saver.begin ({ "python3", helper_path ("codexbar-accounts.py"), "restart-app" }, + "ChatGPT restarted", "Couldn't restart ChatGPT", true); + }); + } + private void add_codex_account () { ask_to_save ("Add a ChatGPT account", "Your current account is already remembered. Sign in to the other account in Codex, then choose Save account. You can switch back from the panel at any time.", From cdc0682a977f7e2a459b9e0c44b19b200c2278ca Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 11:44:56 -0400 Subject: [PATCH 004/798] CodexBar: name the new account in the post-switch restart prompt Co-Authored-By: Claude Sonnet 5.5 --- mseries/codexbar-wingpanel/Indicator.vala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 673ff54c6..193d25b61 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -607,7 +607,7 @@ namespace MSeries { refresh_codex_accounts.begin (); read_snapshot.begin (); if (arguments[0] == "switch" && process.get_successful ()) { - offer_chatgpt_restart.begin (); + offer_chatgpt_restart.begin (arguments[1].split ("|")[0]); } }); } catch (Error e) { @@ -616,7 +616,7 @@ namespace MSeries { } /* The ChatGPT desktop app keeps the account it started with; only a relaunch picks up the new sign-in. */ - private async void offer_chatgpt_restart () { + private async void offer_chatgpt_restart (string email) { try { var check = new GLib.Subprocess.newv ({ "python3", helper_path ("codexbar-accounts.py"), "app-running" }, SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE); @@ -627,8 +627,8 @@ namespace MSeries { } catch (Error e) { return; } - ask_to_save ("Restart ChatGPT?", - "The account is switched, but the open ChatGPT app keeps the account it started with. Restart it to use the new account; unsaved drafts in it may be lost.", + ask_to_save ("Switched Codex to " + email, + "The open ChatGPT window is still signed in as the previous account until it restarts. Restart it now to use " + email + "; unsaved drafts in it may be lost.", "Restart ChatGPT", () => { run_saver.begin ({ "python3", helper_path ("codexbar-accounts.py"), "restart-app" }, "ChatGPT restarted", "Couldn't restart ChatGPT", true); From 599838020754c7ff40d93d2d0be8ae2a947d2593 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 11:52:34 -0400 Subject: [PATCH 005/798] Wingpanel: pair of eyes while the screen is being shared Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/Indicator.vala | 85 +++++++++++++++++++ mseries/share-eyes-wingpanel/ShareState.vala | 52 ++++++++++++ mseries/share-eyes-wingpanel/icons/eyes.svg | 1 + mseries/share-eyes-wingpanel/meson.build | 32 +++++++ .../probe/render-probe.vala | 26 ++++++ .../share-eyes-wingpanel/probe/run-probe.sh | 16 ++++ .../test-share-state.vala | 15 ++++ 7 files changed, 227 insertions(+) create mode 100644 mseries/share-eyes-wingpanel/Indicator.vala create mode 100644 mseries/share-eyes-wingpanel/ShareState.vala create mode 100644 mseries/share-eyes-wingpanel/icons/eyes.svg create mode 100644 mseries/share-eyes-wingpanel/meson.build create mode 100644 mseries/share-eyes-wingpanel/probe/render-probe.vala create mode 100755 mseries/share-eyes-wingpanel/probe/run-probe.sh create mode 100644 mseries/share-eyes-wingpanel/test-share-state.vala diff --git a/mseries/share-eyes-wingpanel/Indicator.vala b/mseries/share-eyes-wingpanel/Indicator.vala new file mode 100644 index 000000000..5a35a536b --- /dev/null +++ b/mseries/share-eyes-wingpanel/Indicator.vala @@ -0,0 +1,85 @@ +/* A pair of eyes in Wingpanel while this machine's screen is being shared (see ShareState for the file contract). */ +using Gtk; +using Wingpanel; + +namespace MSeries { + public class ShareEyesIndicator : Wingpanel.Indicator { + private Gtk.Image eyes; + private Gtk.Box menu; + private Gtk.Label status_label; + private ShareState state = new ShareState (); + private string path = ShareState.default_path (); + private uint poll_source = 0; + + public ShareEyesIndicator () { + GLib.Object (code_name: "mseries-share-eyes"); + } + + construct { + eyes = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 18 }; + eyes.set_tooltip_text (state.tooltip ()); + this.visible = false; + + status_label = new Gtk.Label ("") { halign = Gtk.Align.START, margin_bottom = 4 }; + var open = new Gtk.Button.with_label ("Open the share page to stop") { halign = Gtk.Align.FILL }; + open.clicked.connect (() => { + try { + GLib.AppInfo.launch_default_for_uri ("http://127.0.0.1:8799/share", null); + } catch (GLib.Error e) { + warning ("could not open the share page: %s", e.message); + } + }); + menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 4) { + margin_top = 8, margin_bottom = 8, margin_start = 12, margin_end = 12, width_request = 240 + }; + menu.append (status_label); + menu.append (open); + + refresh (); + poll_source = GLib.Timeout.add_seconds (2, () => { refresh (); return GLib.Source.CONTINUE; }); + } + + ~ShareEyesIndicator () { + if (poll_source != 0) { + GLib.Source.remove (poll_source); + } + } + + private void refresh () { + string? text = null; + try { + string contents; + GLib.FileUtils.get_contents (path, out contents); + text = contents; + } catch (GLib.Error e) { + text = null; + } + state.parse (text, GLib.get_real_time () / 1000000); + this.visible = state.live; + eyes.set_tooltip_text (state.tooltip ()); + status_label.set_text (state.tooltip ()); + } + + public override Gtk.Widget get_display_widget () { + return eyes; + } + + public override Gtk.Widget? get_widget () { + return menu; + } + + public override void opened () { + refresh (); + } + + public override void closed () { + } + } +} + +public Wingpanel.Indicator? get_indicator (Module module, Wingpanel.ServerType server_type) { + if (server_type != Wingpanel.ServerType.SESSION) { + return null; + } + return new MSeries.ShareEyesIndicator (); +} diff --git a/mseries/share-eyes-wingpanel/ShareState.vala b/mseries/share-eyes-wingpanel/ShareState.vala new file mode 100644 index 000000000..53399f917 --- /dev/null +++ b/mseries/share-eyes-wingpanel/ShareState.vala @@ -0,0 +1,52 @@ +/* Reads the share-state file the screen-share launcher keeps up to date. + * Contract: $XDG_RUNTIME_DIR/rupert-share/state.json = {"live":bool,"viewers":int,"updated":unix_seconds}. + * A file not refreshed for STALE_SECS counts as "not sharing", so a crashed launcher never leaves eyes stuck on. */ +namespace MSeries { + public class ShareState : GLib.Object { + public const int64 STALE_SECS = 15; + public bool live { get; private set; default = false; } + public int viewers { get; private set; default = 0; } + + public static string default_path () { + return GLib.Path.build_filename (GLib.Environment.get_user_runtime_dir (), "rupert-share", "state.json"); + } + + /* Returns true when live/viewers changed. */ + public bool parse (string? text, int64 now) { + bool new_live = false; + int new_viewers = 0; + if (text != null) { + try { + var parser = new Json.Parser (); + parser.load_from_data (text); + var root = parser.get_root (); + if (root != null && root.get_node_type () == Json.NodeType.OBJECT) { + var o = root.get_object (); + int64 updated = o.has_member ("updated") ? o.get_int_member ("updated") : 0; + if (o.has_member ("live") && o.get_boolean_member ("live") && now - updated <= STALE_SECS) { + new_live = true; + if (o.has_member ("viewers")) { + new_viewers = (int) o.get_int_member ("viewers").clamp (0, 99); + } + } + } + } catch (GLib.Error e) { + new_live = false; + } + } + bool changed = new_live != live || new_viewers != viewers; + live = new_live; + viewers = new_viewers; + return changed; + } + + public string tooltip () { + if (!live) { + return "Not sharing your screen"; + } + return viewers == 1 ? "Sharing your screen · 1 person watching" + : viewers == 0 ? "Sharing your screen · nobody watching yet" + : "Sharing your screen · %d people watching".printf (viewers); + } + } +} diff --git a/mseries/share-eyes-wingpanel/icons/eyes.svg b/mseries/share-eyes-wingpanel/icons/eyes.svg new file mode 100644 index 000000000..3ed916d71 --- /dev/null +++ b/mseries/share-eyes-wingpanel/icons/eyes.svg @@ -0,0 +1 @@ + diff --git a/mseries/share-eyes-wingpanel/meson.build b/mseries/share-eyes-wingpanel/meson.build new file mode 100644 index 000000000..100ee57b8 --- /dev/null +++ b/mseries/share-eyes-wingpanel/meson.build @@ -0,0 +1,32 @@ +project('share-eyes-wingpanel', 'vala', version: '0.1.0', meson_version: '>= 0.59.0') + +libwingpanel_dep = dependency('wingpanel-9') +granite_dep = dependency('granite-7', version: '>= 7.7.0') +json_dep = dependency('json-glib-1.0') +indicators_dir = libwingpanel_dep.get_variable(pkgconfig: 'indicatorsdir') + +shared_module( + 'shareeyes', + 'Indicator.vala', + 'ShareState.vala', + dependencies: [libwingpanel_dep, granite_dep, json_dep], + install: true, + install_dir: indicators_dir, +) + +share_state_test = executable('test-share-state', 'ShareState.vala', 'test-share-state.vala', dependencies: json_dep) +test('share-state', share_state_test) + +install_data( + 'icons/eyes.svg', + install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps'), + rename: 'mseries-share-eyes-symbolic.svg', +) + +# Headless render probe (not installed): see probe/run-probe.sh. +executable( + 'render-probe', + 'Indicator.vala', 'ShareState.vala', 'probe/render-probe.vala', + dependencies: [libwingpanel_dep, granite_dep, json_dep], + build_by_default: false, +) diff --git a/mseries/share-eyes-wingpanel/probe/render-probe.vala b/mseries/share-eyes-wingpanel/probe/render-probe.vala new file mode 100644 index 000000000..9b71aceee --- /dev/null +++ b/mseries/share-eyes-wingpanel/probe/render-probe.vala @@ -0,0 +1,26 @@ +/* Headless: shows the eyes indicator widget with a live state, snapshots it to a PNG, and prints visibility/tooltip. */ +int main (string[] args) { + Gtk.init (); + var ind = new MSeries.ShareEyesIndicator (); + print ("visible-before=%s\n", ind.visible.to_string ()); + var w = new Gtk.Window (); + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { margin_start = 20, margin_end = 20, margin_top = 10, margin_bottom = 10 }; + box.append (ind.get_display_widget ()); + w.child = box; + w.present (); + var loop = new MainLoop (); + GLib.Timeout.add (3500, () => { + print ("visible-after=%s tooltip=%s\n", ind.visible.to_string (), ind.get_display_widget ().get_tooltip_text ()); + var paintable = new Gtk.WidgetPaintable (box); + var snap = new Gtk.Snapshot (); + paintable.snapshot (snap, w.get_width (), w.get_height ()); + var node = snap.free_to_node (); + var r = w.get_native ().get_renderer (); + var tex = r.render_texture (node, null); + tex.save_to_png (args[1]); + loop.quit (); + return false; + }); + loop.run (); + return 0; +} diff --git a/mseries/share-eyes-wingpanel/probe/run-probe.sh b/mseries/share-eyes-wingpanel/probe/run-probe.sh new file mode 100755 index 000000000..f7d639853 --- /dev/null +++ b/mseries/share-eyes-wingpanel/probe/run-probe.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Render the eyes headlessly (Xvfb + cairo + fake HOME/runtime dir/icon dir). Usage: run-probe.sh BUILD_DIR OUT_DIR +set -euo pipefail +build=$(realpath "$1"); out=$2; here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +mkdir -p "$out"; home=$(mktemp -d); trap 'rm -rf "$home" 2>/dev/null || true' EXIT +mkdir -p "$home/run/rupert-share" "$home/share/icons/hicolor/scalable/apps" +cp "$here/icons/eyes.svg" "$home/share/icons/hicolor/scalable/apps/mseries-share-eyes-symbolic.svg" +run() { # name, state-json + printf '%s' "$2" > "$home/run/rupert-share/state.json" + env -i PATH="$PATH" HOME="$home" XDG_RUNTIME_DIR="$home/run" XDG_DATA_DIRS="$home/share:/usr/share" LANG=C.UTF-8 \ + GTK_THEME=io.elementary.stylesheet.blueberry GSK_RENDERER=cairo GDK_BACKEND=x11 \ + xvfb-run -a -s "-screen 0 400x200x24" "$build/render-probe" "$out/$1.png" +} +now=$(date +%s) +run live "{\"live\":true,\"viewers\":1,\"updated\":$now}" +run stale "{\"live\":true,\"viewers\":1,\"updated\":$((now-60))}" diff --git a/mseries/share-eyes-wingpanel/test-share-state.vala b/mseries/share-eyes-wingpanel/test-share-state.vala new file mode 100644 index 000000000..2d89b3afa --- /dev/null +++ b/mseries/share-eyes-wingpanel/test-share-state.vala @@ -0,0 +1,15 @@ +void main () { + var s = new MSeries.ShareState (); + int64 now = 1000; + assert (!s.parse (null, now) && !s.live); + assert (s.parse ("{\"live\":true,\"viewers\":2,\"updated\":995}", now) && s.live && s.viewers == 2); + assert (!s.parse ("{\"live\":true,\"viewers\":2,\"updated\":999}", now)); + assert (s.parse ("{\"live\":true,\"viewers\":2,\"updated\":900}", now) && !s.live); /* stale */ + assert (!s.parse ("not json", now) && !s.live); + assert (!s.parse ("{\"live\":false,\"viewers\":3,\"updated\":1000}", now) && !s.live && s.viewers == 0); + s.parse ("{\"live\":true,\"viewers\":1,\"updated\":1000}", now); + assert (s.tooltip () == "Sharing your screen · 1 person watching"); + s.parse ("{\"live\":true,\"viewers\":0,\"updated\":1000}", now); + assert (s.tooltip ().contains ("nobody")); + print ("share-state: ok\n"); +} From ec0d404c1081a358e1967c704263963b463ec0e7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 11:59:12 -0400 Subject: [PATCH 006/798] Share eyes: whole interaction in the popover (login copy, viewer link, approve/remove watchers, stop) Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/Indicator.vala | 182 +++++++++++++++--- mseries/share-eyes-wingpanel/ShareClient.vala | 143 ++++++++++++++ mseries/share-eyes-wingpanel/ShareState.vala | 54 ++++-- mseries/share-eyes-wingpanel/meson.build | 8 +- .../probe/render-probe.vala | 15 +- .../share-eyes-wingpanel/probe/run-probe.sh | 32 +-- .../test-share-state.vala | 20 +- 7 files changed, 386 insertions(+), 68 deletions(-) create mode 100644 mseries/share-eyes-wingpanel/ShareClient.vala diff --git a/mseries/share-eyes-wingpanel/Indicator.vala b/mseries/share-eyes-wingpanel/Indicator.vala index 5a35a536b..6e6cc3290 100644 --- a/mseries/share-eyes-wingpanel/Indicator.vala +++ b/mseries/share-eyes-wingpanel/Indicator.vala @@ -1,4 +1,5 @@ -/* A pair of eyes in Wingpanel while this machine's screen is being shared (see ShareState for the file contract). */ +/* Screen share in Wingpanel: a pair of eyes whose popover is the whole interaction + * (login to copy, viewer link, approve or remove watchers, stop). See ShareState for the launcher contract. */ using Gtk; using Wingpanel; @@ -6,10 +7,14 @@ namespace MSeries { public class ShareEyesIndicator : Wingpanel.Indicator { private Gtk.Image eyes; private Gtk.Box menu; - private Gtk.Label status_label; + private Gtk.Box content; private ShareState state = new ShareState (); + private ShareClient client = new ShareClient (); private string path = ShareState.default_path (); + private string last_digest = ""; + private string toast = ""; private uint poll_source = 0; + private bool busy = false; public ShareEyesIndicator () { GLib.Object (code_name: "mseries-share-eyes"); @@ -20,23 +25,14 @@ namespace MSeries { eyes.set_tooltip_text (state.tooltip ()); this.visible = false; - status_label = new Gtk.Label ("") { halign = Gtk.Align.START, margin_bottom = 4 }; - var open = new Gtk.Button.with_label ("Open the share page to stop") { halign = Gtk.Align.FILL }; - open.clicked.connect (() => { - try { - GLib.AppInfo.launch_default_for_uri ("http://127.0.0.1:8799/share", null); - } catch (GLib.Error e) { - warning ("could not open the share page: %s", e.message); - } - }); - menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 4) { - margin_top = 8, margin_bottom = 8, margin_start = 12, margin_end = 12, width_request = 240 + content = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { + margin_top = 10, margin_bottom = 10, margin_start = 12, margin_end = 12, width_request = 320 }; - menu.append (status_label); - menu.append (open); + menu.append (content); - refresh (); - poll_source = GLib.Timeout.add_seconds (2, () => { refresh (); return GLib.Source.CONTINUE; }); + tick.begin (); + poll_source = GLib.Timeout.add_seconds (2, () => { tick.begin (); return GLib.Source.CONTINUE; }); } ~ShareEyesIndicator () { @@ -45,7 +41,9 @@ namespace MSeries { } } - private void refresh () { + private async void tick () { + if (busy) return; + busy = true; string? text = null; try { string contents; @@ -55,9 +53,148 @@ namespace MSeries { text = null; } state.parse (text, GLib.get_real_time () / 1000000); - this.visible = state.live; - eyes.set_tooltip_text (state.tooltip ()); - status_label.set_text (state.tooltip ()); + this.visible = state.running; + if (state.running) { + client.configure (state.local_url, state.email, state.password); + yield client.refresh (); + } + // Watchers come from the server itself; the file only says whether a launcher is running. + int watching = 0; + foreach (var v in client.viewers) if (v.state == "admitted") watching++; + int waiting = 0; + foreach (var v in client.viewers) if (v.state == "pending") waiting++; + string tip = !state.running ? state.tooltip () + : waiting > 0 ? "Someone wants to watch your screen" + : client.live ? (watching == 1 ? "Sharing your screen · 1 person watching" + : watching == 0 ? "Sharing your screen · nobody watching yet" + : "Sharing your screen · %d people watching".printf (watching)) + : "Screen share ready · not sharing yet"; + eyes.set_tooltip_text (tip); + if (waiting > 0) eyes.add_css_class ("accent"); else eyes.remove_css_class ("accent"); + var digest = state.running.to_string () + client.digest () + state.password + toast; + if (digest != last_digest) { + last_digest = digest; + rebuild (tip); + } + busy = false; + } + + private void copy (string text, string what) { + eyes.get_clipboard ().set_text (text); + toast = what + " copied"; + last_digest = ""; + GLib.Timeout.add_seconds (3, () => { toast = ""; last_digest = ""; return GLib.Source.REMOVE; }); + tick.begin (); + } + + private Gtk.Widget header (string text) { + var l = new Gtk.Label (text) { halign = Gtk.Align.START, margin_top = 8 }; + l.add_css_class ("h4"); + return l; + } + + private Gtk.Widget dim (string text) { + var l = new Gtk.Label (text) { halign = Gtk.Align.START, wrap = true, xalign = 0 }; + l.add_css_class ("dim-label"); + return l; + } + + private Gtk.Widget field_row (string label, string value, bool secret) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + var name = new Gtk.Label (label) { halign = Gtk.Align.START, width_chars = 9, xalign = 0 }; + name.add_css_class ("dim-label"); + var val = new Gtk.Label (value) { halign = Gtk.Align.START, hexpand = true, xalign = 0, selectable = true, ellipsize = Pango.EllipsizeMode.MIDDLE }; + val.add_css_class ("monospace"); + row.append (name); + row.append (val); + var cp = new Gtk.Button.from_icon_name ("edit-copy-symbolic") { tooltip_text = "Copy " + label.down () }; + cp.add_css_class ("flat"); + cp.clicked.connect (() => copy (value, label)); + row.append (cp); + return row; + } + + private void rebuild (string tip) { + Gtk.Widget? child; + while ((child = content.get_first_child ()) != null) content.remove (child); + + var title = new Gtk.Label (state.running ? (client.live ? "Sharing your screen" : "Screen share") : "Screen share is off") { halign = Gtk.Align.START }; + title.add_css_class ("title-3"); + content.append (title); + content.append (dim (state.running ? tip : "Start the demo with core/scripts/share-demo.sh.")); + if (!state.running) return; + if (!client.reachable) { + content.append (dim ("The share server is not answering.")); + return; + } + + // 1. Start / stop + if (!client.live) { + content.append (dim ("Open the share page, sign in with the login below and click Start sharing. The browser asks which window to share.")); + var open = new Gtk.Button.with_label ("Open share page"); + open.add_css_class ("suggested-action"); + open.clicked.connect (() => { + try { GLib.AppInfo.launch_default_for_uri (state.share_page, null); } catch (GLib.Error e) { warning ("%s", e.message); } + }); + content.append (open); + } + + // 2. Login + content.append (header ("Login")); + content.append (field_row ("Username", state.email, false)); + content.append (field_row ("Password", state.password, true)); + var copy_login = new Gtk.Button.with_label ("Copy login"); + copy_login.clicked.connect (() => copy (state.login_text (), "Login")); + content.append (copy_login); + + // 3. Link for the watcher + content.append (header ("Link for your viewer")); + var link = client.viewer_link (state.viewer_base); + if (link != null) { + content.append (field_row ("Link", link, false)); + content.append (dim ("Valid until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%a %H:%M") + + ". Anyone with it can ask to watch; you still approve them.")); + } else { + var mk = new Gtk.Button.with_label ("Create viewer link"); + mk.clicked.connect (() => { client.create_link.begin ((o, r) => { client.create_link.end (r); last_digest = ""; tick.begin (); }); }); + content.append (mk); + } + + // 4. Watchers + content.append (header ("Watchers")); + if (client.viewers.length == 0) { + content.append (dim ("Nobody yet.")); + } + foreach (var v in client.viewers) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + var who = new Gtk.Label (v.state == "pending" ? "Wants to watch · " + v.ip : "Watching · " + v.ip) + { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }; + row.append (who); + var vid = v.vid; + if (v.state == "pending") { + var deny = new Gtk.Button.with_label ("Deny"); + deny.clicked.connect (() => client.viewer_action.begin (vid, "deny", (o, r) => { client.viewer_action.end (r); last_digest = ""; tick.begin (); })); + var allow = new Gtk.Button.with_label ("Allow"); + allow.add_css_class ("suggested-action"); + allow.clicked.connect (() => client.viewer_action.begin (vid, "admit", (o, r) => { client.viewer_action.end (r); last_digest = ""; tick.begin (); })); + row.append (deny); + row.append (allow); + } else { + var rm = new Gtk.Button.with_label ("Remove"); + rm.add_css_class ("destructive-action"); + rm.clicked.connect (() => client.viewer_action.begin (vid, "kick", (o, r) => { client.viewer_action.end (r); last_digest = ""; tick.begin (); })); + row.append (rm); + } + content.append (row); + } + + if (client.live) { + var stop = new Gtk.Button.with_label ("Stop sharing") { margin_top = 8 }; + stop.add_css_class ("destructive-action"); + stop.clicked.connect (() => client.stop.begin ((o, r) => { client.stop.end (r); last_digest = ""; tick.begin (); })); + content.append (stop); + } + if (toast != "") content.append (dim (toast)); } public override Gtk.Widget get_display_widget () { @@ -69,7 +206,8 @@ namespace MSeries { } public override void opened () { - refresh (); + last_digest = ""; + tick.begin (); } public override void closed () { diff --git a/mseries/share-eyes-wingpanel/ShareClient.vala b/mseries/share-eyes-wingpanel/ShareClient.vala new file mode 100644 index 000000000..6785fc87d --- /dev/null +++ b/mseries/share-eyes-wingpanel/ShareClient.vala @@ -0,0 +1,143 @@ +/* Talks to the local screen-share server as its owner (cookie session, same CSRF header the owner page sends). */ +namespace MSeries { + public class ViewerInfo : GLib.Object { + public string vid = ""; + public string state = ""; + public string ip = ""; + } + + public class ShareClient : GLib.Object { + private Soup.Session session = new Soup.Session () { timeout = 4 }; + private bool logged_in = false; + public string base_url = ""; + public string email = ""; + public string password = ""; + public ViewerInfo[] viewers = {}; + public bool live = false; + public bool reachable = false; + public string? link_id = null; + public string? link_token = null; + public int64 link_expires = 0; + + construct { + session.add_feature (new Soup.CookieJar ()); + } + + public void configure (string url, string mail, string pw) { + if (url != base_url || mail != email || pw != password) { + logged_in = false; + link_id = null; + link_token = null; + } + base_url = url; email = mail; password = pw; + } + + private async Json.Object? call (string method, string path, string? body, bool retry = true) { + if (base_url == "") return null; + var msg = new Soup.Message (method, base_url + path); + msg.request_headers.append ("x-rupert-share", "1"); + if (body != null) { + msg.set_request_body_from_bytes ("application/json", new GLib.Bytes (body.data)); + } + try { + var bytes = yield session.send_and_read_async (msg, GLib.Priority.DEFAULT, null); + if (msg.status_code == 401 && retry && path != "/auth/login") { + logged_in = false; + if (yield login ()) return yield call (method, path, body, false); + return null; + } + if (msg.status_code < 200 || msg.status_code >= 300) return null; + var text = (string) bytes.get_data (); + var parser = new Json.Parser (); + parser.load_from_data (text != null && text.length > 0 ? text : "{}"); + var root = parser.get_root (); + return root != null && root.get_node_type () == Json.NodeType.OBJECT ? root.get_object ().ref () : new Json.Object (); + } catch (GLib.Error e) { + return null; + } + } + + private async bool login () { + var b = new Json.Builder (); + b.begin_object ().set_member_name ("email").add_string_value (email) + .set_member_name ("password").add_string_value (password).end_object (); + var gen = new Json.Generator (); + gen.root = b.get_root (); + var r = yield call ("POST", "/auth/login", gen.to_data (null), false); + logged_in = r != null; + return logged_in; + } + + /* Fetch /status; returns true when what the popover shows changed. */ + public async bool refresh () { + if (!logged_in && !(yield login ())) { + bool changed = reachable; + reachable = false; viewers = {}; live = false; + return changed; + } + var s = yield call ("GET", "/api/v1/share/status", null); + if (s == null) { + bool changed = reachable; + reachable = false; viewers = {}; live = false; + return changed; + } + var before = digest (); + reachable = true; + live = s.has_member ("live") && s.get_boolean_member ("live"); + ViewerInfo[] vs = {}; + if (s.has_member ("viewers")) { + s.get_array_member ("viewers").foreach_element ((a, i, n) => { + var o = n.get_object (); + var v = new ViewerInfo (); + v.vid = o.get_string_member ("vid"); + v.state = o.get_string_member ("state"); + v.ip = o.has_member ("ip") ? o.get_string_member ("ip") : ""; + vs += v; + }); + } + viewers = vs; + bool link_alive = false; + if (s.has_member ("links")) { + s.get_array_member ("links").foreach_element ((a, i, n) => { + var o = n.get_object (); + if (link_id != null && o.get_string_member ("id") == link_id) { + link_alive = true; + link_expires = o.get_int_member ("expires_at"); + } + }); + } + if (!link_alive) { link_id = null; link_token = null; } + return before != digest (); + } + + public string digest () { + var sb = new StringBuilder (); + sb.append ("%s|%s|%s|".printf (reachable.to_string (), live.to_string (), link_id ?? "")); + foreach (var v in viewers) sb.append ("%s:%s;".printf (v.vid, v.state)); + return sb.str; + } + + public string? viewer_link (string base_for_viewers) { + return link_token == null ? null : base_for_viewers + "/v#" + link_token; + } + + public async bool create_link () { + var r = yield call ("POST", "/api/v1/share/links", "{}"); + if (r == null || !r.has_member ("token")) return false; + link_id = r.get_string_member ("id"); + link_token = r.get_string_member ("token"); + yield refresh (); + return true; + } + + public async void viewer_action (string vid, string action) { + yield call ("POST", "/api/v1/share/viewers/" + GLib.Uri.escape_string (vid, null, true) + "/" + action, "{}"); + yield refresh (); + } + + public async void stop () { + yield call ("POST", "/api/v1/share/stop", "{}"); + yield refresh (); + } + } +} diff --git a/mseries/share-eyes-wingpanel/ShareState.vala b/mseries/share-eyes-wingpanel/ShareState.vala index 53399f917..b12168973 100644 --- a/mseries/share-eyes-wingpanel/ShareState.vala +++ b/mseries/share-eyes-wingpanel/ShareState.vala @@ -1,20 +1,31 @@ -/* Reads the share-state file the screen-share launcher keeps up to date. - * Contract: $XDG_RUNTIME_DIR/rupert-share/state.json = {"live":bool,"viewers":int,"updated":unix_seconds}. - * A file not refreshed for STALE_SECS counts as "not sharing", so a crashed launcher never leaves eyes stuck on. */ +/* Reads the file the screen-share launcher keeps up to date (it only exists while the launcher runs, mode 0600, in a 0700 dir). + * Contract: $XDG_RUNTIME_DIR/rupert-share/state.json = + * {"live":bool,"viewers":int,"updated":unix_seconds,"port":int,"email":str,"password":str,"public_url":str} + * A file not refreshed for STALE_SECS counts as "no launcher running", so a crashed launcher never leaves the eyes stuck on. */ namespace MSeries { public class ShareState : GLib.Object { public const int64 STALE_SECS = 15; + public bool running { get; private set; default = false; } public bool live { get; private set; default = false; } public int viewers { get; private set; default = 0; } + public int port { get; private set; default = 0; } + public string email { get; private set; default = ""; } + public string password { get; private set; default = ""; } + public string public_url { get; private set; default = ""; } public static string default_path () { return GLib.Path.build_filename (GLib.Environment.get_user_runtime_dir (), "rupert-share", "state.json"); } - /* Returns true when live/viewers changed. */ + public string local_url { owned get { return "http://127.0.0.1:%d".printf (port); } } + public string share_page { owned get { return local_url + "/share"; } } + public string viewer_base { owned get { return public_url != "" ? public_url : local_url; } } + + /* Returns true when anything the indicator shows changed. */ public bool parse (string? text, int64 now) { - bool new_live = false; - int new_viewers = 0; + bool n_running = false, n_live = false; + int n_viewers = 0, n_port = 0; + string n_email = "", n_password = "", n_public = ""; if (text != null) { try { var parser = new Json.Parser (); @@ -23,30 +34,43 @@ namespace MSeries { if (root != null && root.get_node_type () == Json.NodeType.OBJECT) { var o = root.get_object (); int64 updated = o.has_member ("updated") ? o.get_int_member ("updated") : 0; - if (o.has_member ("live") && o.get_boolean_member ("live") && now - updated <= STALE_SECS) { - new_live = true; - if (o.has_member ("viewers")) { - new_viewers = (int) o.get_int_member ("viewers").clamp (0, 99); + if (now - updated <= STALE_SECS) { + n_running = true; + n_live = o.has_member ("live") && o.get_boolean_member ("live"); + if (n_live && o.has_member ("viewers")) { + n_viewers = (int) o.get_int_member ("viewers").clamp (0, 99); } + if (o.has_member ("port")) n_port = (int) o.get_int_member ("port"); + if (o.has_member ("email")) n_email = o.get_string_member ("email"); + if (o.has_member ("password")) n_password = o.get_string_member ("password"); + if (o.has_member ("public_url")) n_public = o.get_string_member ("public_url"); } } } catch (GLib.Error e) { - new_live = false; + n_running = false; } } - bool changed = new_live != live || new_viewers != viewers; - live = new_live; - viewers = new_viewers; + bool changed = n_running != running || n_live != live || n_viewers != viewers || n_port != port + || n_email != email || n_password != password || n_public != public_url; + running = n_running; live = n_live; viewers = n_viewers; port = n_port; + email = n_email; password = n_password; public_url = n_public; return changed; } public string tooltip () { + if (!running) { + return "Screen share is not running"; + } if (!live) { - return "Not sharing your screen"; + return "Screen share ready · not sharing yet"; } return viewers == 1 ? "Sharing your screen · 1 person watching" : viewers == 0 ? "Sharing your screen · nobody watching yet" : "Sharing your screen · %d people watching".printf (viewers); } + + public string login_text () { + return "Screen share (this computer)\nOpen: %s\nUsername: %s\nPassword: %s\n".printf (share_page, email, password); + } } } diff --git a/mseries/share-eyes-wingpanel/meson.build b/mseries/share-eyes-wingpanel/meson.build index 100ee57b8..b2e5d37e7 100644 --- a/mseries/share-eyes-wingpanel/meson.build +++ b/mseries/share-eyes-wingpanel/meson.build @@ -3,13 +3,15 @@ project('share-eyes-wingpanel', 'vala', version: '0.1.0', meson_version: '>= 0.5 libwingpanel_dep = dependency('wingpanel-9') granite_dep = dependency('granite-7', version: '>= 7.7.0') json_dep = dependency('json-glib-1.0') +soup_dep = dependency('libsoup-3.0') indicators_dir = libwingpanel_dep.get_variable(pkgconfig: 'indicatorsdir') shared_module( 'shareeyes', 'Indicator.vala', 'ShareState.vala', - dependencies: [libwingpanel_dep, granite_dep, json_dep], + 'ShareClient.vala', + dependencies: [libwingpanel_dep, granite_dep, json_dep, soup_dep], install: true, install_dir: indicators_dir, ) @@ -26,7 +28,7 @@ install_data( # Headless render probe (not installed): see probe/run-probe.sh. executable( 'render-probe', - 'Indicator.vala', 'ShareState.vala', 'probe/render-probe.vala', - dependencies: [libwingpanel_dep, granite_dep, json_dep], + 'Indicator.vala', 'ShareState.vala', 'ShareClient.vala', 'probe/render-probe.vala', + dependencies: [libwingpanel_dep, granite_dep, json_dep, soup_dep], build_by_default: false, ) diff --git a/mseries/share-eyes-wingpanel/probe/render-probe.vala b/mseries/share-eyes-wingpanel/probe/render-probe.vala index 9b71aceee..8310e5e80 100644 --- a/mseries/share-eyes-wingpanel/probe/render-probe.vala +++ b/mseries/share-eyes-wingpanel/probe/render-probe.vala @@ -1,22 +1,21 @@ -/* Headless: shows the eyes indicator widget with a live state, snapshots it to a PNG, and prints visibility/tooltip. */ +/* Headless: shows the indicator (panel widget + popover content) and snapshots both to PNGs. args: out-prefix [create-link] */ int main (string[] args) { Gtk.init (); var ind = new MSeries.ShareEyesIndicator (); - print ("visible-before=%s\n", ind.visible.to_string ()); var w = new Gtk.Window (); - var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { margin_start = 20, margin_end = 20, margin_top = 10, margin_bottom = 10 }; + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) { margin_start = 20, margin_end = 20, margin_top = 10, margin_bottom = 10 }; box.append (ind.get_display_widget ()); + box.append (ind.get_widget ()); w.child = box; w.present (); + ind.opened (); var loop = new MainLoop (); - GLib.Timeout.add (3500, () => { - print ("visible-after=%s tooltip=%s\n", ind.visible.to_string (), ind.get_display_widget ().get_tooltip_text ()); + GLib.Timeout.add (4500, () => { + print ("visible=%s tooltip=%s\n", ind.visible.to_string (), ind.get_display_widget ().get_tooltip_text ()); var paintable = new Gtk.WidgetPaintable (box); var snap = new Gtk.Snapshot (); paintable.snapshot (snap, w.get_width (), w.get_height ()); - var node = snap.free_to_node (); - var r = w.get_native ().get_renderer (); - var tex = r.render_texture (node, null); + var tex = w.get_native ().get_renderer ().render_texture (snap.free_to_node (), null); tex.save_to_png (args[1]); loop.quit (); return false; diff --git a/mseries/share-eyes-wingpanel/probe/run-probe.sh b/mseries/share-eyes-wingpanel/probe/run-probe.sh index f7d639853..3d7547dbe 100755 --- a/mseries/share-eyes-wingpanel/probe/run-probe.sh +++ b/mseries/share-eyes-wingpanel/probe/run-probe.sh @@ -1,16 +1,24 @@ #!/bin/bash -# Render the eyes headlessly (Xvfb + cairo + fake HOME/runtime dir/icon dir). Usage: run-probe.sh BUILD_DIR OUT_DIR +# Render the eyes + popover headlessly against a REAL throwaway share server (Xvfb + cairo + private runtime dir/icons). +# Usage: run-probe.sh BUILD_DIR OUT_DIR SHARE_REPO_CORE_DIR set -euo pipefail -build=$(realpath "$1"); out=$2; here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -mkdir -p "$out"; home=$(mktemp -d); trap 'rm -rf "$home" 2>/dev/null || true' EXIT -mkdir -p "$home/run/rupert-share" "$home/share/icons/hicolor/scalable/apps" +build=$(realpath "$1"); out=$2; core=$(realpath "$3"); here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +mkdir -p "$out"; home=$(mktemp -d); run="$home/run"; mkdir -p "$run" "$home/share/icons/hicolor/scalable/apps" cp "$here/icons/eyes.svg" "$home/share/icons/hicolor/scalable/apps/mseries-share-eyes-symbolic.svg" -run() { # name, state-json - printf '%s' "$2" > "$home/run/rupert-share/state.json" - env -i PATH="$PATH" HOME="$home" XDG_RUNTIME_DIR="$home/run" XDG_DATA_DIRS="$home/share:/usr/share" LANG=C.UTF-8 \ +port=8821 +env XDG_RUNTIME_DIR="$run" "$core/scripts/share-demo.sh" --port $port >"$home/demo.out" 2>&1 & +demo=$! +cleanup() { kill -TERM "$demo" 2>/dev/null; wait "$demo" 2>/dev/null || true; rm -rf "$home" 2>/dev/null || true; } +trap cleanup EXIT +for _ in $(seq 1 120); do [ -s "$run/rupert-share/state.json" ] && break; sleep 0.5; done +probe() { env -i PATH="$PATH" HOME="$home" XDG_RUNTIME_DIR="$run" XDG_DATA_DIRS="$home/share:/usr/share" LANG=C.UTF-8 \ GTK_THEME=io.elementary.stylesheet.blueberry GSK_RENDERER=cairo GDK_BACKEND=x11 \ - xvfb-run -a -s "-screen 0 400x200x24" "$build/render-probe" "$out/$1.png" -} -now=$(date +%s) -run live "{\"live\":true,\"viewers\":1,\"updated\":$now}" -run stale "{\"live\":true,\"viewers\":1,\"updated\":$((now-60))}" + xvfb-run -a -s "-screen 0 500x700x24" "$build/render-probe" "$out/$1.png"; } +probe idle +# a viewer link and a pending watcher through the same API the popover uses +email=$(python3 -I -c "import json,sys;print(json.load(open('$run/rupert-share/state.json'))['email'])") +pass=$(python3 -I -c "import json,sys;print(json.load(open('$run/rupert-share/state.json'))['password'])") +jar="$home/jar"; curl -fs -c "$jar" "http://127.0.0.1:$port/auth/login" -H 'content-type: application/json' -d "{\"email\":\"$email\",\"password\":\"$pass\"}" >/dev/null +tok=$(curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d '{}' "http://127.0.0.1:$port/api/v1/share/links" | python3 -I -c "import json,sys;print(json.load(sys.stdin)['token'])") +probe with-link +echo "tok=$tok" >"$home/tok" diff --git a/mseries/share-eyes-wingpanel/test-share-state.vala b/mseries/share-eyes-wingpanel/test-share-state.vala index 2d89b3afa..c873b3bfd 100644 --- a/mseries/share-eyes-wingpanel/test-share-state.vala +++ b/mseries/share-eyes-wingpanel/test-share-state.vala @@ -1,15 +1,19 @@ void main () { var s = new MSeries.ShareState (); int64 now = 1000; - assert (!s.parse (null, now) && !s.live); - assert (s.parse ("{\"live\":true,\"viewers\":2,\"updated\":995}", now) && s.live && s.viewers == 2); - assert (!s.parse ("{\"live\":true,\"viewers\":2,\"updated\":999}", now)); - assert (s.parse ("{\"live\":true,\"viewers\":2,\"updated\":900}", now) && !s.live); /* stale */ - assert (!s.parse ("not json", now) && !s.live); - assert (!s.parse ("{\"live\":false,\"viewers\":3,\"updated\":1000}", now) && !s.live && s.viewers == 0); - s.parse ("{\"live\":true,\"viewers\":1,\"updated\":1000}", now); + assert (!s.parse (null, now) && !s.running); + string full = "{\"live\":true,\"viewers\":2,\"updated\":995,\"port\":8799,\"email\":\"a@b\",\"password\":\"pw\",\"public_url\":\"http://h:8799\"}"; + assert (s.parse (full, now) && s.running && s.live && s.viewers == 2 && s.port == 8799); + assert (s.share_page == "http://127.0.0.1:8799/share" && s.viewer_base == "http://h:8799"); + assert (s.login_text () == "Screen share (this computer)\nOpen: http://127.0.0.1:8799/share\nUsername: a@b\nPassword: pw\n"); + assert (!s.parse (full, now)); + assert (s.parse (full, now + 100) && !s.running && !s.live); /* stale: launcher gone */ + assert (!s.parse ("not json", now) && !s.running); + s.parse ("{\"live\":false,\"viewers\":3,\"updated\":1000,\"port\":1}", now); + assert (s.running && !s.live && s.viewers == 0 && s.tooltip ().contains ("not sharing yet")); + s.parse ("{\"live\":true,\"viewers\":1,\"updated\":1000,\"port\":1}", now); assert (s.tooltip () == "Sharing your screen · 1 person watching"); - s.parse ("{\"live\":true,\"viewers\":0,\"updated\":1000}", now); + s.parse ("{\"live\":true,\"viewers\":0,\"updated\":1000,\"port\":1}", now); assert (s.tooltip ().contains ("nobody")); print ("share-state: ok\n"); } From 3d786a544711c24e95525832ea277b211d98cf57 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:11:13 -0400 Subject: [PATCH 007/798] Add native arm64 Discord installer (pinned Vesktop) Co-Authored-By: Claude Sonnet 5.5 --- mseries/DESKTOP-SETUP.md | 12 ++++++++ mseries/OS9-HANDOFF.md | 9 ++++++ mseries/install-discord.sh | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100755 mseries/install-discord.sh diff --git a/mseries/DESKTOP-SETUP.md b/mseries/DESKTOP-SETUP.md index 45e80df40..1ea608993 100644 --- a/mseries/DESKTOP-SETUP.md +++ b/mseries/DESKTOP-SETUP.md @@ -86,6 +86,18 @@ opens multitasking, three-finger swipe left/right switches workspaces, and four-finger gestures are unset (`io.elementary.desktop.wm.gestures`). Remove everything with `bash mseries/install-touchegg.sh --uninstall`. +## Discord + +Run `bash mseries/install-discord.sh`. Discord Inc. publishes no Linux ARM64 +build, so this installs [Vesktop](https://github.com/Vencord/Vesktop) (GPL-3.0), +an Electron Discord client with native aarch64 releases, pinned to v1.6.7 and +checked against its SHA-256 before install. It lands in `~/.local/lib/vesktop` +with a `discord` launcher (native Wayland hint) and an app-menu entry. It is a +third-party client, not Discord's own app. Remove with +`bash mseries/install-discord.sh --uninstall` (login and settings in +`~/.config/vesktop` are kept). Voice, screen share and login are not yet +verified on this machine. + ## Signal Run `bash mseries/install-signal.sh`. Installs the official ARM64 beta, diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 2f92e6af1..058e62e69 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,14 @@ # OS 9 implementation / Claude Code handoff +## Native Discord (Vesktop arm64) — 2026-10-18 (Claude Code) + +Added `mseries/install-discord.sh`: pinned Vesktop v1.6.7 aarch64 AppImage (SHA-256 checked), extracted to +`~/.local/lib/vesktop`, `~/.local/bin/discord` launcher, `discord.desktop`. Official Discord has no Linux arm64 +build, so this third-party client is the native option. **Installed on this machine** and verified: checksum OK, +`vesktop.bin` is ELF aarch64, desktop file validates, headless Xvfb start ran 15 s without crashing. **Not +verified:** login, voice/video, screen share, tray, Wayland rendering on the real session (no window was opened +on the owner's screen). Next: owner logs in; test a call. + ## Power audit and Aurora status after `.20`; dock monitor seems live — 2026-10-18 (Claude Code) Read-only pass; nothing on the machine was changed. [Power audit](power/AUDIT-2026-10-18.md): docked on AC with a diff --git a/mseries/install-discord.sh b/mseries/install-discord.sh new file mode 100755 index 000000000..4abb91984 --- /dev/null +++ b/mseries/install-discord.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Native ARM64 Discord. Discord Inc. ships no Linux ARM64 build (its tarball/deb are x86_64 only), so this +# installs Vesktop (https://github.com/Vencord/Vesktop, GPL-3.0), an Electron Discord client that publishes +# aarch64 releases. The release is pinned and its SHA-256 is checked before anything is installed. +# install-discord.sh install or update to the pinned version +# install-discord.sh --uninstall remove (keeps ~/.config/vesktop, your login and settings) +set -euo pipefail +version=1.6.7 +sha256=260144352ce10291ee7ecd0b8aa060f3e36a0d8a9c8f6b509db244ee6c36a2ae +appdir=$HOME/.local/lib/vesktop +launcher=$HOME/.local/bin/discord +desktop=$HOME/.local/share/applications/discord.desktop +icon=$HOME/.local/share/icons/hicolor/scalable/apps/discord.svg +if [[ ${1:-} == --uninstall ]]; then + rm -rf "$appdir" "$launcher" "$desktop" "$icon" + echo 'Removed Discord (Vesktop). ~/.config/vesktop kept.' + exit 0 +fi +[[ -z ${1:-} ]] || { echo 'Usage: install-discord.sh [--uninstall]' >&2; exit 2; } +[[ $(uname -m) == aarch64 ]] || { echo 'This installer requires ARM64' >&2; exit 1; } +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +asset="Vesktop-${version}-arm64.AppImage" +curl -fLsS "https://github.com/Vencord/Vesktop/releases/download/v${version}/${asset}" -o "$work/$asset" +echo "$sha256 $work/$asset" | sha256sum -c - +chmod 755 "$work/$asset" +# Extract instead of running the AppImage: no FUSE needed and the app starts faster. +(cd "$work" && "./$asset" --appimage-extract >/dev/null) +new="$appdir.new" +rm -rf "$new" +mkdir -p "$(dirname "$appdir")" "$HOME/.local/bin" "$(dirname "$desktop")" "$(dirname "$icon")" +cp -a "$work/squashfs-root" "$new" +rm -rf "$appdir.old" +[[ ! -e $appdir ]] || mv "$appdir" "$appdir.old" +mv "$new" "$appdir" +rm -rf "$appdir.old" +install -m644 "$appdir/usr/share/icons/hicolor/scalable/apps/vesktop.svg" "$icon" +# Native Wayland via the Electron ozone hint; Vesktop's own AppRun passes the rest through. +tmp=$(mktemp "$HOME/.local/bin/.discord.XXXXXX") +printf '#!/bin/bash\nexec %q --ozone-platform-hint=auto "$@"\n' "$appdir/AppRun" >"$tmp" +chmod 755 "$tmp" +mv -f "$tmp" "$launcher" +cat >"$desktop" </dev/null || true +echo "Installed Discord (Vesktop ${version}, native aarch64). Launch it from the app menu or run 'discord'." From 02970bc389bd48bcbd2b924092e93b493c4bc818 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:12:31 -0400 Subject: [PATCH 008/798] Add native arm64 RDP route for Horizon-hosted desktops (FreeRDP 3, no proprietary client) Co-Authored-By: Claude Sonnet 5.5 --- mseries/HORIZON-RDP.md | 22 ++++++++++++++++++++++ mseries/horizon-rdp | 18 ++++++++++++++++++ mseries/horizon-rdp.desktop | 8 ++++++++ mseries/install-horizon-rdp.sh | 27 +++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 mseries/HORIZON-RDP.md create mode 100755 mseries/horizon-rdp create mode 100644 mseries/horizon-rdp.desktop create mode 100755 mseries/install-horizon-rdp.sh diff --git a/mseries/HORIZON-RDP.md b/mseries/HORIZON-RDP.md new file mode 100644 index 000000000..c4ae6b33f --- /dev/null +++ b/mseries/HORIZON-RDP.md @@ -0,0 +1,22 @@ +# Horizon-hosted desktops over native RDP (arm64) + +Omnissa Horizon Client is closed source and has no public arm64 Linux build, so this machine cannot run it natively. +`mseries/install-horizon-rdp.sh` installs Ubuntu's FreeRDP 3 (native arm64, SDL3 client for Wayland, xfreerdp for X11) and a small launcher. + + bash mseries/install-horizon-rdp.sh # install + horizon-rdp [--x11] HOST [USER] [DOMAIN] # connect; password is prompted + bash mseries/install-horizon-rdp.sh --uninstall # remove launcher (keeps FreeRDP) + +Also available as "Horizon RDP" in the app launcher. + +## What this covers +- Direct RDP (TCP/UDP 3389) to a Windows or Linux desktop whose RDP is enabled and reachable from here (VPN or + internal network). Clipboard, audio, microphone, dynamic resolution and H.264 (AVC444) are on by default. + Certificates use trust-on-first-use. + +## What it does not cover +- Horizon's Connection Server broker, the Blast Extreme protocol, USB redirection and Horizon-specific features. + Pools that only allow Blast, or admin policy that blocks direct RDP, will not work here. Ask the Horizon admin + whether RDP is allowed to the target desktop, or use the browser-based HTML Access if the deployment offers it. +- Not tested against a real Horizon deployment; only the install and launcher were checked locally (FreeRDP + 3.32.1 from Ubuntu resolute-updates). diff --git a/mseries/horizon-rdp b/mseries/horizon-rdp new file mode 100755 index 000000000..7ad471044 --- /dev/null +++ b/mseries/horizon-rdp @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Launch a native FreeRDP session to a desktop. Usage: horizon-rdp [--x11] HOST [USER] [DOMAIN] [extra xfreerdp args] +# The password is prompted by FreeRDP (never put it on the command line). +set -euo pipefail +if [[ ${1:-} == --help || ${1:-} == -h || $# -eq 0 ]]; then + sed -n '2,3p' "$0"; exit 0 +fi +client=sdl-freerdp3 +if [[ ${1:-} == --x11 || -z ${WAYLAND_DISPLAY:-} ]]; then + [[ ${1:-} == --x11 ]] && shift + client=xfreerdp3 +fi +command -v "$client" >/dev/null || client=$(command -v sdl-freerdp3 xfreerdp3 xfreerdp | head -1) +host=$1; shift +args=(/v:"$host" /dynamic-resolution +clipboard /sound /microphone /gfx:AVC444 /cert:tofu) +[[ -n ${1:-} && $1 != /* && $1 != +* && $1 != -* ]] && { args+=(/u:"$1"); shift; } +[[ -n ${1:-} && $1 != /* && $1 != +* && $1 != -* ]] && { args+=(/d:"$1"); shift; } +exec "$client" "${args[@]}" "$@" diff --git a/mseries/horizon-rdp.desktop b/mseries/horizon-rdp.desktop new file mode 100644 index 000000000..90857063d --- /dev/null +++ b/mseries/horizon-rdp.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=Horizon RDP +Comment=Native arm64 RDP client (FreeRDP) for remote desktops +Exec=sh -c 'host=$(zenity --entry --title="Horizon RDP" --text="Desktop host or address:") && [ -n "$host" ] && exec horizon-rdp "$host"' +Icon=preferences-desktop-remote-desktop +Terminal=false +Categories=Network;RemoteAccess; diff --git a/mseries/install-horizon-rdp.sh b/mseries/install-horizon-rdp.sh new file mode 100755 index 000000000..c7c6cedcf --- /dev/null +++ b/mseries/install-horizon-rdp.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Native arm64 RDP client for Horizon-hosted desktops, from Ubuntu's FreeRDP 3 packages (no proprietary client). +# bash mseries/install-horizon-rdp.sh # install FreeRDP + the horizon-rdp launcher +# bash mseries/install-horizon-rdp.sh --uninstall # remove the launcher and desktop entry (keeps FreeRDP) +# Omnissa's Horizon Client has no public arm64 Linux build. This reaches a desktop over plain RDP only, so the +# desktop's RDP must be enabled and reachable; see mseries/HORIZON-RDP.md for what that does and does not cover. +set -euo pipefail +here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$here/lib-apt.sh" +bin=/usr/local/bin/horizon-rdp +entry=/usr/share/applications/horizon-rdp.desktop + +if [[ ${1:-} == --uninstall ]]; then + sudo rm -f "$bin" "$entry" + echo 'Removed launcher (FreeRDP packages left installed).' + exit 0 +fi + +if ! dpkg -s freerdp3-sdl freerdp3-x11 >/dev/null 2>&1; then + mseries_apt_refresh + sudo apt-get install -y freerdp3-sdl freerdp3-x11 +fi +sudo install -m 0755 "$here/horizon-rdp" "$bin" +sudo install -m 0644 "$here/horizon-rdp.desktop" "$entry" +command -v update-desktop-database >/dev/null && sudo update-desktop-database /usr/share/applications || true +dpkg-query -W -f='FreeRDP ${Version}\n' freerdp3-sdl +echo 'Installed: run "horizon-rdp --help" or open "Horizon RDP" from the app launcher.' From 19dd8b59512c03b063d4d5dcfa6ad05574036de1 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:23:05 -0400 Subject: [PATCH 009/798] Screenshots: click a window with Cmd+Shift+4; fixed-size persistent shelf thumbnail Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 12 ++ mseries/install-gala-screen-corners.sh | 4 + .../patches/gala-screenshot-window-pick.patch | 143 ++++++++++++++++++ mseries/screenshot-shelf/README.md | 9 +- .../screenshot-shelf/mseries-screenshot-shelf | 75 +++++---- 5 files changed, 202 insertions(+), 41 deletions(-) create mode 100644 mseries/patches/gala-screenshot-window-pick.patch diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 058e62e69..975b9e692 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,17 @@ # OS 9 implementation / Claude Code handoff +## Screenshots: click a window with Command+Shift+4; fixed persistent thumbnail — 2026-10-18 (Claude Code) + +Command is Ctrl here, so Command+Shift+4 is Gala's `area-screenshot`. New `mseries/patches/gala-screenshot-window-pick.patch` +(on top of screen-corners + panel-offset, applied by `install-gala-screen-corners.sh`): in that selector a plain click +(no drag) captures the topmost window under the pointer via Gala's own window capture; dragging still selects an +area. The shelf (`screenshot-shelf/mseries-screenshot-shelf`) now draws every shot in a fixed 280x180 card, bottom-right, +that stays until the x, a drag-and-drop or the next shot (no 7 s expiry; Markup no longer dismisses it). +**Verified:** patched Gala compiles and links; installed to the private prefix (takes effect at next login); +shelf installed and running; card renders at identical size for wide and tall images (headless snapshot). +**Not verified:** the click-to-capture on the real session (needs re-login), card position and drag-out on the +real screen (no test window was put on the owner's display). Next: re-login, press Command+Shift+4, click a window. + ## Native Discord (Vesktop arm64) — 2026-10-18 (Claude Code) Added `mseries/install-discord.sh`: pinned Vesktop v1.6.7 aarch64 AppImage (SHA-256 checked), extracted to diff --git a/mseries/install-gala-screen-corners.sh b/mseries/install-gala-screen-corners.sh index 4a1a498d4..26ad96d9e 100755 --- a/mseries/install-gala-screen-corners.sh +++ b/mseries/install-gala-screen-corners.sh @@ -19,6 +19,9 @@ # It also applies gala-panel-offset.patch, which lets a panel (the dock) ask the compositor to # slide along its edge (io_elementary_pantheon_panel_v1.set_offset, protocol v2). The Dock's # Command-drag uses it; without this Gala the dock simply stays centred. +# +# And gala-screenshot-window-pick.patch: in the Command+Shift+4 selector a plain click on a window +# screenshots that window (macOS style); dragging still selects an area. set -euo pipefail repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) @@ -69,6 +72,7 @@ git -C "$source_dir" fetch -q --depth=1 origin "$commit" git -C "$source_dir" checkout -q --detach FETCH_HEAD git -C "$source_dir" apply "$repo/mseries/patches/gala-screen-corners.patch" git -C "$source_dir" apply "$repo/mseries/patches/gala-panel-offset.patch" +git -C "$source_dir" apply "$repo/mseries/patches/gala-screenshot-window-pick.patch" meson setup "$build_dir" "$source_dir" --prefix="$prefix" --libdir=lib --buildtype=release >/dev/null ninja -C "$build_dir" >/dev/null diff --git a/mseries/patches/gala-screenshot-window-pick.patch b/mseries/patches/gala-screenshot-window-pick.patch new file mode 100644 index 000000000..766eda105 --- /dev/null +++ b/mseries/patches/gala-screenshot-window-pick.patch @@ -0,0 +1,143 @@ +diff --git a/src/Misc/ScreenshotManager.vala b/src/Misc/ScreenshotManager.vala +index 8d7eb19..15566ad 100644 +--- a/src/Misc/ScreenshotManager.vala ++++ b/src/Misc/ScreenshotManager.vala +@@ -46,6 +46,9 @@ public class Gala.ScreenshotManager : Object { + } + } + ++ // Window chosen by a click in the area selector; used instead of the focused window. ++ private Meta.Window? window_override = null; ++ + private Settings desktop_settings; + private GLib.HashTable notifications_id_to_path; + +@@ -149,8 +152,19 @@ public class Gala.ScreenshotManager : Object { + string filename_used = ""; + + int x, y, w, h; +- yield select_area_internal (out x, out y, out w, out h); +- yield screenshot_area_internal (x, y, w, h, true, filename, out success, out filename_used); ++ Meta.Window? picked_window; ++ yield select_area_internal (true, out x, out y, out w, out h, out picked_window); ++ if (picked_window != null) { ++ // macOS-style: a click on a window captures that window. ++ window_override = picked_window; ++ try { ++ yield screenshot_window_internal (true, false, true, filename, out success, out filename_used); ++ } finally { ++ window_override = null; ++ } ++ } else { ++ yield screenshot_area_internal (x, y, w, h, true, filename, out success, out filename_used); ++ } + + if (success) { + send_screenshot_notification.begin (filename_used); +@@ -370,7 +384,7 @@ public class Gala.ScreenshotManager : Object { + private async void screenshot_window_internal (bool include_frame, bool include_cursor, bool flash, string filename, out bool success, out string filename_used) throws DBusError, IOError { + debug ("Taking window screenshot"); + +- var window = wm.get_display ().get_focus_window (); ++ var window = window_override ?? wm.get_display ().get_focus_window (); + if (window == null) { + unconceal_text (); + throw new DBusError.FAILED ("Cannot find active window"); +@@ -476,11 +490,44 @@ public class Gala.ScreenshotManager : Object { + + public async void select_area (GLib.BusName sender, out int x, out int y, out int width, out int height) throws DBusError, IOError { + yield check_sender (sender); +- yield select_area_internal (out x, out y, out width, out height); ++ Meta.Window? unused; ++ yield select_area_internal (false, out x, out y, out width, out height, out unused); ++ } ++ ++ private Meta.Window? window_at (float px, float py) { ++ unowned var display = wm.get_display (); ++ var workspace = display.get_workspace_manager ().get_active_workspace (); ++ var on_workspace = new SList (); ++ foreach (unowned var candidate in workspace.list_windows ()) { ++ on_workspace.prepend (candidate); ++ } ++ ++ // sort_windows_by_stacking is bottom to top; keep the last (topmost) window under the point. ++ Meta.Window? found = null; ++ foreach (unowned var window in display.sort_windows_by_stacking (on_workspace)) { ++ if (window.minimized || window.is_override_redirect ()) { ++ continue; ++ } ++ ++ var type = window.window_type; ++ if (type != Meta.WindowType.NORMAL && type != Meta.WindowType.DIALOG && ++ type != Meta.WindowType.MODAL_DIALOG && type != Meta.WindowType.UTILITY) { ++ continue; ++ } ++ ++ var rect = window.get_frame_rect (); ++ if (px >= rect.x && px < rect.x + rect.width && py >= rect.y && py < rect.y + rect.height) { ++ found = window; ++ } ++ } ++ ++ return found; + } + +- private async void select_area_internal (out int x, out int y, out int width, out int height) throws DBusError, IOError { ++ private async void select_area_internal (bool allow_window_pick, out int x, out int y, out int width, out int height, out Meta.Window? picked_window) throws DBusError, IOError { ++ picked_window = null; + var selection_area = new SelectionArea (wm); ++ selection_area.allow_click_pick = allow_window_pick; + selection_area.closed.connect (() => Idle.add (select_area_internal.callback)); + wm.ui_group.add_child (selection_area); + selection_area.start_selection (); +@@ -493,6 +540,17 @@ public class Gala.ScreenshotManager : Object { + } + + yield wait_stage_repaint (); ++ ++ if (selection_area.picked) { ++ picked_window = window_at (selection_area.picked_point.x, selection_area.picked_point.y); ++ if (picked_window == null) { ++ throw new GLib.IOError.CANCELLED ("No window at the clicked point"); ++ } ++ ++ x = y = width = height = 0; ++ return; ++ } ++ + var rect = selection_area.get_selection_rectangle (); + x = (int) GLib.Math.roundf (rect.origin.x); + y = (int) GLib.Math.roundf (rect.origin.y); +diff --git a/src/Widgets/SelectionArea.vala b/src/Widgets/SelectionArea.vala +index 87244b3..6723065 100644 +--- a/src/Widgets/SelectionArea.vala ++++ b/src/Widgets/SelectionArea.vala +@@ -12,6 +12,11 @@ public class Gala.SelectionArea : CanvasActor { + + public bool cancelled { get; private set; } + ++ // When set, a plain click (no drag) picks the point instead of cancelling. ++ public bool allow_click_pick { get; set; default = false; } ++ public bool picked { get; private set; default = false; } ++ public Graphene.Point picked_point { get; private set; } ++ + private ModalProxy? modal_proxy; + private Graphene.Point start_point; + private Graphene.Point end_point; +@@ -66,7 +71,15 @@ public class Gala.SelectionArea : CanvasActor { + + if (!dragging) { + close (); +- cancelled = true; ++ if (allow_click_pick && clicked) { ++ float px, py; ++ e.get_coords (out px, out py); ++ picked_point = { px, py }; ++ picked = true; ++ this.hide (); ++ } else { ++ cancelled = true; ++ } + closed (); + return true; + } diff --git a/mseries/screenshot-shelf/README.md b/mseries/screenshot-shelf/README.md index c842a6421..d0a7971ed 100644 --- a/mseries/screenshot-shelf/README.md +++ b/mseries/screenshot-shelf/README.md @@ -1,13 +1,16 @@ # Screenshot shelf and Markup -After a screenshot (Ctrl+Shift+3 or 4), a thumbnail appears bottom-right for about 7 seconds, like macOS. +**Command+Shift+4** (Command is Ctrl on this keyboard) opens the selector: drag to capture an area, or just **click a window** to capture that window (needs the patched Gala from `install-gala-screen-corners.sh`, active after next login; stock Gala treats a click as cancel). Command+Shift+3 captures the screen. + +After a screenshot, a thumbnail appears bottom-right at a fixed size (280x180 card, any screenshot shape) and **stays** until you dismiss it, drag it away or take the next shot. | You do | What happens | | --- | --- | | Drag it into an app | The screenshot is **used, not kept**: its saved file is deleted 2 minutes after an accepted drop. | | Hold **Alt** while dragging | The screenshot is used **and a copy is kept**; the thumbnail text changes to "Saving a copy (Alt)". | -| Click it, or press **Markup** | Opens Swappy (arrows, shapes, text, highlight, blur, crop). The result is saved next to the original as ` (markup).png`. | -| Ignore it, or click the x | The thumbnail goes away and the screenshot stays in `~/Downloads/Screenshots`. | +| Click it, or press **Markup** | Opens Swappy (the card stays) (arrows, shapes, text, highlight, blur, crop). The result is saved next to the original as ` (markup).png`. | +| Click the x | The thumbnail goes away and the screenshot stays in `~/Downloads/Screenshots`. | +| Ignore it | It stays where it is; the next screenshot replaces it. | A screenshot is only ever deleted by a drag without Alt, and only after the drop was accepted. A cancelled drag keeps it. diff --git a/mseries/screenshot-shelf/mseries-screenshot-shelf b/mseries/screenshot-shelf/mseries-screenshot-shelf index 7adcaf9f6..46fb8b99c 100755 --- a/mseries/screenshot-shelf/mseries-screenshot-shelf +++ b/mseries/screenshot-shelf/mseries-screenshot-shelf @@ -1,5 +1,5 @@ #!/usr/bin/python3 -"""Screenshot shelf: after a screenshot, show its thumbnail bottom-right for a few seconds. +"""Screenshot shelf: after a screenshot, show its thumbnail bottom-right, fixed size, until you dismiss it. Gala saves every screenshot to ~/Pictures/Screenshots (a link to ~/Downloads/Screenshots). This helper watches that folder and shows the newest shot as a draggable thumbnail, like macOS. @@ -7,7 +7,7 @@ watches that folder and shows the newest shot as a draggable thumbnail, like mac Drag it into an app the shot is used and not kept: the saved file is deleted after the drop. Hold Alt while dragging the shot is used and a copy is kept (the thumbnail says so). Click it opens Markup on the saved file; the file is kept. - Ignore it the thumbnail fades after a few seconds and the file is kept. + Ignore it the thumbnail stays (same size, same corner) until the next shot or the x; the file is kept. Click the x dismisses it; the file is kept. A screenshot is only ever deleted by a drag without Alt, and only after the drag was accepted. @@ -57,16 +57,16 @@ WATCH = os.path.realpath(os.environ.get("MSERIES_SHELF_WATCH", os.path.expanduse STAGE = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "mseries-shelf") LOG = os.path.expanduser("~/.cache/mseries-screenshot-shelf.log") MARKUP = os.environ.get("MSERIES_MARKUP", os.path.expanduser("~/.local/bin/mseries-markup")) -SHOW_SECONDS = 7 # how long the thumbnail stays when ignored DELETE_AFTER = 120 # seconds after an accepted drag before the unsaved file is removed -THUMB_WIDTH = 280 +THUMB_WIDTH = 280 # the card is always THUMB_WIDTH x THUMB_HEIGHT, whatever the screenshot's shape +THUMB_HEIGHT = 180 MARGIN = 24 CSS = b""" window.shelf { background: transparent; box-shadow: none; } .shelf-card { background: #1c1c20; color: #ffffff; border-radius: 14px; padding: 10px; border: 1px solid rgba(255,255,255,0.18); } -.shelf-card picture { border-radius: 8px; } +.shelf-thumb { border-radius: 8px; background: rgba(255,255,255,0.06); } .shelf-hint { color: rgba(255,255,255,0.80); font-size: 12px; } .shelf-hint.saving { color: #9ad1a4; font-weight: bold; } .shelf-card button.flat { color: #ffffff; min-height: 22px; min-width: 22px; padding: 0 6px; } @@ -93,6 +93,29 @@ def clean_env(): return env +class Thumb(Gtk.Widget): + __gtype_name__ = "ShelfThumb" + """The screenshot, drawn centred and fitted inside a card that is always THUMB_WIDTH x THUMB_HEIGHT.""" + + def __init__(self, texture): + super().__init__() + self.texture = texture + self.set_overflow(Gtk.Overflow.HIDDEN) + self.add_css_class("shelf-thumb") + + def do_measure(self, orientation, _for_size): + size = THUMB_WIDTH if orientation == Gtk.Orientation.HORIZONTAL else THUMB_HEIGHT + return size, size, -1, -1 + + def do_snapshot(self, snapshot): + if self.texture is None: + return + width, height = self.get_width(), self.get_height() + ratio = self.texture.get_width() / max(1, self.texture.get_height()) + fit_w, fit_h = (width, width / ratio) if width / height < ratio else (height * ratio, height) + snapshot.append_texture(self.texture, Graphene.Rect().init((width - fit_w) / 2, (height - fit_h) / 2, fit_w, fit_h)) + + class Shelf(Gtk.Window): """One thumbnail for one screenshot.""" @@ -104,7 +127,6 @@ class Shelf(Gtk.Window): self.alt_seen = False self.cancelled = False self.hover = False - self.timer = None self.add_css_class("shelf") self.set_decorated(False) self.set_resizable(False) @@ -114,25 +136,19 @@ class Shelf(Gtk.Window): card.add_css_class("shelf-card") texture = None try: - # Scale once, here: a Gtk.Picture would otherwise ask for the screenshot's full size. - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(staged, THUMB_WIDTH * 2, 440, True) + # Scale once, here, to twice the card so it stays sharp on the Retina panel. + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(staged, THUMB_WIDTH * 2, THUMB_HEIGHT * 2, True) texture = Gdk.Texture.new_for_pixbuf(pixbuf) - picture = Gtk.Picture.new_for_paintable(texture) - ratio = pixbuf.get_width() / max(1, pixbuf.get_height()) - width = min(THUMB_WIDTH, int(220 * ratio)) - picture.set_size_request(width, int(width / max(ratio, 0.01))) except GLib.Error: - picture = Gtk.Picture() - picture.set_size_request(THUMB_WIDTH, 160) - picture.set_can_shrink(True) - picture.set_content_fit(Gtk.ContentFit.CONTAIN) - picture.set_halign(Gtk.Align.CENTER) + pass + picture = Thumb(texture) self.texture = texture self.picture = picture card.append(picture) row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - self.hint = Gtk.Label(label="Drag to use · hold Alt to also save", xalign=0, hexpand=True) + self.hint = Gtk.Label(label="Drag to use · Alt keeps", xalign=0, hexpand=True) + self.hint.set_ellipsize(3) # Pango.EllipsizeMode.END: never widen the card self.hint.add_css_class("shelf-hint") markup = Gtk.Button(label="Markup") markup.add_css_class("markup") @@ -169,7 +185,6 @@ class Shelf(Gtk.Window): Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.present() GLib.timeout_add(60, self.place) - self.restart_timer() # ------------------------------------------------------------------ placement @@ -200,18 +215,7 @@ class Shelf(Gtk.Window): # ------------------------------------------------------------------ timing - def restart_timer(self): - if self.timer: - GLib.source_remove(self.timer) - self.timer = GLib.timeout_add_seconds(SHOW_SECONDS, self.on_timeout) - - def on_timeout(self): - self.timer = None - if self.hover or self.dragging: - self.restart_timer() - return False - self.finish("expired") - return False + # The card does not expire: it stays until the x, a drag-and-drop, or the next screenshot. def set_hover(self, value): self.hover = value @@ -242,7 +246,7 @@ class Shelf(Gtk.Window): return False if self.alt_held(): self.alt_seen = True - self.hint.set_label("Saving a copy (Alt)" if self.alt_seen else "Drag to use · hold Alt to also save") + self.hint.set_label("Keeping a copy (Alt)" if self.alt_seen else "Drag to use · Alt keeps") (self.hint.add_css_class if self.alt_seen else self.hint.remove_css_class)("saving") return True @@ -255,8 +259,7 @@ class Shelf(Gtk.Window): self.dragging = False if self.cancelled: self.alt_seen = False - self.hint.set_label("Drag to use · hold Alt to also save") - self.restart_timer() + self.hint.set_label("Drag to use · Alt keeps") return if self.alt_seen: log(f"used and kept (Alt): {os.path.basename(self.path)}") @@ -273,15 +276,11 @@ class Shelf(Gtk.Window): subprocess.Popen([MARKUP, self.path], env=clean_env(), start_new_session=True) except OSError as error: log(f"could not start markup: {error}") - self.finish("markup") def finish(self, reason, delay=0, delete_saved=False): if self.finished: return self.finished = True - if self.timer: - GLib.source_remove(self.timer) - self.timer = None log(f"shelf done: {reason}") # The staged copy is what an app may still be reading after a drop, so it outlives the window. GLib.timeout_add_seconds(max(delay, 1), self.cleanup, delete_saved) From c7a58fc4ddddeb07014a99c8c395643f4b29687f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:50:58 -0400 Subject: [PATCH 010/798] Share eyes: Wingpanel-style popover, login link with prefilled sign-in Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/Indicator.vala | 132 ++++++++++++------ mseries/share-eyes-wingpanel/ShareState.vala | 5 + .../test-share-state.vala | 1 + 3 files changed, 93 insertions(+), 45 deletions(-) diff --git a/mseries/share-eyes-wingpanel/Indicator.vala b/mseries/share-eyes-wingpanel/Indicator.vala index 6e6cc3290..d8ad1b389 100644 --- a/mseries/share-eyes-wingpanel/Indicator.vala +++ b/mseries/share-eyes-wingpanel/Indicator.vala @@ -88,18 +88,40 @@ namespace MSeries { } private Gtk.Widget header (string text) { - var l = new Gtk.Label (text) { halign = Gtk.Align.START, margin_top = 8 }; - l.add_css_class ("h4"); + var l = new Gtk.Label (text.up ()) { halign = Gtk.Align.START, margin_top = 6, margin_bottom = 2 }; + l.add_css_class ("dim-label"); + l.add_css_class ("caption-heading"); return l; } private Gtk.Widget dim (string text) { - var l = new Gtk.Label (text) { halign = Gtk.Align.START, wrap = true, xalign = 0 }; + var l = new Gtk.Label (text) { halign = Gtk.Align.START, wrap = true, xalign = 0, max_width_chars = 38 }; l.add_css_class ("dim-label"); + l.add_css_class ("caption"); return l; } - private Gtk.Widget field_row (string label, string value, bool secret) { + private Gtk.Widget separator () { + return new Gtk.Separator (Gtk.Orientation.HORIZONTAL) { margin_top = 6, margin_bottom = 6 }; + } + + /* A flat, full-width menu row: icon, label, optional detail, like Wingpanel's own menu items. */ + private Gtk.Button menu_row (string icon, string label, string? detail = null) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 9); + row.append (new Gtk.Image.from_icon_name (icon)); + var l = new Gtk.Label (label) { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }; + row.append (l); + if (detail != null) { + var d = new Gtk.Label (detail) { halign = Gtk.Align.END }; + d.add_css_class ("dim-label"); + row.append (d); + } + var b = new Gtk.Button () { child = row }; + b.add_css_class ("flat"); + return b; + } + + private Gtk.Widget field_row (string label, string value) { var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); var name = new Gtk.Label (label) { halign = Gtk.Align.START, width_chars = 9, xalign = 0 }; name.add_css_class ("dim-label"); @@ -114,86 +136,106 @@ namespace MSeries { return row; } + private void act (owned GLib.AsyncReadyCallback? cb = null) { + last_digest = ""; + tick.begin (); + } + private void rebuild (string tip) { Gtk.Widget? child; while ((child = content.get_first_child ()) != null) content.remove (child); - var title = new Gtk.Label (state.running ? (client.live ? "Sharing your screen" : "Screen share") : "Screen share is off") { halign = Gtk.Align.START }; - title.add_css_class ("title-3"); - content.append (title); - content.append (dim (state.running ? tip : "Start the demo with core/scripts/share-demo.sh.")); + // Status card: the icon says it at a glance, the line under it says what to do. + var head = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10) { margin_bottom = 2 }; + var big = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 32, valign = Gtk.Align.START }; + if (client.live) big.add_css_class ("accent"); + head.append (big); + var texts = new Gtk.Box (Gtk.Orientation.VERTICAL, 2) { hexpand = true }; + var title = new Gtk.Label (!state.running ? "Screen share is off" : client.live ? "Sharing your screen" : "Screen share") + { halign = Gtk.Align.START, xalign = 0 }; + title.add_css_class ("title-4"); + texts.append (title); + texts.append (dim (state.running ? tip : "Start the demo with core/scripts/share-demo.sh.")); + head.append (texts); + content.append (head); if (!state.running) return; if (!client.reachable) { content.append (dim ("The share server is not answering.")); return; } - // 1. Start / stop if (!client.live) { - content.append (dim ("Open the share page, sign in with the login below and click Start sharing. The browser asks which window to share.")); - var open = new Gtk.Button.with_label ("Open share page"); + var open = new Gtk.Button.with_label ("Open share page and sign in") { margin_top = 6 }; open.add_css_class ("suggested-action"); open.clicked.connect (() => { - try { GLib.AppInfo.launch_default_for_uri (state.share_page, null); } catch (GLib.Error e) { warning ("%s", e.message); } + try { GLib.AppInfo.launch_default_for_uri (state.login_url (), null); } catch (GLib.Error e) { warning ("%s", e.message); } }); content.append (open); - } - - // 2. Login - content.append (header ("Login")); - content.append (field_row ("Username", state.email, false)); - content.append (field_row ("Password", state.password, true)); - var copy_login = new Gtk.Button.with_label ("Copy login"); - copy_login.clicked.connect (() => copy (state.login_text (), "Login")); - content.append (copy_login); - - // 3. Link for the watcher - content.append (header ("Link for your viewer")); - var link = client.viewer_link (state.viewer_base); - if (link != null) { - content.append (field_row ("Link", link, false)); - content.append (dim ("Valid until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%a %H:%M") - + ". Anyone with it can ask to watch; you still approve them.")); + content.append (dim ("Then click Start sharing and pick one window.")); } else { - var mk = new Gtk.Button.with_label ("Create viewer link"); - mk.clicked.connect (() => { client.create_link.begin ((o, r) => { client.create_link.end (r); last_digest = ""; tick.begin (); }); }); - content.append (mk); + var stop = new Gtk.Button.with_label ("Stop sharing") { margin_top = 6 }; + stop.add_css_class ("destructive-action"); + stop.clicked.connect (() => client.stop.begin ((o, r) => { client.stop.end (r); act (); })); + content.append (stop); } - // 4. Watchers + // Watchers first once someone is waiting: that is the thing you have to act on. + content.append (separator ()); content.append (header ("Watchers")); if (client.viewers.length == 0) { content.append (dim ("Nobody yet.")); } foreach (var v in client.viewers) { - var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); - var who = new Gtk.Label (v.state == "pending" ? "Wants to watch · " + v.ip : "Watching · " + v.ip) - { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }; - row.append (who); + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + row.append (new Gtk.Image.from_icon_name (v.state == "pending" ? "avatar-default-symbolic" : "view-reveal-symbolic")); + var who = new Gtk.Label (v.state == "pending" ? "Wants to watch" : "Watching") + { halign = Gtk.Align.START, xalign = 0 }; + var where = new Gtk.Label (v.ip) { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }; + where.add_css_class ("dim-label"); + var names = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { hexpand = true }; + names.append (who); + names.append (where); + row.append (names); var vid = v.vid; if (v.state == "pending") { var deny = new Gtk.Button.with_label ("Deny"); - deny.clicked.connect (() => client.viewer_action.begin (vid, "deny", (o, r) => { client.viewer_action.end (r); last_digest = ""; tick.begin (); })); + deny.clicked.connect (() => client.viewer_action.begin (vid, "deny", (o, r) => { client.viewer_action.end (r); act (); })); var allow = new Gtk.Button.with_label ("Allow"); allow.add_css_class ("suggested-action"); - allow.clicked.connect (() => client.viewer_action.begin (vid, "admit", (o, r) => { client.viewer_action.end (r); last_digest = ""; tick.begin (); })); + allow.clicked.connect (() => client.viewer_action.begin (vid, "admit", (o, r) => { client.viewer_action.end (r); act (); })); row.append (deny); row.append (allow); } else { var rm = new Gtk.Button.with_label ("Remove"); rm.add_css_class ("destructive-action"); - rm.clicked.connect (() => client.viewer_action.begin (vid, "kick", (o, r) => { client.viewer_action.end (r); last_digest = ""; tick.begin (); })); + rm.clicked.connect (() => client.viewer_action.begin (vid, "kick", (o, r) => { client.viewer_action.end (r); act (); })); row.append (rm); } content.append (row); } - if (client.live) { - var stop = new Gtk.Button.with_label ("Stop sharing") { margin_top = 8 }; - stop.add_css_class ("destructive-action"); - stop.clicked.connect (() => client.stop.begin ((o, r) => { client.stop.end (r); last_digest = ""; tick.begin (); })); - content.append (stop); + content.append (separator ()); + content.append (header ("Share")); + var link = client.viewer_link (state.viewer_base); + if (link != null) { + var cl = menu_row ("insert-link-symbolic", "Copy viewer link", + "until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%H:%M")); + cl.clicked.connect (() => copy (link, "Viewer link")); + content.append (cl); + } else { + var mk = menu_row ("list-add-symbolic", "Create viewer link"); + mk.clicked.connect (() => { client.create_link.begin ((o, r) => { client.create_link.end (r); act (); }); }); + content.append (mk); } + var cll = menu_row ("dialog-password-symbolic", "Copy login link"); + cll.tooltip_text = "Opens the share page already signed in. For this computer only; never send it to your viewer."; + cll.clicked.connect (() => copy (state.login_url (), "Login link")); + content.append (cll); + + content.append (separator ()); + content.append (header ("Login")); + content.append (field_row ("Username", state.email)); + content.append (field_row ("Password", state.password)); if (toast != "") content.append (dim (toast)); } diff --git a/mseries/share-eyes-wingpanel/ShareState.vala b/mseries/share-eyes-wingpanel/ShareState.vala index b12168973..528b3d673 100644 --- a/mseries/share-eyes-wingpanel/ShareState.vala +++ b/mseries/share-eyes-wingpanel/ShareState.vala @@ -69,6 +69,11 @@ namespace MSeries { : "Sharing your screen · %d people watching".printf (viewers); } + /* Owner page with the login in the URL fragment (never sent to the server; the page clears it and signs in). */ + public string login_url () { + return "%s#u=%s&p=%s".printf (share_page, GLib.Uri.escape_string (email, null, true), GLib.Uri.escape_string (password, null, true)); + } + public string login_text () { return "Screen share (this computer)\nOpen: %s\nUsername: %s\nPassword: %s\n".printf (share_page, email, password); } diff --git a/mseries/share-eyes-wingpanel/test-share-state.vala b/mseries/share-eyes-wingpanel/test-share-state.vala index c873b3bfd..e10c19a65 100644 --- a/mseries/share-eyes-wingpanel/test-share-state.vala +++ b/mseries/share-eyes-wingpanel/test-share-state.vala @@ -6,6 +6,7 @@ void main () { assert (s.parse (full, now) && s.running && s.live && s.viewers == 2 && s.port == 8799); assert (s.share_page == "http://127.0.0.1:8799/share" && s.viewer_base == "http://h:8799"); assert (s.login_text () == "Screen share (this computer)\nOpen: http://127.0.0.1:8799/share\nUsername: a@b\nPassword: pw\n"); + assert (s.login_url () == "http://127.0.0.1:8799/share#u=a%40b&p=pw"); assert (!s.parse (full, now)); assert (s.parse (full, now + 100) && !s.running && !s.live); /* stale: launcher gone */ assert (!s.parse ("not json", now) && !s.running); From b86eff66d208bc192d5a26a06fa2f3549372b8cb Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:52:42 -0400 Subject: [PATCH 011/798] Official Discord under FEX in a 4K-page muvm microVM (experimental) Co-Authored-By: Claude Sonnet 5.5 --- mseries/DESKTOP-SETUP.md | 13 ++++++ mseries/OS9-HANDOFF.md | 12 ++++++ mseries/install-discord-official.sh | 67 +++++++++++++++++++++++++++++ mseries/install-muvm.sh | 61 ++++++++++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100755 mseries/install-discord-official.sh create mode 100755 mseries/install-muvm.sh diff --git a/mseries/DESKTOP-SETUP.md b/mseries/DESKTOP-SETUP.md index 1ea608993..213dc00c1 100644 --- a/mseries/DESKTOP-SETUP.md +++ b/mseries/DESKTOP-SETUP.md @@ -98,6 +98,19 @@ third-party client, not Discord's own app. Remove with `~/.config/vesktop` are kept). Voice, screen share and login are not yet verified on this machine. +### Official Discord (x86-64, emulated) + +Discord's own Linux build is amd64 only, so it runs under FEX. FEX aborts on this +16 KB-page kernel, so `bash mseries/install-muvm.sh` builds muvm, libkrun and +libkrunfw (pinned tags; the guest kernel uses 4 KB pages), adds the FEX PPA +(key fingerprint pinned) and fetches the Ubuntu 24.04 x86-64 rootfs. Then +`bash mseries/install-discord-official.sh` runs Discord's own updater +bootstrap, which fetches the official app, and runs its unmodified `app.asar` +and modules on stock Electron 43.2.0 x86-64, because Discord's bundled Electron +42 dies with SIGTRAP under FEX 2610 (stock Electron 42.11.12 does too). Launch +with `discord-official`. Login, voice (native modules were built for Electron +42), rendering and speed are not verified. Third-party Vesktop stays as `discord`. + ## Signal Run `bash mseries/install-signal.sh`. Installs the official ARM64 beta, diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 975b9e692..6337c9999 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,17 @@ # OS 9 implementation / Claude Code handoff +## Official Discord via FEX in a 4 KB-page microVM — 2026-10-18 (Claude Code) + +Owner required official Discord (amd64 only). FEX alone aborts here (16 KB pages, `: Unsupported system +page size`). `mseries/install-muvm.sh` builds muvm 0.7.0 + libkrun 1.19.6 + libkrunfw 5.6.2 (guest 6.12.109, 4K pages), +installs FEX 2610 from its PPA and the Ubuntu 24.04 rootfs; verified `muvm -- FEXBash -c 'uname -m; getconf PAGESIZE'` += x86_64 / 4096. Discord 1.0.162's bundled Electron 42 (and stock 42.11.12) SIGTRAPs at startup under FEX; Electron 43 +(x86 Vesktop and stock 43.2.0) runs. `mseries/install-discord-official.sh` therefore runs Discord's official +app.asar + modules on stock Electron 43.2.0 (`discord-official`). Discord started and stayed up 40 s; **login, voice, +rendering, performance unverified**. System changes: FEX apt source + key, /usr/local libkrun/muvm, +`/etc/ld.so.conf.d/mseries-libkrun.conf`. Prior art: no official Discord arm64 Linux build exists (support thread open +for years); ArmCord/Legcord and Vesktop wrap the web app. macOS Discord is Mach-O; only its JS could be reused. + ## Screenshots: click a window with Command+Shift+4; fixed persistent thumbnail — 2026-10-18 (Claude Code) Command is Ctrl here, so Command+Shift+4 is Gala's `area-screenshot`. New `mseries/patches/gala-screenshot-window-pick.patch` diff --git a/mseries/install-discord-official.sh b/mseries/install-discord-official.sh new file mode 100755 index 000000000..b45fe3025 --- /dev/null +++ b/mseries/install-discord-official.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Discord's official amd64 app, run under FEX inside muvm (needs install-muvm.sh first). +# Discord's own Electron 42 binary dies with SIGTRAP at startup under FEX 2610 (stock Electron 42.11.12 does the +# same), while Electron 43 runs fine. So this takes Discord's official, unmodified app.asar and modules (fetched by +# the deb's own updater_bootstrap) and runs them on stock Electron 43.2.0 x86-64 (SHA-256 pinned). +# Status 2026-10-18: Discord 1.0.162 starts and stays up for 40 s headless-ish; login, voice (native modules +# were built for Electron 42), rendering and performance are NOT verified. Launcher: ~/.local/bin/discord-official. +# install-discord-official.sh install +# install-discord-official.sh --uninstall remove (keeps ~/.config/discord) +set -euo pipefail +dir=$HOME/.local/lib/discord-official +launcher=$HOME/.local/bin/discord-official +desktop=$HOME/.local/share/applications/discord-official.desktop +if [[ ${1:-} == --uninstall ]]; then + rm -rf "$dir" "$launcher" "$desktop" + echo 'Removed. ~/.config/discord kept.' + exit 0 +fi +[[ -z ${1:-} ]] || { echo 'Usage: install-discord-official.sh [--uninstall]' >&2; exit 2; } +command -v muvm >/dev/null || { echo 'Run mseries/install-muvm.sh first.' >&2; exit 1; } +version=1.0.162 +deb_sha256=e7f346569784ef47eaa530c058d810bef686c7c063efcee6493d56d138f139cc +electron=43.2.0 +electron_sha256=f77ca6ed67bbc68702b69b56ad499bca6ae090705ade7d04f0ac545e409dec68 +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +curl -fLsS "https://stable.dl2.discordapp.net/apps/linux/$version/discord-$version.deb" -o "$work/discord.deb" +echo "$deb_sha256 $work/discord.deb" | sha256sum -c - +curl -fLsS "https://github.com/electron/electron/releases/download/v$electron/electron-v$electron-linux-x64.zip" \ + -o "$work/electron.zip" +echo "$electron_sha256 $work/electron.zip" | sha256sum -c - +mkdir -p "$work/deb" "$HOME/.config/discord" "$HOME/.local/bin" "$HOME/.local/share/applications" +dpkg-deb -x "$work/discord.deb" "$work/deb" +# Official bootstrap downloads the real app into ~/.config/discord/app-*/ (resources/app.asar + modules/). +muvm -- "$work/deb/usr/share/discord/updater_bootstrap" --no-zenity "$HOME/.config/discord" stable https://updates.discord.com/ +app=$(ls -d "$HOME"/.config/discord/app-*/ | sort -V | tail -1) +new=$dir.new +rm -rf "$new" +mkdir -p "$new" +unzip -q "$work/electron.zip" -d "$new" +cp -a "$app/resources/." "$new/resources/" +cp -a "$app/modules" "$new/modules" +install -Dm644 "$work/deb/usr/share/discord/discord.png" "$HOME/.local/share/icons/hicolor/512x512/apps/discord-official.png" +rm -rf "$dir.old" +[[ ! -e $dir ]] || mv "$dir" "$dir.old" +mv "$new" "$dir" +rm -rf "$dir.old" +tmp=$(mktemp "$HOME/.local/bin/.discord-official.XXXXXX") +# --no-sandbox: the setuid sandbox and user namespaces are unavailable under FEX in the microVM. +printf '#!/bin/bash\ncd %q\nexec muvm -- %q --no-sandbox "$@"\n' "$dir" "$dir/electron" >"$tmp" +chmod 755 "$tmp" +mv -f "$tmp" "$launcher" +cat >"$desktop" </dev/null || true +echo "Installed official Discord $version on Electron $electron. Run 'discord-official'." diff --git a/mseries/install-muvm.sh b/mseries/install-muvm.sh new file mode 100755 index 000000000..78067d4d5 --- /dev/null +++ b/mseries/install-muvm.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# x86-64 emulation on this 16 KB-page kernel: FEX (from its PPA) runs inside a 4 KB-page microVM (muvm + libkrun +# + libkrunfw, built from source at pinned tags). FEX alone aborts here (": Unsupported system page +# size"). Verified 2026-10-18: `muvm -- FEXBash -c 'uname -m; getconf PAGESIZE'` prints x86_64 and 4096. +# Installs under /usr/local and /etc (apt source, ld.so.conf); nothing is registered with binfmt on the host. +# install-muvm.sh install +# install-muvm.sh --uninstall remove binaries, libs and the FEX apt source (keeps the rootfs in ~/.local/share/fex-emu) +set -euo pipefail +[[ $(uname -m) == aarch64 ]] || { echo 'This installer requires ARM64' >&2; exit 1; } +repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +source "$repo/mseries/lib-apt.sh" +if [[ ${1:-} == --uninstall ]]; then + sudo rm -f /usr/local/bin/muvm /usr/local/bin/muvm-guest /etc/ld.so.conf.d/mseries-libkrun.conf \ + /usr/local/lib64/libkrun.so* /usr/local/lib/aarch64-linux-gnu/libkrunfw.so* \ + /etc/apt/sources.list.d/fex-emu.list /usr/share/keyrings/fex-emu-ppa.gpg + sudo ldconfig + echo 'Removed muvm, libkrun, libkrunfw and the FEX apt source. fex-emu-armv8.4 stays; remove with apt if wanted.' + exit 0 +fi +[[ -r /dev/kvm && -w /dev/kvm ]] || { echo '/dev/kvm is not accessible to this user' >&2; exit 1; } +# FEX PPA key: the signer of the resolute InRelease; the fingerprint is pinned and checked after download. +fex_fpr=EDB98BFE8A2310DC9C4A376E76DBFEBEA206F5AC +muvm_tag=muvm-0.7.0 krun_tag=v1.19.6 krunfw_tag=v5.6.2 +build=${MSERIES_BUILD_DIR:-$HOME/.cache/mseries-build/muvm} +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +curl -fsS "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$fex_fpr" -o "$work/key.asc" +gpg --show-keys --with-colons "$work/key.asc" | grep -q "^fpr:::::::::$fex_fpr:" \ + || { echo 'FEX PPA key fingerprint mismatch' >&2; exit 1; } +gpg --dearmor <"$work/key.asc" >"$work/fex.gpg" +sudo install -Dm644 "$work/fex.gpg" /usr/share/keyrings/fex-emu-ppa.gpg +echo 'deb [arch=arm64 signed-by=/usr/share/keyrings/fex-emu-ppa.gpg] https://ppa.launchpadcontent.net/fex-emu/fex/ubuntu resolute main' \ + | sudo tee /etc/apt/sources.list.d/fex-emu.list >/dev/null +mseries_apt_refresh +sudo apt-get install -y --no-install-recommends fex-emu-armv8.4 passt \ + clang libclang-dev flex bison bc libelf-dev python3-pyelftools libvirglrenderer-dev libepoxy-dev \ + libdrm-dev libpipewire-0.3-dev libcap-ng-dev libssl-dev libudev-dev libgbm-dev pkg-config patch xz-utils +mkdir -p "$build" +cd "$build" +clone() { [[ -d $2 ]] || git clone -q --depth 1 --branch "$3" "https://github.com/$1.git" "$2"; } +clone containers/libkrunfw libkrunfw "$krunfw_tag" +clone containers/libkrun libkrun "$krun_tag" +clone AsahiLinux/muvm muvm "$muvm_tag" +# libkrunfw's default aarch64 guest config is CONFIG_ARM64_4K_PAGES=y, which is the point of the VM. +grep -q '^CONFIG_ARM64_4K_PAGES=y' libkrunfw/config-libkrunfw_aarch64 +make -C libkrunfw -j"$(nproc)" +make -C libkrun BLK=1 NET=1 GPU=1 SND=1 +sudo make -C libkrunfw install PREFIX=/usr/local +sudo make -C libkrun install PREFIX=/usr/local BLK=1 NET=1 GPU=1 SND=1 +printf '/usr/local/lib64\n' | sudo tee /etc/ld.so.conf.d/mseries-libkrun.conf >/dev/null +sudo ldconfig +(cd muvm && PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig cargo build --release --locked) +sudo install -m755 muvm/target/release/muvm muvm/target/release/muvm-guest /usr/local/bin/ +# x86-64 Ubuntu rootfs for FEX, plus the config the fetcher fails to write. +FEXRootFSFetcher -y -x --distro-name=ubuntu --distro-version=24.04 --force-ui=tty +mkdir -p "$HOME/.config/fex-emu" +[[ -f $HOME/.config/fex-emu/Config.json ]] \ + || printf '{"Config":{"RootFS":"Ubuntu_24_04"}}\n' >"$HOME/.config/fex-emu/Config.json" +out=$(muvm -- FEXBash -c 'uname -m; getconf PAGESIZE' 2>&1 | tr '\n' ' ') +[[ $out == *x86_64*4096* ]] || { echo "muvm/FEX self-test failed: $out" >&2; exit 1; } +echo 'muvm + FEX OK: x86_64 with 4096-byte pages inside the microVM.' From a94d69b383381b6129b83747303adae598dd9186 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:53:40 -0400 Subject: [PATCH 012/798] Share eyes: match the CodexBar panel look (cards, chip, compact type), hide the login URL Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/Indicator.vala | 167 +++++++++++--------- 1 file changed, 95 insertions(+), 72 deletions(-) diff --git a/mseries/share-eyes-wingpanel/Indicator.vala b/mseries/share-eyes-wingpanel/Indicator.vala index d8ad1b389..82c277bcb 100644 --- a/mseries/share-eyes-wingpanel/Indicator.vala +++ b/mseries/share-eyes-wingpanel/Indicator.vala @@ -25,10 +25,12 @@ namespace MSeries { eyes.set_tooltip_text (state.tooltip ()); this.visible = false; - content = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + content = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); + install_style (content.get_display ()); menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { - margin_top = 10, margin_bottom = 10, margin_start = 12, margin_end = 12, width_request = 320 + margin_top = 8, margin_bottom = 8, margin_start = 8, margin_end = 8, width_request = 320 }; + menu.add_css_class ("mseries-share"); menu.append (content); tick.begin (); @@ -87,56 +89,74 @@ namespace MSeries { tick.begin (); } - private Gtk.Widget header (string text) { - var l = new Gtk.Label (text.up ()) { halign = Gtk.Align.START, margin_top = 6, margin_bottom = 2 }; - l.add_css_class ("dim-label"); - l.add_css_class ("caption-heading"); - return l; + // Same visual language as the CodexBar panel: 9pt text, rounded translucent cards, pill chips. + private static void install_style (Gdk.Display display) { + var style = new Gtk.CssProvider (); + style.load_from_string ( + ".mseries-share { font-size: 9pt; } " + + ".mseries-share .caption { font-size: 8pt; } " + + ".mseries-share .card { padding: 4px 10px; border-radius: 8px; background: alpha(currentColor, 0.06); } " + + ".mseries-share .card-title { font-weight: 700; font-size: 8pt; opacity: 0.7; margin-top: 2px; } " + + ".mseries-share .chip { padding: 1px 8px; border-radius: 99px; font-size: 7.5pt; font-weight: 700; " + + " background: alpha(currentColor, 0.1); } " + + ".mseries-share .chip.live { background: alpha(@success_color, 0.22); color: @success_color; } " + + ".mseries-share .chip.ask { background: alpha(@warning_color, 0.22); color: @warning_color; } " + + ".mseries-share .state-title { font-weight: 700; font-size: 10pt; } " + + ".mseries-share .mono { font-family: monospace; } " + + ".mseries-share button { padding: 3px 8px; min-height: 0; } " + + ".mseries-share button.flat { padding: 3px 5px; } " + + ".mseries-share button.primary-wide { padding: 6px 10px; border-radius: 9px; font-weight: 600; } "); + Gtk.StyleContext.add_provider_for_display (display, style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); } private Gtk.Widget dim (string text) { - var l = new Gtk.Label (text) { halign = Gtk.Align.START, wrap = true, xalign = 0, max_width_chars = 38 }; + var l = new Gtk.Label (text) { halign = Gtk.Align.START, wrap = true, xalign = 0, max_width_chars = 40 }; l.add_css_class ("dim-label"); l.add_css_class ("caption"); return l; } - private Gtk.Widget separator () { - return new Gtk.Separator (Gtk.Orientation.HORIZONTAL) { margin_top = 6, margin_bottom = 6 }; - } - - /* A flat, full-width menu row: icon, label, optional detail, like Wingpanel's own menu items. */ - private Gtk.Button menu_row (string icon, string label, string? detail = null) { - var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 9); - row.append (new Gtk.Image.from_icon_name (icon)); - var l = new Gtk.Label (label) { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }; - row.append (l); - if (detail != null) { - var d = new Gtk.Label (detail) { halign = Gtk.Align.END }; - d.add_css_class ("dim-label"); - row.append (d); + private Gtk.Box card (string? title) { + var c = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + c.add_css_class ("card"); + if (title != null) { + var t = new Gtk.Label (title.up ()) { halign = Gtk.Align.START, xalign = 0 }; + t.add_css_class ("card-title"); + c.append (t); } - var b = new Gtk.Button () { child = row }; - b.add_css_class ("flat"); - return b; + return c; } - private Gtk.Widget field_row (string label, string value) { + /* label on the left, value in the middle, an icon button on the right. */ + private Gtk.Widget field_row (string label, string value, string tip, bool mono = true, string? shown = null) { var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); - var name = new Gtk.Label (label) { halign = Gtk.Align.START, width_chars = 9, xalign = 0 }; + var name = new Gtk.Label (label) { halign = Gtk.Align.START, width_chars = 8, xalign = 0 }; name.add_css_class ("dim-label"); - var val = new Gtk.Label (value) { halign = Gtk.Align.START, hexpand = true, xalign = 0, selectable = true, ellipsize = Pango.EllipsizeMode.MIDDLE }; - val.add_css_class ("monospace"); + var val = new Gtk.Label (shown ?? value) { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.MIDDLE }; + if (mono) val.add_css_class ("mono"); row.append (name); row.append (val); - var cp = new Gtk.Button.from_icon_name ("edit-copy-symbolic") { tooltip_text = "Copy " + label.down () }; + var cp = new Gtk.Button.from_icon_name ("edit-copy-symbolic") { tooltip_text = tip }; cp.add_css_class ("flat"); cp.clicked.connect (() => copy (value, label)); row.append (cp); return row; } - private void act (owned GLib.AsyncReadyCallback? cb = null) { + private delegate void RowAction (); + + private Gtk.Widget action_row (string label, string icon, string tip, RowAction cb) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + var name = new Gtk.Label (label) { halign = Gtk.Align.START, hexpand = true, xalign = 0 }; + row.append (name); + var b = new Gtk.Button.from_icon_name (icon) { tooltip_text = tip }; + b.add_css_class ("flat"); + b.clicked.connect (() => cb ()); + row.append (b); + return row; + } + + private void act () { last_digest = ""; tick.begin (); } @@ -145,18 +165,28 @@ namespace MSeries { Gtk.Widget? child; while ((child = content.get_first_child ()) != null) content.remove (child); - // Status card: the icon says it at a glance, the line under it says what to do. - var head = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10) { margin_bottom = 2 }; - var big = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 32, valign = Gtk.Align.START }; - if (client.live) big.add_css_class ("accent"); - head.append (big); - var texts = new Gtk.Box (Gtk.Orientation.VERTICAL, 2) { hexpand = true }; - var title = new Gtk.Label (!state.running ? "Screen share is off" : client.live ? "Sharing your screen" : "Screen share") - { halign = Gtk.Align.START, xalign = 0 }; - title.add_css_class ("title-4"); + int waiting = 0; + foreach (var v in client.viewers) if (v.state == "pending") waiting++; + + // Header: icon, state, a pill chip, and one line about what to do. + var head = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + head.append (new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 28, valign = Gtk.Align.CENTER }); + var texts = new Gtk.Box (Gtk.Orientation.VERTICAL, 1) { hexpand = true, valign = Gtk.Align.CENTER }; + var title = new Gtk.Label (!state.running ? "Screen share is off" : waiting > 0 ? "Someone wants to watch" : client.live ? "Sharing your screen" : "Ready to share") { halign = Gtk.Align.START, xalign = 0 }; + title.add_css_class ("state-title"); texts.append (title); - texts.append (dim (state.running ? tip : "Start the demo with core/scripts/share-demo.sh.")); + int watching = 0; + foreach (var v in client.viewers) if (v.state == "admitted") watching++; + texts.append (dim (!state.running ? "Start the demo with core/scripts/share-demo.sh." + : client.live ? (watching == 0 ? "Nobody is watching yet" : watching == 1 ? "1 person watching" : "%d people watching".printf (watching)) + : "Not sharing anything yet")); head.append (texts); + if (state.running && client.reachable) { + var chip = new Gtk.Label (waiting > 0 ? "WAITING" : client.live ? "LIVE" : "READY") { valign = Gtk.Align.CENTER }; + chip.add_css_class ("chip"); + if (waiting > 0) chip.add_css_class ("ask"); else if (client.live) chip.add_css_class ("live"); + head.append (chip); + } content.append (head); if (!state.running) return; if (!client.reachable) { @@ -165,35 +195,33 @@ namespace MSeries { } if (!client.live) { - var open = new Gtk.Button.with_label ("Open share page and sign in") { margin_top = 6 }; + var open = new Gtk.Button.with_label ("Open share page and sign in"); open.add_css_class ("suggested-action"); + open.add_css_class ("primary-wide"); open.clicked.connect (() => { try { GLib.AppInfo.launch_default_for_uri (state.login_url (), null); } catch (GLib.Error e) { warning ("%s", e.message); } }); content.append (open); - content.append (dim ("Then click Start sharing and pick one window.")); + content.append (dim ("Click Start sharing there, then pick one window.")); } else { - var stop = new Gtk.Button.with_label ("Stop sharing") { margin_top = 6 }; + var stop = new Gtk.Button.with_label ("Stop sharing"); stop.add_css_class ("destructive-action"); + stop.add_css_class ("primary-wide"); stop.clicked.connect (() => client.stop.begin ((o, r) => { client.stop.end (r); act (); })); content.append (stop); } - // Watchers first once someone is waiting: that is the thing you have to act on. - content.append (separator ()); - content.append (header ("Watchers")); + var watchers = card ("Watchers"); if (client.viewers.length == 0) { - content.append (dim ("Nobody yet.")); + watchers.append (dim ("Nobody yet.")); } foreach (var v in client.viewers) { var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); - row.append (new Gtk.Image.from_icon_name (v.state == "pending" ? "avatar-default-symbolic" : "view-reveal-symbolic")); - var who = new Gtk.Label (v.state == "pending" ? "Wants to watch" : "Watching") - { halign = Gtk.Align.START, xalign = 0 }; - var where = new Gtk.Label (v.ip) { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }; + var names = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { hexpand = true, valign = Gtk.Align.CENTER }; + names.append (new Gtk.Label (v.state == "pending" ? "Wants to watch" : "Watching") { halign = Gtk.Align.START, xalign = 0 }); + var where = new Gtk.Label (v.ip) { halign = Gtk.Align.START, xalign = 0 }; where.add_css_class ("dim-label"); - var names = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { hexpand = true }; - names.append (who); + where.add_css_class ("caption"); names.append (where); row.append (names); var vid = v.vid; @@ -211,31 +239,26 @@ namespace MSeries { rm.clicked.connect (() => client.viewer_action.begin (vid, "kick", (o, r) => { client.viewer_action.end (r); act (); })); row.append (rm); } - content.append (row); + watchers.append (row); } + content.append (watchers); - content.append (separator ()); - content.append (header ("Share")); + var links = card ("Links"); var link = client.viewer_link (state.viewer_base); if (link != null) { - var cl = menu_row ("insert-link-symbolic", "Copy viewer link", - "until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%H:%M")); - cl.clicked.connect (() => copy (link, "Viewer link")); - content.append (cl); + links.append (field_row ("Viewer", link, "Copy the link for your viewer", false, "Link for your buddy")); } else { - var mk = menu_row ("list-add-symbolic", "Create viewer link"); - mk.clicked.connect (() => { client.create_link.begin ((o, r) => { client.create_link.end (r); act (); }); }); - content.append (mk); + links.append (action_row ("Viewer link", "list-add-symbolic", "Create a link for your viewer", () => { + client.create_link.begin ((o, r) => { client.create_link.end (r); act (); }); + })); } - var cll = menu_row ("dialog-password-symbolic", "Copy login link"); - cll.tooltip_text = "Opens the share page already signed in. For this computer only; never send it to your viewer."; - cll.clicked.connect (() => copy (state.login_url (), "Login link")); - content.append (cll); + links.append (field_row ("Sign-in", state.login_url (), "Copy a link that signs in on this computer. Never send it to your viewer.", false, "This computer only")); + content.append (links); - content.append (separator ()); - content.append (header ("Login")); - content.append (field_row ("Username", state.email)); - content.append (field_row ("Password", state.password)); + var login = card ("Login"); + login.append (field_row ("Username", state.email, "Copy username")); + login.append (field_row ("Password", state.password, "Copy password")); + content.append (login); if (toast != "") content.append (dim (toast)); } From c83c7987999c4c16442ad607a55dc8c2c0f4af66 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:55:46 -0400 Subject: [PATCH 013/798] Share eyes: UX pass (state-first header, waiting viewers on top, tucked login, live/waiting panel colours, better eyes glyph) Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/Indicator.vala | 165 +++++++++++------- mseries/share-eyes-wingpanel/icons/eyes.svg | 2 +- .../share-eyes-wingpanel/probe/run-probe.sh | 8 +- 3 files changed, 109 insertions(+), 66 deletions(-) diff --git a/mseries/share-eyes-wingpanel/Indicator.vala b/mseries/share-eyes-wingpanel/Indicator.vala index 82c277bcb..0354b44c7 100644 --- a/mseries/share-eyes-wingpanel/Indicator.vala +++ b/mseries/share-eyes-wingpanel/Indicator.vala @@ -15,6 +15,7 @@ namespace MSeries { private string toast = ""; private uint poll_source = 0; private bool busy = false; + private bool details_open = false; public ShareEyesIndicator () { GLib.Object (code_name: "mseries-share-eyes"); @@ -22,6 +23,7 @@ namespace MSeries { construct { eyes = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 18 }; + eyes.add_css_class ("mseries-eyes"); eyes.set_tooltip_text (state.tooltip ()); this.visible = false; @@ -72,7 +74,9 @@ namespace MSeries { : "Sharing your screen · %d people watching".printf (watching)) : "Screen share ready · not sharing yet"; eyes.set_tooltip_text (tip); - if (waiting > 0) eyes.add_css_class ("accent"); else eyes.remove_css_class ("accent"); + eyes.remove_css_class ("live"); + eyes.remove_css_class ("ask"); + if (waiting > 0) eyes.add_css_class ("ask"); else if (state.running && client.live) eyes.add_css_class ("live"); var digest = state.running.to_string () + client.digest () + state.password + toast; if (digest != last_digest) { last_digest = digest; @@ -93,6 +97,8 @@ namespace MSeries { private static void install_style (Gdk.Display display) { var style = new Gtk.CssProvider (); style.load_from_string ( + ".mseries-eyes.live { color: @success_color; } " + + ".mseries-eyes.ask { color: @warning_color; } " + ".mseries-share { font-size: 9pt; } " + ".mseries-share .caption { font-size: 8pt; } " + ".mseries-share .card { padding: 4px 10px; border-radius: 8px; background: alpha(currentColor, 0.06); } " + @@ -161,25 +167,77 @@ namespace MSeries { tick.begin (); } + /* An icon + label row that is one flat button, like a menu item, with a dim detail on the right. */ + private Gtk.Button item (string icon, string label, string? detail, string tip) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 9); + row.append (new Gtk.Image.from_icon_name (icon)); + row.append (new Gtk.Label (label) { halign = Gtk.Align.START, hexpand = true, xalign = 0, ellipsize = Pango.EllipsizeMode.END }); + if (detail != null) { + var d = new Gtk.Label (detail); + d.add_css_class ("dim-label"); + d.add_css_class ("caption"); + row.append (d); + } + var b = new Gtk.Button () { child = row, tooltip_text = tip }; + b.add_css_class ("flat"); + return b; + } + + private Gtk.Widget watchers_card () { + var w = card ("Watchers"); + foreach (var v in client.viewers) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + row.append (new Gtk.Image.from_icon_name (v.state == "pending" ? "avatar-default-symbolic" : "view-reveal-symbolic")); + var names = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { hexpand = true, valign = Gtk.Align.CENTER }; + names.append (new Gtk.Label (v.state == "pending" ? "Wants to watch" : "Watching") { halign = Gtk.Align.START, xalign = 0 }); + var where = new Gtk.Label (v.ip) { halign = Gtk.Align.START, xalign = 0 }; + where.add_css_class ("dim-label"); + where.add_css_class ("caption"); + names.append (where); + row.append (names); + var vid = v.vid; + if (v.state == "pending") { + var deny = new Gtk.Button.with_label ("Deny"); + deny.clicked.connect (() => client.viewer_action.begin (vid, "deny", (o, r) => { client.viewer_action.end (r); act (); })); + var allow = new Gtk.Button.with_label ("Allow"); + allow.add_css_class ("suggested-action"); + allow.clicked.connect (() => client.viewer_action.begin (vid, "admit", (o, r) => { client.viewer_action.end (r); act (); })); + row.append (deny); + row.append (allow); + } else { + var rm = new Gtk.Button.with_label ("Remove"); + rm.add_css_class ("destructive-action"); + rm.clicked.connect (() => client.viewer_action.begin (vid, "kick", (o, r) => { client.viewer_action.end (r); act (); })); + row.append (rm); + } + w.append (row); + } + return w; + } + private void rebuild (string tip) { Gtk.Widget? child; while ((child = content.get_first_child ()) != null) content.remove (child); - int waiting = 0; - foreach (var v in client.viewers) if (v.state == "pending") waiting++; + int waiting = 0, watching = 0; + foreach (var v in client.viewers) { + if (v.state == "pending") waiting++; else if (v.state == "admitted") watching++; + } - // Header: icon, state, a pill chip, and one line about what to do. + // Header: what is happening now, in one glance. A copy confirmation replaces the subtitle for a moment. var head = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); head.append (new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 28, valign = Gtk.Align.CENTER }); var texts = new Gtk.Box (Gtk.Orientation.VERTICAL, 1) { hexpand = true, valign = Gtk.Align.CENTER }; - var title = new Gtk.Label (!state.running ? "Screen share is off" : waiting > 0 ? "Someone wants to watch" : client.live ? "Sharing your screen" : "Ready to share") { halign = Gtk.Align.START, xalign = 0 }; + var title = new Gtk.Label (!state.running ? "Screen share is off" : waiting > 0 ? "Someone wants to watch" : client.live ? "Sharing your screen" : "Ready to share") { + halign = Gtk.Align.START, xalign = 0 }; title.add_css_class ("state-title"); texts.append (title); - int watching = 0; - foreach (var v in client.viewers) if (v.state == "admitted") watching++; - texts.append (dim (!state.running ? "Start the demo with core/scripts/share-demo.sh." + texts.append (dim (toast != "" ? toast + " ✓" + : !state.running ? "Start the demo with core/scripts/share-demo.sh." + : !client.reachable ? "The share server is not answering." + : waiting > 0 ? "Allow or deny below. Nothing is sent until you allow." : client.live ? (watching == 0 ? "Nobody is watching yet" : watching == 1 ? "1 person watching" : "%d people watching".printf (watching)) - : "Not sharing anything yet")); + : "Click Start sharing on the page, then pick one window")); head.append (texts); if (state.running && client.reachable) { var chip = new Gtk.Label (waiting > 0 ? "WAITING" : client.live ? "LIVE" : "READY") { valign = Gtk.Align.CENTER }; @@ -188,13 +246,18 @@ namespace MSeries { head.append (chip); } content.append (head); - if (!state.running) return; - if (!client.reachable) { - content.append (dim ("The share server is not answering.")); - return; - } + if (!state.running || !client.reachable) return; + + // Anyone waiting is the one thing that needs you: it goes straight under the header. + if (waiting > 0) content.append (watchers_card ()); - if (!client.live) { + if (client.live) { + var stop = new Gtk.Button.with_label ("Stop sharing"); + stop.add_css_class ("destructive-action"); + stop.add_css_class ("primary-wide"); + stop.clicked.connect (() => client.stop.begin ((o, r) => { client.stop.end (r); act (); })); + content.append (stop); + } else { var open = new Gtk.Button.with_label ("Open share page and sign in"); open.add_css_class ("suggested-action"); open.add_css_class ("primary-wide"); @@ -202,64 +265,38 @@ namespace MSeries { try { GLib.AppInfo.launch_default_for_uri (state.login_url (), null); } catch (GLib.Error e) { warning ("%s", e.message); } }); content.append (open); - content.append (dim ("Click Start sharing there, then pick one window.")); - } else { - var stop = new Gtk.Button.with_label ("Stop sharing"); - stop.add_css_class ("destructive-action"); - stop.add_css_class ("primary-wide"); - stop.clicked.connect (() => client.stop.begin ((o, r) => { client.stop.end (r); act (); })); - content.append (stop); } - var watchers = card ("Watchers"); - if (client.viewers.length == 0) { - watchers.append (dim ("Nobody yet.")); - } - foreach (var v in client.viewers) { - var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); - var names = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { hexpand = true, valign = Gtk.Align.CENTER }; - names.append (new Gtk.Label (v.state == "pending" ? "Wants to watch" : "Watching") { halign = Gtk.Align.START, xalign = 0 }); - var where = new Gtk.Label (v.ip) { halign = Gtk.Align.START, xalign = 0 }; - where.add_css_class ("dim-label"); - where.add_css_class ("caption"); - names.append (where); - row.append (names); - var vid = v.vid; - if (v.state == "pending") { - var deny = new Gtk.Button.with_label ("Deny"); - deny.clicked.connect (() => client.viewer_action.begin (vid, "deny", (o, r) => { client.viewer_action.end (r); act (); })); - var allow = new Gtk.Button.with_label ("Allow"); - allow.add_css_class ("suggested-action"); - allow.clicked.connect (() => client.viewer_action.begin (vid, "admit", (o, r) => { client.viewer_action.end (r); act (); })); - row.append (deny); - row.append (allow); - } else { - var rm = new Gtk.Button.with_label ("Remove"); - rm.add_css_class ("destructive-action"); - rm.clicked.connect (() => client.viewer_action.begin (vid, "kick", (o, r) => { client.viewer_action.end (r); act (); })); - row.append (rm); - } - watchers.append (row); - } - content.append (watchers); + if (waiting == 0 && watching > 0) content.append (watchers_card ()); - var links = card ("Links"); + // Links: one flat menu-style item each, so the card reads as a short list. + var links = card (null); var link = client.viewer_link (state.viewer_base); if (link != null) { - links.append (field_row ("Viewer", link, "Copy the link for your viewer", false, "Link for your buddy")); + var cv = item ("insert-link-symbolic", "Copy viewer link", + "until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%H:%M"), "Copy the link for your viewer"); + cv.clicked.connect (() => copy (link, "Viewer link")); + links.append (cv); } else { - links.append (action_row ("Viewer link", "list-add-symbolic", "Create a link for your viewer", () => { - client.create_link.begin ((o, r) => { client.create_link.end (r); act (); }); - })); + var mk = item ("list-add-symbolic", "Create viewer link", null, "Make a link to send to your viewer"); + mk.clicked.connect (() => { client.create_link.begin ((o, r) => { client.create_link.end (r); act (); }); }); + links.append (mk); } - links.append (field_row ("Sign-in", state.login_url (), "Copy a link that signs in on this computer. Never send it to your viewer.", false, "This computer only")); + var cs = item ("dialog-password-symbolic", "Copy sign-in link", "this computer only", + "Opens the share page already signed in. Never send it to your viewer."); + cs.clicked.connect (() => copy (state.login_url (), "Sign-in link")); + links.append (cs); content.append (links); - var login = card ("Login"); - login.append (field_row ("Username", state.email, "Copy username")); - login.append (field_row ("Password", state.password, "Copy password")); - content.append (login); - if (toast != "") content.append (dim (toast)); + // Raw login is a fallback for another browser, so it is tucked away. + var more = new Gtk.Expander ("Login details") { margin_top = 2 }; + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 2) { margin_top = 4 }; + box.append (field_row ("Username", state.email, "Copy username")); + box.append (field_row ("Password", state.password, "Copy password")); + more.child = box; + more.expanded = details_open; + more.notify["expanded"].connect (() => { details_open = more.expanded; }); + content.append (more); } public override Gtk.Widget get_display_widget () { diff --git a/mseries/share-eyes-wingpanel/icons/eyes.svg b/mseries/share-eyes-wingpanel/icons/eyes.svg index 3ed916d71..42083f556 100644 --- a/mseries/share-eyes-wingpanel/icons/eyes.svg +++ b/mseries/share-eyes-wingpanel/icons/eyes.svg @@ -1 +1 @@ - + diff --git a/mseries/share-eyes-wingpanel/probe/run-probe.sh b/mseries/share-eyes-wingpanel/probe/run-probe.sh index 3d7547dbe..75e7669df 100755 --- a/mseries/share-eyes-wingpanel/probe/run-probe.sh +++ b/mseries/share-eyes-wingpanel/probe/run-probe.sh @@ -21,4 +21,10 @@ pass=$(python3 -I -c "import json,sys;print(json.load(open('$run/rupert-share/st jar="$home/jar"; curl -fs -c "$jar" "http://127.0.0.1:$port/auth/login" -H 'content-type: application/json' -d "{\"email\":\"$email\",\"password\":\"$pass\"}" >/dev/null tok=$(curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d '{}' "http://127.0.0.1:$port/api/v1/share/links" | python3 -I -c "import json,sys;print(json.load(sys.stdin)['token'])") probe with-link -echo "tok=$tok" >"$home/tok" +# go live with a waiting viewer, then admit it (the same calls the popover and owner page make) +api() { curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d "${3:-{\}}" "http://127.0.0.1:$port/api/v1/share/$2"; } +api x start >/dev/null +vid=$(curl -fs -H "authorization: Bearer $tok" -X POST "http://127.0.0.1:$port/api/v1/share/view/join" | python3 -I -c "import json,sys;print(json.load(sys.stdin)['vid'])") +probe waiting +api x "viewers/$vid/admit" >/dev/null +probe watching From 38fa730107988b1ed219bc08b026511e7aed6751 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 12:56:24 -0400 Subject: [PATCH 014/798] Add Omnissa Horizon Client (x64) under FEX/muvm installer, --help verified Co-Authored-By: Claude Sonnet 5.5 --- mseries/HORIZON-CLIENT.md | 22 ++++++++++ mseries/install-horizon-client.sh | 73 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 mseries/HORIZON-CLIENT.md create mode 100755 mseries/install-horizon-client.sh diff --git a/mseries/HORIZON-CLIENT.md b/mseries/HORIZON-CLIENT.md new file mode 100644 index 000000000..adab919a2 --- /dev/null +++ b/mseries/HORIZON-CLIENT.md @@ -0,0 +1,22 @@ +# Omnissa Horizon Client (x64, emulated) + +Omnissa publishes no arm64 Linux Horizon Client: the 2606 tarball has x64 and 32-bit armhf only, and Apple silicon +cannot run armhf. The source is closed, so there is nothing to port. `install-horizon-client.sh` runs Omnissa's +own x64 client under FEX inside a 4 KB-page microVM, using the muvm setup from `install-muvm.sh`. + + bash mseries/install-muvm.sh # once + bash mseries/install-horizon-client.sh # install to ~/.local/opt/omnissa-horizon + horizon-client-x64 -s broker.example.com + bash mseries/install-horizon-client.sh --uninstall + +- Download: `Omnissa-Horizon-Client-Linux-2606-8.19.0-32216036411.tar.gz` from download3.omnissa.com (about 341 MB), + sha256 pinned in the script (it matches the Arch AUR pin). Nothing proprietary is committed to the repo. +- Four Ubuntu 24.04 x64 libraries the FEX rootfs lacks (libxkbfile, libpcsclite, libv4l, lttng-ust and their + dependencies) are fetched with pinned sha256s into `extra/`. `liblttng-ust.so.0` is not provided; only the + optional `horizon-client-next` bundle needs it. +- muvm allows one VM at a time (`muvm.lock`); a second `muvm` call prints nothing while another runs. + +## Verified 2026-10-18 +`horizon-client-x64 --help` prints the client's usage through FEX/muvm. Not tested: opening the GUI, logging in to a +broker, Blast or PCoIP sessions, USB redirection, audio, performance under emulation. Until those are checked, +`horizon-rdp` ([HORIZON-RDP.md](HORIZON-RDP.md)) remains the tested-to-install fallback. diff --git a/mseries/install-horizon-client.sh b/mseries/install-horizon-client.sh new file mode 100755 index 000000000..206946e3f --- /dev/null +++ b/mseries/install-horizon-client.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# EXPERIMENTAL: Omnissa's official x64 Horizon Client for Linux, run under FEX inside muvm (needs install-muvm.sh first). +# Omnissa ships no arm64 Linux build (only x64, and 32-bit armhf which Apple silicon cannot run). The proprietary +# tarball is downloaded from Omnissa at install time, checked against a pinned sha256, and never committed here. +# install-horizon-client.sh install under ~/.local/opt/omnissa-horizon +# install-horizon-client.sh --uninstall remove it +set -euo pipefail +[[ $(uname -m) == aarch64 ]] || { echo 'This installer requires ARM64' >&2; exit 1; } +command -v muvm >/dev/null || { echo 'Run mseries/install-muvm.sh first.' >&2; exit 1; } +ver=2606-8.19.0-32216036411 +sha=e64a5900f7a26384776bc5912cb8d62e77565c686aae0eeb0b877a101a02d80a +url=https://download3.omnissa.com/software/CART27FQ2_LIN_2606_TARBALL/Omnissa-Horizon-Client-Linux-$ver.tar.gz +dest=$HOME/.local/opt/omnissa-horizon +bin=$HOME/.local/bin/horizon-client-x64 +entry=$HOME/.local/share/applications/horizon-client-x64.desktop + +if [[ ${1:-} == --uninstall ]]; then + rm -rf "$dest" "$bin" "$entry" + echo 'Removed.' + exit 0 +fi + +tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT +curl -fL --silent --show-error "$url" -o "$tmp/h.tar.gz" +echo "$sha $tmp/h.tar.gz" | sha256sum -c - +tar xzf "$tmp/h.tar.gz" -C "$tmp" +rm -rf "$dest"; mkdir -p "$dest" +for part in Client PCoIP; do + tar xzf "$tmp"/*/x64/Omnissa-Horizon-$part-"$ver".x64.tar.gz -C "$dest" --strip-components=1 +done + +# Ubuntu 24.04 x64 libraries the FEX rootfs lacks (pinned path + sha256 from the noble archive index). +while read -r file digest; do + curl -fsS "https://archive.ubuntu.com/ubuntu/$file" -o "$tmp/lib.deb" + echo "$digest $tmp/lib.deb" | sha256sum -c - >/dev/null + dpkg-deb -x "$tmp/lib.deb" "$dest/extra" +done <"$dest/run.sh" <"$bin" <"$entry" < Date: Sun, 18 Oct 2026 13:18:06 -0400 Subject: [PATCH 015/798] Compact low network panel rates and reveal values on Command-hover --- mseries/OS9-HANDOFF.md | 6 + mseries/ROADMAP.md | 2 +- mseries/install-network-throughput.sh | 1 + mseries/patches/network-panel-compact.patch | 115 ++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 mseries/patches/network-panel-compact.patch diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 6337c9999..89706110e 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -2752,3 +2752,9 @@ Owner's two live F5 attempts (05:56) recognized 0 characters. Reproduced without Upstream's Settings window (Qt Quick, `QQuickStyle::setStyle("Fusion")` hard-coded in `main.cpp`; confirmed that `QT_QUICK_CONTROLS_STYLE`, the GTK3 platform theme and the elementary stylesheet do not change it) looked dated. We now ship a native GTK4/Granite replacement, `codexbar-settings` (Vala, `mseries/codexbar-wingpanel/settings/`), with General, Display and Providers pages in elementary's settings style: cards with switches, spin buttons and drop-downs, company marks in the provider list, reorder/remove/add providers, start at login. No Save button: changes apply after a short pause through `codexbar-linux --configure` and an error from the app is shown inline. The panel's Settings button opens it (upstream's Qt window stays as the fallback, and the tray menu's Settings… still opens the Qt one). OpenCode Go's Sign in… now opens opencode.ai. Verified: model unit tests (3) and Meson (4 suites); headless renders of all pages in light and dark; an isolated headless app accepted a `--configure` patch (linux.json updated) and rejected `refreshSeconds: 5` with an error. Not verified: the window on the live desktop by eye, and start at login toggling for real. `deploy-codexbar.sh` installs the binary to `~/.local/bin/codexbar-settings`; `CODEXBAR-PORT.md` lists the files and the upgrade diff to check. + +## Network panel: compact low rates and Command-hover — 2026-10-18 + +`network-panel-compact.patch`, applied after saturation by `install-network-throughput.sh`, shows `<1 MB/s` independently for down/up below 1,000,000 bytes/s. The panel retains decimal byte units and its existing 10-second average. Command (Super/Meta) while hovering reveals the existing measured B/s, kB/s or MB/s values; releasing it or leaving restores compact values. A motion controller samples modifiers every 50 ms only while hovered so pressing Command after entering works without keyboard focus. Unmap/dispose removes the hover timer; dispose also removes the rate timer. The less-than label is escaped before Pango markup, preserving saturation highlighting. The image payload already ships every patches/*.patch file. + +Verified: all installer patches apply against pinned panel-network `8685ec8e55a45e48da0c12931a353cd440d11664`, full release Meson/Ninja build and link, no missing shared libraries, bash syntax check. An isolated GTK/Xvfb harness checked 0, 999, 1,000, 999,999, 1,000,000 and 2,000,000 bytes/s in compact/exact modes and the rendered literal less-than. Installed with `bash mseries/install-network-throughput.sh`; Wingpanel reloaded and remains running, no network/markup errors in the reload logs. Live Command-hover by eye remains unverified; next check is owner hover/Command/release on the menu bar. This affects the compact panel text; popover graph readings retain their existing precision. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ff4ad72ad..1b98fabb6 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -263,7 +263,7 @@ with the corrected live-session hardware overlay before progressing. | Login-screen stall fixed | `lightdm` drop-in waits for the eDP connector; display crossbar in the fairydust initramfs (`install-initramfs-crossbar.sh`) | greeter 119.7 s -> 0.82 s; dp-xbar deferrals 14 -> 0; first-dcp -> DRM 2.57 s -> 0.01 s | | USB4 dock data (hub, mouse, headset, 1 Gb/s Ethernet) | Custom kernel `fairydust-usb4`, m1n1 + DTBs, thunderbolt quirks (`mseries/fairydust/usb4/`) | checks `dock-usb4-ethernet`, `dock-dp-retry-silent` passed | | Boots Linux by default | Fairydust default helper is implemented, preserving stock Asahi as recovery; applying the persistent override is still pending. The last recorded stock-kernel boot lost dock mouse and Ethernet. | Apply the helper, then confirm kernel, m1n1/DTB chain, dock USB and Ethernet on-device. | -| Network indicator speed, connected-only Wi-Fi list, and usage views | Installed on the M1 Pro; user `netusage` service is active. Manual post-install includes the panel module and user logger. Overlay now includes the matching panel package for version-gated plugin compilation, then preserves the plugin. | Rebuild the image and verify the plugin loads against the final panel; check joined-network collapse, chevron expansion, and menu rendering. | +| Network indicator speed, connected-only Wi-Fi list, and usage views | Panel rates below 1 MB/s now display `<1 MB/s`; Command-hover reveals measured values (built and installed October 18; live hover check pending). Installed on the M1 Pro; user `netusage` service is active. Manual post-install includes the panel module and user logger. Overlay now includes the matching panel package for version-gated plugin compilation, then preserves the plugin. | Rebuild the image and verify the plugin loads against the final panel; check joined-network collapse, chevron expansion, and menu rendering. | | Rounded screen corners | patched Gala, radius 14 (`install-gala-screen-corners.sh`) | **needs your look**: `gala-corners-look` | | Pastel blue/rose theme | `install-theme-tweaks.sh` | **needs your look**: `colors-look` | | Memory/swap/service tuning | `install-system-tuning.sh` (zram, no ModemManager/AppCenter daemon) | `zram-swap`, `services-trimmed` | diff --git a/mseries/install-network-throughput.sh b/mseries/install-network-throughput.sh index 095d06c28..b82caf166 100755 --- a/mseries/install-network-throughput.sh +++ b/mseries/install-network-throughput.sh @@ -58,6 +58,7 @@ git -C "$source_dir" apply "$repo/mseries/patches/network-throughput.patch" git -C "$source_dir" apply "$repo/mseries/patches/network-null-devices.patch" git -C "$source_dir" apply "$repo/mseries/patches/network-panel-speed.patch" git -C "$source_dir" apply "$repo/mseries/patches/network-saturation.patch" +git -C "$source_dir" apply "$repo/mseries/patches/network-panel-compact.patch" git -C "$source_dir" apply \ --include=data/Indicator.css \ --include=src/Widgets/UsageWidget.vala \ diff --git a/mseries/patches/network-panel-compact.patch b/mseries/patches/network-panel-compact.patch new file mode 100644 index 000000000..ae57ebef6 --- /dev/null +++ b/mseries/patches/network-panel-compact.patch @@ -0,0 +1,115 @@ +--- a/src/Widgets/PanelSpeed.vala ++++ b/src/Widgets/PanelSpeed.vala +@@ -59,6 +59,12 @@ + private int64 last_time = 0; + private bool have_baseline = false; + ++ private double down = 0; ++ private double up = 0; ++ private bool have_rate = false; ++ private bool show_exact = false; ++ private uint hover_timeout_id = 0; ++ + construct { + speed_label = new Gtk.Label (null) { + use_markup = true +@@ -68,15 +74,79 @@ + margin_start = 4; + visible = false; + ++ tooltip_text = _("Hold Command while hovering to show measured speeds"); ++ var motion = new Gtk.EventControllerMotion (); ++ motion.enter.connect (() => { ++ update_hover (); ++ if (hover_timeout_id == 0) { ++ // Modifier events go to the focused application, so sample the keyboard ++ // only while hovered, including when Command is pressed after entering. ++ hover_timeout_id = Timeout.add (50, update_hover); ++ } ++ }); ++ motion.leave.connect (stop_hover); ++ add_controller (motion); ++ unmap.connect (stop_hover); ++ + var settings = PanelSpeedSettings.get_default (); + settings.notify["enabled"].connect (sync); + sync (); ++ } ++ ++ public override void dispose () { ++ stop_hover (); ++ if (timeout_id != 0) { ++ Source.remove (timeout_id); ++ timeout_id = 0; ++ } ++ base.dispose (); ++ } ++ ++ private bool update_hover () { ++ var seat = get_display ().get_default_seat (); ++ var keyboard = seat != null ? seat.get_keyboard () : null; ++ var state = keyboard != null ? keyboard.get_modifier_state () : (Gdk.ModifierType) 0; ++ bool exact = (state & (Gdk.ModifierType.SUPER_MASK | Gdk.ModifierType.META_MASK)) != 0; ++ if (show_exact != exact) { ++ show_exact = exact; ++ render_rates (); ++ } ++ return Source.CONTINUE; ++ } ++ ++ private void stop_hover () { ++ if (hover_timeout_id != 0) { ++ Source.remove (hover_timeout_id); ++ hover_timeout_id = 0; ++ } ++ show_exact = false; ++ render_rates (); ++ } ++ ++ public static string format_panel_rate (double rate, bool exact) { ++ if (!exact && rate < 1000000.0) { ++ return "<1 MB/s"; ++ } ++ return ThroughputWidget.format_rate (rate); ++ } ++ ++ private void render_rates () { ++ if (!have_rate) { ++ return; ++ } ++ double down_capacity, up_capacity; ++ ThroughputWidget.get_capacity (out down_capacity, out up_capacity); ++ speed_label.label = "↓ %s ↑ %s".printf ( ++ ThroughputWidget.tint (Markup.escape_text (format_panel_rate (down, show_exact)), ThroughputWidget.is_saturated (down, down_capacity)), ++ ThroughputWidget.tint (Markup.escape_text (format_panel_rate (up, show_exact)), ThroughputWidget.is_saturated (up, up_capacity)) ++ ); + } + + private void sync () { + if (PanelSpeedSettings.get_default ().enabled) { + if (timeout_id == 0) { + have_baseline = false; ++ have_rate = false; + speed_label.label = "↓ … ↑ …"; + visible = true; + tick (); +@@ -100,15 +170,10 @@ + var now = get_monotonic_time (); + if (have_baseline && now > last_time && rx >= last_rx && tx >= last_tx) { + double seconds = (now - last_time) / 1000000.0; +- double down = (rx - last_rx) / seconds; +- double up = (tx - last_tx) / seconds; +- double down_capacity, up_capacity; +- ThroughputWidget.get_capacity (out down_capacity, out up_capacity); +- +- speed_label.label = "↓ %s ↑ %s".printf ( +- ThroughputWidget.tint (ThroughputWidget.format_rate (down), ThroughputWidget.is_saturated (down, down_capacity)), +- ThroughputWidget.tint (ThroughputWidget.format_rate (up), ThroughputWidget.is_saturated (up, up_capacity)) +- ); ++ down = (rx - last_rx) / seconds; ++ up = (tx - last_tx) / seconds; ++ have_rate = true; ++ render_rates (); + } + + last_rx = rx; From bb43eef646dc7ed9825a53ebf40ec3d4ac02b998 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 13:25:58 -0400 Subject: [PATCH 016/798] Add persistent battery watts switch and reliable network hover --- mseries/BATTERY-ETA.md | 6 ++ mseries/OS9-HANDOFF.md | 8 +++ mseries/ROADMAP.md | 4 +- mseries/install-battery-eta.sh | 2 +- mseries/patches/network-panel-compact.patch | 47 +++++-------- .../patches/panel-power-watts-toggle.patch | 67 +++++++++++++++++++ 6 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 mseries/patches/panel-power-watts-toggle.patch diff --git a/mseries/BATTERY-ETA.md b/mseries/BATTERY-ETA.md index 344296e77..ad55af12d 100644 --- a/mseries/BATTERY-ETA.md +++ b/mseries/BATTERY-ETA.md @@ -49,3 +49,9 @@ Update (2026-10-15): the label now shows the whole machine's power from the SMC version showed UPower's battery flow, which is exactly 0 W when the charger is connected and the battery sits at the 80% limit, so the label hid and the owner reported the meter as lost. Falls back to UPower's rate if the sensor is missing. Compiled and installed on the M1 Pro; how it looks in the panel is for the owner to confirm. + +## Menu-bar watts toggle — 2026-10-18 + +Open the battery menu and turn **Show Watts in Menu Bar** off to hide the watts label immediately. Turn it on to show the latest reading again. The choice persists in `~/.config/mseries/battery-meter.ini` (`[panel] show-watts`), shared by the menu and bar; missing configuration keeps the existing enabled default. Percentage and charging-time displays keep their existing controls. + +The installer applies `panel-power-watts-toggle.patch` after the ETA/watts patches. Verified with a full release build and an isolated GTK harness: menu binding hides/shows the label immediately, later sensor updates cannot unhide it, and a second process restores the saved off choice. Installed live with `bash mseries/install-battery-eta.sh`; Wingpanel restarted successfully. The live menu still needs an owner visual check. diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 89706110e..c0db7dfea 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -2758,3 +2758,11 @@ Verified: model unit tests (3) and Meson (4 suites); headless renders of all pag `network-panel-compact.patch`, applied after saturation by `install-network-throughput.sh`, shows `<1 MB/s` independently for down/up below 1,000,000 bytes/s. The panel retains decimal byte units and its existing 10-second average. Command (Super/Meta) while hovering reveals the existing measured B/s, kB/s or MB/s values; releasing it or leaving restores compact values. A motion controller samples modifiers every 50 ms only while hovered so pressing Command after entering works without keyboard focus. Unmap/dispose removes the hover timer; dispose also removes the rate timer. The less-than label is escaped before Pango markup, preserving saturation highlighting. The image payload already ships every patches/*.patch file. Verified: all installer patches apply against pinned panel-network `8685ec8e55a45e48da0c12931a353cd440d11664`, full release Meson/Ninja build and link, no missing shared libraries, bash syntax check. An isolated GTK/Xvfb harness checked 0, 999, 1,000, 999,999, 1,000,000 and 2,000,000 bytes/s in compact/exact modes and the rendered literal less-than. Installed with `bash mseries/install-network-throughput.sh`; Wingpanel reloaded and remains running, no network/markup errors in the reload logs. Live Command-hover by eye remains unverified; next check is owner hover/Command/release on the menu bar. This affects the compact panel text; popover graph readings retain their existing precision. + +## Battery watts switch and network hover repair — 2026-10-18 + +Owner reported Command-hover did not work. This desktop uses `ctrl:swap_lwin_lctl`, so physical Command maps to Control, which the original mask missed. A keyboard injection harness confirmed Control support, but Wayland modifier updates also depend on keyboard focus. The final fix avoids this dependency: hovering anywhere over the network icon/readout reveals measured values, with Command optional; leaving/unmapping restores compact `<1 MB/s` values. The motion controller now attaches in capture phase to the whole display. The modifier polling timer was removed. The existing 10-second network sampling interval remains. + +Battery menu now includes **Show Watts in Menu Bar**, alongside Show Percentage. `panel-power-watts-toggle.patch` adds a shared per-user preference in `~/.config/mseries/battery-meter.ini`, enabled by default; binding it to the watts revealer visibility keeps it hidden across sensor updates and restores the latest reading when re-enabled. `install-battery-eta.sh` applies the third patch; no system GSettings schemas are changed. + +Verified: both full release Meson/Ninja builds; installer bash syntax; GTK harness for whole-display hover, leave and unmap transitions; battery switch binding, immediate hide/show, hidden state across fresh readings, saved-off reload in a second process. Both installers ran on the desktop; power restart passed the repair script's survival check. Final network build/reinstall and panel survival verified. Live hover and switch by eye remain for the owner. Changes committed/pushed on the dedicated network-panel-compact branch because the shared checkout branch has divergent remote history; unrelated working files are preserved. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 1b98fabb6..27b3d10af 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -263,7 +263,7 @@ with the corrected live-session hardware overlay before progressing. | Login-screen stall fixed | `lightdm` drop-in waits for the eDP connector; display crossbar in the fairydust initramfs (`install-initramfs-crossbar.sh`) | greeter 119.7 s -> 0.82 s; dp-xbar deferrals 14 -> 0; first-dcp -> DRM 2.57 s -> 0.01 s | | USB4 dock data (hub, mouse, headset, 1 Gb/s Ethernet) | Custom kernel `fairydust-usb4`, m1n1 + DTBs, thunderbolt quirks (`mseries/fairydust/usb4/`) | checks `dock-usb4-ethernet`, `dock-dp-retry-silent` passed | | Boots Linux by default | Fairydust default helper is implemented, preserving stock Asahi as recovery; applying the persistent override is still pending. The last recorded stock-kernel boot lost dock mouse and Ethernet. | Apply the helper, then confirm kernel, m1n1/DTB chain, dock USB and Ethernet on-device. | -| Network indicator speed, connected-only Wi-Fi list, and usage views | Panel rates below 1 MB/s now display `<1 MB/s`; Command-hover reveals measured values (built and installed October 18; live hover check pending). Installed on the M1 Pro; user `netusage` service is active. Manual post-install includes the panel module and user logger. Overlay now includes the matching panel package for version-gated plugin compilation, then preserves the plugin. | Rebuild the image and verify the plugin loads against the final panel; check joined-network collapse, chevron expansion, and menu rendering. | +| Network indicator speed, connected-only Wi-Fi list, and usage views | Panel rates below 1 MB/s now display `<1 MB/s`; Hover over the icon/readout to reveal measured values (Command optional; built and installed October 18; GTK interaction check passed, live visual check pending). Installed on the M1 Pro; user `netusage` service is active. Manual post-install includes the panel module and user logger. Overlay now includes the matching panel package for version-gated plugin compilation, then preserves the plugin. | Rebuild the image and verify the plugin loads against the final panel; check joined-network collapse, chevron expansion, and menu rendering. | | Rounded screen corners | patched Gala, radius 14 (`install-gala-screen-corners.sh`) | **needs your look**: `gala-corners-look` | | Pastel blue/rose theme | `install-theme-tweaks.sh` | **needs your look**: `colors-look` | | Memory/swap/service tuning | `install-system-tuning.sh` (zram, no ModemManager/AppCenter daemon) | `zram-swap`, `services-trimmed` | @@ -362,7 +362,7 @@ Performance work stays measured: use `mseries/benchmarks/` before and after each 5. **Publish benchmark history** for every kernel/Mesa update, plot trends. 6. **Install media / transform script** for other M1/M2 Macs (see `TRANSFORM-DESIGN.md`). 7. **Tether continuity add-on**: pinned, removable ARM64 package installer and optional Brave integration are implemented (merged to main 2026-10-14); iPhone/Brave qualification remains. Current BlueZ 5.85 is below Tether's 5.86 minimum for Messages. See [`TETHER-CONTINUITY.md`](TETHER-CONTINUITY.md). -8. **Native battery time-to-full:** implemented as an optional patch and installer for elementary's Power indicator; built and installed on the M1 desktop. See [`BATTERY-ETA.md`](BATTERY-ETA.md). +8. **Native battery time-to-full:** battery menu also has a persistent Show Watts in Menu Bar toggle (October 18); implemented as an optional patch and installer for elementary's Power indicator; built and installed on the M1 desktop. See [`BATTERY-ETA.md`](BATTERY-ETA.md). 9. **Torrential ARM64 native app port and optimization.** Native GTK/Granite package installed on the desktop; official Ubuntu torrent data transfer and full-ISO SHA-256 verified; RPC pause/resume checks passed. UI interaction, diff --git a/mseries/install-battery-eta.sh b/mseries/install-battery-eta.sh index f0ccdcec9..5695c7847 100755 --- a/mseries/install-battery-eta.sh +++ b/mseries/install-battery-eta.sh @@ -46,7 +46,7 @@ git -C "$source_dir" remote add origin https://github.com/elementary/panel-power git -C "$source_dir" fetch -q --depth=1 origin "$commit" git -C "$source_dir" checkout -q --detach FETCH_HEAD # Order matters: the watt meter patch is written against the source with the ETA patch applied. -for patch in panel-power-charge-eta.patch panel-power-watts.patch; do +for patch in panel-power-charge-eta.patch panel-power-watts.patch panel-power-watts-toggle.patch; do git -C "$source_dir" apply "$repo/mseries/patches/$patch" done diff --git a/mseries/patches/network-panel-compact.patch b/mseries/patches/network-panel-compact.patch index ae57ebef6..fe2e616c8 100644 --- a/mseries/patches/network-panel-compact.patch +++ b/mseries/patches/network-panel-compact.patch @@ -1,6 +1,6 @@ --- a/src/Widgets/PanelSpeed.vala +++ b/src/Widgets/PanelSpeed.vala -@@ -59,6 +59,12 @@ +@@ -59,6 +59,11 @@ private int64 last_time = 0; private bool have_baseline = false; @@ -8,27 +8,32 @@ + private double up = 0; + private bool have_rate = false; + private bool show_exact = false; -+ private uint hover_timeout_id = 0; + construct { speed_label = new Gtk.Label (null) { use_markup = true -@@ -68,15 +74,79 @@ +@@ -68,15 +73,69 @@ margin_start = 4; visible = false; -+ tooltip_text = _("Hold Command while hovering to show measured speeds"); -+ var motion = new Gtk.EventControllerMotion (); ++ tooltip_text = _("Hover to show measured speeds"); ++ var motion = new Gtk.EventControllerMotion () { ++ propagation_phase = Gtk.PropagationPhase.CAPTURE ++ }; + motion.enter.connect (() => { -+ update_hover (); -+ if (hover_timeout_id == 0) { -+ // Modifier events go to the focused application, so sample the keyboard -+ // only while hovered, including when Command is pressed after entering. -+ hover_timeout_id = Timeout.add (50, update_hover); -+ } ++ show_exact = true; ++ render_rates (); + }); + motion.leave.connect (stop_hover); -+ add_controller (motion); ++ // Attach to the icon's whole display, including the text and icon hit areas. ++ notify["parent"].connect (() => { ++ if (motion.get_widget () != null) { ++ motion.get_widget ().remove_controller (motion); ++ } ++ if (parent != null) { ++ parent.add_controller (motion); ++ } ++ }); + unmap.connect (stop_hover); + var settings = PanelSpeedSettings.get_default (); @@ -45,23 +50,7 @@ + base.dispose (); + } + -+ private bool update_hover () { -+ var seat = get_display ().get_default_seat (); -+ var keyboard = seat != null ? seat.get_keyboard () : null; -+ var state = keyboard != null ? keyboard.get_modifier_state () : (Gdk.ModifierType) 0; -+ bool exact = (state & (Gdk.ModifierType.SUPER_MASK | Gdk.ModifierType.META_MASK)) != 0; -+ if (show_exact != exact) { -+ show_exact = exact; -+ render_rates (); -+ } -+ return Source.CONTINUE; -+ } -+ + private void stop_hover () { -+ if (hover_timeout_id != 0) { -+ Source.remove (hover_timeout_id); -+ hover_timeout_id = 0; -+ } + show_exact = false; + render_rates (); + } @@ -93,7 +82,7 @@ speed_label.label = "↓ … ↑ …"; visible = true; tick (); -@@ -100,15 +170,10 @@ +@@ -100,15 +159,10 @@ var now = get_monotonic_time (); if (have_baseline && now > last_time && rx >= last_rx && tx >= last_tx) { double seconds = (now - last_time) / 1000000.0; diff --git a/mseries/patches/panel-power-watts-toggle.patch b/mseries/patches/panel-power-watts-toggle.patch new file mode 100644 index 000000000..c549f26c5 --- /dev/null +++ b/mseries/patches/panel-power-watts-toggle.patch @@ -0,0 +1,67 @@ +--- a/src/Widgets/DisplayWidget.vala ++++ b/src/Widgets/DisplayWidget.vala +@@ -78,6 +78,9 @@ + + append (charge_eta_revealer); + append (power_draw_revealer); ++ WattSettings.get_default ().bind_property ( ++ "enabled", power_draw_revealer, "visible", GLib.BindingFlags.SYNC_CREATE ++ ); + + settings.bind ("show-percentage", percent_revealer, "reveal-child", GLib.SettingsBindFlags.GET); + bind_property ("allow-percent", percent_revealer, "visible", GLib.BindingFlags.SYNC_CREATE); +@@ -149,3 +152,38 @@ + } + } + } ++ ++/** Per-user preference shared by the menu and the bar, without replacing system schemas. */ ++public class Power.Widgets.WattSettings : Object { ++ private static WattSettings? instance = null; ++ public bool enabled { get; set; default = true; } ++ ++ public static unowned WattSettings get_default () { ++ if (instance == null) instance = new WattSettings (); ++ return instance; ++ } ++ ++ private static string path () { ++ return Path.build_filename (Environment.get_user_config_dir (), "mseries", "battery-meter.ini"); ++ } ++ ++ construct { ++ try { ++ var file = new KeyFile (); ++ file.load_from_file (path (), KeyFileFlags.NONE); ++ enabled = file.get_boolean ("panel", "show-watts"); ++ } catch (Error e) { ++ // Preserve the existing display until the owner chooses otherwise. ++ } ++ notify["enabled"].connect (() => { ++ var file = new KeyFile (); ++ file.set_boolean ("panel", "show-watts", enabled); ++ DirUtils.create_with_parents (Path.get_dirname (path ()), 0755); ++ try { ++ FileUtils.set_contents (path (), file.to_data ()); ++ } catch (Error e) { ++ warning ("Could not save watt display preference: %s", e.message); ++ } ++ }); ++ } ++} +--- a/src/Widgets/PopoverWidget.vala ++++ b/src/Widgets/PopoverWidget.vala +@@ -72,6 +72,13 @@ + + var show_percent_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + show_percent_box.append (show_percent_switch); ++ var show_watts_switch = new Granite.SwitchModelButton (_("Show Watts in Menu Bar")); ++ show_watts_switch.add_css_class (Granite.STYLE_CLASS_H4_LABEL); ++ WattSettings.get_default ().bind_property ( ++ "enabled", show_watts_switch, "active", ++ GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL ++ ); ++ show_percent_box.append (show_watts_switch); + show_percent_box.append (show_percent_sep); + + var show_percent_revealer = new Gtk.Revealer () { From 60b8d8a0e1ce0682fd5bea5061432faf154657ad Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 13:27:35 -0400 Subject: [PATCH 017/798] Use recognizable Ethernet socket icons in the network panel --- mseries/OS9-HANDOFF.md | 6 + mseries/ROADMAP.md | 2 +- mseries/install-network-throughput.sh | 1 + mseries/patches/network-ethernet-icon.patch | 129 ++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 mseries/patches/network-ethernet-icon.patch diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c0db7dfea..5374738de 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -2766,3 +2766,9 @@ Owner reported Command-hover did not work. This desktop uses `ctrl:swap_lwin_lct Battery menu now includes **Show Watts in Menu Bar**, alongside Show Percentage. `panel-power-watts-toggle.patch` adds a shared per-user preference in `~/.config/mseries/battery-meter.ini`, enabled by default; binding it to the watts revealer visibility keeps it hidden across sensor updates and restores the latest reading when re-enabled. `install-battery-eta.sh` applies the third patch; no system GSettings schemas are changed. Verified: both full release Meson/Ninja builds; installer bash syntax; GTK harness for whole-display hover, leave and unmap transitions; battery switch binding, immediate hide/show, hidden state across fresh readings, saved-off reload in a second process. Both installers ran on the desktop; power restart passed the repair script's survival check. Final network build/reinstall and panel survival verified. Live hover and switch by eye remain for the owner. Changes committed/pushed on the dedicated network-panel-compact branch because the shared checkout branch has divergent remote history; unrelated working files are preserved. + +## Ethernet panel icon: recognizable socket — 2026-10-18 + +Owner found elementary's connected Ethernet glyph (`<…>`) unrecognizable. `network-ethernet-icon.patch` replaces the bundled 16/24px connected wired assets with a monochrome Ethernet socket/contact glyph and preserves a padlock on the secure/VPN variant. Icon names, network-state selection and Wi-Fi/cellular assets are unchanged. The installer applies the new patch after the compact panel patch; image payload already includes all patches. New vector artwork is CC0-1.0, attributed in the SVG headers. + +Verified: full network module rebuild; connected and VPN assets rendered through GdkPixbuf and visually inspected; installer applied the full pinned patch series and installed/reloaded Wingpanel. Live panel appearance by eye remains for the owner. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 27b3d10af..680f44afa 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -263,7 +263,7 @@ with the corrected live-session hardware overlay before progressing. | Login-screen stall fixed | `lightdm` drop-in waits for the eDP connector; display crossbar in the fairydust initramfs (`install-initramfs-crossbar.sh`) | greeter 119.7 s -> 0.82 s; dp-xbar deferrals 14 -> 0; first-dcp -> DRM 2.57 s -> 0.01 s | | USB4 dock data (hub, mouse, headset, 1 Gb/s Ethernet) | Custom kernel `fairydust-usb4`, m1n1 + DTBs, thunderbolt quirks (`mseries/fairydust/usb4/`) | checks `dock-usb4-ethernet`, `dock-dp-retry-silent` passed | | Boots Linux by default | Fairydust default helper is implemented, preserving stock Asahi as recovery; applying the persistent override is still pending. The last recorded stock-kernel boot lost dock mouse and Ethernet. | Apply the helper, then confirm kernel, m1n1/DTB chain, dock USB and Ethernet on-device. | -| Network indicator speed, connected-only Wi-Fi list, and usage views | Panel rates below 1 MB/s now display `<1 MB/s`; Hover over the icon/readout to reveal measured values (Command optional; built and installed October 18; GTK interaction check passed, live visual check pending). Installed on the M1 Pro; user `netusage` service is active. Manual post-install includes the panel module and user logger. Overlay now includes the matching panel package for version-gated plugin compilation, then preserves the plugin. | Rebuild the image and verify the plugin loads against the final panel; check joined-network collapse, chevron expansion, and menu rendering. | +| Network indicator speed, connected-only Wi-Fi list, and usage views | Connected Ethernet now uses a socket glyph, including the VPN lock variant (October 18). Panel rates below 1 MB/s now display `<1 MB/s`; Hover over the icon/readout to reveal measured values (Command optional; built and installed October 18; GTK interaction check passed, live visual check pending). Installed on the M1 Pro; user `netusage` service is active. Manual post-install includes the panel module and user logger. Overlay now includes the matching panel package for version-gated plugin compilation, then preserves the plugin. | Rebuild the image and verify the plugin loads against the final panel; check joined-network collapse, chevron expansion, and menu rendering. | | Rounded screen corners | patched Gala, radius 14 (`install-gala-screen-corners.sh`) | **needs your look**: `gala-corners-look` | | Pastel blue/rose theme | `install-theme-tweaks.sh` | **needs your look**: `colors-look` | | Memory/swap/service tuning | `install-system-tuning.sh` (zram, no ModemManager/AppCenter daemon) | `zram-swap`, `services-trimmed` | diff --git a/mseries/install-network-throughput.sh b/mseries/install-network-throughput.sh index b82caf166..e3addb33f 100755 --- a/mseries/install-network-throughput.sh +++ b/mseries/install-network-throughput.sh @@ -59,6 +59,7 @@ git -C "$source_dir" apply "$repo/mseries/patches/network-null-devices.patch" git -C "$source_dir" apply "$repo/mseries/patches/network-panel-speed.patch" git -C "$source_dir" apply "$repo/mseries/patches/network-saturation.patch" git -C "$source_dir" apply "$repo/mseries/patches/network-panel-compact.patch" +git -C "$source_dir" apply "$repo/mseries/patches/network-ethernet-icon.patch" git -C "$source_dir" apply \ --include=data/Indicator.css \ --include=src/Widgets/UsageWidget.vala \ diff --git a/mseries/patches/network-ethernet-icon.patch b/mseries/patches/network-ethernet-icon.patch new file mode 100644 index 000000000..be3dfe12b --- /dev/null +++ b/mseries/patches/network-ethernet-icon.patch @@ -0,0 +1,129 @@ +--- a/data/24/network-wired-connected-symbolic.svg ++++ b/data/24/network-wired-connected-symbolic.svg +@@ -1,23 +1,5 @@ +- +- +- ++ ++ ++ ++ + +--- a/data/16/network-wired-connected-symbolic.svg ++++ b/data/16/network-wired-connected-symbolic.svg +@@ -1,58 +1,5 @@ +- +- +- +- +- +- image/svg+xml +- +- +- +- +- +- +- +- +- ++ ++ ++ ++ + +--- a/data/24/network-wired-secure-symbolic.svg ++++ b/data/24/network-wired-secure-symbolic.svg +@@ -1,23 +1,9 @@ +- +- +- ++ ++ ++ ++ ++ ++ ++ ++ + From aaeb88042a8ed8df6a370a166bad3d5eecd99ba5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 14:22:51 -0400 Subject: [PATCH 018/798] Add opt-in experimental Apple Passwords integration for Brave --- mseries/OS9-HANDOFF.md | 20 +++ mseries/ROADMAP.md | 20 +++ mseries/enhance.sh | 1 + mseries/icloud-passwords/README.md | 103 +++++++++++++++ mseries/icloud-passwords/chromium-key.pub | 1 + mseries/icloud-passwords/setup.py | 146 ++++++++++++++++++++++ mseries/icloud-passwords/test_setup.py | 61 +++++++++ mseries/install-icloud-passwords.sh | 4 + 8 files changed, 356 insertions(+) create mode 100644 mseries/icloud-passwords/README.md create mode 100644 mseries/icloud-passwords/chromium-key.pub create mode 100644 mseries/icloud-passwords/setup.py create mode 100644 mseries/icloud-passwords/test_setup.py create mode 100644 mseries/install-icloud-passwords.sh diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index b1906b668..655739f56 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5188,3 +5188,23 @@ actual no-inference transient user unit confirmed these variables are absent in the child even when inherited assignments are present. Its sanitized receipt is work/f5-debug-env-v1/receipt.json. Deployed verification must inspect the live process environment because systemctl Environment still lists unset assignments. + + +## Optional Apple Passwords experiment — 2026-10-18 + +Added `install-icloud-passwords.sh --experimental`, a per-user opt-in installer +and three application launchers. Default first-boot behavior stays disabled; +`enhance.sh` ships the installer/public key only. Pinned upstream client +916ef17a5a14965f353d556a53ca3eedc1439113 and ARM64 anisette image digest; +rootless user service binds localhost and persists its identity volume. +Brave receives a stable Chromium MV3 extension manifest and native-host registration. +Keyring failure is fatal instead of falling back to a disk master key. + +Locally installed and reinstallation checked; helper HTTP/JSON and isolated +empty-vault native ping pass. Keyring is unlocked and listener is loopback-only. +92 upstream tests + 11 subtests, three integration contracts, shell syntax and +shellcheck pass. No Apple account credentials entered or real vault inspected. +Owner must load the extension once in Brave, then sign in directly via the +Set Up Apple Passwords launcher. Account recovery, actual sync/autofill, ADP, +reboot and factory-image behavior remain unverified. See +[setup, limitations, disable steps and source pins](icloud-passwords/README.md). diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 178d827f2..492099b19 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1483,3 +1483,23 @@ passed and the original selection was restored; distinct-account round trips use - Publication follow-through: PR86 merged077bfe5; current-source CI39130696811 passed all5 jobs. Initial Pages39138713351 succeeded and canonical report bytes matched reviewed source.46 dictation/72 assistant/6 image checks pass. Publishing the opt-in candidate does not complete physical F5, panel/lazy rollout or continuous translation gates. - Deployment follow-through (October18): qualified lazy CPU speech/IBus and Dictation launcher installed after idle/quiet/pref checks. Fresh listener32.67MiB before SDK load; not after-use memory. Owner reports physical F5 speech appears in Original transcript: native-app human pass, earlier popup failure retained. New installed focus-retry regression failed then passed after fix; receipts retain both and installed-source checks.46 contracts pass. Real spoken content/private backups excluded. Continuous translated previews and final grammar-after-release remain the next work; current translation is post-utterance. + + +## Optional Apple Passwords experiment — 2026-10-18 + +Added `install-icloud-passwords.sh --experimental`, a per-user opt-in installer +and three application launchers. Default first-boot behavior stays disabled; +`enhance.sh` ships the installer/public key only. Pinned upstream client +916ef17a5a14965f353d556a53ca3eedc1439113 and ARM64 anisette image digest; +rootless user service binds localhost and persists its identity volume. +Brave receives a stable Chromium MV3 extension manifest and native-host registration. +Keyring failure is fatal instead of falling back to a disk master key. + +Locally installed and reinstallation checked; helper HTTP/JSON and isolated +empty-vault native ping pass. Keyring is unlocked and listener is loopback-only. +92 upstream tests + 11 subtests, three integration contracts, shell syntax and +shellcheck pass. No Apple account credentials entered or real vault inspected. +Owner must load the extension once in Brave, then sign in directly via the +Set Up Apple Passwords launcher. Account recovery, actual sync/autofill, ADP, +reboot and factory-image behavior remain unverified. See +[setup, limitations, disable steps and source pins](icloud-passwords/README.md). diff --git a/mseries/enhance.sh b/mseries/enhance.sh index 8b0b983d7..15cf62701 100755 --- a/mseries/enhance.sh +++ b/mseries/enhance.sh @@ -329,6 +329,7 @@ mod_theme() { mod_payload() { local prefix=/usr/share/elementary-mac f for f in install-desktop-space.sh install-dock-drawers.sh install-wingpanel-codexbar-left.sh \ + install-icloud-passwords.sh icloud-passwords/setup.py icloud-passwords/chromium-key.pub icloud-passwords/README.md \ display-space/full_height_boot.py display-space/dock_profile.py \ calendar/accounts lib-backup.sh lib-apt.sh install-brave.sh install-video-decode.sh video-decode/90-mseries-avd.conf video-decode/brave-youtube install-native-firefox.sh install-touchegg.sh \ install-network-throughput.sh install-quick-settings-brightness.sh install-bluetooth-control-center.sh \ diff --git a/mseries/icloud-passwords/README.md b/mseries/icloud-passwords/README.md new file mode 100644 index 000000000..3885c3bfe --- /dev/null +++ b/mseries/icloud-passwords/README.md @@ -0,0 +1,103 @@ +# Apple Passwords — optional experiment + +This ARM64 desktop feature is disabled by default. Image payloads carry only the +installer and public extension identity; no accounts, tokens, passwords, container +identity or private keys belong in the image or repository. + +## Enable + +As the logged-in desktop user, without sudo: + +```sh +bash mseries/install-icloud-passwords.sh --experimental +``` + +Prerequisites: native Brave, Python 3.10+ with venv/pip support, git, rootless Podman, +a systemd user session and an unlocked GNOME Keyring/Secret Service. This initial +container digest supports ARM64 only. Installation downloads dependencies and the +helper image; no Apple credentials are requested by the installer. + +## Finish Brave setup + +1. Open `brave://extensions`, enable **Developer mode**, and choose **Load unpacked**. +2. Select `~/.local/share/mseries/icloud-passwords/source/extension` + (or `$XDG_DATA_HOME/mseries/icloud-passwords/source/extension` if configured). +3. Confirm **Apple Passwords**, ID `hagjmolifioccmojhoplpopjklljajno`, is enabled. +4. Open **Set Up Apple Passwords (Experimental)** from Applications, or run: + +```sh +~/.local/bin/mseries-icloud-passwords login +``` + +Enter your Apple account password, 2FA code and requested device passcode directly +in that terminal. Never paste these in chat or public logs. Keep a trusted Apple +device available. Upstream warns that too many wrong recovery passcodes can +permanently lock recovery: stop on an unexpected failure, do not guess or loop +login attempts. Advanced Data Protection remains unqualified. + +After successful sign-in, click the extension on a site with a saved login. Test +one known username/password, then a saved TOTP code if available. **Apple Passwords +(Experimental)** opens the searchable terminal vault; **Sync Apple Passwords +(Experimental)** refreshes it. Browser use can trigger a refresh after six hours. +Never capture real credentials in test screenshots or diagnostics. + +The extension has access to HTTP/HTTPS pages for autofill. It reads a local encrypted +snapshot, and can show existing Hide My Email aliases. It does not save edited/new +passwords to iCloud, implement passkeys or fill native desktop applications. Joining +iCloud Keychain registers a client/trust identity; read-only refers to password +content, not an absence of account authentication or device registration. + +## Local storage and service + +- Client/source/venv: `~/.local/share/mseries/icloud-passwords/source` (XDG-aware). +- Encrypted account/vault data: `~/.config/icp` (upstream, XDG-aware). +- Master key: GNOME Keyring; this port disables upstream's file-key fallback. +- Helper: `mseries-icloud-anisette.service` under the user systemd instance. +- Helper network listener: `127.0.0.1:6969`; never exposed to the LAN. +- Helper identity: rootless Podman volume `mseries-icloud-anisette`, preserved across + restarts. Do not delete it during routine upgrades. +- Native messaging registration: Brave's `NativeMessagingHosts/org.icp.native.json`. +- Installation receipt: `~/.local/share/mseries/icloud-passwords/installation.json`. + +An unlocked user session can access the vault through the host. No extra lock-screen +or biometric gating is implemented here. The container service runs only after +explicit installation; default first-boot jobs never enable it. + +## Disable / remove + +Disable the extension in `brave://extensions` and stop automatic helper startup: + +```sh +systemctl --user disable --now mseries-icloud-anisette.service +``` + +To sign out locally, run `~/.local/bin/mseries-icloud-passwords logout`. +Upstream also supports `logout --wipe-device` to forget its local device identity; +this is not represented as revoking a device from Apple's account website. +The installer leaves encrypted data and the Podman volume in place when disabled. +Remove the three `mseries-icloud-passwords-*.desktop` launchers and the dedicated +Brave native-host JSON if uninstalling. Re-enable by rerunning the opt-in installer. + +## Sources / changes / qualification + +- Client: https://github.com/Sank6/iCloud-Keychain-for-Linux + at `916ef17a5a14965f353d556a53ca3eedc1439113`, MIT. The cloned source retains + `LICENSE.md`; upstream code is fetched, not vendored in the OS repository. +- Authentication helper: https://github.com/Dadoum/anisette-v3-server + ARM64 image `docker.io/dadoum/anisette-v3-server@sha256:12ebe8bedbffe96953ce4fd20b578997bc7ed266f7a5015d4f96526e9c65af1f`. +- Local modifications: require Secret Service, adapt the shared Firefox/Chromium + manifest to Chromium MV3, add a stable public extension key. That public key is + an identity seed, not an encryption key; no corresponding private key is stored. +- Python dependencies follow upstream's minimum constraints and are not locked; + client and helper pins do not imply an independently audited dependency chain. + +2026-10-18 local verification: installer and repeat installation succeed on +Python 3.14 / elementary OS 9 / ARM64; helper HTTP 200 with valid JSON; loopback-only +listener; GNOME Keyring unlocked; native protocol ping with an isolated empty vault; +92 upstream host/TOTP/keychain crypto/escrow SRP/Octagon-key tests plus 11 subtests; +three integration contracts including unavailable-keyring rejection, stable extension +identity and patch-drift rejection. Shell syntax and shellcheck pass. + +Not yet verified: loading the extension in the owner's Brave UI, actual Apple login, +keychain recovery, password download, live autofill, TOTP, Hide My Email, ADP, reboot +behavior, or factory-image boot. The owner's authentication and live test are next. diff --git a/mseries/icloud-passwords/chromium-key.pub b/mseries/icloud-passwords/chromium-key.pub new file mode 100644 index 000000000..d35583f09 --- /dev/null +++ b/mseries/icloud-passwords/chromium-key.pub @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiUZKHu2kLhl2/xJrAmGzPqjxL1DtBr1Q7GpLPvIxRQqzZd4h5pASqJVe/MWU6n7r4+RF0arQK01oEbAE6s4a8Br4adAUxuWi9VAMq6tCdlscJKxdLO4hwEpm6BkVhNaWxofyu39AC2RX6CquizjyAgm9JhJgSa/ZA0qVDtIhbCMYzfUAv0tlPpqSrNcN3rTyLyXQ1R94ZND4F4kL07LRUwrUQz6aBAm1elfdXvyL/PHtcJLzFk2+MjI+2T3yJYK6CG/IRM94iu3NLhICkuOz4mxqASwOfjgrIw52V6DQKmfuIfuMKdQ74el0jlXzHLef3MUmBfZKKQj1dqIpHFk3EwIDAQAB \ No newline at end of file diff --git a/mseries/icloud-passwords/setup.py b/mseries/icloud-passwords/setup.py new file mode 100644 index 000000000..c1fa82a49 --- /dev/null +++ b/mseries/icloud-passwords/setup.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Opt-in, user-local experimental Apple Passwords integration.""" +import argparse +import base64 +import hashlib +import json +import os +from pathlib import Path +import shlex +import shutil +import subprocess +import sys + +REV = "916ef17a5a14965f353d556a53ca3eedc1439113" +IMAGE = "docker.io/dadoum/anisette-v3-server@sha256:12ebe8bedbffe96953ce4fd20b578997bc7ed266f7a5015d4f96526e9c65af1f" +HERE = Path(__file__).resolve().parent + + +def run(*args): + subprocess.run(args, check=True) + + +def write(path, content, mode=0o600): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + path.chmod(mode) + + +def prepare_source(source): + # Fail closed instead of upstream's cleartext-on-disk master-key fallback. + session = source / "icp/auth/session.py" + old = "return key if key is not None else _key_from_file()" + new = 'if key is None:\n raise SessionError("GNOME Keyring must be available and unlocked; file fallback disabled")\n return key' + text = session.read_text() + if text.count(old) != 1: + raise RuntimeError("Upstream keyring patch no longer applies") + session.write_text(text.replace(old, new)) + manifest_path = source / "extension/manifest.json" + manifest = json.loads(manifest_path.read_text()) + manifest.pop("browser_specific_settings", None) + manifest["background"].pop("scripts", None) + manifest["key"] = (HERE / "chromium-key.pub").read_text().strip() + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") + digest = hashlib.sha256(base64.b64decode(manifest["key"], validate=True)).hexdigest()[:32] + return "".join(chr(ord("a") + int(c, 16)) for c in digest) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--experimental", action="store_true", required=True, + help="enable this unofficial, read-only integration") + parser.parse_args() + if os.geteuid() == 0: + parser.error("Run as your desktop user, without sudo") + if os.uname().machine != "aarch64": + parser.error("This initial helper image pin is ARM64 only") + for tool in ("git", "python3", "podman", "systemctl", "brave-browser"): + if not shutil.which(tool): + parser.error(f"Install prerequisite: {tool}") + os.umask(0o077) + home = Path.home() + data = Path(os.environ.get("XDG_DATA_HOME", home / ".local/share")) + config = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config")) + base = data / "mseries/icloud-passwords" + source = base / "source" + if source.exists(): + installed = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() + if installed != REV: + raise RuntimeError("Existing source has a different pin; refusing to overwrite") + manifest = json.loads((source / "extension/manifest.json").read_text()) + key = manifest.get("key") + expected = (HERE / "chromium-key.pub").read_text().strip() + if key != expected or "file fallback disabled" not in (source / "icp/auth/session.py").read_text(): + raise RuntimeError("Existing source is not this integration; refusing to overwrite") + digest = hashlib.sha256(base64.b64decode(key)).hexdigest()[:32] + extension_id = "".join(chr(ord("a") + int(c, 16)) for c in digest) + else: + source.parent.mkdir(parents=True, exist_ok=True) + run("git", "clone", "https://github.com/Sank6/iCloud-Keychain-for-Linux.git", str(source)) + run("git", "-C", str(source), "checkout", "--detach", REV) + extension_id = prepare_source(source) + run("python3", "-m", "venv", str(source / ".venv")) + run(str(source / ".venv/bin/pip"), "install", "-e", str(source)) + (source / "host/icp-host.sh").chmod(0o700) + native = {"name": "org.icp.native", "description": "Experimental Apple Passwords", + "path": str(source / "host/icp-host.sh"), "type": "stdio", + "allowed_origins": [f"chrome-extension://{extension_id}/"]} + write(config / "BraveSoftware/Brave-Browser/NativeMessagingHosts/org.icp.native.json", + json.dumps(native, indent=2) + "\n") + # Persist anisette machine identity between restarts, bound only to loopback. + unit = f"""[Unit] +Description=Experimental Apple Passwords local authentication helper +After=network-online.target +Wants=network-online.target +[Service] +ExecStartPre=-/usr/bin/podman rm -f mseries-icloud-anisette +ExecStart=/usr/bin/podman run --name mseries-icloud-anisette --publish 127.0.0.1:6969:6969 --volume mseries-icloud-anisette:/home/Alcoholic/.config/anisette-v3/lib {IMAGE} +ExecStop=/usr/bin/podman stop -t 10 mseries-icloud-anisette +Restart=on-failure +RestartSec=10 +TimeoutStartSec=300 +[Install] +WantedBy=default.target +""" + write(config / "systemd/user/mseries-icloud-anisette.service", unit) + run("podman", "pull", IMAGE) + run("systemctl", "--user", "daemon-reload") + run("systemctl", "--user", "enable", "--now", "mseries-icloud-anisette.service") + launcher = home / ".local/bin/mseries-icloud-passwords" + write(launcher, f"""#!/usr/bin/env bash +set -euo pipefail +umask 077 +export ICP_ANISETTE_URL=http://127.0.0.1:6969 +case "${{1:-show}}" in + login) + {shlex.quote(str(source / '.venv/bin/python'))} -c 'import sys, secretstorage; c=secretstorage.get_default_collection(secretstorage.dbus_init()); c.is_locked() and sys.exit("Unlock GNOME Keyring before signing in")' + systemctl --user start mseries-icloud-anisette.service + echo 'Experimental unofficial client. Enter Apple credentials here, never in chat.' + echo 'Use your known device passcode; stop on recovery failure rather than retrying guesses.' + ;; + show|sync|logout) ;; + *) echo 'Usage: mseries-icloud-passwords [login|show|sync|logout]' >&2; exit 2 ;; +esac +exec {shlex.quote(str(source / '.venv/bin/icp'))} "${{@:-show}}" +""", 0o700) + for action, label in (("show", "Apple Passwords (Experimental)"), + ("login", "Set Up Apple Passwords (Experimental)"), + ("sync", "Sync Apple Passwords (Experimental)")): + write(data / f"applications/mseries-icloud-passwords-{action}.desktop", f"""[Desktop Entry] +Type=Application +Name={label} +Comment=Unofficial read-only iCloud Keychain integration +Exec="{launcher}" {action} +Icon=dialog-password +Terminal=true +Categories=Utility;Security; +""", 0o644) + write(base / "installation.json", json.dumps({"upstream_revision": REV, "anisette_image": IMAGE, + "extension_id": extension_id, "experimental": True}, indent=2) + "\n") + print(f"Ready. In brave://extensions enable Developer mode and Load unpacked: {source / 'extension'}") + print(f"Expected extension ID: {extension_id}") + print("Then open Set Up Apple Passwords (Experimental) to sign in privately.") + + +if __name__ == "__main__": + main() diff --git a/mseries/icloud-passwords/test_setup.py b/mseries/icloud-passwords/test_setup.py new file mode 100644 index 000000000..8bfa1eee2 --- /dev/null +++ b/mseries/icloud-passwords/test_setup.py @@ -0,0 +1,61 @@ +"""Offline integration contracts; no owner credentials, keyring writes or Apple calls.""" +import base64 +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('setup', HERE / 'setup.py') +setup = importlib.util.module_from_spec(spec) +spec.loader.exec_module(setup) + + +class IntegrationTests(unittest.TestCase): + def fixture(self, root): + (root / 'icp/auth').mkdir(parents=True) + (root / 'extension').mkdir() + (root / 'icp/auth/session.py').write_text( + 'def _master_key():\n key = _key_from_secret_service()\n' + ' return key if key is not None else _key_from_file()\n') + (root / 'extension/manifest.json').write_text(json.dumps({ + 'background': {'service_worker': 'background.js', 'scripts': ['background.js']}, + 'browser_specific_settings': {'gecko': {}}, 'manifest_version': 3})) + + def test_fail_closed_and_available_key(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + self.fixture(root) + setup.prepare_source(root) + class SessionError(Exception): + pass + scope = {'SessionError': SessionError, '_key_from_secret_service': lambda: None} + exec(compile((root / 'icp/auth/session.py').read_text(), 'session', 'exec'), scope) + with self.assertRaises(SessionError): + scope['_master_key']() + scope['_key_from_secret_service'] = lambda: b'test-key' + self.assertEqual(scope['_master_key'](), b'test-key') + + def test_manifest_id_stable_and_chromium_only(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + self.fixture(root) + extension_id = setup.prepare_source(root) + manifest = json.loads((root / 'extension/manifest.json').read_text()) + self.assertNotIn('scripts', manifest['background']) + self.assertNotIn('browser_specific_settings', manifest) + self.assertEqual(extension_id, 'hagjmolifioccmojhoplpopjklljajno') + self.assertTrue(base64.b64decode(manifest['key'], validate=True)) + + def test_upstream_drift_rejected(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + self.fixture(root) + (root / 'icp/auth/session.py').write_text('changed upstream') + with self.assertRaises(RuntimeError): + setup.prepare_source(root) + + +if __name__ == '__main__': + unittest.main() diff --git a/mseries/install-icloud-passwords.sh b/mseries/install-icloud-passwords.sh new file mode 100644 index 000000000..a3e2e45df --- /dev/null +++ b/mseries/install-icloud-passwords.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Explicit per-user opt-in. Never called by first-boot enhancement jobs. +set -euo pipefail +exec python3 "$(dirname "$(readlink -f "$0")")/icloud-passwords/setup.py" "$@" From 69cfc457580e064a154e3afd36cfc8e80acc9dc0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 14:33:40 -0400 Subject: [PATCH 019/798] Add native Apple Passwords onboarding and secure sign-in compatibility --- mseries/OS9-HANDOFF.md | 24 ++ mseries/ROADMAP.md | 24 ++ mseries/enhance.sh | 2 +- mseries/icloud-passwords/README.md | 72 ++++- mseries/icloud-passwords/app.py | 374 ++++++++++++++++++++++ mseries/icloud-passwords/gsa-errors.patch | 40 +++ mseries/icloud-passwords/setup.py | 115 ++++++- mseries/icloud-passwords/test_gui.py | 94 ++++++ mseries/icloud-passwords/test_network.py | 61 ++++ mseries/icloud-passwords/test_setup.py | 9 + mseries/icloud-passwords/worker.py | 144 +++++++++ 11 files changed, 939 insertions(+), 20 deletions(-) create mode 100644 mseries/icloud-passwords/app.py create mode 100644 mseries/icloud-passwords/gsa-errors.patch create mode 100644 mseries/icloud-passwords/test_gui.py create mode 100644 mseries/icloud-passwords/test_network.py create mode 100644 mseries/icloud-passwords/worker.py diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 655739f56..6b7509488 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5208,3 +5208,27 @@ Owner must load the extension once in Brave, then sign in directly via the Set Up Apple Passwords launcher. Account recovery, actual sync/autofill, ADP, reboot and factory-image behavior remain unverified. See [setup, limitations, disable steps and source pins](icloud-passwords/README.md). + + +### Apple Passwords native GTK/Granite onboarding and -80045 follow-up + +Owner requested a native elementary GUI after GSA init returned -80045. Added +GTK4/Granite7 sign-in, private pipe worker, 2FA/device selection, unchecked explicit +recovery consent, search/reveal, Sync Now and local sign-out. The UI blocks closing +or cancellation while an irreversible recovery request is in flight. Password +retention is opt-in; credentials never enter process argv/environment or logs. +Installed a single native Applications entry, retired the terminal setup/sync entries +and opened the GUI locally. Default feature opt-in boundary remains unchanged. + +Applied the reported endpoint-specific identity workaround from Dadoum/anisette-v3-server#61 +without resetting the client/helper identity. Enforced TLS verification across GSA, +iCloud and web auth, with fingerprint-pinned Apple PKI roots in an app-scoped CA +bundle; system trust unchanged. Verified GSA/setup/idmsa TLS using credential-free +GETs. Added safe GSA transport/non-plist errors. + +Validation: ten GUI/IPC/keyring/CA-pin tests, four mocked endpoint/TLS/consent tests, +129 upstream tests + 11 subtests; the one obsolete Mac-escrow-header assertion is +superseded by the port endpoint test. Installer repeat, shell syntax and compilation +pass. First owner attempt never reached passcode recovery. No agent login attempted; +actual compatibility fix, account recovery, sync/autofill and factory boot remain +unverified pending owner testing in the native app. Details: icloud-passwords/README.md. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 492099b19..91e6c344c 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1503,3 +1503,27 @@ Owner must load the extension once in Brave, then sign in directly via the Set Up Apple Passwords launcher. Account recovery, actual sync/autofill, ADP, reboot and factory-image behavior remain unverified. See [setup, limitations, disable steps and source pins](icloud-passwords/README.md). + + +### Apple Passwords native GTK/Granite onboarding and -80045 follow-up + +Owner requested a native elementary GUI after GSA init returned -80045. Added +GTK4/Granite7 sign-in, private pipe worker, 2FA/device selection, unchecked explicit +recovery consent, search/reveal, Sync Now and local sign-out. The UI blocks closing +or cancellation while an irreversible recovery request is in flight. Password +retention is opt-in; credentials never enter process argv/environment or logs. +Installed a single native Applications entry, retired the terminal setup/sync entries +and opened the GUI locally. Default feature opt-in boundary remains unchanged. + +Applied the reported endpoint-specific identity workaround from Dadoum/anisette-v3-server#61 +without resetting the client/helper identity. Enforced TLS verification across GSA, +iCloud and web auth, with fingerprint-pinned Apple PKI roots in an app-scoped CA +bundle; system trust unchanged. Verified GSA/setup/idmsa TLS using credential-free +GETs. Added safe GSA transport/non-plist errors. + +Validation: ten GUI/IPC/keyring/CA-pin tests, four mocked endpoint/TLS/consent tests, +129 upstream tests + 11 subtests; the one obsolete Mac-escrow-header assertion is +superseded by the port endpoint test. Installer repeat, shell syntax and compilation +pass. First owner attempt never reached passcode recovery. No agent login attempted; +actual compatibility fix, account recovery, sync/autofill and factory boot remain +unverified pending owner testing in the native app. Details: icloud-passwords/README.md. diff --git a/mseries/enhance.sh b/mseries/enhance.sh index 15cf62701..db2b6e5ba 100755 --- a/mseries/enhance.sh +++ b/mseries/enhance.sh @@ -329,7 +329,7 @@ mod_theme() { mod_payload() { local prefix=/usr/share/elementary-mac f for f in install-desktop-space.sh install-dock-drawers.sh install-wingpanel-codexbar-left.sh \ - install-icloud-passwords.sh icloud-passwords/setup.py icloud-passwords/chromium-key.pub icloud-passwords/README.md \ + install-icloud-passwords.sh icloud-passwords/setup.py icloud-passwords/app.py icloud-passwords/worker.py icloud-passwords/gsa-errors.patch icloud-passwords/chromium-key.pub icloud-passwords/README.md \ display-space/full_height_boot.py display-space/dock_profile.py \ calendar/accounts lib-backup.sh lib-apt.sh install-brave.sh install-video-decode.sh video-decode/90-mseries-avd.conf video-decode/brave-youtube install-native-firefox.sh install-touchegg.sh \ install-network-throughput.sh install-quick-settings-brightness.sh install-bluetooth-control-center.sh \ diff --git a/mseries/icloud-passwords/README.md b/mseries/icloud-passwords/README.md index 3885c3bfe..89fad167a 100644 --- a/mseries/icloud-passwords/README.md +++ b/mseries/icloud-passwords/README.md @@ -13,7 +13,7 @@ bash mseries/install-icloud-passwords.sh --experimental ``` Prerequisites: native Brave, Python 3.10+ with venv/pip support, git, rootless Podman, -a systemd user session and an unlocked GNOME Keyring/Secret Service. This initial +a systemd user session, GTK 4/Granite 7 with system Python GI bindings, and an unlocked GNOME Keyring/Secret Service. This initial container digest supports ARM64 only. Installation downloads dependencies and the helper image; no Apple credentials are requested by the installer. @@ -23,24 +23,35 @@ helper image; no Apple credentials are requested by the installer. 2. Select `~/.local/share/mseries/icloud-passwords/source/extension` (or `$XDG_DATA_HOME/mseries/icloud-passwords/source/extension` if configured). 3. Confirm **Apple Passwords**, ID `hagjmolifioccmojhoplpopjklljajno`, is enabled. -4. Open **Set Up Apple Passwords (Experimental)** from Applications, or run: +4. Open **Apple Passwords (Experimental)** from Applications, or run: ```sh -~/.local/bin/mseries-icloud-passwords login +~/.local/bin/mseries-icloud-passwords-gui ``` -Enter your Apple account password, 2FA code and requested device passcode directly -in that terminal. Never paste these in chat or public logs. Keep a trusted Apple +Enter your Apple account email and password in the native window, then follow its +verification-code and trusted-device prompts. Recovery requires a separate unchecked +consent box before you enter the selected device’s known passcode. The GUI never +automatically repeats a recovery attempt. Cancellation and closing are blocked while +a submitted recovery attempt is in flight. Enter credentials directly in the app. Never paste these in chat or public logs. Keep a trusted Apple device available. Upstream warns that too many wrong recovery passcodes can permanently lock recovery: stop on an unexpected failure, do not guess or loop login attempts. Advanced Data Protection remains unqualified. -After successful sign-in, click the extension on a site with a saved login. Test -one known username/password, then a saved TOTP code if available. **Apple Passwords -(Experimental)** opens the searchable terminal vault; **Sync Apple Passwords -(Experimental)** refreshes it. Browser use can trigger a refresh after six hours. +After successful sign-in, the native window displays a searchable saved-login list +and masked password details with an explicit reveal control. **Sync Now** refreshes +the snapshot. The header follows elementary light/dark preferences. The terminal +client remains available as `~/.local/bin/mseries-icloud-passwords login|show|sync|logout`. +Use the Brave extension for live autofill and saved TOTP codes. Browser use can +trigger a refresh after six hours. The GUI does not display TOTP or Hide My Email yet. Never capture real credentials in test screenshots or diagnostics. +The GUI does not save your Apple password unless you enable **Remember password**. +That optional encrypted storage supports background token renewal and Hide My Email; +without it, some renewals require signing in again. IPC carries secrets over private +child pipes only; no password argv/environment fields, raw server errors or protocol +logging. GUI error messages expose only known failure categories and numeric codes. + The extension has access to HTTP/HTTPS pages for autofill. It reads a local encrypted snapshot, and can show existing Hide My Email aliases. It does not save edited/new passwords to iCloud, implement passkeys or fill native desktop applications. Joining @@ -57,6 +68,11 @@ content, not an absence of account authentication or device registration. - Helper identity: rootless Podman volume `mseries-icloud-anisette`, preserved across restarts. Do not delete it during routine upgrades. - Native messaging registration: Brave's `NativeMessagingHosts/org.icp.native.json`. +- GUI: `~/.local/share/mseries/icloud-passwords/gui` and `~/.local/bin/mseries-icloud-passwords-gui`. +- Private-service CA roots: downloaded over verified HTTPS from Apple PKI with pinned + DER SHA-256 digests, combined with system roots in a client-only `ca-bundle.pem`. + Certificate verification is enabled in all patched authentication paths; the + operating system trust store is unchanged. - Installation receipt: `~/.local/share/mseries/icloud-passwords/installation.json`. An unlocked user session can access the vault through the host. No extra lock-screen @@ -71,11 +87,15 @@ Disable the extension in `brave://extensions` and stop automatic helper startup: systemctl --user disable --now mseries-icloud-anisette.service ``` -To sign out locally, run `~/.local/bin/mseries-icloud-passwords logout`. +The native **Sign Out** action clears the local session and cached passwords/aliases. +It keeps the client and helper identities and does not revoke account devices. +The terminal `~/.local/bin/mseries-icloud-passwords logout` retains upstream behavior: +it clears the session but keeps the downloaded vault. Disable the extension when +disabling the feature, since it reads this cache independently of the GUI. Upstream also supports `logout --wipe-device` to forget its local device identity; this is not represented as revoking a device from Apple's account website. The installer leaves encrypted data and the Podman volume in place when disabled. -Remove the three `mseries-icloud-passwords-*.desktop` launchers and the dedicated +Remove the `org.techmore.mseries.ApplePasswords.desktop` launcher and the dedicated Brave native-host JSON if uninstalling. Re-enable by rerunning the opt-in installer. ## Sources / changes / qualification @@ -101,3 +121,33 @@ identity and patch-drift rejection. Shell syntax and shellcheck pass. Not yet verified: loading the extension in the owner's Brave UI, actual Apple login, keychain recovery, password download, live autofill, TOTP, Hide My Email, ADP, reboot behavior, or factory-image boot. The owner's authentication and live test are next. + +## Native GUI and failed-login follow-up — 2026-10-18 + +The owner’s first terminal attempt failed at GSA init with `ec=-80045`, before +2FA or recovery. No recovery passcode was submitted by that attempt. +[anisette-v3-server issue 61](https://github.com/Dadoum/anisette-v3-server/issues/61) +reports an endpoint-specific client-description workaround. This integration applies +its Windows descriptions to GSA, loginDelegates and escrow, retaining the original +Mac descriptions for account settings and CloudKit. This is a reported workaround, +not proof that this account can now sign in, and Apple can change acceptance again. + +Added native GTK 4/Granite 7 sign-in, private worker prompts, explicit recovery consent, +snapshot search/reveal, last-sync display, Sync Now and local sign-out. Deprecated +terminal setup/sync shortcuts are replaced by one native Applications entry. +`worker.py` does not expose authentication records or TOTP seeds to the GUI. + +Upstream disabled certificate verification for GSA/iCloud/web auth; this port now +enables it and scopes [Apple PKI](https://www.apple.com/certificateauthority/) roots +to the client bundle. Plain HTTPS probes without account data verified GSA, setup +and idmsa TLS successfully. `gsa-errors.patch` adds safe non-plist/transport error +handling so server failures do not crash onboarding. + +Validation: ten offline GUI/IPC/keyring/CA-pin integration tests, four mocked endpoint +identity/TLS/consent tests in the client venv, and 129 upstream tests plus 11 subtests. +One upstream fixture hardcoding the original Mac escrow header is deselected; its +updated Windows-header and no-consent/no-network contract is covered by +`test_network.py`. Repeat installation and compiled Python/shell checks pass. +Native window constructed under Xvfb; installed native process launched locally. +No real account authentication was attempted by the agent. Successful sign-in, +recovery, sync, live autofill, ADP and factory-image boot remain unverified. diff --git a/mseries/icloud-passwords/app.py b/mseries/icloud-passwords/app.py new file mode 100644 index 000000000..b0ddcee15 --- /dev/null +++ b/mseries/icloud-passwords/app.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +"""Native elementary desktop UI; secrets travel only over private child pipes.""" +import json +import os +from pathlib import Path +import subprocess +import threading +from datetime import datetime + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Granite', '7.0') +from gi.repository import Gtk, Granite, GLib + +HERE = Path(__file__).resolve().parent +DATA = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local/share')) +SOURCE = DATA / 'mseries/icloud-passwords/source' + + +class Passwords(Gtk.Application): + def __init__(self): + super().__init__(application_id='org.techmore.mseries.ApplePasswords') + self.proc = None + self.items = [] + self.recovering = False + self.dialog = None + self.error_seen = False + self.credentials = {} + self.connect('activate', self.activate) + + def activate(self, *_): + if hasattr(self, 'window'): + self.window.present() + return + prefs = Granite.Settings.get_default() + settings = Gtk.Settings.get_default() + def scheme(*_): + settings.set_property('gtk-application-prefer-dark-theme', + prefs.get_prefers_color_scheme() == Granite.SettingsColorScheme.DARK) + prefs.connect('notify::prefers-color-scheme', scheme) + scheme() + self.window = Gtk.ApplicationWindow(application=self, title='Apple Passwords') + self.window.set_default_size(900, 640) + self.window.connect('close-request', self.close) + header = Gtk.HeaderBar() + header.set_title_widget(Gtk.Label(label='Apple Passwords')) + badge = Gtk.Label(label='Experimental') + badge.add_css_class('warning') + header.pack_start(badge) + self.sync = Gtk.Button(label='Sync Now', sensitive=False) + self.sync.connect('clicked', lambda *_: self.start('sync')) + header.pack_end(self.sync) + self.window.set_titlebar(header) + root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + for edge in ('top', 'bottom', 'start', 'end'): + getattr(root, 'set_margin_' + edge)(20) + self.window.set_child(root) + self.status = Gtk.Label(label='Read your iCloud passwords on this Mac.', xalign=0, wrap=True) + root.append(self.status) + self.stack = Gtk.Stack(vexpand=True) + root.append(self.stack) + self.build_login() + self.build_vault() + footer = Gtk.Box(spacing=10) + self.spinner = Gtk.Spinner() + footer.append(self.spinner) + self.cancel = Gtk.Button(label='Cancel', visible=False) + self.cancel.connect('clicked', self.cancel_work) + footer.append(self.cancel) + self.logout = Gtk.Button(label='Sign Out', visible=False) + self.logout.connect('clicked', lambda *_: self.start('logout')) + footer.append(self.logout) + self.unlock = Gtk.Button(label='Sign In Again', visible=False) + self.unlock.connect('clicked', lambda *_: self.stack.set_visible_child_name('login')) + footer.append(self.unlock) + self.browser = Gtk.Button(label='Brave Setup') + self.browser.connect('clicked', self.brave_help) + footer.append(self.browser) + root.append(footer) + self.window.present() + self.start('load') + + def build_login(self): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16, + halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) + box.set_size_request(460, -1) + icon = Gtk.Image.new_from_icon_name('dialog-password') + icon.set_pixel_size(64) + box.append(icon) + title = Gtk.Label(label='Your Apple passwords, on elementary OS') + title.add_css_class('title-2') + box.append(title) + note = Gtk.Label(label='Sign in to download an encrypted, read-only snapshot.\n' + 'This unofficial client is experimental.', wrap=True) + box.append(note) + self.email = Gtk.Entry(placeholder_text='Apple Account email', input_purpose=Gtk.InputPurpose.EMAIL) + self.email.set_max_length(320) + self.password = Gtk.PasswordEntry(placeholder_text='Apple Account password', show_peek_icon=True) + self.password.connect('activate', self.login) + box.append(self.email) + box.append(self.password) + self.remember = Gtk.CheckButton(label='Remember password in encrypted storage for background refresh') + self.remember.set_tooltip_text('Uses your login keyring. Without this, some token renewals and Hide My Email need sign-in again.') + box.append(self.remember) + self.signin = Gtk.Button(label='Sign In with Apple') + self.signin.add_css_class('suggested-action') + self.signin.connect('clicked', self.login) + box.append(self.signin) + warning = Gtk.Label(label='Keep a trusted Apple device nearby for verification.\n' + 'Recovery requires its known passcode; never guess.', wrap=True) + warning.add_css_class('dim-label') + box.append(warning) + self.stack.add_named(box, 'login') + + def build_vault(self): + pane = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL, vexpand=True) + left = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + self.search = Gtk.SearchEntry(placeholder_text='Search passwords') + self.search.connect('search-changed', lambda *_: self.populate()) + left.append(self.search) + self.list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.SINGLE) + self.list.connect('row-selected', self.select) + scroll = Gtk.ScrolledWindow(vexpand=True, min_content_width=280) + scroll.set_child(self.list) + left.append(scroll) + pane.set_start_child(left) + pane.set_resize_start_child(True) + pane.set_position(330) + detail = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) + detail.set_margin_start(24) + self.site = Gtk.Label(label='Choose a saved login', xalign=0, wrap=True) + self.site.add_css_class('title-2') + self.username = Gtk.Label(xalign=0, selectable=True, wrap=True) + self.saved_password = Gtk.PasswordEntry(show_peek_icon=True, editable=False) + self.saved_password.set_visible(False) + detail.append(self.site) + detail.append(self.username) + detail.append(self.saved_password) + self.snapshot_label = Gtk.Label(xalign=0, wrap=True) + self.snapshot_label.add_css_class('dim-label') + detail.append(self.snapshot_label) + detail.append(Gtk.Label(label='Read-only snapshot. Use the Brave extension for autofill and verification codes.', + xalign=0, wrap=True)) + pane.set_end_child(detail) + self.stack.add_named(pane, 'vault') + + def login(self, *_): + if self.proc: + return + email, password = self.email.get_text().strip(), self.password.get_text() + if not email or not password: + self.status.set_text('Enter your Apple Account email and password.') + return + self.credentials = {'email': email, 'password': password} + self.password.set_text('') + self.start('login') + + def start(self, action): + if self.proc: + return + self.action = action + self.error_seen = False + self.recovering = False + if action in ('login', 'sync'): + result = subprocess.run(['systemctl', '--user', 'start', 'mseries-icloud-anisette.service'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if result.returncode: + self.status.set_text('The local authentication helper could not start.') + self.credentials.clear() + return + args = [str(SOURCE / '.venv/bin/python'), str(HERE / 'worker.py'), action] + if action == 'login' and self.remember.get_active(): + args.append('--remember-password') + env = dict(os.environ, ICP_ANISETTE_URL='http://127.0.0.1:6969', + REQUESTS_CA_BUNDLE=str(DATA / 'mseries/icloud-passwords/ca-bundle.pem')) + try: + self.proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, bufsize=1, env=env) + except OSError: + self.status.set_text('The client is unavailable. Run the experimental installer first.') + self.credentials.clear() + return + proc = self.proc + self.busy(True) + self.status.set_text('Opening your encrypted snapshot…' if action == 'load' else 'Connecting to Apple…') + def reader(): + try: + for line in proc.stdout: + try: + event = json.loads(line) + except (ValueError, TypeError): + continue # never print unrecognized output + GLib.idle_add(self.event, proc, event) + finally: + proc.wait() + GLib.idle_add(self.finished, proc) + threading.Thread(target=reader, daemon=True).start() + + def busy(self, value): + self.signin.set_sensitive(not value) + self.sync.set_sensitive(not value and getattr(self, 'joined', False)) + self.logout.set_sensitive(not value) + self.unlock.set_sensitive(not value) + self.cancel.set_visible(value) + self.cancel.set_sensitive(not self.recovering) + self.spinner.set_spinning(value) + + def reply(self, value=None, cancel=False): + if self.proc and self.proc.poll() is None: + try: + self.proc.stdin.write(json.dumps({'value': value, 'cancel': cancel}) + '\n') + self.proc.stdin.flush() + except (BrokenPipeError, OSError): + pass + + def event(self, proc, event): + if proc is not self.proc: + return False + kind = event.get('type') + if kind == 'prompt': + field = event.get('kind') + if field in self.credentials: + self.reply(self.credentials.pop(field)) + else: + self.show_prompt(event) + elif kind in ('error', 'warning'): + self.error_seen = True + self.status.set_text(event['text']) + elif kind == 'progress' and not self.error_seen: + self.status.set_text(event['text']) + elif kind == 'recovery': + self.recovering = True + self.cancel.set_sensitive(False) + self.status.set_text('Recovering keychain access. Keep this window open; this attempt cannot be undone.') + elif kind == 'vault': + self.items = event['credentials'] + self.joined = event['joined'] + self.signed_in = event['signed_in'] + self.logout.set_visible(self.signed_in) + self.unlock.set_visible(self.signed_in) + self.stack.set_visible_child_name('vault' if self.joined else 'login') + stamp = event.get('updated') + self.snapshot_label.set_text('Last synced ' + datetime.fromtimestamp(stamp).strftime('%b %d, %H:%M') if stamp else 'No password snapshot yet') + self.populate() + if not self.error_seen: + self.status.set_text(f'{len(self.items)} saved passwords' if self.joined else + 'Signed in. Complete keychain setup to download passwords.' if self.signed_in else + 'Sign in to read your iCloud passwords.') + return False + + def show_prompt(self, event): + dialog = Gtk.Dialog(title=event['label'], transient_for=self.window, modal=True) + self.dialog = dialog + dialog.set_default_size(460, -1) + dialog.add_button('Cancel', Gtk.ResponseType.CANCEL) + accept = dialog.add_button('Continue', Gtk.ResponseType.OK) + body = dialog.get_content_area() + body.set_spacing(16) + for edge in ('top', 'bottom', 'start', 'end'): + getattr(body, 'set_margin_' + edge)(20) + kind = event['kind'] + if kind == 'confirm': + body.append(Gtk.Label(label='A wrong device passcode uses one of a limited number of recovery attempts. ' + 'Too many wrong attempts can permanently destroy the recovery record. ' + 'Continue only if you know the passcode for the device you selected.', wrap=True, max_width_chars=52)) + control = Gtk.CheckButton(label='I know this device’s passcode and allow one attempt') + accept.set_sensitive(False) + control.connect('toggled', lambda c: accept.set_sensitive(c.get_active())) + elif kind == 'device': + control = Gtk.DropDown.new_from_strings(event.get('choices', [])) + body.append(Gtk.Label(label='Choose the device whose unlock passcode you know.', wrap=True)) + else: + body.append(Gtk.Label(label='Enter the code shown on your trusted device.' if kind == 'code' else + 'Enter the unlock passcode for the device you selected.', wrap=True)) + control = Gtk.PasswordEntry(show_peek_icon=kind != 'code') + control.connect('activate', lambda *_: dialog.response(Gtk.ResponseType.OK)) + body.append(control) + def respond(_, response): + if response == Gtk.ResponseType.OK: + value = 'yes' if kind == 'confirm' else str(control.get_selected()) if kind == 'device' else control.get_text() + if kind not in ('confirm', 'device') and not value: + return + if kind == 'passcode': + self.recovering = True + self.cancel.set_sensitive(False) + self.status.set_text('Recovering keychain access. Keep this window open until the attempt finishes.') + self.reply(value) + else: + self.reply(cancel=True) + if kind not in ('confirm', 'device'): + control.set_text('') + dialog.destroy() + self.dialog = None + dialog.connect('response', respond) + dialog.present() + control.grab_focus() + + def finished(self, proc): + if proc is not self.proc: + return False + self.proc = None + self.credentials.clear() + self.recovering = False + if self.dialog: + self.dialog.destroy() + self.dialog = None + self.busy(False) + if proc.returncode and not self.error_seen: + self.status.set_text('Operation cancelled.' if proc.returncode in (-15, 130) else 'The client stopped before completing this operation.') + return False + + def cancel_work(self, *_): + self.credentials.clear() + if self.proc and not self.recovering: + self.proc.terminate() + self.credentials.clear() + + def populate(self): + self.saved_password.get_delegate().set_visibility(False) + self.saved_password.set_text('') + self.saved_password.set_visible(False) + self.site.set_text('Choose a saved login') + self.username.set_text('') + while child := self.list.get_first_child(): + self.list.remove(child) + query = self.search.get_text().casefold() + for item in self.items: + if query and query not in ' '.join((item['domain'], item['username'], item['title'])).casefold(): + continue + row = Gtk.ListBoxRow() + row.item = item + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + for edge in ('top', 'bottom', 'start', 'end'): + getattr(box, 'set_margin_' + edge)(10) + box.append(Gtk.Label(label=item['title'] or item['domain'], xalign=0, ellipsize=3)) + name = Gtk.Label(label=item['username'], xalign=0, ellipsize=3) + name.add_css_class('dim-label') + box.append(name) + row.set_child(box) + self.list.append(row) + + def select(self, _, row): + self.saved_password.get_delegate().set_visibility(False) + self.saved_password.set_text('') + self.saved_password.set_visible(False) + if row: + self.site.set_text(row.item['domain'] or row.item['title']) + self.username.set_text(row.item['username']) + self.saved_password.set_text(row.item['password']) + self.saved_password.set_visible(True) + + def brave_help(self, *_): + dialog = Gtk.MessageDialog(transient_for=self.window, modal=True, + message_type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.CLOSE, + text='Enable Apple Passwords in Brave', + secondary_text='Open brave://extensions, enable Developer mode, then Load unpacked and choose:\n\n' + + str(SOURCE / 'extension') + '\n\nExpected ID: hagjmolifioccmojhoplpopjklljajno') + dialog.connect('response', lambda d, *_: d.destroy()) + dialog.present() + + def close(self, *_): + if self.recovering: + self.status.set_text('Please keep this window open until the recovery attempt finishes.') + return True + self.cancel_work() + self.items.clear() + self.saved_password.get_delegate().set_visibility(False) + self.saved_password.set_text('') + self.password.set_text('') + return False + + +if __name__ == '__main__': + Passwords().run() diff --git a/mseries/icloud-passwords/gsa-errors.patch b/mseries/icloud-passwords/gsa-errors.patch new file mode 100644 index 000000000..939dfa70e --- /dev/null +++ b/mseries/icloud-passwords/gsa-errors.patch @@ -0,0 +1,40 @@ +--- a/icp/auth/gsa.py ++++ b/icp/auth/gsa.py +@@ -65,18 +65,26 @@ + "User-Agent": const.GSA_USER_AGENT, + "X-MMe-Client-Info": const.GSA_CLIENT_INFO, + } +- resp = requests.post( +- const.GSA_ENDPOINT, headers=headers, data=plist.dumps(body), +- verify=True, timeout=10, +- ) +- +- payload = plist.loads(resp.content) +- response = payload.get("Response") +- +- if not isinstance(response, dict) or not resp.ok: ++ try: ++ resp = requests.post( ++ const.GSA_ENDPOINT, headers=headers, data=plist.dumps(body), ++ verify=True, timeout=10, ++ ) ++ except requests.RequestException as exc: ++ raise GSAError(f"GSA {operation} request failed: {exc}") from exc ++ try: ++ payload = plist.loads(resp.content) ++ except Exception as exc: ++ content_type = resp.headers.get("Content-Type", "unknown") + raise GSAError( +- f"GSA {operation} failed: HTTP {resp.status_code}: {_status(response)}" +- ) ++ f"GSA {operation} returned HTTP {resp.status_code}; expected a plist " ++ f"({content_type}, {len(resp.content)} bytes)" ++ ) from exc ++ response = payload.get("Response") if isinstance(payload, dict) else None ++ if not isinstance(response, dict): ++ raise GSAError(f"GSA {operation} returned HTTP {resp.status_code} without a Response dictionary") ++ if not resp.ok: ++ raise GSAError(f"GSA {operation} failed: HTTP {resp.status_code}: {_status(response)}") + return response + + def authenticate(self, username: str, password: str, stage: str) -> tuple[dict, dict]: diff --git a/mseries/icloud-passwords/setup.py b/mseries/icloud-passwords/setup.py index c1fa82a49..142dd197c 100644 --- a/mseries/icloud-passwords/setup.py +++ b/mseries/icloud-passwords/setup.py @@ -10,6 +10,8 @@ import shutil import subprocess import sys +import ssl +import urllib.request REV = "916ef17a5a14965f353d556a53ca3eedc1439113" IMAGE = "docker.io/dadoum/anisette-v3-server@sha256:12ebe8bedbffe96953ce4fd20b578997bc7ed266f7a5015d4f96526e9c65af1f" @@ -45,6 +47,86 @@ def prepare_source(source): return "".join(chr(ord("a") + int(c, 16)) for c in digest) + +def replace_checked(text, old, new, count=1): + if old in text: + if text.count(old) != count: + raise RuntimeError("Pinned upstream patch count changed") + return text.replace(old, new) + if text.count(new) != count: + raise RuntimeError("Pinned upstream patch no longer applies") + return text + + +def apply_network_patches(source): + """Endpoint-specific workaround reported in Dadoum/anisette-v3-server#61. + + Do not change account-settings or CloudKit identity. No remote helper involved. + """ + patches = { + "icp/const.py": [ + ('" "', + '" "', 1)], + "icp/auth/icloud.py": [ + ('class ICloudError(AppleError):', + 'DELEGATES_CLIENT_INFO = (" "\n' + ' "")\n\n\n' + 'class ICloudError(AppleError):', 1), + ('"X-Mme-Client-Info": ICLOUD_CLIENT_INFO,\n "Accept": "*/*",', + '"X-Mme-Client-Info": DELEGATES_CLIENT_INFO,\n "Accept": "*/*",', 1), + ('verify=False', 'verify=True', 2)], + "icp/escrow/srp.py": [ + ('" "', + '" "', 1)], + "icp/auth/gsa.py": [('verify=False', 'verify=True', 5)], + "icp/auth/webauth.py": [('self.http.verify = False', 'self.http.verify = True', 1)], + } + # Validate every replacement before touching any file. + updated = {} + for relative, edits in patches.items(): + path = source / relative + text = path.read_text() + for old, new, count in edits: + # Insertion markers can be contained in the already-patched replacement. + if new in text and old in new: + continue + text = replace_checked(text, old, new, count) + updated[path] = text + for path, text in updated.items(): + path.write_text(text) + patch = str(HERE / "gsa-errors.patch") + applied = subprocess.run(["git", "-C", str(source), "apply", "--reverse", "--check", patch], + capture_output=True) + if applied.returncode: + run("git", "-C", str(source), "apply", "--check", patch) + run("git", "-C", str(source), "apply", patch) + + + +def install_ca_bundle(base): + # Apple publishes its private-service roots over publicly trusted HTTPS. + # Trust is scoped to this client; never alter the operating system CA store. + roots = ( + ("https://www.apple.com/appleca/AppleIncRootCertificate.cer", + "b0b1730ecbc7ff4505142c49f1295e6eda6bcaed7e2c68c5be91b5a11001f024"), + ("https://www.apple.com/certificateauthority/AppleRootCA-G3.cer", + "63343abfb89a6a03ebb57e9b3f5fa7be7c4f5c756f3017b3a8c488c3653e9179"), + ) + cafile = ssl.get_default_verify_paths().cafile + if not cafile: + raise RuntimeError("System trust bundle is unavailable") + bundle = Path(cafile).read_text() + for url, digest in roots: + with urllib.request.urlopen(url, timeout=20) as response: + der = response.read(16385) + if len(der) > 16384 or hashlib.sha256(der).hexdigest() != digest: + raise RuntimeError("Apple CA certificate pin mismatch") + bundle += "\n" + ssl.DER_cert_to_PEM_cert(der) + path = base / "ca-bundle.pem" + write(path, bundle) + return path + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--experimental", action="store_true", required=True, @@ -57,6 +139,7 @@ def main(): for tool in ("git", "python3", "podman", "systemctl", "brave-browser"): if not shutil.which(tool): parser.error(f"Install prerequisite: {tool}") + run("/usr/bin/python3", "-c", "import gi; gi.require_version('Gtk', '4.0'); gi.require_version('Granite', '7.0'); from gi.repository import Gtk, Granite") os.umask(0o077) home = Path.home() data = Path(os.environ.get("XDG_DATA_HOME", home / ".local/share")) @@ -79,9 +162,16 @@ def main(): run("git", "clone", "https://github.com/Sank6/iCloud-Keychain-for-Linux.git", str(source)) run("git", "-C", str(source), "checkout", "--detach", REV) extension_id = prepare_source(source) + ca_bundle = install_ca_bundle(base) + apply_network_patches(source) run("python3", "-m", "venv", str(source / ".venv")) run(str(source / ".venv/bin/pip"), "install", "-e", str(source)) - (source / "host/icp-host.sh").chmod(0o700) + host = source / "host/icp-host.sh" + host_text = host.read_text() + if "export REQUESTS_CA_BUNDLE=" not in host_text: + host_text = host_text.replace('exec "$PY"', + 'export REQUESTS_CA_BUNDLE=' + shlex.quote(str(ca_bundle)) + '\nexec "$PY"') + write(host, host_text, 0o700) native = {"name": "org.icp.native", "description": "Experimental Apple Passwords", "path": str(source / "host/icp-host.sh"), "type": "stdio", "allowed_origins": [f"chrome-extension://{extension_id}/"]} @@ -111,6 +201,7 @@ def main(): set -euo pipefail umask 077 export ICP_ANISETTE_URL=http://127.0.0.1:6969 +export REQUESTS_CA_BUNDLE={shlex.quote(str(ca_bundle))} case "${{1:-show}}" in login) {shlex.quote(str(source / '.venv/bin/python'))} -c 'import sys, secretstorage; c=secretstorage.get_default_collection(secretstorage.dbus_init()); c.is_locked() and sys.exit("Unlock GNOME Keyring before signing in")' @@ -123,23 +214,31 @@ def main(): esac exec {shlex.quote(str(source / '.venv/bin/icp'))} "${{@:-show}}" """, 0o700) - for action, label in (("show", "Apple Passwords (Experimental)"), - ("login", "Set Up Apple Passwords (Experimental)"), - ("sync", "Sync Apple Passwords (Experimental)")): - write(data / f"applications/mseries-icloud-passwords-{action}.desktop", f"""[Desktop Entry] + # Native GUI uses system Python for GTK/Granite and a separate venv worker. + for filename in ("app.py", "worker.py"): + write(base / "gui" / filename, (HERE / filename).read_text(), 0o700) + gui = home / ".local/bin/mseries-icloud-passwords-gui" + write(gui, '#!/usr/bin/env bash\nset -euo pipefail\numask 077\nexec /usr/bin/python3 ' + + shlex.quote(str(base / "gui/app.py")) + '\n', 0o700) + # Retire this integration's older terminal-only setup/sync shortcuts. + for old_action in ("login", "sync", "show"): + (data / f"applications/mseries-icloud-passwords-{old_action}.desktop").unlink(missing_ok=True) + for action, label in (("show", "Apple Passwords (Experimental)"),): + write(data / "applications/org.techmore.mseries.ApplePasswords.desktop", f"""[Desktop Entry] Type=Application Name={label} Comment=Unofficial read-only iCloud Keychain integration -Exec="{launcher}" {action} +Exec="{gui}" Icon=dialog-password -Terminal=true +Terminal=false +StartupNotify=true Categories=Utility;Security; """, 0o644) write(base / "installation.json", json.dumps({"upstream_revision": REV, "anisette_image": IMAGE, "extension_id": extension_id, "experimental": True}, indent=2) + "\n") print(f"Ready. In brave://extensions enable Developer mode and Load unpacked: {source / 'extension'}") print(f"Expected extension ID: {extension_id}") - print("Then open Set Up Apple Passwords (Experimental) to sign in privately.") + print("Then open Apple Passwords (Experimental) to sign in privately in the native GUI.") if __name__ == "__main__": diff --git a/mseries/icloud-passwords/test_gui.py b/mseries/icloud-passwords/test_gui.py new file mode 100644 index 000000000..fa4dccb7d --- /dev/null +++ b/mseries/icloud-passwords/test_gui.py @@ -0,0 +1,94 @@ +"""Run with xvfb-run -a /usr/bin/python3 -m unittest discover ...""" +import importlib.util +import io +import json +from pathlib import Path +import unittest +from unittest.mock import patch + +HERE = Path(__file__).resolve().parent + +def load(name, filename): + spec = importlib.util.spec_from_file_location(name, HERE / filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +worker = load('worker', 'worker.py') + +class WorkerTests(unittest.TestCase): + def test_private_prompt_protocol(self): + out = io.StringIO() + with patch('sys.stdout', out), patch('sys.stdin', io.StringIO('{"value":"TEST_SECRET"}\n')): + self.assertEqual(worker.prompt('password', 'Password'), 'TEST_SECRET') + self.assertNotIn('TEST_SECRET', out.getvalue()) + self.assertEqual(json.loads(out.getvalue())['kind'], 'password') + + def test_cancel_and_error_redaction(self): + with patch('sys.stdout', io.StringIO()), patch('sys.stdin', io.StringIO('{"cancel":true}\n')): + with self.assertRaises(KeyboardInterrupt): + worker.prompt('passcode', 'Passcode') + for text in ('token=TEST_SECRET ec=-80045', 'join failed: TEST_SECRET', + 'SSLError TEST_SECRET', 'TEST_SECRET'): + self.assertNotIn('TEST_SECRET', worker.friendly_error(text)) + +class GuiTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + import os + if not os.environ.get('DISPLAY'): + raise unittest.SkipTest('Run native GUI tests under xvfb-run') + cls.gui = load('passwords_gui', 'app.py') + + def setUp(self): + self.app = self.gui.Passwords() + self.app.start = lambda action: None + self.app.set_application_id("org.techmore.mseries.Test" + self._testMethodName.replace("_", "")) + self.app.register() + self.app.activate() + + def tearDown(self): + if self.app.dialog: + self.app.dialog.destroy() + self.app.window.destroy() + self.app.quit() + + def test_empty_and_synced_vault(self): + self.app.event(None, {'type':'vault', 'credentials':[], 'signed_in':False, 'joined':False}) + self.assertEqual(self.app.stack.get_visible_child_name(), 'login') + self.app.event(None, {'type':'vault', 'credentials':[ + {'domain':'example.invalid','title':'Example','username':'test-user','password':'DEMO_PASSWORD'}], + 'signed_in':True,'joined':True,'updated':1}) + self.assertEqual(self.app.stack.get_visible_child_name(), 'vault') + row=self.app.list.get_row_at_index(0) + self.app.select(self.app.list,row) + self.assertEqual(self.app.saved_password.get_text(),'DEMO_PASSWORD') + self.app.search.set_text('no-match') + self.app.populate() + self.assertIsNone(self.app.list.get_row_at_index(0)) + self.assertEqual(self.app.saved_password.get_text(),'') + + def test_recovery_requires_explicit_consent(self): + self.app.show_prompt({'kind':'confirm','label':'Allow one recovery attempt?'}) + button=self.app.dialog.get_widget_for_response(self.gui.Gtk.ResponseType.OK) + self.assertFalse(button.get_sensitive()) + self.app.dialog.response(self.gui.Gtk.ResponseType.CANCEL) + self.assertIsNone(self.app.dialog) + + def test_login_clears_widget_and_cancel_clears_pending_secrets(self): + self.app.email.set_text('test@example.invalid') + self.app.password.set_text('DEMO_PASSWORD') + self.app.login() + self.assertEqual(self.app.password.get_text(),'') + self.assertEqual(self.app.credentials['password'],'DEMO_PASSWORD') + self.app.close() + self.assertFalse(self.app.credentials) + + def test_recovery_blocks_close_and_cancellation(self): + self.app.event(None,{'type':'recovery','active':True}) + self.assertFalse(self.app.cancel.get_sensitive()) + self.assertTrue(self.app.close()) + self.app.recovering=False + +if __name__=='__main__': + unittest.main() diff --git a/mseries/icloud-passwords/test_network.py b/mseries/icloud-passwords/test_network.py new file mode 100644 index 000000000..e8aefd6d6 --- /dev/null +++ b/mseries/icloud-passwords/test_network.py @@ -0,0 +1,61 @@ +"""Run with the installed client venv; all Apple calls are mocked.""" +import importlib.util +import plistlib +import unittest +from unittest.mock import patch + +CLIENT_AVAILABLE = all(importlib.util.find_spec(name) is not None for name in ('icp', 'srp', 'nacl', 'secretstorage')) + +class Device: + def meta_headers(self): + return {'X-Mme-Device-Id':'TEST-DEVICE'} + +class Anisette: + def headers(self): + return {'X-Apple-I-MD':'TEST-MACHINE-DATA'} + +class Response: + status_code = 200 + ok = True + headers = {'Content-Type':'text/x-xml-plist'} + def __init__(self, data): + self.content = plistlib.dumps(data) + +@unittest.skipUnless(CLIENT_AVAILABLE, 'Use the installed client venv') +class NetworkTests(unittest.TestCase): + def test_grandslam_identity_and_verified_tls(self): + from icp.auth.gsa import GSAClient + with patch('icp.auth.gsa.requests.post', return_value=Response({'Response':{'Status':{'ec':0}}})) as post: + GSAClient(Device(), Anisette())._request({'o':'init'}) + kwargs=post.call_args.kwargs + self.assertIs(kwargs['verify'],True) + self.assertTrue(kwargs['headers']['X-MMe-Client-Info'].startswith(' ')) + self.assertIs(post.call_args.kwargs['verify'],True) + icloud.fetch_account_settings({'mme':{'dsid':'TEST','mmeAuthToken':'TEST-TOKEN'}},Device(),Anisette()) + self.assertTrue(post.call_args.kwargs['headers']['X-Mme-Client-Info'].startswith(' Date: Sun, 18 Oct 2026 14:33:48 -0400 Subject: [PATCH 020/798] Normalize compatibility patch whitespace --- mseries/icloud-passwords/gsa-errors.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mseries/icloud-passwords/gsa-errors.patch b/mseries/icloud-passwords/gsa-errors.patch index 939dfa70e..e04500121 100644 --- a/mseries/icloud-passwords/gsa-errors.patch +++ b/mseries/icloud-passwords/gsa-errors.patch @@ -36,5 +36,5 @@ + if not resp.ok: + raise GSAError(f"GSA {operation} failed: HTTP {resp.status_code}: {_status(response)}") return response - + def authenticate(self, username: str, password: str, stage: str) -> tuple[dict, dict]: From 71dbb9f582c90b3f9a05116f79b0b1e6bce078e8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 14:38:24 -0400 Subject: [PATCH 021/798] Offer Apple verification code entry before waiting on device approval --- mseries/OS9-HANDOFF.md | 17 +++++ mseries/ROADMAP.md | 17 +++++ mseries/enhance.sh | 2 +- mseries/icloud-passwords/README.md | 17 +++++ mseries/icloud-passwords/app.py | 2 +- .../icloud-passwords/notification-wait.patch | 26 +++++++ mseries/icloud-passwords/setup.py | 13 ++-- mseries/icloud-passwords/test_2fa.py | 67 +++++++++++++++++++ mseries/icloud-passwords/worker.py | 34 +++++++++- 9 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 mseries/icloud-passwords/notification-wait.patch create mode 100644 mseries/icloud-passwords/test_2fa.py diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 6b7509488..c1523bb34 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5232,3 +5232,20 @@ superseded by the port endpoint test. Installer repeat, shell syntax and compila pass. First owner attempt never reached passcode recovery. No agent login attempted; actual compatibility fix, account recovery, sync/autofill and factory boot remain unverified pending owner testing in the native app. Details: icloud-passwords/README.md. + + +### Approval/code waiting fix — 2026-10-18 + +Owner reports phone approval arrives but Linux fails before code entry. The native +worker now offers the code dialog before waiting on the trusted-device notification +HTTP response. Code entry waits on the private input pipe without a user-entry timeout; +there is only one code dialog and one notification request. A notification read timeout +continues to normal code validation without resending; TLS, connection and explicit +HTTP errors remain fatal. Errors now retain a safe stage label. No recovery behavior +or retry budget changed. This addresses a demonstrated timeout path, not a proven +root cause of the owner’s current failure. + +Three mocked tests cover prompt-before-response, one-request read-timeout handoff, +and fatal TLS failure. Six native GUI/IPC tests pass. Installed the updated worker +and notification patch for the next owner-initiated sign-in. Live authentication +and sync remain unverified; no credentials or codes were entered by the agent. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 91e6c344c..99c1416a4 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1527,3 +1527,20 @@ superseded by the port endpoint test. Installer repeat, shell syntax and compila pass. First owner attempt never reached passcode recovery. No agent login attempted; actual compatibility fix, account recovery, sync/autofill and factory boot remain unverified pending owner testing in the native app. Details: icloud-passwords/README.md. + + +### Approval/code waiting fix — 2026-10-18 + +Owner reports phone approval arrives but Linux fails before code entry. The native +worker now offers the code dialog before waiting on the trusted-device notification +HTTP response. Code entry waits on the private input pipe without a user-entry timeout; +there is only one code dialog and one notification request. A notification read timeout +continues to normal code validation without resending; TLS, connection and explicit +HTTP errors remain fatal. Errors now retain a safe stage label. No recovery behavior +or retry budget changed. This addresses a demonstrated timeout path, not a proven +root cause of the owner’s current failure. + +Three mocked tests cover prompt-before-response, one-request read-timeout handoff, +and fatal TLS failure. Six native GUI/IPC tests pass. Installed the updated worker +and notification patch for the next owner-initiated sign-in. Live authentication +and sync remain unverified; no credentials or codes were entered by the agent. diff --git a/mseries/enhance.sh b/mseries/enhance.sh index db2b6e5ba..857ecabe5 100755 --- a/mseries/enhance.sh +++ b/mseries/enhance.sh @@ -329,7 +329,7 @@ mod_theme() { mod_payload() { local prefix=/usr/share/elementary-mac f for f in install-desktop-space.sh install-dock-drawers.sh install-wingpanel-codexbar-left.sh \ - install-icloud-passwords.sh icloud-passwords/setup.py icloud-passwords/app.py icloud-passwords/worker.py icloud-passwords/gsa-errors.patch icloud-passwords/chromium-key.pub icloud-passwords/README.md \ + install-icloud-passwords.sh icloud-passwords/setup.py icloud-passwords/app.py icloud-passwords/worker.py icloud-passwords/gsa-errors.patch icloud-passwords/notification-wait.patch icloud-passwords/chromium-key.pub icloud-passwords/README.md \ display-space/full_height_boot.py display-space/dock_profile.py \ calendar/accounts lib-backup.sh lib-apt.sh install-brave.sh install-video-decode.sh video-decode/90-mseries-avd.conf video-decode/brave-youtube install-native-firefox.sh install-touchegg.sh \ install-network-throughput.sh install-quick-settings-brightness.sh install-bluetooth-control-center.sh \ diff --git a/mseries/icloud-passwords/README.md b/mseries/icloud-passwords/README.md index 89fad167a..600480c2a 100644 --- a/mseries/icloud-passwords/README.md +++ b/mseries/icloud-passwords/README.md @@ -151,3 +151,20 @@ updated Windows-header and no-consent/no-network contract is covered by Native window constructed under Xvfb; installed native process launched locally. No real account authentication was attempted by the agent. Successful sign-in, recovery, sync, live autofill, ADP and factory-image boot remain unverified. + + +### Approval/code waiting fix — 2026-10-18 + +Owner reports phone approval arrives but Linux fails before code entry. The native +worker now offers the code dialog before waiting on the trusted-device notification +HTTP response. Code entry waits on the private input pipe without a user-entry timeout; +there is only one code dialog and one notification request. A notification read timeout +continues to normal code validation without resending; TLS, connection and explicit +HTTP errors remain fatal. Errors now retain a safe stage label. No recovery behavior +or retry budget changed. This addresses a demonstrated timeout path, not a proven +root cause of the owner’s current failure. + +Three mocked tests cover prompt-before-response, one-request read-timeout handoff, +and fatal TLS failure. Six native GUI/IPC tests pass. Installed the updated worker +and notification patch for the next owner-initiated sign-in. Live authentication +and sync remain unverified; no credentials or codes were entered by the agent. diff --git a/mseries/icloud-passwords/app.py b/mseries/icloud-passwords/app.py index b0ddcee15..dd41508b7 100644 --- a/mseries/icloud-passwords/app.py +++ b/mseries/icloud-passwords/app.py @@ -270,7 +270,7 @@ def show_prompt(self, event): control = Gtk.DropDown.new_from_strings(event.get('choices', [])) body.append(Gtk.Label(label='Choose the device whose unlock passcode you know.', wrap=True)) else: - body.append(Gtk.Label(label='Enter the code shown on your trusted device.' if kind == 'code' else + body.append(Gtk.Label(label=event.get('description', 'Tap Allow on your Apple device, then enter the six-digit code. This window will wait.') if kind == 'code' else 'Enter the unlock passcode for the device you selected.', wrap=True)) control = Gtk.PasswordEntry(show_peek_icon=kind != 'code') control.connect('activate', lambda *_: dialog.response(Gtk.ResponseType.OK)) diff --git a/mseries/icloud-passwords/notification-wait.patch b/mseries/icloud-passwords/notification-wait.patch new file mode 100644 index 000000000..00aaf3f3f --- /dev/null +++ b/mseries/icloud-passwords/notification-wait.patch @@ -0,0 +1,26 @@ +--- a/icp/auth/gsa.py ++++ b/icp/auth/gsa.py +@@ -120,11 +120,18 @@ + return complete, spd + + def trigger_trusted_factor(self, dsid: str, idms_token: str) -> None: +- requests.get( +- "https://gsa.apple.com/auth/verify/trusteddevice", +- headers=self._twofa_headers(dsid, idms_token), +- verify=True, timeout=10, +- ) ++ try: ++ response = requests.get( ++ "https://gsa.apple.com/auth/verify/trusteddevice", ++ headers=self._twofa_headers(dsid, idms_token), ++ verify=True, timeout=10, ++ ) ++ except requests.ReadTimeout: ++ # Apple may already have delivered the notification. Do not resend it; ++ # allow the caller to wait for the code and validate it normally. ++ return ++ if not response.ok: ++ raise GSAError(f"trusted-device notification HTTP {response.status_code}") + + def submit_trusted_factor(self, code: str, dsid: str, idms_token: str) -> None: + h = self._twofa_headers(dsid, idms_token) diff --git a/mseries/icloud-passwords/setup.py b/mseries/icloud-passwords/setup.py index 142dd197c..e74dd4087 100644 --- a/mseries/icloud-passwords/setup.py +++ b/mseries/icloud-passwords/setup.py @@ -94,12 +94,13 @@ def apply_network_patches(source): updated[path] = text for path, text in updated.items(): path.write_text(text) - patch = str(HERE / "gsa-errors.patch") - applied = subprocess.run(["git", "-C", str(source), "apply", "--reverse", "--check", patch], - capture_output=True) - if applied.returncode: - run("git", "-C", str(source), "apply", "--check", patch) - run("git", "-C", str(source), "apply", patch) + for filename in ("gsa-errors.patch", "notification-wait.patch"): + patch = str(HERE / filename) + applied = subprocess.run(["git", "-C", str(source), "apply", "--reverse", "--check", patch], + capture_output=True) + if applied.returncode: + run("git", "-C", str(source), "apply", "--check", patch) + run("git", "-C", str(source), "apply", patch) diff --git a/mseries/icloud-passwords/test_2fa.py b/mseries/icloud-passwords/test_2fa.py new file mode 100644 index 000000000..cb6a63e76 --- /dev/null +++ b/mseries/icloud-passwords/test_2fa.py @@ -0,0 +1,67 @@ +"""Approval/code handoff: mocked Apple, no real account or recovery requests.""" +import importlib.util +import io +from pathlib import Path +import unittest +from unittest.mock import patch, MagicMock + +AVAILABLE = all(importlib.util.find_spec(name) is not None for name in ('icp','srp','secretstorage')) + +@unittest.skipUnless(AVAILABLE, 'Use the client venv') +class ApprovalTests(unittest.TestCase): + def run_flow(self, network_error=None): + from icp.auth.gsa import GSAClient + from icp.cli import app, ui + import requests + spec=importlib.util.spec_from_file_location('worker_2fa',Path(__file__).with_name('worker.py')) + worker=importlib.util.module_from_spec(spec); spec.loader.exec_module(worker) + events=[] + class Device: + def meta_headers(self): return {} + class Anisette: + def headers(self): return {} + def send(*args, **kwargs): + # The code box must be offered BEFORE Apple's request returns. + self.assertEqual(events[0]['type'],'prompt') + self.assertEqual(events[0]['kind'],'code') + self.assertTrue(kwargs['verify']) + if network_error: + raise network_error + response=MagicMock(); response.ok=True + return response + def login(argv): + GSAClient(Device(),Anisette()).trigger_trusted_factor('TEST-ID','TEST-TOKEN') + self.assertEqual(ui.ask('6-digit code: '),'123456') + return 1 # avoid loading any real snapshot + collection=MagicMock(); collection.is_locked.return_value=False + original=GSAClient.trigger_trusted_factor + try: + with patch.object(worker,'emit',side_effect=lambda event_type,**fields:events.append(dict(type=event_type,**fields))), \ + patch('sys.stdin',io.StringIO('{"value":"123456"}\n')), \ + patch('sys.argv',['worker.py','login']), \ + patch('icp.auth.gsa.requests.get',side_effect=send) as request, \ + patch('secretstorage.dbus_init'), \ + patch('secretstorage.get_default_collection',return_value=collection), \ + patch.object(app,'main',side_effect=login), \ + patch.multiple(ui,ask=ui.ask,secret=ui.secret,step=ui.step,out=ui.out,warn=ui.warn,err=ui.err,confirm_yn=ui.confirm_yn), \ + patch.object(app,'_select_bottle',app._select_bottle): + worker.main() + self.assertEqual(request.call_count,1) + self.assertEqual(sum(e['type']=='prompt' for e in events),1) + if isinstance(network_error, requests.exceptions.SSLError): + self.assertTrue(any(e['type']=='error' and 'secure connection' in e['text'] for e in events)) + finally: + GSAClient.trigger_trusted_factor=original + + def test_code_box_precedes_notification_response(self): + self.run_flow() + + def test_notification_read_timeout_still_waits_for_code_once(self): + import requests + self.run_flow(requests.ReadTimeout('TEST-TIMEOUT')) + + def test_tls_error_is_fatal(self): + import requests + self.run_flow(requests.exceptions.SSLError('TEST-CERTIFICATE-FAILURE')) + +if __name__=='__main__': unittest.main() diff --git a/mseries/icloud-passwords/worker.py b/mseries/icloud-passwords/worker.py index a8a26231f..2eee1d511 100644 --- a/mseries/icloud-passwords/worker.py +++ b/mseries/icloud-passwords/worker.py @@ -12,6 +12,10 @@ def emit(event_type, **fields): def prompt(kind, label, **fields): emit('prompt', kind=kind, label=label, **fields) + return read_reply() + + +def read_reply(): line = sys.stdin.readline(65536) if not line: raise KeyboardInterrupt @@ -73,16 +77,38 @@ def main(): if action not in ('login', 'sync', 'load', 'logout'): raise ValueError('Unknown operation') failed = False + phase = 'Sign-in' + code_box_open = False + + # Show code entry before the notification HTTP request finishes. The request + # happens once; the actual code submission still waits for the user's reply. + from icp.auth.gsa import GSAClient + original_trigger = GSAClient.trigger_trusted_factor + + def trigger(gsa, *args, **kwargs): + nonlocal phase, code_box_open + phase = 'Sending device approval' + code_box_open = True + emit('prompt', kind='code', label='Approve sign-in on your iPhone', + description='Tap Allow on your trusted Apple device, then enter its six-digit code here. This window will wait for you.') + return original_trigger(gsa, *args, **kwargs) + + GSAClient.trigger_trusted_factor = trigger def error(message): nonlocal failed failed = True - emit('error', text=friendly_error(message)) + emit('error', text=phase + ': ' + friendly_error(message)) def ask(label): + nonlocal phase, code_box_open if 'Apple ID' in label: return prompt('email', 'Apple Account') if 'code' in label.lower(): + phase = 'Verifying your device code' + if code_box_open: + code_box_open = False + return read_reply() return prompt('code', 'Verification code') raise RuntimeError('Unsupported sign-in prompt') @@ -121,7 +147,11 @@ def choose(bottles): argv = [action] if action == 'login' and '--remember-password' not in sys.argv[2:]: argv.append('--no-save-password') - result = app.main(argv) + try: + result = app.main(argv) + except Exception as exc: + error(exc) + result = 1 if result == 0 and not failed: if action == 'logout': from icp import paths From 5b022e01e5bcf40e4f95c670f6058b35b101cc58 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 14:41:18 -0400 Subject: [PATCH 022/798] Refine native Passwords sign-in, verification and empty states --- mseries/OS9-HANDOFF.md | 18 ++++ mseries/ROADMAP.md | 18 ++++ mseries/icloud-passwords/README.md | 18 ++++ mseries/icloud-passwords/app.py | 132 +++++++++++++++++++-------- mseries/icloud-passwords/test_2fa.py | 5 +- mseries/icloud-passwords/test_gui.py | 24 +++++ mseries/icloud-passwords/worker.py | 9 ++ 7 files changed, 186 insertions(+), 38 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c1523bb34..4089a541a 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5249,3 +5249,21 @@ Three mocked tests cover prompt-before-response, one-request read-timeout handof and fatal TLS failure. Six native GUI/IPC tests pass. Installed the updated worker and notification patch for the next owner-initiated sign-in. Live authentication and sync remain unverified; no credentials or codes were entered by the agent. + + +### Native Apple Passwords UX refinement — 2026-10-18 + +Added persistent mnemonic account/password labels, clearer optional password-storage +copy, sign-in step guidance, and contextual Verify Code / Use This Device / Unlock +Keychain actions. Verification codes are visible for proofreading, accept pasted +spacing, and require exactly six ASCII digits before submission. Waiting for the +owner pauses the spinner and explicitly says the app is waiting; notification +progress cannot overwrite that state. Recovery consent remains unchecked by default. + +Added sorted password results, empty/no-match guidance, labeled username/password +details, distinct error styling, and contextual Complete Setup. Sync is hidden +until keychain setup is complete; sign-in fields disable during an operation. +Helper startup moved into the background worker so the GTK thread stays responsive. +Eight GUI/IPC tests and three mocked approval-handoff tests pass. Installed updated +app/worker bytes locally without restarting an active session; reopen the app for +new layout code. Authentication/sync success is still unqualified. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 99c1416a4..18ec09db3 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1544,3 +1544,21 @@ Three mocked tests cover prompt-before-response, one-request read-timeout handof and fatal TLS failure. Six native GUI/IPC tests pass. Installed the updated worker and notification patch for the next owner-initiated sign-in. Live authentication and sync remain unverified; no credentials or codes were entered by the agent. + + +### Native Apple Passwords UX refinement — 2026-10-18 + +Added persistent mnemonic account/password labels, clearer optional password-storage +copy, sign-in step guidance, and contextual Verify Code / Use This Device / Unlock +Keychain actions. Verification codes are visible for proofreading, accept pasted +spacing, and require exactly six ASCII digits before submission. Waiting for the +owner pauses the spinner and explicitly says the app is waiting; notification +progress cannot overwrite that state. Recovery consent remains unchecked by default. + +Added sorted password results, empty/no-match guidance, labeled username/password +details, distinct error styling, and contextual Complete Setup. Sync is hidden +until keychain setup is complete; sign-in fields disable during an operation. +Helper startup moved into the background worker so the GTK thread stays responsive. +Eight GUI/IPC tests and three mocked approval-handoff tests pass. Installed updated +app/worker bytes locally without restarting an active session; reopen the app for +new layout code. Authentication/sync success is still unqualified. diff --git a/mseries/icloud-passwords/README.md b/mseries/icloud-passwords/README.md index 600480c2a..09ded1bc2 100644 --- a/mseries/icloud-passwords/README.md +++ b/mseries/icloud-passwords/README.md @@ -168,3 +168,21 @@ Three mocked tests cover prompt-before-response, one-request read-timeout handof and fatal TLS failure. Six native GUI/IPC tests pass. Installed the updated worker and notification patch for the next owner-initiated sign-in. Live authentication and sync remain unverified; no credentials or codes were entered by the agent. + + +### Native Apple Passwords UX refinement — 2026-10-18 + +Added persistent mnemonic account/password labels, clearer optional password-storage +copy, sign-in step guidance, and contextual Verify Code / Use This Device / Unlock +Keychain actions. Verification codes are visible for proofreading, accept pasted +spacing, and require exactly six ASCII digits before submission. Waiting for the +owner pauses the spinner and explicitly says the app is waiting; notification +progress cannot overwrite that state. Recovery consent remains unchecked by default. + +Added sorted password results, empty/no-match guidance, labeled username/password +details, distinct error styling, and contextual Complete Setup. Sync is hidden +until keychain setup is complete; sign-in fields disable during an operation. +Helper startup moved into the background worker so the GTK thread stays responsive. +Eight GUI/IPC tests and three mocked approval-handoff tests pass. Installed updated +app/worker bytes locally without restarting an active session; reopen the app for +new layout code. Authentication/sync success is still unqualified. diff --git a/mseries/icloud-passwords/app.py b/mseries/icloud-passwords/app.py index dd41508b7..2527c83eb 100644 --- a/mseries/icloud-passwords/app.py +++ b/mseries/icloud-passwords/app.py @@ -55,7 +55,7 @@ def scheme(*_): for edge in ('top', 'bottom', 'start', 'end'): getattr(root, 'set_margin_' + edge)(20) self.window.set_child(root) - self.status = Gtk.Label(label='Read your iCloud passwords on this Mac.', xalign=0, wrap=True) + self.status = Gtk.Label(label='Read your iCloud passwords on this Mac.', xalign=0, wrap=True, selectable=True) root.append(self.status) self.stack = Gtk.Stack(vexpand=True) root.append(self.stack) @@ -73,7 +73,7 @@ def scheme(*_): self.unlock = Gtk.Button(label='Sign In Again', visible=False) self.unlock.connect('clicked', lambda *_: self.stack.set_visible_child_name('login')) footer.append(self.unlock) - self.browser = Gtk.Button(label='Brave Setup') + self.browser = Gtk.Button(label='Browser Autofill') self.browser.connect('clicked', self.brave_help) footer.append(self.browser) root.append(footer) @@ -83,11 +83,11 @@ def scheme(*_): def build_login(self): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) - box.set_size_request(460, -1) + box.set_size_request(420, -1) icon = Gtk.Image.new_from_icon_name('dialog-password') icon.set_pixel_size(64) box.append(icon) - title = Gtk.Label(label='Your Apple passwords, on elementary OS') + title = Gtk.Label(label='Sign in to Apple Passwords', wrap=True) title.add_css_class('title-2') box.append(title) note = Gtk.Label(label='Sign in to download an encrypted, read-only snapshot.\n' @@ -97,12 +97,22 @@ def build_login(self): self.email.set_max_length(320) self.password = Gtk.PasswordEntry(placeholder_text='Apple Account password', show_peek_icon=True) self.password.connect('activate', self.login) - box.append(self.email) - box.append(self.password) - self.remember = Gtk.CheckButton(label='Remember password in encrypted storage for background refresh') + for label, entry in (('_Apple Account email', self.email), ('_Password', self.password)): + field = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + heading = Gtk.Label(label=label, use_underline=True, xalign=0) + heading.set_mnemonic_widget(entry) + field.append(heading) + field.append(entry) + box.append(field) + self.email.connect('activate', lambda *_: self.password.grab_focus()) + self.remember = Gtk.CheckButton(label='Remember Apple Account password') self.remember.set_tooltip_text('Uses your login keyring. Without this, some token renewals and Hide My Email need sign-in again.') box.append(self.remember) - self.signin = Gtk.Button(label='Sign In with Apple') + remember_note = Gtk.Label(label='Encrypted with your login keyring. Enables automatic renewal.', + xalign=0, wrap=True, max_width_chars=48) + remember_note.add_css_class('dim-label') + box.append(remember_note) + self.signin = Gtk.Button(label='Sign In') self.signin.add_css_class('suggested-action') self.signin.connect('clicked', self.login) box.append(self.signin) @@ -110,6 +120,9 @@ def build_login(self): 'Recovery requires its known passcode; never guess.', wrap=True) warning.add_css_class('dim-label') box.append(warning) + steps = Gtk.Label(label='Sign in → Verify device → Unlock keychain', wrap=True) + steps.add_css_class('dim-label') + box.append(steps) self.stack.add_named(box, 'login') def build_vault(self): @@ -122,7 +135,13 @@ def build_vault(self): self.list.connect('row-selected', self.select) scroll = Gtk.ScrolledWindow(vexpand=True, min_content_width=280) scroll.set_child(self.list) - left.append(scroll) + self.results = Gtk.Stack(vexpand=True) + self.results.add_named(scroll, 'list') + self.empty_results = Gtk.Label(wrap=True, justify=Gtk.Justification.CENTER, + margin_start=12, margin_end=12) + self.empty_results.add_css_class('dim-label') + self.results.add_named(self.empty_results, 'empty') + left.append(self.results) pane.set_start_child(left) pane.set_resize_start_child(True) pane.set_position(330) @@ -134,8 +153,13 @@ def build_vault(self): self.saved_password = Gtk.PasswordEntry(show_peek_icon=True, editable=False) self.saved_password.set_visible(False) detail.append(self.site) - detail.append(self.username) - detail.append(self.saved_password) + self.detail_fields = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8, visible=False) + for label, widget in (('Username', self.username), ('Password', self.saved_password)): + heading = Gtk.Label(label=label, xalign=0) + heading.add_css_class('dim-label') + self.detail_fields.append(heading) + self.detail_fields.append(widget) + detail.append(self.detail_fields) self.snapshot_label = Gtk.Label(xalign=0, wrap=True) self.snapshot_label.add_css_class('dim-label') detail.append(self.snapshot_label) @@ -144,12 +168,20 @@ def build_vault(self): pane.set_end_child(detail) self.stack.add_named(pane, 'vault') + def set_status(self, text, error=False): + self.status.set_text(text) + if error: + self.status.add_css_class('error') + else: + self.status.remove_css_class('error') + def login(self, *_): if self.proc: return email, password = self.email.get_text().strip(), self.password.get_text() if not email or not password: - self.status.set_text('Enter your Apple Account email and password.') + self.set_status('Enter your Apple Account email and password.', error=True) + (self.email if not email else self.password).grab_focus() return self.credentials = {'email': email, 'password': password} self.password.set_text('') @@ -161,13 +193,6 @@ def start(self, action): self.action = action self.error_seen = False self.recovering = False - if action in ('login', 'sync'): - result = subprocess.run(['systemctl', '--user', 'start', 'mseries-icloud-anisette.service'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - if result.returncode: - self.status.set_text('The local authentication helper could not start.') - self.credentials.clear() - return args = [str(SOURCE / '.venv/bin/python'), str(HERE / 'worker.py'), action] if action == 'login' and self.remember.get_active(): args.append('--remember-password') @@ -177,12 +202,12 @@ def start(self, action): self.proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1, env=env) except OSError: - self.status.set_text('The client is unavailable. Run the experimental installer first.') + self.set_status('The client is unavailable. Run the experimental installer first.') self.credentials.clear() return proc = self.proc self.busy(True) - self.status.set_text('Opening your encrypted snapshot…' if action == 'load' else 'Connecting to Apple…') + self.set_status('Opening your encrypted snapshot…' if action == 'load' else 'Connecting to Apple…') def reader(): try: for line in proc.stdout: @@ -198,6 +223,9 @@ def reader(): def busy(self, value): self.signin.set_sensitive(not value) + for field in (self.email, self.password, self.remember): + field.set_sensitive(not value) + self.sync.set_visible(getattr(self, 'joined', False)) self.sync.set_sensitive(not value and getattr(self, 'joined', False)) self.logout.set_sensitive(not value) self.unlock.set_sensitive(not value) @@ -224,26 +252,28 @@ def event(self, proc, event): else: self.show_prompt(event) elif kind in ('error', 'warning'): - self.error_seen = True - self.status.set_text(event['text']) - elif kind == 'progress' and not self.error_seen: - self.status.set_text(event['text']) + self.error_seen = self.error_seen or kind == 'error' + self.set_status(event['text'], error=kind == 'error') + elif kind == 'progress' and not self.error_seen and not self.dialog: + self.set_status(event['text']) elif kind == 'recovery': self.recovering = True self.cancel.set_sensitive(False) - self.status.set_text('Recovering keychain access. Keep this window open; this attempt cannot be undone.') + self.set_status('Recovering keychain access. Keep this window open; this attempt cannot be undone.') elif kind == 'vault': self.items = event['credentials'] self.joined = event['joined'] self.signed_in = event['signed_in'] self.logout.set_visible(self.signed_in) self.unlock.set_visible(self.signed_in) + self.unlock.set_label('Sign In Again' if self.joined else 'Complete Setup') + self.logout.set_tooltip_text('Sign out and remove this app’s cached passwords. Your Apple account devices are kept.') self.stack.set_visible_child_name('vault' if self.joined else 'login') stamp = event.get('updated') self.snapshot_label.set_text('Last synced ' + datetime.fromtimestamp(stamp).strftime('%b %d, %H:%M') if stamp else 'No password snapshot yet') self.populate() if not self.error_seen: - self.status.set_text(f'{len(self.items)} saved passwords' if self.joined else + self.set_status(f'{len(self.items)} saved passwords' if self.joined else 'Signed in. Complete keychain setup to download passwords.' if self.signed_in else 'Sign in to read your iCloud passwords.') return False @@ -252,13 +282,18 @@ def show_prompt(self, event): dialog = Gtk.Dialog(title=event['label'], transient_for=self.window, modal=True) self.dialog = dialog dialog.set_default_size(460, -1) - dialog.add_button('Cancel', Gtk.ResponseType.CANCEL) - accept = dialog.add_button('Continue', Gtk.ResponseType.OK) + dialog.add_button('Cancel Sign-In', Gtk.ResponseType.CANCEL) + labels = {'code':'Verify Code', 'device':'Use This Device', 'confirm':'Continue to Passcode', 'passcode':'Unlock Keychain'} + kind = event['kind'] + accept = dialog.add_button(labels.get(kind, 'Continue'), Gtk.ResponseType.OK) + accept.add_css_class('suggested-action') body = dialog.get_content_area() body.set_spacing(16) for edge in ('top', 'bottom', 'start', 'end'): getattr(body, 'set_margin_' + edge)(20) - kind = event['kind'] + self.spinner.set_spinning(False) + if kind == 'code': + self.set_status('Waiting for you: approve on your Apple device, then enter its code.') if kind == 'confirm': body.append(Gtk.Label(label='A wrong device passcode uses one of a limited number of recovery attempts. ' 'Too many wrong attempts can permanently destroy the recovery record. ' @@ -272,18 +307,36 @@ def show_prompt(self, event): else: body.append(Gtk.Label(label=event.get('description', 'Tap Allow on your Apple device, then enter the six-digit code. This window will wait.') if kind == 'code' else 'Enter the unlock passcode for the device you selected.', wrap=True)) - control = Gtk.PasswordEntry(show_peek_icon=kind != 'code') - control.connect('activate', lambda *_: dialog.response(Gtk.ResponseType.OK)) + if kind == 'code': + control = Gtk.Entry(placeholder_text='123456', input_purpose=Gtk.InputPurpose.DIGITS, + max_length=16, xalign=0.5) + control.update_property([Gtk.AccessibleProperty.LABEL], ['Six-digit verification code']) + accept.set_sensitive(False) + def valid(entry): + code = entry.get_text().replace(' ', '').replace('-', '') + accept.set_sensitive(len(code) == 6 and code.isascii() and code.isdigit()) + control.connect('changed', valid) + else: + control = Gtk.PasswordEntry(show_peek_icon=True) + control.connect('activate', lambda *_: dialog.response(Gtk.ResponseType.OK) if accept.get_sensitive() else None) body.append(control) + self.prompt_input = control def respond(_, response): if response == Gtk.ResponseType.OK: value = 'yes' if kind == 'confirm' else str(control.get_selected()) if kind == 'device' else control.get_text() + if kind == 'code': + value = value.replace(' ', '').replace('-', '') + if len(value) != 6 or not value.isascii() or not value.isdigit(): + return if kind not in ('confirm', 'device') and not value: return if kind == 'passcode': self.recovering = True self.cancel.set_sensitive(False) - self.status.set_text('Recovering keychain access. Keep this window open until the attempt finishes.') + self.set_status('Recovering keychain access. Keep this window open until the attempt finishes.') + self.spinner.set_spinning(True) + if kind == 'code': + self.set_status('Verifying your code with Apple…') self.reply(value) else: self.reply(cancel=True) @@ -306,7 +359,7 @@ def finished(self, proc): self.dialog = None self.busy(False) if proc.returncode and not self.error_seen: - self.status.set_text('Operation cancelled.' if proc.returncode in (-15, 130) else 'The client stopped before completing this operation.') + self.set_status('Operation cancelled.' if proc.returncode in (-15, 130) else 'The client stopped before completing this operation.') return False def cancel_work(self, *_): @@ -321,12 +374,15 @@ def populate(self): self.saved_password.set_visible(False) self.site.set_text('Choose a saved login') self.username.set_text('') + self.detail_fields.set_visible(False) while child := self.list.get_first_child(): self.list.remove(child) query = self.search.get_text().casefold() - for item in self.items: + shown = 0 + for item in sorted(self.items, key=lambda item: (item['title'] or item['domain']).casefold()): if query and query not in ' '.join((item['domain'], item['username'], item['title'])).casefold(): continue + shown += 1 row = Gtk.ListBoxRow() row.item = item box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) @@ -338,11 +394,15 @@ def populate(self): box.append(name) row.set_child(box) self.list.append(row) + self.empty_results.set_text('No matching passwords.\nTry another website or username.' if query else + 'No saved passwords yet.\nChoose Sync Now to refresh your snapshot.') + self.results.set_visible_child_name('list' if shown else 'empty') def select(self, _, row): self.saved_password.get_delegate().set_visibility(False) self.saved_password.set_text('') self.saved_password.set_visible(False) + self.detail_fields.set_visible(bool(row)) if row: self.site.set_text(row.item['domain'] or row.item['title']) self.username.set_text(row.item['username']) @@ -360,7 +420,7 @@ def brave_help(self, *_): def close(self, *_): if self.recovering: - self.status.set_text('Please keep this window open until the recovery attempt finishes.') + self.set_status('Please keep this window open until the recovery attempt finishes.') return True self.cancel_work() self.items.clear() diff --git a/mseries/icloud-passwords/test_2fa.py b/mseries/icloud-passwords/test_2fa.py index cb6a63e76..517e1aff3 100644 --- a/mseries/icloud-passwords/test_2fa.py +++ b/mseries/icloud-passwords/test_2fa.py @@ -22,8 +22,8 @@ class Anisette: def headers(self): return {} def send(*args, **kwargs): # The code box must be offered BEFORE Apple's request returns. - self.assertEqual(events[0]['type'],'prompt') - self.assertEqual(events[0]['kind'],'code') + self.assertEqual(next(e for e in events if e['type']=='prompt')['type'],'prompt') + self.assertEqual(next(e for e in events if e['type']=='prompt')['kind'],'code') self.assertTrue(kwargs['verify']) if network_error: raise network_error @@ -43,6 +43,7 @@ def login(argv): patch('secretstorage.dbus_init'), \ patch('secretstorage.get_default_collection',return_value=collection), \ patch.object(app,'main',side_effect=login), \ + patch.object(worker.subprocess,'run',return_value=MagicMock(returncode=0)), \ patch.multiple(ui,ask=ui.ask,secret=ui.secret,step=ui.step,out=ui.out,warn=ui.warn,err=ui.err,confirm_yn=ui.confirm_yn), \ patch.object(app,'_select_bottle',app._select_bottle): worker.main() diff --git a/mseries/icloud-passwords/test_gui.py b/mseries/icloud-passwords/test_gui.py index fa4dccb7d..ec85d8dfc 100644 --- a/mseries/icloud-passwords/test_gui.py +++ b/mseries/icloud-passwords/test_gui.py @@ -68,6 +68,30 @@ def test_empty_and_synced_vault(self): self.assertIsNone(self.app.list.get_row_at_index(0)) self.assertEqual(self.app.saved_password.get_text(),'') + def test_code_waits_validates_and_normalizes(self): + self.app.show_prompt({'kind':'code','label':'Verify your device'}) + button=self.app.dialog.get_widget_for_response(self.gui.Gtk.ResponseType.OK) + self.assertFalse(button.get_sensitive()) + self.assertIn('Waiting for you',self.app.status.get_text()) + self.assertFalse(self.app.spinner.get_spinning()) + self.app.prompt_input.set_text('12345') + self.assertFalse(button.get_sensitive()) + self.app.prompt_input.set_text('123456') + self.assertFalse(button.get_sensitive()) + self.app.prompt_input.set_text('123 456') + self.assertTrue(button.get_sensitive()) + sent=[] + self.app.reply=lambda value=None,**kwargs:sent.append(value) + self.app.dialog.response(self.gui.Gtk.ResponseType.OK) + self.assertEqual(sent,['123456']) + self.assertIsNone(self.app.dialog) + + def test_empty_search_has_recovery_hint(self): + self.app.search.set_text('missing') + self.app.populate() + self.assertEqual(self.app.results.get_visible_child_name(),'empty') + self.assertIn('Try another',self.app.empty_results.get_text()) + def test_recovery_requires_explicit_consent(self): self.app.show_prompt({'kind':'confirm','label':'Allow one recovery attempt?'}) button=self.app.dialog.get_widget_for_response(self.gui.Gtk.ResponseType.OK) diff --git a/mseries/icloud-passwords/worker.py b/mseries/icloud-passwords/worker.py index 2eee1d511..f88111f41 100644 --- a/mseries/icloud-passwords/worker.py +++ b/mseries/icloud-passwords/worker.py @@ -4,6 +4,7 @@ import logging import re import sys +import subprocess def emit(event_type, **fields): @@ -34,6 +35,8 @@ def friendly_error(message): return 'The secure connection to Apple could not be verified. No insecure fallback is allowed.' if 'passcode' in text.lower() or 'escrow' in text.lower() or 'join failed' in text.lower(): return 'Keychain recovery failed. Do not guess or automatically retry the device passcode. Your account may already be signed in.' + if 'authentication helper' in text.lower(): + return 'The local authentication helper could not start. Check its installation before signing in again.' if 'keyring' in text.lower() or 'Secret Service' in text: return 'Unlock your login keyring, then try again.' codes = re.findall(r'(?:ec=|HTTP |status )(-?\d+)', text) @@ -139,6 +142,12 @@ def choose(bottles): if action == 'load': snapshot() return 0 + if action in ('login', 'sync'): + emit('progress', text='Starting the local authentication helper…') + result = subprocess.run(['systemctl', '--user', 'start', 'mseries-icloud-anisette.service'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20) + if result.returncode: + raise RuntimeError('Local authentication helper could not start') if action == 'login': import secretstorage collection = secretstorage.get_default_collection(secretstorage.dbus_init()) From 722b0331045ec948300e94bcec8ef9ac93879525 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 15:02:09 -0400 Subject: [PATCH 023/798] Prepare English F5 source update and staged ChatGPT input fix --- .github/workflows/validate-mseries.yml | 17 + README.md | 5 + docs/local-ai.html | 3 +- mseries/OS9-HANDOFF.md | 33 ++ mseries/README.md | 4 + mseries/ROADMAP.md | 33 ++ mseries/dictation/F5-ENGLISH-ACCEPTANCE.md | 8 + mseries/dictation/QUALIFICATION-INDEX.md | 14 +- mseries/dictation/dictation-panel.py | 14 + mseries/dictation/enable-app-input.py | 244 +++++++++++++ mseries/dictation/qualification-index.json | 18 +- mseries/dictation/speech-only-panel-smoke.py | 146 ++++++++ mseries/dictation/test_app_input.py | 245 +++++++++++++ mseries/dictation/test_deploy.py | 13 + mseries/dictation/test_engine_selection.py | 120 +++++++ mseries/install-dictation-english.py | 94 ++++- mseries/install-dictation-input.py | 8 +- mseries/releases/package-f5.py | 280 +++++++++++++++ mseries/releases/test_package_f5.py | 299 ++++++++++++++++ mseries/releases/v0.0.1/README.md | 113 ++++++ mseries/releases/v0.0.1/THIRD-PARTY.md | 18 + mseries/releases/v0.0.1/install.sh | 53 +++ mseries/releases/v0.0.1/preflight.py | 326 ++++++++++++++++++ .../v0.0.1/receipts/chatgpt-human-v1.json | 30 ++ .../v0.0.1/receipts/chatgpt-launcher-v1.json | 27 ++ mseries/releases/v0.0.1/test_preflight.py | 306 ++++++++++++++++ 26 files changed, 2454 insertions(+), 17 deletions(-) create mode 100644 mseries/dictation/enable-app-input.py create mode 100644 mseries/dictation/speech-only-panel-smoke.py create mode 100644 mseries/dictation/test_app_input.py create mode 100644 mseries/releases/package-f5.py create mode 100644 mseries/releases/test_package_f5.py create mode 100644 mseries/releases/v0.0.1/README.md create mode 100644 mseries/releases/v0.0.1/THIRD-PARTY.md create mode 100755 mseries/releases/v0.0.1/install.sh create mode 100644 mseries/releases/v0.0.1/preflight.py create mode 100644 mseries/releases/v0.0.1/receipts/chatgpt-human-v1.json create mode 100644 mseries/releases/v0.0.1/receipts/chatgpt-launcher-v1.json create mode 100644 mseries/releases/v0.0.1/test_preflight.py diff --git a/.github/workflows/validate-mseries.yml b/.github/workflows/validate-mseries.yml index 84745f5c1..a1096a1e3 100644 --- a/.github/workflows/validate-mseries.yml +++ b/.github/workflows/validate-mseries.yml @@ -24,6 +24,23 @@ on: - ".github/workflows/**" jobs: + speech-release: + name: English F5 release contracts (arm64) + runs-on: ubuntu-26.04-arm + steps: + - uses: actions/checkout@v7 + - name: Install private GTK test dependencies + run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-gtk-4.0 gir1.2-ibus-1.0 gir1.2-atspi-2.0 dbus-x11 xvfb xauth + - name: Verify source-only release, rollback and host checks + run: | + bash -n mseries/releases/v0.0.1/install.sh + python3 -B -m unittest discover -s mseries/releases -p 'test_*.py' + python3 -B mseries/releases/v0.0.1/test_preflight.py + - name: Verify transcript-only UI without microphone or models + run: | + python3 -B mseries/dictation/speech-only-panel-smoke.py + python3 -B mseries/dictation/speech-only-panel-smoke.py --default-review + bluetooth: name: Quick Settings Bluetooth controls (arm64) runs-on: ubuntu-26.04-arm diff --git a/README.md b/README.md index f8b4189d4..cd5d00212 100644 --- a/README.md +++ b/README.md @@ -349,6 +349,11 @@ All seven installed app routes passed within the quiet and memory guards. Read t benchmarks or continuation work; reuse completed evidence and inspect the existing queue first. +The [v0.0.1 English F5 desktop update](mseries/releases/v0.0.1/README.md) is a +release candidate. A fresh ChatGPT physical-F5 attempt failed before recording; +its input compatibility launcher is staged and awaits a full app relaunch and +owner check. No v0.0.1 release is published yet. + --- ## Other ways in diff --git a/docs/local-ai.html b/docs/local-ai.html index 98fbadefa..da17ef6f8 100644 --- a/docs/local-ai.html +++ b/docs/local-ai.html @@ -6,6 +6,7 @@

Local AI on the M1 Pro 32 GB

Measured October 16, 2026 on the working elementary OS 9 desktop. Qwen3.5 4B through private Linux MLX is the practical default: it passed 9/15 short tasks, 9/9 corrected MCP workflows and real Newelle/memory/coding checks. Qwen9 scored 12/15 short tasks but later crossed the desktop RAM reserve; it now requires a larger optional budget.

Newelle provides local desktop chat and personal summary memory. The coding client uses a separate 16K profile. A six-GiB desktop memory reserve and fan/temperature limits take priority over completing an inference request. The real coding edit passed in 87.31 seconds, leaving at least 8.51 GiB available at no more than 2513 fan RPM. Bonsai completed a resumed 12/15 quality composite and 9/9 corrected tool workflows; native generation averaged 5.57 tokens/s. The resumed task sessions include a fan-limit interruption and are not a quiet-policy pass.

English F5 integration and sustained tests · October 18

+

v0.0.1 remains a release candidate. The owner subsequently tried physical F5 in ChatGPT and got no words or an unexpected prompt. The key reached the listener; input was refused before microphone capture because the app exposed no focused editable field. A per-user launcher correction enables renderer accessibility and IBus on the next full app launch. Seventeen launcher contracts and desktop-entry resolution pass; post-relaunch human F5 acceptance is pending. No model benchmark was repeated or working v0.0.1 release declared. The candidate contains only English speech-to-text source for the existing configured M1 Pro desktop. Candidate instructions, scope and rollback · Retained failure and staging evidence.

Merged and installed. PR #155 passed all six head-specific CI checks and merged as 5c26ff6. Installed runtime/model hashes, strict live CPU/IBus settings, enabled readiness, preserved toggle mode/input settings and the unchanged everyday 4B process passed read-only verification. Native ANE/MLX assets are retained; the optional Voice Assistant is installed with automatic replies off. All seven installed routes passed: GTK4 with public-file ASR, GTK3/Qt6/Brave with no-inference input fixtures, and Code/Newelle/Tasks with no-inference field-discovery/lease/cancel checks. The last three do not repeat positive transcription; earlier pinned real-app ASR receipts supply that evidence. The three initial missing-argument setup failures are retained; only their unstarted fixtures resumed. The installed queues retained at least10.48GiB available at≤2530RPM/49.01°C, with no guard interruption. These tests used isolated app state. No fresh owner microphone or physical-F5 acceptance is claimed.

The current integration selects English Nemotron CPU streaming, with native ANE and Parakeet MLX alternatives and configurable0–900second residency. Exact English words arrived during speech in actual elementary Code (1.671s), Newelle (1.839s) and Tasks (1.885s), plus GTK3/GTK4/Qt6/Brave fixtures. Times include controller/input preparation. No message-send or task-save action was requested. A synthetic Gmail-style rich compose/textarea regression passes; authenticated Gmail remains untested.

Testing exposed Chromium confirming speculative draft composition on focus loss. Those failures remain retained. App input now uses only stable committed words; the original transcript is preserved and English grammar suggestions stay separate. Native worker deadlines, cancellation, field leases, pressure reclamation and private rollback are covered. No owner microphone or window was used.

@@ -47,4 +48,4 @@

Reproduce and inspect

  • Setup, launchers, resource policy and removal
  • The offline factory runtime stages successfully and image pruning/configuration checks pass. A complete image build and hardware boot with this AI layer remain separate qualification steps.

    Updated October 18, 2026. Personal task/calendar contents, memory, journals, credentials and screenshots are excluded from publication.
    - \ No newline at end of file + diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index aa1c30ce5..e26a0b3fc 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,38 @@ # OS 9 implementation / Claude Code handoff +## v0.0.1 English F5 candidate; ChatGPT input correction staged — 2026-10-18 + +The owner narrowed v0.0.1 to English F5 speech to text on the existing M1 Pro +32 GB desktop; factory ISO, voice replies and diagnosis remain separate. +The fresh physical F5 check in ChatGPT FAILED: the key reached the listener, +but focused-field metadata was unavailable and it refused before microphone +capture. This is retained app-input failure evidence, not model WER. The earlier +seven installed routes and completed CPU/MLX comparisons are unchanged. + +A reviewed per-user lowercase `chatgpt.desktop` override is now staged through +`dictation-launch.py --chromium`, enabling renderer accessibility and the IBus +environment at next full app launch. Seventeen launcher contracts pass, including +independent GLib quoting/spacing, URL/action preservation, owner-edit refusal and +private rollback. Desktop resolution selects that override. ChatGPT, listener, +input bridge and everyday4B were not restarted. No microphone/model was used. +Full owner-controlled Quit, reopen from Applications, then physical F5 in the chat +composer is the remaining acceptance gate. Preserve password/active-field guards. +The owner permits OS-level dictation integration; it is not a reason to type into +an unknown field. Native compositor input/broker work remains future development. + +Release worktree: `ElementaryOS_MSeries-f5-v001`, branch `release/f5-v0.0.1`. +Source-only deterministic Git-blob packaging, strict read-only preflight, +versioned rollback and a transcript-only UI are prepared. Native/ANE/MLX assets +stay available on the host but are not bundled or requalified. See +[release candidate instructions](releases/v0.0.1/README.md) and its sanitized +[receipts](releases/v0.0.1/receipts/). No v0.0.1 tag/release has been published. +Advanced diagnostic work is isolated on `feat/quiet-diagnostic-agent`, disabled +and not qualified; never add it to the STT artifact or use everyday4B for repairs. +The existing six-hour developer watcher continues; do not duplicate it or rerun +finished ASR benchmarks. Next: commit/push candidate, verify exact-commit archive +and host preflight, record the owner's post-relaunch F5 result, then release only +when that result passes. Preserve all owner main changes; one Git writer. + ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 Removed “local” from the seven-day panel/card labels and moved counting scope into diff --git a/mseries/README.md b/mseries/README.md index c859ae2e6..463d1dc11 100644 --- a/mseries/README.md +++ b/mseries/README.md @@ -17,6 +17,10 @@ or continuation work. Reuse completed receipts, including failures, and inspect the existing owned queue before starting anything. Current controls and scoped results are in [English F5 acceptance](dictation/F5-ENGLISH-ACCEPTANCE.md). +[v0.0.1](releases/v0.0.1/README.md) is an English STT-only desktop-update candidate. +Physical F5 in ChatGPT failed before capture; an accessibility/IBus launcher fix +is staged for its next full launch. Post-relaunch human acceptance is pending. + Upstream bases (pinned at first commit — see `mseries/UPSTREAM-PINS.md`): - build system: [elementary/os](https://github.com/elementary/os) `main` - Apple Silicon hardware reference: [omacom/omarchy-mac-pkgs](https://github.com/omacom/omarchy-mac-pkgs) (audit pin in UPSTREAM-PINS.md) diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 4eafee729..b49612b42 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,5 +1,38 @@ # Roadmap, features and follow-ups (MacBook Pro 14" M1 Pro, elementary OS 9 on Asahi) +## v0.0.1 English F5 candidate; ChatGPT input correction staged — 2026-10-18 + +The owner narrowed v0.0.1 to English F5 speech to text on the existing M1 Pro +32 GB desktop; factory ISO, voice replies and diagnosis remain separate. +The fresh physical F5 check in ChatGPT FAILED: the key reached the listener, +but focused-field metadata was unavailable and it refused before microphone +capture. This is retained app-input failure evidence, not model WER. The earlier +seven installed routes and completed CPU/MLX comparisons are unchanged. + +A reviewed per-user lowercase `chatgpt.desktop` override is now staged through +`dictation-launch.py --chromium`, enabling renderer accessibility and the IBus +environment at next full app launch. Seventeen launcher contracts pass, including +independent GLib quoting/spacing, URL/action preservation, owner-edit refusal and +private rollback. Desktop resolution selects that override. ChatGPT, listener, +input bridge and everyday4B were not restarted. No microphone/model was used. +Full owner-controlled Quit, reopen from Applications, then physical F5 in the chat +composer is the remaining acceptance gate. Preserve password/active-field guards. +The owner permits OS-level dictation integration; it is not a reason to type into +an unknown field. Native compositor input/broker work remains future development. + +Release worktree: `ElementaryOS_MSeries-f5-v001`, branch `release/f5-v0.0.1`. +Source-only deterministic Git-blob packaging, strict read-only preflight, +versioned rollback and a transcript-only UI are prepared. Native/ANE/MLX assets +stay available on the host but are not bundled or requalified. See +[release candidate instructions](releases/v0.0.1/README.md) and its sanitized +[receipts](releases/v0.0.1/receipts/). No v0.0.1 tag/release has been published. +Advanced diagnostic work is isolated on `feat/quiet-diagnostic-agent`, disabled +and not qualified; never add it to the STT artifact or use everyday4B for repairs. +The existing six-hour developer watcher continues; do not duplicate it or rerun +finished ASR benchmarks. Next: commit/push candidate, verify exact-commit archive +and host preflight, record the owner's post-relaunch F5 result, then release only +when that result passes. Preserve all owner main changes; one Git writer. + ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 Removed “local” from the seven-day panel/card labels and moved counting scope into diff --git a/mseries/dictation/F5-ENGLISH-ACCEPTANCE.md b/mseries/dictation/F5-ENGLISH-ACCEPTANCE.md index 487ad02f2..f7267782a 100644 --- a/mseries/dictation/F5-ENGLISH-ACCEPTANCE.md +++ b/mseries/dictation/F5-ENGLISH-ACCEPTANCE.md @@ -5,6 +5,14 @@ It is the continuation record; completed trials and failures must be reused. [Earlier latency research](REALTIME-ENGLISH.md) remains the benchmark baseline. This report covers the subsequent controller/app implementation, not factory boot. +**Later v0.0.1 gate:** physical F5 in the owner's ChatGPT app failed before +microphone capture because no focused editable field was exposed. The key was +received. A reviewed per-user launcher enables accessibility and IBus at the next +full owner-controlled app launch; 17 launcher contracts pass. Human acceptance +after that relaunch is pending. See the [candidate instructions](../releases/v0.0.1/README.md) +and [retained failure/staging receipts](../releases/v0.0.1/receipts/). Earlier +toolkit/real-app successes below do not erase this app-specific failure. + ## Delivered design F5 selects English Nemotron streaming on CPU, four threads, trained 160 ms operating diff --git a/mseries/dictation/QUALIFICATION-INDEX.md b/mseries/dictation/QUALIFICATION-INDEX.md index f5f238a24..708668c71 100644 --- a/mseries/dictation/QUALIFICATION-INDEX.md +++ b/mseries/dictation/QUALIFICATION-INDEX.md @@ -12,6 +12,16 @@ queue remains active. PR #155 merged as `5c26ff6`; its CI and Pages byte check passed. This update publishes the final deployment/installed evidence. Inspect live units/receipts before future work; the snapshot is not a live process monitor. +**v0.0.1 release gate:** the owner subsequently tried physical F5 in the ChatGPT +desktop app and got no words or an unexpected prompt. Generic controller events +show two input refusals before microphone capture; the running app exposed no +focused editable field. This app-input failure is retained in the +[release receipts](../releases/v0.0.1/receipts/). A per-user accessibility/IBus +launcher correction is staged, with 17 passing contracts and verified desktop-entry +resolution. It requires a full owner-controlled app quit/relaunch and a fresh +physical F5 check. Staging is not a pass. The source-only English STT release +remains a candidate; no model benchmark was repeated and no v0.0.1 was published. + ## Current decision and limits - **Default candidate: Nemotron English Q8 on CPU, four threads, 160 ms trained @@ -199,8 +209,8 @@ remain; only their unstarted fixtures continued. Supervisor samples retained 10.48 GiB available at ≤2530 RPM / 49.01°C with no guard interruption. Post-fixture readiness/preservation passed. No completed model benchmark was repeated. -No qualification or activation gate remains for this scoped delivery. Fresh -physical-F5/microphone acceptance, authenticated Gmail, native Calendar GUI, +The earlier scoped deployment is complete; the later v0.0.1 physical ChatGPT +acceptance gate above remains open. Authenticated Gmail, native Calendar GUI, natural voice/full-duplex WebRTC, indefinite soak, battery/power and factory boot remain distinct future work. They do not justify rerunning the completed cohorts. A future backend/model change needs a new scoped question and explicit identity. diff --git a/mseries/dictation/dictation-panel.py b/mseries/dictation/dictation-panel.py index cd76b454a..f6ec7a52d 100644 --- a/mseries/dictation/dictation-panel.py +++ b/mseries/dictation/dictation-panel.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 """English dictation and optional English grammar suggestions. Text stays ephemeral.""" +import os import threading import time import gi @@ -17,6 +18,7 @@ def __init__(self): self.busy = False self.closed = False self.generation = 0 + self.speech_only = os.environ.get('MSERIES_DICTATION_SPEECH_ONLY') == '1' self.review = review self.read_status = read_status self.live = LiveEnglishReview() @@ -42,6 +44,11 @@ def do_activate(self): self.state = Gtk.Label(label='', xalign=0, wrap=True); box.append(self.state) self.original = self.editor(box, 'Original English transcript', editable=True) self.original.get_buffer().connect('changed', self.changed) + if self.speech_only: + self.message = Gtk.Label(label='Original English words stay in this field.', xalign=0, wrap=True) + box.append(self.message) + self.poll(); self.timer = GLib.timeout_add(500, self.poll); self.window.present() + return self.live_toggle = Gtk.CheckButton(label='Improve English grammar after dictation') self.live_toggle.set_active(True) self.live_toggle.connect('toggled', self.changed); box.append(self.live_toggle) @@ -73,6 +80,7 @@ def text(view): def changed(self, widget, *_): self.generation += 1 + if self.speech_only: return self.copy.set_sensitive(False) self.edit_phase = getattr(self, 'phase', 'unavailable') self.force_clear = getattr(self, 'force_clear', False) or widget != self.original.get_buffer() @@ -80,6 +88,7 @@ def changed(self, widget, *_): self.edit_id = GLib.idle_add(lambda: self.flush_changed(True)) def flush_changed(self, from_idle=False): + if self.speech_only: return False if not getattr(self, 'edit_id', None): return False if not from_idle: GLib.source_remove(self.edit_id) self.edit_id = None @@ -98,6 +107,7 @@ def flush_changed(self, from_idle=False): return False def observe_live(self): + if self.speech_only: return self.flush_changed() self.live.observe(self.text(self.original), self.live_toggle.get_active() and self.phase in ('ready','loading','listening','finishing'), @@ -114,6 +124,7 @@ def poll(self): 'unavailable':'Speech service is unavailable'} self.state.set_label(names.get(self.phase, names['unavailable'])) self.record.set_sensitive(self.phase not in ('loading','listening','finishing')) + if self.speech_only: return True self.transform.set_sensitive(not self.busy and self.phase not in ('loading','listening','finishing')) self.observe_live() if not self.busy: @@ -123,11 +134,13 @@ def poll(self): return True def start_review(self, *_): + if self.speech_only: return if self.busy or self.phase in ('loading','listening','finishing'): return self.live.session = False self.launch_review(self.text(self.original)) def launch_review(self, source, request=None): + if self.speech_only: return generation = self.generation; self.busy = True; self.copy.set_sensitive(False) self.transform.set_sensitive(False) self.message.set_label('Reviewing English locally… Original text is preserved.') @@ -150,6 +163,7 @@ def finished(): threading.Thread(target=worker, daemon=True).start() def show_proposal(self, source, text, final): + if self.speech_only: return self.proposed.get_buffer().set_text(text); self.preview_source = source self.copy.set_sensitive(final) self.message.set_label('Check names, numbers and meaning before copying.' if final else 'English cleanup preview — wording may change as you speak.') diff --git a/mseries/dictation/enable-app-input.py b/mseries/dictation/enable-app-input.py new file mode 100644 index 000000000..34a728e27 --- /dev/null +++ b/mseries/dictation/enable-app-input.py @@ -0,0 +1,244 @@ +#!/usr/bin/python3 +"""Prepare ChatGPT's next full launch for strict focus-bound dictation. + +Only a per-user desktop entry is changed. Never launches, stops, inspects the +contents of, or changes preferences inside the running application. +""" +import argparse +import contextlib +import fcntl +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import stat + +HERE = Path(__file__).resolve().parent +HOME = Path.home() +BASE = HOME / '.local/share/mseries/stream' +USER_ENTRY = HOME / '.local/share/applications/chatgpt.desktop' +SYSTEM_ENTRY = Path('/usr/share/applications/chatgpt.desktop') +STATE = HOME / '.local/state/mseries-app-input/chatgpt-v1' +KNOWN_COMMANDS = {'chatgpt', '/usr/bin/chatgpt', '/usr/lib/chatgpt/codex-launcher', '/usr/lib/chatgpt/ChatGPT'} +MAX_ENTRY_BYTES = 256 * 1024 + + +def manager(): + source = HERE.parent / 'install-dictation-input.py' + if not source.is_file(): + source = BASE / 'dictation-manage.py' + if not source.is_file(): + raise ValueError('The reviewed dictation payload manager is unavailable') + spec = importlib.util.spec_from_file_location('app_input_payload', source) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def read_entry(path): + if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_ENTRY_BYTES: + raise ValueError('Desktop entry is absent, oversized or a symlink; it was preserved') + raw = path.read_bytes() + if len(raw) > MAX_ENTRY_BYTES: + raise ValueError('Desktop entry exceeded its bound; it was preserved') + raw.decode('utf-8') + return raw + + +def desktop_argument(value): + """Quote a literal Exec argument through both desktop-entry escape layers.""" + value = str(value) + if any(char in value for char in '\0\r\n'): + raise ValueError('Unsupported control character in launcher path') + result = [] + for char in value: + if char == '%': + result.append('%%') + elif char == '\\': + result.append('\\' * 4) + elif char in '"$`': + result.append('\\' * 2 + char) + else: + result.append(char) + return '"' + ''.join(result) + '"' + + +def prefix(): + return ' '.join(desktop_argument(value) for value in + ('/usr/bin/python3', BASE / 'dictation-launch.py', '--chromium')) + ' ' + + +def first_command(value): + """Recognize only a simple known executable; retain its argument tail exactly. + + Desktop Exec is not a shell command. In particular, never round-trip its + argument/field-code tail through shell splitting or shell quoting. + """ + value = value.lstrip(' \t') + if not value: + raise ValueError('Empty ChatGPT Exec entry was preserved') + if value.startswith('"'): + end = value.find('"', 1) + if end < 0 or '\\' in value[1:end] or (end + 1 < len(value) and value[end + 1] not in ' \t'): + raise ValueError('Custom ChatGPT launcher quoting was preserved') + executable, tail = value[1:end], value[end + 1:] + else: + end = next((index for index, char in enumerate(value) if char in ' \t'), len(value)) + executable, tail = value[:end], value[end:] + if executable not in KNOWN_COMMANDS: + raise ValueError('Custom ChatGPT launcher was preserved; only the known packaged executable is supported') + return executable, tail + + +def rewrite(data): + text = data.decode('utf-8') + lines = [] + group = None + groups, keys = set(), set() + main_exec = False + for line in text.splitlines(keepends=True): + ending = '\r\n' if line.endswith('\r\n') else '\n' if line.endswith('\n') else '' + body = line[:-len(ending)] if ending else line + stripped = body.strip(' \t') + if not stripped or stripped.startswith('#'): + lines.append(line) + continue + if stripped.startswith('[') and stripped.endswith(']'): + group = stripped[1:-1] + if not group or group in groups: + raise ValueError('Ambiguous desktop entry group was preserved') + groups.add(group) + lines.append(line) + continue + key_part, separator, raw_value = body.partition('=') + key = key_part.strip(' \t') + if not separator or not key or group is None or any(ord(char) < 32 for char in key): + raise ValueError('Unsupported desktop entry format was preserved') + identity = (group, key) + if identity in keys or key.startswith(('Exec[', 'DBusActivatable[')): + raise ValueError('Duplicate or localized launcher control key was preserved') + keys.add(identity) + if key == 'DBusActivatable': + # Boolean control keys are not localized. Only explicit false is + # safe; unknown spellings must not make activation bypass Exec. + if raw_value.strip(' \t') != 'false': + raise ValueError('D-Bus activated or ambiguous launcher was preserved because Exec flags may be ignored') + if key == 'Exec': + if group != 'Desktop Entry' and not group.startswith('Desktop Action '): + raise ValueError('Custom desktop Exec group was preserved') + leading = raw_value[:len(raw_value) - len(raw_value.lstrip(' \t'))] + value = raw_value[len(leading):] + managed = value.startswith(prefix()) + original = value[len(prefix()):] if managed else value + executable, tail = first_command(original) + line = key_part + '=' + leading + prefix() + desktop_argument(executable) + tail + ending + if group == 'Desktop Entry': + main_exec = True + lines.append(line) + if not main_exec: + raise ValueError('No known main ChatGPT Exec entry was found; desktop entry was preserved') + return ''.join(lines).encode('utf-8') + + +def source_entry(): + return USER_ENTRY if USER_ENTRY.exists() or USER_ENTRY.is_symlink() else SYSTEM_ENTRY + + +def _validate_target_scope(payload): + if set(payload.data['files']) - {str(USER_ENTRY)}: + raise ValueError('Private app-input checkpoint has an unexpected target; no file changed') + + +def check(payload): + _validate_target_scope(payload) + raw = read_entry(USER_ENTRY) if USER_ENTRY.exists() or USER_ENTRY.is_symlink() else None + row = payload.data['files'].get(str(USER_ENTRY), {}) + actual = digest(raw) if raw is not None else None + expected = row.get('installed_sha256') + mode_matches = raw is not None and stat.S_IMODE(USER_ENTRY.stat().st_mode) == 0o644 + prepared = bool(expected and actual == expected and mode_matches) + return {'app': 'chatgpt', 'override_exists': raw is not None, + 'override_sha256': actual, 'staged': prepared, + 'owner_changed': bool(expected and (actual != expected or not mode_matches)), + 'private_backup_retained': payload.path.is_file(), + 'next_launch_flags': ['--force-renderer-accessibility'] if prepared else [], + 'next_launch_input_environment': ['GTK_IM_MODULE=ibus', 'QT_IM_MODULE=ibus', 'XMODIFIERS=@im=ibus'] if prepared else [], + 'running_app_compatibility': 'not checked', + 'activation': 'requires an owner-controlled full app quit and launch' if prepared else 'not staged'} + + +def stage(payload): + _validate_target_scope(payload) + if check(payload)['owner_changed']: + raise ValueError('Owner changed the managed launcher; it was preserved') + if not (BASE / 'dictation-launch.py').is_file(): + raise ValueError('Install the reviewed dictation launcher before preparing app input') + source = source_entry() + raw = read_entry(source) + candidate = rewrite(raw) # Reject every unknown action before writing anything. + if not payload.data['metadata']: + payload.data['metadata'] = {'app': 'chatgpt', 'source': 'user' if source == USER_ENTRY else 'system', + 'source_sha256': digest(raw), 'activation': 'next full owner-controlled launch'} + payload.install(USER_ENTRY, candidate, 0o644) + return check(payload) + + +def rollback(payload): + _validate_target_scope(payload) + if not payload.data['files'].get(str(USER_ENTRY), {}).get('installed_sha256'): + raise ValueError('No active app-input checkpoint exists') + if check(payload)['owner_changed']: + raise ValueError('Owner changed the managed launcher; rollback preserved it') + payload.restore() # Validates current owner bytes and private backup first. + + +@contextlib.contextmanager +def transaction(): + if STATE.is_symlink() or (STATE / 'baseline.json').is_symlink(): + raise ValueError('Private app-input state is a symlink; no file changed') + STATE.mkdir(parents=True, exist_ok=True, mode=0o700) + if STATE.stat().st_uid != os.getuid(): + raise ValueError('Private app-input state belongs to another user') + STATE.chmod(0o700) + fd = os.open(STATE / '.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + yield + finally: + os.close(fd) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--app', choices=('chatgpt',), required=True) + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument('--stage', action='store_true') + action.add_argument('--rollback', action='store_true') + action.add_argument('--check', action='store_true') + args = parser.parse_args() + if os.geteuid() == 0: + raise SystemExit('Run as the desktop user') + if STATE.is_symlink() or (STATE / 'baseline.json').is_symlink(): + raise ValueError('Private app-input state is a symlink; no file changed') + payload_type = manager().Payload + if args.check: + print(json.dumps(check(payload_type(STATE)), indent=2)) + else: + # Read the baseline only after taking the lock, so concurrent stages + # cannot replace the first pre-upgrade owner baseline with a stale copy. + with transaction(): + payload = payload_type(STATE) + if args.stage: + print(json.dumps(stage(payload), indent=2)) + else: + rollback(payload) + print('Previous per-user ChatGPT launcher restored. The running app was untouched.') + + +if __name__ == '__main__': + main() diff --git a/mseries/dictation/qualification-index.json b/mseries/dictation/qualification-index.json index d0d1e7ef3..f506cc238 100644 --- a/mseries/dictation/qualification-index.json +++ b/mseries/dictation/qualification-index.json @@ -2220,5 +2220,21 @@ "installed_app_acceptance": "complete: seven distinct installed routes passed; one GTK4 public-file ASR plus six no-inference app fixtures. Original argv setup failures retained, only three unstarted fixtures continued.", "post_fixture_verification_ref": "../local-ai/receipts/f5-english-2026-10-18/f5-postdeploy-v1/receipt.json" }, - "completion_scope": "Qualification, installation and installed routes complete; fresh human microphone, authenticated Gmail, natural/full-duplex speech, factory boot, indefinite soak and power/battery are separate future acceptance gaps. Never repeat completed comparable cohorts." + "completion_scope": "Qualification, installation and installed routes complete; fresh human microphone, authenticated Gmail, natural/full-duplex speech, factory boot, indefinite soak and power/battery are separate future acceptance gaps. Never repeat completed comparable cohorts.", + "release_readiness": { + "version": "0.0.1", + "scope": "existing M1 Pro 32 GB desktop, English F5 STT only", + "status": "candidate; physical ChatGPT F5 failed; launcher correction staged and awaiting full app relaunch/manual check", + "model_cohorts_repeated": false, + "receipts": [ + { + "path": "../releases/v0.0.1/receipts/chatgpt-human-v1.json", + "sha256": "5c100124ac4b6f2c99f4f82a7ec3a2184bad70366e710c9e389de50577b39192" + }, + { + "path": "../releases/v0.0.1/receipts/chatgpt-launcher-v1.json", + "sha256": "62729514f7e2769a060b8c534861a428d9dd523dd4d7737fde0f388bb7e988e5" + } + ] + } } diff --git a/mseries/dictation/speech-only-panel-smoke.py b/mseries/dictation/speech-only-panel-smoke.py new file mode 100644 index 000000000..e5ae1cc4e --- /dev/null +++ b/mseries/dictation/speech-only-panel-smoke.py @@ -0,0 +1,146 @@ +#!/usr/bin/python3 +"""Private GTK4 STT-only wiring; synthetic text/status, no microphone or models. + +Use --default-review to run the existing grammar-panel smoke in the same private +display/bus/home isolation with the speech-only environment flag explicitly off. +""" +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import time + + +def isolated(default_review): + with tempfile.TemporaryDirectory(prefix='mseries-speech-only-panel-') as directory: + root = Path(directory) + env = dict(os.environ) + for key in ('DISPLAY', 'WAYLAND_DISPLAY', 'XAUTHORITY', 'DBUS_SESSION_BUS_ADDRESS', + 'AT_SPI_BUS_ADDRESS', 'IBUS_ADDRESS'): + env.pop(key, None) + for variable, name in [('HOME', 'home'), ('XDG_RUNTIME_DIR', 'runtime'), + ('XDG_CONFIG_HOME', 'config'), ('XDG_CACHE_HOME', 'cache'), + ('XDG_DATA_HOME', 'data'), ('XDG_STATE_HOME', 'state')]: + path = root / name + path.mkdir(mode=0o700) + env[variable] = str(path) + env.update(MSERIES_SPEECH_PANEL_TEST='1', MSERIES_INPUT_TEST_SESSION='1', + GSETTINGS_BACKEND='memory', GSK_RENDERER='cairo', GDK_BACKEND='x11', + NO_AT_BRIDGE='1', GTK_A11Y='none', GTK_USE_PORTAL='0', GIO_USE_VFS='local') + if default_review: + env.pop('MSERIES_DICTATION_SPEECH_ONLY', None) + script = Path(__file__).with_name('panel-smoke.py') + else: + env['MSERIES_DICTATION_SPEECH_ONLY'] = '1' + script = Path(__file__).resolve() + config = root / 'session.conf' + config.write_text('sessionunix:tmpdir=/tmp' + 'EXTERNAL' + '' + '') + return subprocess.run(['dbus-run-session', '--config-file=' + str(config), '--', + 'xvfb-run', '-a', sys.executable, str(script)], env=env, + timeout=30).returncode + + +def main(): + default_review = sys.argv[1:] == ['--default-review'] + if sys.argv[1:] and not default_review: + raise SystemExit('Use no arguments or --default-review') + if os.environ.get('MSERIES_SPEECH_PANEL_TEST') != '1' or default_review: + return isolated(default_review) + import gi + gi.require_version('Gtk', '4.0') + from gi.repository import GLib, Gtk + spec = importlib.util.spec_from_file_location('speech_only_panel', Path(__file__).with_name('dictation-panel.py')) + panel = importlib.util.module_from_spec(spec) + spec.loader.exec_module(panel) + app = panel.DictationPanel() + phase = ['ready'] + calls = [] + app.read_status = lambda: {'phase': phase[0]} + def forbidden_review(*args): + calls.append(args) + raise AssertionError('Speech-only panel tried to invoke a language model') + class ForbiddenLive: + def observe(self, *_): + raise AssertionError('Speech-only panel entered the review scheduler') + def take(self, *_): + raise AssertionError('Speech-only panel requested a review') + app.review = forbidden_review + app.live = ForbiddenLive() + assert app.register(None) + app.activate() + def settle(): + until = time.monotonic() + .04 + while time.monotonic() < until: + while GLib.MainContext.default().pending(): + GLib.MainContext.default().iteration(False) + time.sleep(.005) + def descendants(widget): + yield widget + child = widget.get_first_child() + while child: + yield from descendants(child) + child = child.get_next_sibling() + try: + assert app.speech_only + assert app.original.get_editable() and app.original.get_visible() + assert app.state.get_visible() and app.record.get_visible() + assert 'Original English transcript' in [w.get_label() for w in descendants(app.window) if isinstance(w, Gtk.Label)] + for control in ('live_toggle', 'preview_toggle', 'transform', 'proposed', 'copy'): + assert not hasattr(app, control), 'Speech-only mode constructed review control: ' + control + assert not any(isinstance(w, Gtk.CheckButton) for w in descendants(app.window)) + assert [w.get_label() for w in descendants(app.window) if isinstance(w, Gtk.Button) and w.get_label()] == ['Focus transcript for F5'] + app.record.emit('clicked') + assert app.window.get_focus() == app.original + source = 'These are my original English words.\nKeep 42, punctuation, and literal "review this" unchanged.' + app.original.get_buffer().set_text('These are my') + for current in ('loading', 'listening', 'finishing', 'ready'): + phase[0] = current + app.poll() + assert app.record.get_sensitive() == (current == 'ready') + app.original.get_buffer().set_text(source) + app.start_review() + app.launch_review(source) + app.show_proposal(source, 'MUST NOT replace anything', True) + app.observe_live() + app.flush_changed() + settle() + assert app.text(app.original) == source and not app.busy + assert not getattr(app, 'edit_id', None) + for current in ('error', 'waiting', 'cancelled', 'stopped', 'unavailable', 'ready'): + phase[0] = current + app.poll() + assert app.record.get_sensitive() and app.state.get_label() + assert app.text(app.original) == source + # Exercise more than one status timer tick after a completed dictation. + until = time.monotonic() + 1.1 + while time.monotonic() < until: + settle() + assert calls == [] and app.text(app.original) == source and not app.busy + app.window.close() + assert app.closed and not app.poll() + app.start_review() + app.launch_review(source) + assert calls == [] + print(json.dumps({'passed': True, 'native_gtk4_speech_only_panel': True, + 'speech_only_environment_flag': True, 'original_transcript_preserved': True, + 'focus_and_status_controls': True, 'review_controls_absent': True, + 'capture_finalization_no_review': True, 'programmatic_review_blocked': True, + 'status_timer_no_review': True, 'model_inference': False, + 'microphone_recorded': False, 'owner_windows_opened': False, + 'dictation_panel_sha256': hashlib.sha256(Path(__file__).with_name('dictation-panel.py').read_bytes()).hexdigest()})) + finally: + if not app.closed: + app.window.close() + app.quit() + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/mseries/dictation/test_app_input.py b/mseries/dictation/test_app_input.py new file mode 100644 index 000000000..934a4b602 --- /dev/null +++ b/mseries/dictation/test_app_input.py @@ -0,0 +1,245 @@ +"""Launcher preparation contracts; no app, user service, input or model runs.""" +import contextlib +import importlib.util +import io +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + + +HERE = Path(__file__).resolve().parent + + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +app = load('tested_app_input', HERE / 'enable-app-input.py') +payload_manager = load('tested_app_input_payload', HERE.parent / 'install-dictation-input.py') + + +class AppInputTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + root = Path(self.temporary.name) + self.stack = contextlib.ExitStack() + self.addCleanup(self.stack.close) + for key, value in {'HOME': root, 'BASE': root / 'runtime with spaces', + 'USER_ENTRY': root / 'applications/chatgpt.desktop', + 'SYSTEM_ENTRY': root / 'system/chatgpt.desktop', 'STATE': root / 'private'}.items(): + self.stack.enter_context(patch.object(app, key, value)) + app.BASE.mkdir() + (app.BASE / 'dictation-launch.py').write_text('# synthetic launcher, never executed\n') + app.SYSTEM_ENTRY.parent.mkdir() + app.SYSTEM_ENTRY.write_bytes(b'[Desktop Entry]\nName=ChatGPT\nExec=chatgpt %U\nIcon=chatgpt\nType=Application\nStartupNotify=true\n') + self.payload = payload_manager.Payload(app.STATE) + + def test_system_entry_creates_same_id_user_override_and_preserves_keys(self): + original = app.SYSTEM_ENTRY.read_bytes() + result = app.stage(self.payload) + raw = app.USER_ENTRY.read_bytes() + self.assertEqual(app.USER_ENTRY.name, app.SYSTEM_ENTRY.name) + self.assertIn(('Exec=' + app.prefix() + '"chatgpt" %U\n').encode(), raw) + self.assertEqual([line for line in raw.splitlines() if not line.startswith(b'Exec=')], + [line for line in original.splitlines() if not line.startswith(b'Exec=')]) + self.assertEqual(app.SYSTEM_ENTRY.read_bytes(), original) + self.assertTrue(result['staged']) + self.assertEqual(result['running_app_compatibility'], 'not checked') + self.assertIn('owner-controlled', result['activation']) + self.assertEqual(app.STATE.stat().st_mode & 0o777, 0o700) + self.assertEqual(self.payload.path.stat().st_mode & 0o777, 0o600) + + def test_existing_user_entry_and_quoted_argument_tail_survive_exactly(self): + original = (b'[Desktop Entry]\r\nName=My ChatGPT\r\n' + b'Exec="/usr/bin/chatgpt" --profile "A quoted path/profile" %U\r\n' + b'Comment=My description\r\nX-Owner-Setting=leave this intact\r\n') + app.USER_ENTRY.parent.mkdir() + app.USER_ENTRY.write_bytes(original) + app.stage(self.payload) + changed = app.USER_ENTRY.read_bytes() + self.assertIn(b'"/usr/bin/chatgpt" --profile "A quoted path/profile" %U\r\n', changed) + self.assertTrue(changed.endswith(b'X-Owner-Setting=leave this intact\r\n')) + app.rollback(self.payload) + self.assertEqual(app.USER_ENTRY.read_bytes(), original) + + def test_all_desktop_actions_are_wrapped_without_losing_flags_or_field_codes(self): + raw = (b'[Desktop Entry]\nName=ChatGPT\nExec=chatgpt %U\nActions=New;Private;\n' + b'\n[Desktop Action New]\nName=New window\nExec=/usr/bin/chatgpt --new-window %U\n' + b'\n[Desktop Action Private]\nName=Private\nExec=chatgpt --private "literal %%"\n') + changed = app.rewrite(raw) + self.assertEqual(changed.count(app.prefix().encode()), 3) + self.assertIn(b' --new-window %U\n', changed) + self.assertIn(b' --private "literal %%"\n', changed) + self.assertEqual([line for line in changed.splitlines() if not line.startswith(b'Exec=')], + [line for line in raw.splitlines() if not line.startswith(b'Exec=')]) + + def test_unknown_main_or_action_wrapper_is_rejected_before_target_or_backup_write(self): + for text in ('[Desktop Entry]\nExec=custom-wrapper chatgpt %U\n', + '[Desktop Entry]\nExec=chatgpt %U\n[Desktop Action Other]\nExec=/usr/bin/env CHATGPT_FLAGS=foo chatgpt\n', + '[Desktop Entry]\nDBusActivatable=true\nExec=chatgpt %U\n'): + app.SYSTEM_ENTRY.write_text(text) + with self.subTest(text=text), self.assertRaises(ValueError): + app.stage(self.payload) + self.assertFalse(app.USER_ENTRY.exists()) + self.assertFalse(self.payload.path.exists()) + + def test_repeat_stage_is_identical_and_keeps_the_original_private_baseline(self): + app.stage(self.payload) + first = app.USER_ENTRY.read_bytes() + baseline = json.loads(self.payload.path.read_text())['files'][str(app.USER_ENTRY)].copy() + app.stage(self.payload) + self.assertEqual(app.USER_ENTRY.read_bytes(), first) + self.assertEqual(json.loads(self.payload.path.read_text())['files'][str(app.USER_ENTRY)], baseline) + self.assertEqual(first.count(app.prefix().encode()), 1) + app.rollback(self.payload) + self.assertFalse(app.USER_ENTRY.exists()) + + def test_owner_edit_refuses_repeat_stage_and_rollback(self): + app.stage(self.payload) + edited = app.USER_ENTRY.read_bytes() + b'X-Later-Owner-Preference=true\n' + app.USER_ENTRY.write_bytes(edited) + result = app.check(self.payload) + self.assertTrue(result['owner_changed']) + self.assertFalse(result['staged']) + for action in (app.stage, app.rollback): + with self.subTest(action=action.__name__), self.assertRaisesRegex(ValueError, 'Owner changed'): + action(self.payload) + self.assertEqual(app.USER_ENTRY.read_bytes(), edited) + + def test_owner_permission_change_is_preserved(self): + app.stage(self.payload) + app.USER_ENTRY.chmod(0o600) + self.assertTrue(app.check(self.payload)['owner_changed']) + for action in (app.stage, app.rollback): + with self.subTest(action=action.__name__), self.assertRaisesRegex(ValueError, 'Owner changed'): + action(self.payload) + self.assertEqual(app.USER_ENTRY.stat().st_mode & 0o777, 0o600) + + def test_other_checkpoint_target_is_never_restored_or_changed(self): + self.payload.data['files']['/synthetic/unexpected-owner-path'] = {} + for action in (app.stage, app.rollback, app.check): + with self.subTest(action=action.__name__), self.assertRaisesRegex(ValueError, 'unexpected target'): + action(self.payload) + + def test_check_is_read_only_and_does_not_claim_current_application_support(self): + result = app.check(self.payload) + self.assertFalse(app.STATE.exists()) + self.assertFalse(app.USER_ENTRY.exists()) + self.assertFalse(result['staged']) + self.assertEqual(result['next_launch_flags'], []) + self.assertEqual(result['running_app_compatibility'], 'not checked') + + def test_symlink_entry_and_oversized_source_are_preserved(self): + app.USER_ENTRY.parent.mkdir() + app.USER_ENTRY.symlink_to(app.SYSTEM_ENTRY) + with self.assertRaisesRegex(ValueError, 'symlink'): + app.stage(self.payload) + self.assertTrue(app.USER_ENTRY.is_symlink()) + app.USER_ENTRY.unlink() + app.SYSTEM_ENTRY.write_bytes(b'X' * (app.MAX_ENTRY_BYTES + 1)) + with self.assertRaisesRegex(ValueError, 'oversized'): + app.stage(self.payload) + self.assertFalse(self.payload.path.exists()) + + def test_literal_destination_path_escaping_uses_desktop_layers_not_shell_quoting(self): + value = app.desktop_argument('/synthetic/a path/percent%/dollar$/back\\slash/quote"/tick`') + self.assertIn('percent%%', value) + self.assertIn('dollar' + '\\' * 2 + '$', value) + self.assertIn('back' + '\\' * 4 + 'slash', value) + self.assertIn('quote' + '\\' * 2 + '"', value) + with self.assertRaises(ValueError): + app.desktop_argument('/synthetic/new\nline') + + def test_glib_independently_parses_the_two_desktop_escaping_layers(self): + try: + from gi.repository import GLib + except ImportError: + self.skipTest('GLib parser is unavailable on this test host') + destination = Path('/synthetic/A path/dollar$/back\\slash/quote"/tick`') + with patch.object(app, 'BASE', destination): + data = app.rewrite(b'[Desktop Entry]\nExec=chatgpt %U\n') + keyfile = GLib.KeyFile() + keyfile.load_from_data(data.decode(), len(data), GLib.KeyFileFlags.NONE) + _success, arguments = GLib.shell_parse_argv(keyfile.get_string('Desktop Entry', 'Exec')) + self.assertEqual(arguments, ['/usr/bin/python3', str(destination / 'dictation-launch.py'), '--chromium', 'chatgpt', '%U']) + + def test_glib_spaced_main_and_actions_are_wrapped_preserving_layout(self): + try: + from gi.repository import GLib + except ImportError: + self.skipTest('GLib parser is unavailable on this test host') + raw = (b'[Desktop Entry]\r\nName=ChatGPT\r\nType=Application\r\n' + b'Exec \t= \tchatgpt --profile "owner profile" %U \t\r\n' + b'DBusActivatable \t= false\r\nActions=New;\r\n' + b'[Desktop Action New]\r\nName=New\r\nExec = /usr/bin/chatgpt --new-window %U\r\n') + changed = app.rewrite(raw) + self.assertIn(('Exec \t= \t' + app.prefix() + '"chatgpt" --profile "owner profile" %U \t\r\n').encode(), changed) + self.assertIn(('Exec = ' + app.prefix() + '"/usr/bin/chatgpt" --new-window %U\r\n').encode(), changed) + self.assertIn(b'DBusActivatable \t= false\r\n', changed) + self.assertEqual(app.rewrite(changed), changed) + keyfile = GLib.KeyFile() + keyfile.load_from_data(changed.decode(), len(changed), GLib.KeyFileFlags.NONE) + for group in ('Desktop Entry', 'Desktop Action New'): + _success, arguments = GLib.shell_parse_argv(keyfile.get_string(group, 'Exec')) + self.assertEqual(arguments[:3], ['/usr/bin/python3', str(app.BASE / 'dictation-launch.py'), '--chromium']) + self.assertFalse(keyfile.get_boolean('Desktop Entry', 'DBusActivatable')) + + def test_glib_semantic_dbus_activation_is_refused_before_stage(self): + try: + from gi.repository import GLib + except ImportError: + self.skipTest('GLib parser is unavailable on this test host') + for control in ('DBusActivatable = true', 'DBusActivatable\t=\ttrue'): + raw = ('[Desktop Entry]\nName=ChatGPT\nType=Application\n' + control + '\nExec=chatgpt %U\n').encode() + keyfile = GLib.KeyFile() + keyfile.load_from_data(raw.decode(), len(raw), GLib.KeyFileFlags.NONE) + self.assertTrue(keyfile.get_boolean('Desktop Entry', 'DBusActivatable')) + app.SYSTEM_ENTRY.write_bytes(raw) + with self.subTest(control=control), self.assertRaisesRegex(ValueError, 'D-Bus'): + app.stage(self.payload) + self.assertFalse(app.USER_ENTRY.exists()) + self.assertFalse(self.payload.path.exists()) + + def test_duplicate_localized_or_missing_main_control_keys_are_preserved(self): + for text in ( + '[Desktop Entry]\nExec=chatgpt\nExec = chatgpt --other\n', + '[Desktop Entry]\nExec=chatgpt\nDBusActivatable=false\nDBusActivatable = true\n', + '[Desktop Entry]\nExec=chatgpt\nExec[en]=chatgpt --other\n', + '[Desktop Entry]\nExec=chatgpt\nDBusActivatable[en]=true\n', + '[Desktop Entry]\nExec=chatgpt\nDBusActivatable=1\n', + '[Desktop Entry]\nExec=chatgpt\n[Desktop Entry]\nExec=chatgpt\n', + '[Desktop Entry]\nName=ChatGPT\n[Desktop Action New]\nExec=chatgpt\n', + ): + app.SYSTEM_ENTRY.write_text(text) + with self.subTest(text=text), self.assertRaises(ValueError): + app.stage(self.payload) + self.assertFalse(app.USER_ENTRY.exists()) + self.assertFalse(self.payload.path.exists()) + + def test_required_action_and_check_cli_perform_no_subprocess_or_app_work(self): + with patch.object(app, 'manager', return_value=payload_manager), patch.object(app.os, 'geteuid', return_value=1000), patch('sys.argv', ['enable-app-input', '--app', 'chatgpt', '--check']), contextlib.redirect_stdout(io.StringIO()) as output: + app.main() + self.assertFalse(json.loads(output.getvalue())['staged']) + self.assertFalse(app.STATE.exists()) + with patch.object(app.os, 'geteuid', return_value=1000), patch('sys.argv', ['enable-app-input', '--app', 'chatgpt']), contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit): + app.main() + + def test_mutation_lock_prevents_competing_stage_and_private_state_symlinks(self): + with app.transaction(): + with self.assertRaises(BlockingIOError): + with app.transaction(): + self.fail('overlapping launcher mutation') + (app.STATE / 'baseline.json').symlink_to(app.SYSTEM_ENTRY) + with self.assertRaisesRegex(ValueError, 'symlink'): + with app.transaction(): + self.fail('unsafe state admitted') + + +if __name__ == '__main__': + unittest.main() diff --git a/mseries/dictation/test_deploy.py b/mseries/dictation/test_deploy.py index bc37804df..e6c6d0238 100644 --- a/mseries/dictation/test_deploy.py +++ b/mseries/dictation/test_deploy.py @@ -22,6 +22,19 @@ def load(name, path): class DeploymentTests(unittest.TestCase): + def test_speech_only_panel_launcher_is_explicit_and_default_compatible(self): + payload = Mock() + deploy.install_panel(payload) + normal = payload.install.call_args.args[1].decode() + self.assertIn('optional transcript review', normal) + self.assertNotIn('MSERIES_DICTATION_SPEECH_ONLY', normal) + deploy.install_panel(payload, speech_only=True) + speech = payload.install.call_args.args[1].decode() + self.assertIn('Exec="/usr/bin/env" "MSERIES_DICTATION_SPEECH_ONLY=1" "/usr/bin/python3"', speech) + self.assertNotIn('optional transcript review', speech) + self.assertNotIn('voice-assistant', speech) + self.assertEqual(payload.install.call_args.args[2], 0o644) + def test_browser_override_keeps_actions_and_urls(self): text = '[Desktop Entry]\nName=Browser\nExec=/usr/bin/brave-browser-stable %U\n\n[Desktop Action private]\nExec=brave-browser --incognito %U\n' result = deploy.brave_override(text) diff --git a/mseries/dictation/test_engine_selection.py b/mseries/dictation/test_engine_selection.py index 633a210b1..2239073f5 100644 --- a/mseries/dictation/test_engine_selection.py +++ b/mseries/dictation/test_engine_selection.py @@ -1,5 +1,6 @@ """Engine deployment/rollback contracts with fake services; no native inference.""" import importlib.util +import json from pathlib import Path import subprocess import tempfile @@ -189,6 +190,8 @@ def test_activation_stops_owned_asr_before_first_stage_write(self): calls = [] fake_engine.SERVICE = "owned-asr" fake_engine.INPUT_SERVICE = "owned-input" + fake_engine.DROPIN = Path(directory) / "90-english-engine.conf" + fake_engine.latest_change.return_value = (None, None) fake_engine.active.side_effect = lambda: state["active"] fake_engine.transaction_lock.return_value = contextlib.nullcontext() def service(*args, **kwargs): @@ -205,6 +208,48 @@ def staged(*args, **kwargs): self.assertTrue(payload.data["metadata"]["stream_active"]) self.assertTrue(fake_engine.select.call_args.kwargs["was_active"]) + def test_speech_only_uses_separate_immediate_preupdate_baseline(self): + with tempfile.TemporaryDirectory() as directory, patch.object(installer, 'HOME', Path(directory)): + root = Path(directory) + target = root / 'existing-payload.py' + target.write_bytes(b'original before earlier full installer') + old = manager.Payload(root / 'legacy-state') + old.install(target, b'working full installation immediately before v0.0.1', 0o600) + old_bytes = old.path.read_bytes() + state = installer.state_directory(True) + self.assertEqual(state, root / '.local/state/mseries-dictation-stt-v0.0.1') + update = manager.Payload(state) + update.install(target, b'v0.0.1 STT update', 0o600) + installer.restore_files(update) + self.assertEqual(target.read_bytes(), b'working full installation immediately before v0.0.1') + self.assertEqual(old.path.read_bytes(), old_bytes) + + def test_speech_only_stage_excludes_voice_and_optional_copy(self): + with tempfile.TemporaryDirectory() as directory, patch.object(installer, 'HOME', Path(directory)), patch.object(installer, 'BASE', Path(directory) / 'runtime'): + base = installer.BASE + (base / 'venv/bin').mkdir(parents=True) + (base / 'venv/bin/python').touch() + fake = Mock() + fake.active.return_value = False + fake.NEMOTRON = engine.NEMOTRON + fake.INPUT_SERVICE = engine.INPUT_SERVICE + fake.systemctl.return_value = subprocess.CompletedProcess([], 0) + panel = Mock() + payload = manager.Payload(Path(directory) / 'checkpoint') + with patch.object(installer, 'fetch_model', return_value=base / 'models' / engine.NEMOTRON), patch.object(installer, 'input_engine', return_value='xkb:us::eng'), patch.object(installer, 'optional_mlx') as optional: + installer.stage(payload, panel, fake, speech_only=True) + optional.assert_not_called() + panel.install_panel.assert_called_once_with(payload, speech_only=True) + names = set(Path(name).name for name in payload.data['files']) + self.assertFalse({'voice-assistant.py', 'dictation_voice.py', 'org.techmore.VoiceAssistant.desktop'} & names) + self.assertTrue({'stream-dictate.py', 'dictation-panel.py', 'dictation-input.py'} <= names) + + def test_speech_only_rejects_non_cpu_and_optional_asset_arguments(self): + for args in (['--activate', 'mlx'], ['--mlx-model-dir', '/synthetic/assets'], ['--mlx-modules', '/synthetic/modules']): + with self.subTest(args=args), patch.object(installer.os, 'geteuid', return_value=1000), patch.object(installer, 'helper') as helper, patch.object(sys, 'argv', ['installer', '--speech-only', *args]), contextlib.redirect_stderr(__import__('io').StringIO()), self.assertRaises(SystemExit): + installer.main() + helper.assert_not_called() + def test_bridge_restart_keeps_valid_owner_input_selection(self): fake = Mock() with patch.object(installer, "input_engine", return_value="xkb:us::eng"), patch.object(installer.subprocess, "run") as run: @@ -231,5 +276,80 @@ def test_ready_before_keyboard_failure_is_not_deployment_success(self): engine.wait_ready("nemotron") +class InstallerSelectorOwnership(unittest.TestCase): + def setUp(self): + EngineSelection.setUp(self) + self.payload = manager.Payload(Path(self.temporary.name) / 'stt-checkpoint') + self.payload.data['metadata'] = {'stream_active': True, 'input_active': False, 'activation_started': False} + self.input = patch.object(installer, 'input_engine', return_value='xkb:us::eng') + self.input.start(); self.addCleanup(self.input.stop) + engine.DROPIN.write_bytes(b'Original owner engine configuration\n') + with engine.transaction_lock(): + engine.select('nemotron', stage_only=True) + self.previous_path, _ = engine.latest_change() + self.previous_bytes = self.previous_path.read_bytes() + self.previous_dropin = engine.DROPIN.read_bytes() + installer.remember_selector(self.payload, engine) + self.payload.data['metadata']['activation_started'] = True + self.payload.save() + + def new_selection(self): + with engine.transaction_lock(): + engine.select('nemotron', stage_only=True) + + def test_failure_before_select_preserves_previous_successful_selection(self): + installer.capture_selector(self.payload, engine) + with patch.object(engine, 'restore_change', wraps=engine.restore_change) as restore: + installer.restore_payload(self.payload, engine) + restore.assert_not_called() + self.assertEqual(engine.DROPIN.read_bytes(), self.previous_dropin) + self.assertEqual(self.previous_path.read_bytes(), self.previous_bytes) + + def test_failure_after_select_rolls_back_only_this_updates_selection(self): + self.new_selection() + owned_path, _ = engine.latest_change() + installer.capture_selector(self.payload, engine) + installer.restore_payload(self.payload, engine) + self.assertTrue(json.loads(owned_path.read_text())['rolled_back']) + self.assertEqual(self.previous_path.read_bytes(), self.previous_bytes) + self.assertEqual(engine.DROPIN.read_bytes(), self.previous_dropin) + + def test_selectors_own_failure_rollback_is_not_repeated(self): + engine.wait_ready.side_effect = ValueError('synthetic failed health') + with engine.transaction_lock(), self.assertRaisesRegex(ValueError, 'synthetic failed health'): + engine.select('nemotron') + installer.capture_selector(self.payload, engine) + self.calls.clear() + with patch.object(engine, 'restore_change', wraps=engine.restore_change) as restore: + installer.restore_payload(self.payload, engine) + restore.assert_not_called() + self.assertEqual(engine.DROPIN.read_bytes(), self.previous_dropin) + + def test_later_owner_selection_refuses_rollback_before_any_service_stop(self): + self.new_selection() + installer.capture_selector(self.payload, engine) + self.new_selection() # A later independently selected engine, even if bytes happen to match. + self.calls.clear() + with self.assertRaisesRegex(ValueError, 'Owner selected'): + installer.restore_payload(self.payload, engine) + self.assertEqual(self.calls, []) + + def test_edited_owned_dropin_refuses_rollback_before_any_service_stop(self): + self.new_selection() + installer.capture_selector(self.payload, engine) + engine.DROPIN.write_text('later owner configuration\n') + self.calls.clear() + with self.assertRaisesRegex(ValueError, 'Owner changed'): + installer.restore_payload(self.payload, engine) + self.assertEqual(self.calls, []) + + def test_legacy_ambiguous_checkpoint_refuses_before_any_service_stop(self): + self.payload.data['metadata'].pop('selector_schema') + self.calls.clear() + with self.assertRaisesRegex(ValueError, 'Legacy ambiguous'): + installer.restore_payload(self.payload, engine) + self.assertEqual(self.calls, []) + + if __name__ == "__main__": unittest.main() diff --git a/mseries/install-dictation-english.py b/mseries/install-dictation-english.py index 6b4d1097c..ed4a28eef 100644 --- a/mseries/install-dictation-english.py +++ b/mseries/install-dictation-english.py @@ -7,6 +7,7 @@ local sources, never installed into the owner's Python environment. """ import argparse +import base64 import hashlib import importlib.util import json @@ -209,7 +210,7 @@ def restart_bridge(engine, previous): engine.bridge_ready() -def stage(payload, manager, engine, *, model_source=None, download=False, mlx_model=None, mlx_modules=None): +def stage(payload, manager, engine, *, model_source=None, download=False, mlx_model=None, mlx_modules=None, speech_only=False): engine.idle_capture() if engine.active(): raise ValueError("Stop the owned idle F5 service or use --activate before staging its live payload") @@ -224,7 +225,7 @@ def stage(payload, manager, engine, *, model_source=None, download=False, mlx_mo payload.save() for name in RUNTIME_FILES: payload.install(BASE / name, (SOURCE / name).read_bytes(), 0o755 if name.endswith(".py") else 0o644) - for name in ("dictation_voice.py", "voice-assistant.py"): + for name in (() if speech_only else ("dictation_voice.py", "voice-assistant.py")): if (SOURCE / name).is_file(): payload.install(BASE / name, (SOURCE / name).read_bytes(), 0o755) machine = HERE / "local-ai/machine.py" @@ -240,11 +241,12 @@ def stage(payload, manager, engine, *, model_source=None, download=False, mlx_mo target = BASE / "models" / engine.NEMOTRON if artifact != target: install_file(payload, target, artifact) - optional_mlx(payload, mlx_model, mlx_modules) - manager.install_panel(payload) + if not speech_only: + optional_mlx(payload, mlx_model, mlx_modules) + manager.install_panel(payload, speech_only=speech_only) payload.install(HOME / ".config/systemd/user" / engine.INPUT_SERVICE, (SOURCE / engine.INPUT_SERVICE).read_bytes(), 0o644) - if (SOURCE / "voice-assistant.py").is_file(): + if not speech_only and (SOURCE / "voice-assistant.py").is_file(): def quote(value): return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"').replace("`", "\\`").replace("$", "\\$") + '"' desktop = ("[Desktop Entry]\nType=Application\nName=Voice Assistant\n" @@ -255,19 +257,83 @@ def quote(value): payload.install(HOME / ".local/share/applications/org.techmore.VoiceAssistant.desktop", desktop.encode(), 0o644) +def state_directory(speech_only=False): + return HOME / ".local/state/mseries-dictation-stt-v0.0.1" if speech_only else STATE + + +def selector_marker(engine): + path, change = engine.latest_change() + if path is None: + if change is not None: + raise ValueError("Ambiguous selector checkpoint; configuration preserved") + return None + return {"name": path.name, "sha256": file_sha(path)} + + +def dropin_digest(engine): + if engine.DROPIN.is_symlink(): + raise ValueError("Owner changed the managed engine file; configuration preserved") + return file_sha(engine.DROPIN) if engine.DROPIN.exists() else None + + +def remember_selector(payload, engine): + metadata = payload.data["metadata"] + metadata.update(selector_schema=1, selector_before=selector_marker(engine), + selector_owned=None, selector_before_dropin=dropin_digest(engine)) + # A pre-existing managed selection must itself still match its receipt. + path, change = engine.latest_change() + if change and not change.get("rolled_back"): + engine.assert_managed(change) + payload.save() + + +def capture_selector(payload, engine): + """Called only under the shared selector lock, after our select attempt.""" + metadata = payload.data["metadata"] + marker = selector_marker(engine) + if marker != metadata["selector_before"]: + if marker is None or (metadata["selector_before"] and marker["name"] == metadata["selector_before"]["name"]): + raise ValueError("Owner changed the previous selector receipt; rollback preserved it") + metadata["selector_owned"] = marker + payload.save() + + +def validate_selector_restore(payload, engine): + metadata = payload.data["metadata"] + if metadata.get("selector_schema") != 1 or "selector_before_dropin" not in metadata: + raise ValueError("Legacy ambiguous engine checkpoint: no service was stopped; reconcile the previous deployment first") + expected = metadata.get("selector_owned") or metadata.get("selector_before") + if selector_marker(engine) != expected: + raise ValueError("Owner selected or changed another engine; rollback preserved it") + path, change = engine.latest_change() + if metadata.get("selector_owned") and change and not change.get("rolled_back"): + engine.assert_managed(change) + # Verify the rollback bytes before any service is touched. + original = base64.b64decode(change["before"], validate=True) if change["before"] is not None else None + original_sha = hashlib.sha256(original).hexdigest() if original is not None else None + if original is not None and original_sha != change["before_sha256"]: + raise ValueError("Private engine backup digest mismatch; configuration preserved") + if original_sha != metadata["selector_before_dropin"]: + raise ValueError("Engine rollback does not match this update's baseline") + return path, change + if dropin_digest(engine) != metadata["selector_before_dropin"]: + raise ValueError("Owner changed the previous engine configuration; rollback preserved it") + return None, None + + def restore_payload(payload, engine): validate_restore(payload) # Check all owner edits/backups before stopping anything. if not payload.data["metadata"].get("activation_started"): restore_files(payload) return # A stage-only rollback never changes running services. + path, change = validate_selector_restore(payload, engine) engine.idle_capture() engine.quiet_admission() engine.idle_capture() engine.systemctl("stop", engine.SERVICE) previous_input = input_engine() engine.systemctl("stop", engine.INPUT_SERVICE) - path, change = engine.latest_change() - if change and not change.get("rolled_back"): + if change: engine.restore_change(path, change, restart=False) restore_files(payload) engine.systemctl("daemon-reload") @@ -294,13 +360,16 @@ def main(): parser.add_argument("--download-model", action="store_true") parser.add_argument("--mlx-model-dir", type=Path) parser.add_argument("--mlx-modules", type=Path) + parser.add_argument("--speech-only", action="store_true", help="v0.0.1 CPU English STT update; exclude voice/LLM additions and retain a separate rollback") args = parser.parse_args() if os.geteuid() == 0: raise SystemExit("Run as the desktop user") if (args.mode or args.warm_seconds is not None) and not args.activate: parser.error("--mode/--warm-seconds require --activate") + if args.speech_only and (args.mlx_model_dir or args.mlx_modules or args.activate not in (None, "nemotron")): + parser.error("--speech-only accepts only Nemotron CPU and no optional MLX asset copy") manager, engine = helper(), selector() - payload = manager.Payload(STATE) + payload = manager.Payload(state_directory(args.speech_only)) with engine.transaction_lock(): if args.rollback: if not payload.data["metadata"]: @@ -308,6 +377,8 @@ def main(): restore_payload(payload, engine) print("Previous English payload and engine restored; private backups retained.") return + if payload.data["metadata"].get("activation_started"): + raise ValueError("This version's deployment checkpoint is active; roll it back before applying again") if args.activate: engine.idle_capture() engine.quiet_admission(args.activate) @@ -315,6 +386,7 @@ def main(): payload.data["metadata"] = {"stream_active": engine.active(), "activation_started": False, "input_active": engine.systemctl("is-active", "--quiet", engine.INPUT_SERVICE, check=False).returncode == 0, "input_engine": input_engine()} + remember_selector(payload, engine) payload.data["metadata"]["activation_started"] = True payload.save() try: @@ -323,17 +395,19 @@ def main(): previous_input = input_engine() engine.systemctl("stop", engine.SERVICE) stage(payload, manager, engine, model_source=args.model_source, download=args.download_model, - mlx_model=args.mlx_model_dir, mlx_modules=args.mlx_modules) + mlx_model=args.mlx_model_dir, mlx_modules=args.mlx_modules, speech_only=args.speech_only) engine.systemctl("daemon-reload") restart_bridge(engine, previous_input) engine.select(args.activate, args.mode, was_active=payload.data["metadata"]["stream_active"], warm_seconds=args.warm_seconds) + capture_selector(payload, engine) except Exception: + capture_selector(payload, engine) restore_payload(payload, engine) raise print("English-only F5 payload and selected engine deployed; live microphone accuracy remains an owner acceptance check.") else: stage(payload, manager, engine, model_source=args.model_source, download=args.download_model, - mlx_model=args.mlx_model_dir, mlx_modules=args.mlx_modules) + mlx_model=args.mlx_model_dir, mlx_modules=args.mlx_modules, speech_only=args.speech_only) print("English F5 payload/model staged with private backup; services and preferences unchanged.") diff --git a/mseries/install-dictation-input.py b/mseries/install-dictation-input.py index b6fae37c5..097137801 100644 --- a/mseries/install-dictation-input.py +++ b/mseries/install-dictation-input.py @@ -195,14 +195,16 @@ def stage(payload): payload.remember(PANEL_DESKTOP) -def install_panel(payload): +def install_panel(payload, speech_only=False): # Desktop Exec quoting, not shell quoting. Absolute arguments are passed # directly to Python; no shell or environment expansion is involved. def argument(value): return '"' + ''.join(('\\' + c) if c in '\\"`$' else c for c in str(value)) + '"' arguments = ['/usr/bin/python3', BASE / 'dictation-launch.py', '/usr/bin/python3', BASE / 'dictation-panel.py'] - desktop = ('[Desktop Entry]\nType=Application\nName=Dictation\n' - 'Comment=Local F5 speech and optional transcript review\n' + if speech_only: + arguments = ['/usr/bin/env', 'MSERIES_DICTATION_SPEECH_ONLY=1', *arguments] + desktop = ('[Desktop Entry]\nType=Application\nName=Dictation\n' + + ('Comment=Local English F5 speech\n' if speech_only else 'Comment=Local F5 speech and optional transcript review\n') + 'Icon=audio-input-microphone\nExec=' + ' '.join(argument(value) for value in arguments) + '\nTerminal=false\nCategories=Utility;Accessibility;\nStartupNotify=true\n') payload.install(PANEL_DESKTOP, desktop.encode(), 0o644) diff --git a/mseries/releases/package-f5.py b/mseries/releases/package-f5.py new file mode 100644 index 000000000..4f0a3e2e6 --- /dev/null +++ b/mseries/releases/package-f5.py @@ -0,0 +1,280 @@ +#!/usr/bin/python3 +"""Export the scoped English F5 source release from one exact Git commit. + +Only Git blobs supply package bytes; untracked files, index contents and working +tree edits are never read. No installer, model, native library or test worker is +executed. This creates reviewable artifacts, not a tag or published release. +""" +import argparse +import ast +import gzip +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import subprocess +import sys +import tarfile +import tempfile + +sys.dont_write_bytecode = True + +VERSION = "0.0.1" +NAME = "elementaryos-mseries-f5-" + VERSION +ARCHIVE = NAME + ".tar.gz" +SCOPE = "existingM1Pro32GBdesktopSTTonly" +INSTALLER = "mseries/install-dictation-english.py" +PACKAGER = "mseries/releases/package-f5.py" +TEMPLATES = "mseries/releases/v0.0.1/" +RUNTIME_FILES = frozenset(( + "stream-dictate.py", "dictation-input.py", "dictation-input-client.py", "dictation_status.py", + "dictation_resources.py", "dictation_model.py", "dictation_ane.py", "ane-worker-shim.py", + "dictation_mlx.py", "mlx-speech-worker.py", "dictation-engine.py", "dictation-launch.py", + "dictation_review.py", "dictation_pipeline.py", "dictation_live.py", "dictation-panel.py", +)) +FIXED_FILES = { + INSTALLER: INSTALLER, + "mseries/install-dictation-input.py": "mseries/install-dictation-input.py", + "mseries/local-ai/machine.py": "mseries/local-ai/machine.py", + "mseries/dictation/enable-app-input.py": "mseries/dictation/enable-app-input.py", + "mseries/dictation/mseries-dictation-input.service": "mseries/dictation/mseries-dictation-input.service", + "LICENSE": "LICENSE", + "install.sh": TEMPLATES + "install.sh", + "README.md": TEMPLATES + "README.md", + "THIRD-PARTY.md": TEMPLATES + "THIRD-PARTY.md", + TEMPLATES + "preflight.py": TEMPLATES + "preflight.py", + "tools/package-f5.py": PACKAGER, +} +MAX_BLOB = 2 * 1024 * 1024 +MAX_PACKAGE = 16 * 1024 * 1024 +SHA256 = re.compile(r"[0-9a-f]{64}\Z") +COMMIT = re.compile(r"[0-9a-f]{40}\Z") + + +class PackageError(ValueError): + """The exact committed source cannot safely form this release.""" + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def json_bytes(value): + return (json.dumps(value, sort_keys=True, indent=2, ensure_ascii=True) + "\n").encode() + + +def git(repo, *args): + # Disable replace objects so the supplied commit identifies the actual tree. + env = dict(os.environ, GIT_NO_REPLACE_OBJECTS="1", GIT_OPTIONAL_LOCKS="0") + # A caller's Git environment must not redirect this explicit repository. + for key in tuple(env): + if key.startswith("GIT_") and key not in {"GIT_NO_REPLACE_OBJECTS", "GIT_OPTIONAL_LOCKS"}: + del env[key] + try: + result = subprocess.run(["git", "-C", str(repo), *args], env=env, + stdin=subprocess.DEVNULL, capture_output=True, timeout=30) + except (OSError, subprocess.SubprocessError) as error: + raise PackageError("Git object read failed") from error + if result.returncode: + raise PackageError("Requested committed source is unavailable") + return result.stdout + + +def source_commit(repo, ref): + if not isinstance(ref, str) or not COMMIT.fullmatch(ref): + raise PackageError("--source-ref requires a full lowercase 40-character commit SHA") + resolved = git(repo, "rev-parse", "--verify", ref + "^{commit}").decode().strip() + if resolved != ref: + raise PackageError("--source-ref must identify the exact commit object") + return resolved + + +def read_blob(repo, commit, name): + if (not isinstance(name, str) or "\\" in name or "\x00" in name + or PurePosixPath(name).is_absolute() + or any(part in ("", ".", "..") for part in name.split("/"))): + raise PackageError("Unsafe source path") + row = git(repo, "ls-tree", "-z", commit, "--", name).split(b"\x00") + if len(row) != 2 or not row[0] or row[1]: + raise PackageError("Missing or ambiguous committed file: " + name) + header, actual = row[0].split(b"\t", 1) + mode, kind, object_id = header.split() + if actual.decode() != name or mode not in (b"100644", b"100755") or kind != b"blob": + raise PackageError("Only regular committed files are permitted: " + name) + size = int(git(repo, "cat-file", "-s", object_id.decode())) + if not 0 <= size <= MAX_BLOB: + raise PackageError("Committed source file exceeds its bound: " + name) + data = git(repo, "cat-file", "blob", object_id.decode()) + if len(data) != size: + raise PackageError("Committed blob size changed") + return data, 0o755 if mode == b"100755" else 0o644 + + +def parse(data, label): + try: + return ast.parse(data, filename=label) + except (SyntaxError, ValueError, TypeError, RecursionError) as error: + raise PackageError("Malformed committed Python source: " + label) from error + + +def constant(tree, name): + values = [node.value for node in tree.body if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == name for target in node.targets)] + if len(values) != 1: + raise PackageError("Expected one literal committed constant: " + name) + try: + return ast.literal_eval(values[0]) + except (ValueError, TypeError, SyntaxError, RecursionError) as error: + raise PackageError("Committed constant must be a literal: " + name) from error + + +def runtime_names(installer): + names = constant(parse(installer, INSTALLER), "RUNTIME_FILES") + if (not isinstance(names, (tuple, list)) or not all(isinstance(name, str) for name in names) + or len(names) != len(RUNTIME_FILES) or set(names) != RUNTIME_FILES): + raise PackageError("Installer runtime files differ from the scoped sixteen-file allowlist") + return sorted(names) + + +def release_pins(payload): + installer = parse(payload[INSTALLER][0], INSTALLER) + engine_name = "mseries/dictation/dictation-engine.py" + engine = parse(payload[engine_name][0], engine_name) + preflight_name = TEMPLATES + "preflight.py" + preflight = parse(payload[preflight_name][0], preflight_name) + model_name = constant(engine, "NEMOTRON") + model_sha = constant(engine, "NEMOTRON_SHA256") + native_sha = constant(preflight, "NATIVE_SHA256") + if (model_name != "nemotron-speech-streaming-en-0.6b-Q8_0.gguf" + or not isinstance(model_sha, str) or not SHA256.fullmatch(model_sha) + or not isinstance(native_sha, str) or not SHA256.fullmatch(native_sha)): + raise PackageError("Committed model/native-library pins are malformed") + urls = {node.value for node in ast.walk(installer) if isinstance(node, ast.Constant) + and isinstance(node.value, str) and node.value.startswith("https://huggingface.co/")} + pattern = r"https://huggingface\.co/(handy-computer/nemotron-speech-streaming-en-0\.6b-gguf)/resolve/([0-9a-f]{40})/" + if len(urls) != 1 or not (match := re.fullmatch(pattern, next(iter(urls)))): + raise PackageError("Expected one exact pinned Nemotron download origin") + versions = {} + for node in ast.walk(preflight): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + and node.func.id == "metadata" and len(node.args) == 2 + and all(isinstance(arg, ast.Constant) and isinstance(arg.value, str) for arg in node.args)): + name, version = (arg.value for arg in node.args) + if name in versions: + raise PackageError("Ambiguous committed runtime package version") + versions[name] = version + if versions != {"transcribe-cpp": "0.4.0", "numpy": "2.5.4"}: + raise PackageError("Runtime prerequisites differ from scoped v0.0.1 pins") + return { + "model": {"repository": match[1], "revision": match[2], "filename": model_name, + "sha256": model_sha, "distributed": False}, + "runtime": {"packages": versions, "native_library_sha256": native_sha, + "distributed": False, + "optional_engines": "existing ANE/MLX assets retained; no assets provisioned"}, + } + + +def collect(repo, commit): + installer = read_blob(repo, commit, INSTALLER) + mapping = dict(FIXED_FILES) + mapping.update({"mseries/dictation/" + name: "mseries/dictation/" + name + for name in runtime_names(installer[0])}) + payload = {} + for target, source in sorted(mapping.items()): + payload[target] = installer if source == INSTALLER else read_blob(repo, commit, source) + try: + payload[target][0].decode("utf-8") + except UnicodeDecodeError as error: + raise PackageError("Source-only payload contains non-text bytes: " + source) from error + if b"\x00" in payload[target][0]: + raise PackageError("Source-only payload contains binary bytes: " + source) + if target.endswith(".py"): + parse(payload[target][0], source) + if sum(len(data) for data, _ in payload.values()) > MAX_PACKAGE: + raise PackageError("Release source exceeds package bound") + # These public entrypoints must work independently of the source Git mode. + for name in ("install.sh", "tools/package-f5.py", TEMPLATES + "preflight.py", + "mseries/dictation/enable-app-input.py"): + payload[name] = (payload[name][0], 0o755) + pins = release_pins(payload) + manifest = {"version": VERSION, "source_commit": commit, "scope": SCOPE, + "files": {name: digest(data) for name, (data, _) in sorted(payload.items())}, **pins} + payload["manifest.json"] = (json_bytes(manifest), 0o644) + return payload, manifest, mapping + + +def tar_bytes(payload): + raw = io.BytesIO() + with gzip.GzipFile(filename="", mode="wb", fileobj=raw, compresslevel=9, mtime=0) as zipped: + with tarfile.open(fileobj=zipped, mode="w", format=tarfile.USTAR_FORMAT) as archive: + for name, (data, mode) in sorted(payload.items()): + entry = tarfile.TarInfo(NAME + "/" + name) + entry.size, entry.mode, entry.mtime = len(data), mode, 0 + entry.uid = entry.gid = 0 + entry.uname = entry.gname = "" + archive.addfile(entry, io.BytesIO(data)) + return raw.getvalue() + + +def build(repo, ref, output): + """Build from Git objects; the output directory must have no prior artifacts.""" + repo, output = Path(repo), Path(output) + commit = source_commit(repo, ref) + payload, manifest, mapping = collect(repo, commit) + archive = tar_bytes(payload) + provenance = { + "schema_version": 1, "version": VERSION, "source_commit": commit, "scope": SCOPE, + "archive": {"filename": ARCHIVE, "sha256": digest(archive), "size": len(archive)}, + "manifest_sha256": digest(payload["manifest.json"][0]), + "packager_sha256": manifest["files"]["tools/package-f5.py"], + "source_files": dict(sorted(mapping.items())), + "integrity_scope": "manifest files excludes manifest.json; outer archive digest binds all bytes", + "publication": "local artifacts only; physical F5 acceptance and release publication are separate", + "model": manifest["model"], "runtime": manifest["runtime"], + } + artifacts = {ARCHIVE: archive, "provenance.json": json_bytes(provenance)} + artifacts["SHA256SUMS"] = "".join(digest(data) + " " + name + "\n" + for name, data in sorted(artifacts.items())).encode() + if output.is_symlink() or any(parent.is_symlink() for parent in output.absolute().parents): + raise PackageError("Output directory must not follow symlinks") + output.mkdir(parents=True, exist_ok=True) + if not output.is_dir() or any((output / name).exists() or (output / name).is_symlink() for name in artifacts): + raise PackageError("Output artifact already exists or output is not a directory") + # Prepare all bytes before publishing any output; exclusive creation refuses + # collisions instead of replacing a prior artifact or following a symlink. + created = [] + try: + with tempfile.TemporaryDirectory(prefix=".f5-package-", dir=output) as temporary: + for name, data in artifacts.items(): + staged = Path(temporary) / name + staged.write_bytes(data) + staged.chmod(0o644) + for name in artifacts: + os.link(Path(temporary) / name, output / name, follow_symlinks=False) + created.append(output / name) + except OSError as error: + for path in created: + path.unlink() + raise PackageError("Could not exclusively create release outputs") from error + return provenance + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source-ref", required=True, help="Exact full lowercase commit SHA; no branch or tag") + parser.add_argument("--output", required=True, type=Path, help="Directory for the archive and two integrity files") + args = parser.parse_args(argv) + try: + repo = git(Path(__file__).resolve().parent, "rev-parse", "--show-toplevel").decode().strip() + result = build(repo, args.source_ref, args.output) + except (PackageError, OSError) as error: + parser.exit(1, "Source release packaging refused: " + str(error) + "\n") + print(json.dumps({"archive": result["archive"]["filename"], "source_commit": result["source_commit"], + "sha256": result["archive"]["sha256"], "published": False}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/mseries/releases/test_package_f5.py b/mseries/releases/test_package_f5.py new file mode 100644 index 000000000..0c4e4df28 --- /dev/null +++ b/mseries/releases/test_package_f5.py @@ -0,0 +1,299 @@ +#!/usr/bin/python3 +"""Real temporary Git-object packaging tests; no installed runtime or inference.""" +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tarfile +import tempfile +import unittest +from unittest import mock + +sys.dont_write_bytecode = True +SOURCE = Path(__file__).with_name("package-f5.py") +SPEC = importlib.util.spec_from_file_location("package_f5", SOURCE) +package = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(package) +MODEL_SHA = "90d8c89714cd31efc88be62a40c6b2bea57e0cc2063af1ffe2c28f1a228ca110" +NATIVE_SHA = "9f1f839d89a6d3a874c939ecf3108007cc0964a338596a35adb47c7e6c5c921d" +MODEL_REPO = "handy-computer/nemotron-speech-streaming-en-0.6b-gguf" +REVISION = "7df420cc3fb915a3b334560dac712af44fc41ef5" + + +class GitFixture(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="f5-package-test-") + self.addCleanup(self.temporary.cleanup) + self.base = Path(self.temporary.name) + self.repo = self.base / "repo" + self.repo.mkdir() + self.command("init", "--quiet") + self.command("config", "user.name", "Source package fixture") + self.command("config", "user.email", "fixture@example.invalid") + self.command("config", "core.autocrlf", "false") + for source in package.FIXED_FILES.values(): + self.write(source, "# synthetic source-only fixture\n") + self.write(package.PACKAGER, SOURCE.read_bytes()) + self.write(package.INSTALLER, self.installer()) + for name in package.RUNTIME_FILES: + self.write("mseries/dictation/" + name, "# Synthetic " + name + "\n") + self.write("mseries/dictation/dictation-engine.py", self.engine()) + self.write(package.TEMPLATES + "preflight.py", self.preflight()) + self.write(package.TEMPLATES + "install.sh", "#!/bin/sh\n# Synthetic wrapper\nexit 0\n") + # Unwanted files are committed too, proving this is an allowlist export, + # not Git archive of the full tree or an extension-based search. + for name in ("mseries/dictation/voice-assistant.py", "mseries/dictation/dictation_voice.py", + "mseries/local-ai/diagnostic_agent.py", "mseries/mcp/private-profile.json", + "models/test.gguf", "native/libtranscribe.so", "audio/test.wav"): + self.write(name, b"excluded\x00synthetic binary or private fixture\n") + self.write("private-link", b"") + (self.repo / "private-link").unlink() + (self.repo / "private-link").symlink_to("/not-a-real-owner-file") + (self.repo / package.TEMPLATES / "install.sh").chmod(0o755) + self.commit = self.save() + + def command(self, *args): + env = dict(os.environ, GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, + GIT_AUTHOR_DATE="2000-01-01T00:00:00+00:00", GIT_COMMITTER_DATE="2000-01-01T00:00:00+00:00") + for key in tuple(env): + if key.startswith("GIT_") and key not in { + "GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_GLOBAL", "GIT_AUTHOR_DATE", "GIT_COMMITTER_DATE" + }: + del env[key] + result = subprocess.run(["git", "-C", str(self.repo), *args], env=env, + stdin=subprocess.DEVNULL, capture_output=True, check=True) + return result.stdout.decode().strip() + + def write(self, name, contents): + path = self.repo / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents.encode() if isinstance(contents, str) else contents) + + def installer(self, names=None): + values = sorted(package.RUNTIME_FILES) if names is None else names + return ("RUNTIME_FILES = " + repr(tuple(values)) + "\n" + "def download():\n url = 'https://huggingface.co/" + MODEL_REPO + "/resolve/" + REVISION + "/'\n" + "raise RuntimeError('The packager must never execute this fixture')\n") + + def engine(self): + return "NEMOTRON = 'nemotron-speech-streaming-en-0.6b-Q8_0.gguf'\nNEMOTRON_SHA256 = '" + MODEL_SHA + "'\n" + + def preflight(self): + return ("NATIVE_SHA256 = '" + NATIVE_SHA + "'\n" + "def inspect():\n metadata('transcribe-cpp', '0.4.0')\n metadata('numpy', '2.5.4')\n") + + def save(self): + self.command("add", "--all") + self.command("commit", "--quiet", "-m", "Synthetic fixture") + return self.command("rev-parse", "HEAD") + + def build(self, label="out", ref=None): + destination = self.base / label + result = package.build(self.repo, ref or self.commit, destination) + return destination, result + + def archive(self, destination): + with tarfile.open(destination / package.ARCHIVE, "r:gz") as archive: + return {member.name.removeprefix(package.NAME + "/"): archive.extractfile(member).read() + for member in archive.getmembers()} + + def test_reproducible_bytes_normalized_tar_and_complete_integrity(self): + first, provenance = self.build("first") + old_umask = os.umask(0o077) + try: + with mock.patch.dict(os.environ, {"TZ": "Pacific/Kiritimati", "SOURCE_DATE_EPOCH": "999999999"}): + second, second_provenance = self.build("second") + finally: + os.umask(old_umask) + self.assertEqual(provenance, second_provenance) + self.assertEqual(sorted(path.name for path in first.iterdir()), + ["SHA256SUMS", package.ARCHIVE, "provenance.json"]) + for path in first.iterdir(): + self.assertEqual(path.read_bytes(), (second / path.name).read_bytes()) + self.assertEqual(path.stat().st_mode & 0o777, 0o644) + raw = (first / package.ARCHIVE).read_bytes() + self.assertEqual(raw[4:8], b"\x00\x00\x00\x00") # gzip timestamp + self.assertFalse(raw[3] & 8) # no host/output-dependent original filename + files = self.archive(first) + manifest = json.loads(files["manifest.json"]) + self.assertEqual(manifest["version"], "0.0.1") + self.assertEqual(manifest["scope"], "existingM1Pro32GBdesktopSTTonly") + self.assertEqual(manifest["source_commit"], self.commit) + self.assertEqual(set(manifest["files"]), set(files) - {"manifest.json"}) + for name, expected in manifest["files"].items(): + self.assertEqual(expected, hashlib.sha256(files[name]).hexdigest()) + self.assertEqual(provenance["manifest_sha256"], hashlib.sha256(files["manifest.json"]).hexdigest()) + self.assertEqual(provenance["archive"]["sha256"], hashlib.sha256(raw).hexdigest()) + self.assertEqual(manifest["model"]["sha256"], MODEL_SHA) + self.assertEqual(manifest["model"]["revision"], REVISION) + self.assertFalse(manifest["model"]["distributed"]) + self.assertEqual(manifest["runtime"]["native_library_sha256"], NATIVE_SHA) + self.assertFalse(manifest["runtime"]["distributed"]) + with tarfile.open(fileobj=io.BytesIO(raw), mode="r:gz") as archive: + members = archive.getmembers() + self.assertEqual([item.name for item in members], sorted(item.name for item in members)) + for member in members: + self.assertTrue(member.isfile()) + self.assertEqual((member.mtime, member.uid, member.gid, member.uname, member.gname), (0, 0, 0, "", "")) + self.assertIn(member.mode, (0o644, 0o755)) + self.assertTrue(member.name.startswith(package.NAME + "/")) + self.assertNotIn("..", member.name.split("/")) + self.assertEqual(member.pax_headers, {}) + for line in (first / "SHA256SUMS").read_text().splitlines(): + expected, name = line.split(" ", 1) + self.assertEqual(expected, hashlib.sha256((first / name).read_bytes()).hexdigest()) + + def test_exact_allowlist_excludes_committed_extras_index_and_worktree(self): + first, _ = self.build("baseline") + self.write(package.INSTALLER, "not valid Python and never committed\n") + self.write("mseries/dictation/stream-dictate.py", "# staged but not selected\n") + self.command("add", "mseries/dictation/stream-dictate.py") + self.write(package.TEMPLATES + "README.md", "PRIVATE UNCOMMITTED NOTE\n") + self.write("untracked-secret.txt", "PRIVATE UNTRACKED NOTE\n") + # The exported builder itself must also be the committed blob. + self.write(package.PACKAGER, "# ambient builder is excluded\n") + second, _ = self.build("dirty") + self.assertEqual((first / package.ARCHIVE).read_bytes(), (second / package.ARCHIVE).read_bytes()) + files = self.archive(second) + expected = set(package.FIXED_FILES) | {"mseries/dictation/" + name for name in package.RUNTIME_FILES} | {"manifest.json"} + self.assertEqual(set(files), expected) + self.assertEqual(files["tools/package-f5.py"], SOURCE.read_bytes()) + self.assertNotIn(b"PRIVATE", b"".join(files.values())) + self.assertFalse(any("voice-assistant" in name or name.endswith((".gguf", ".so", ".wav")) for name in files)) + + def test_cli_derives_fixture_repository_and_does_not_accept_branch_or_short_sha(self): + output = self.base / "cli" + result = subprocess.run([sys.executable, str(self.repo / package.PACKAGER), + "--source-ref", self.commit, "--output", str(output)], + cwd=self.base, capture_output=True, text=True, check=True) + self.assertFalse(json.loads(result.stdout)["published"]) + self.assertEqual(json.loads(result.stdout)["source_commit"], self.commit) + self.assertEqual(json.loads(self.archive(output)["manifest.json"])["source_commit"], self.commit) + for ref in ("HEAD", "main", self.commit[:12], self.commit.upper(), "-" * 40, "0" * 40): + with self.subTest(ref=ref), self.assertRaises(package.PackageError): + self.build("rejected", ref) + tree_id = self.command("rev-parse", self.commit + "^{tree}") + with self.assertRaises(package.PackageError): + self.build("tree", tree_id) + self.command("tag", "-a", "fixture-tag", "-m", "Annotated tag") + tag_object = self.command("rev-parse", "fixture-tag") + with self.assertRaises(package.PackageError): + self.build("tag-object", tag_object) + + def test_runtime_ast_rejects_new_removed_duplicate_traversal_and_executable_entries(self): + names = sorted(package.RUNTIME_FILES) + for replacement in (names + ["voice-assistant.py"], names[:-1], + names[:-1] + [names[0]], names[:-1] + ["../voice-assistant.py"]): + with self.subTest(entries=replacement), self.assertRaises(package.PackageError): + package.runtime_names(self.installer(replacement).encode()) + for source in ("RUNTIME_FILES = tuple(['stream-dictate.py'])\n", + "RUNTIME_FILES = ()\nRUNTIME_FILES = ()\n", + "RUNTIME_FILES: tuple = ()\n"): + with self.subTest(source=source), self.assertRaises(package.PackageError): + package.runtime_names(source.encode()) + self.write(package.INSTALLER, self.installer(names[:-1] + ["voice-assistant.py"])) + bad_commit = self.save() + with self.assertRaises(package.PackageError): + self.build("bad-runtime", bad_commit) + self.assertFalse((self.base / "bad-runtime").exists()) + + def test_committed_symlinks_and_gitlinks_refused_but_ambient_links_ignored(self): + for index, name in enumerate(("LICENSE", package.TEMPLATES + "README.md", "mseries/dictation/dictation-input.py")): + path = self.repo / name + original = path.read_bytes() + path.unlink() + path.symlink_to("/not-a-real-owner-file") + commit = self.save() + with self.subTest(name=name), self.assertRaises(package.PackageError): + self.build("symlink-" + str(index), commit) + path.unlink() + path.write_bytes(original) + self.save() + # A gitlink is not a regular source file, even with an allowed filename. + self.command("update-index", "--add", "--cacheinfo", "160000," + self.commit + ",LICENSE") + self.command("commit", "--quiet", "-m", "Synthetic gitlink") + with self.assertRaises(package.PackageError): + self.build("gitlink", self.command("rev-parse", "HEAD")) + # An uncommitted link cannot influence the already selected base commit. + (self.repo / "mseries/dictation/stream-dictate.py").unlink() + (self.repo / "mseries/dictation/stream-dictate.py").symlink_to("/not-a-real-owner-file") + output, _ = self.build("ambient-link") + self.assertIn("mseries/dictation/stream-dictate.py", self.archive(output)) + + def test_missing_template_binary_source_and_invalid_pins_fail_without_outputs(self): + required = self.repo / package.TEMPLATES / "THIRD-PARTY.md" + original = required.read_bytes() + required.unlink() + with self.assertRaises(package.PackageError): + self.build("missing", self.save()) + required.write_bytes(original) + self.write("mseries/dictation/stream-dictate.py", b"\x7fELF\x00not-source") + with self.assertRaises(package.PackageError): + self.build("binary", self.save()) + self.write("mseries/dictation/stream-dictate.py", "# synthetic source\n") + self.write("mseries/dictation/dictation-engine.py", self.engine().replace(MODEL_SHA, "not-a-digest")) + with self.assertRaises(package.PackageError): + self.build("pin", self.save()) + for label in ("missing", "binary", "pin"): + self.assertFalse((self.base / label).exists()) + + def test_output_collision_symlinks_and_failed_publication_preserve_existing_bytes(self): + output, _ = self.build() + original = {path.name: path.read_bytes() for path in output.iterdir()} + with self.assertRaises(package.PackageError): + self.build() + self.assertEqual(original, {path.name: path.read_bytes() for path in output.iterdir()}) + linked = self.base / "linked-output" + linked.symlink_to(output, target_is_directory=True) + with self.assertRaises(package.PackageError): + self.build("linked-output") + broken = self.base / "broken-artifact" + broken.mkdir() + (broken / "provenance.json").symlink_to("/not-a-real-owner-file") + with self.assertRaises(package.PackageError): + self.build("broken-artifact") + target = self.base / "publication-fails" + real_link = os.link + count = 0 + def fail_second(*args, **kwargs): + nonlocal count + count += 1 + if count == 2: + raise OSError("Synthetic output interruption") + return real_link(*args, **kwargs) + with mock.patch.object(package.os, "link", side_effect=fail_second): + with self.assertRaises(package.PackageError): + self.build("publication-fails") + self.assertEqual(list(target.iterdir()), []) + + def test_git_environment_cannot_redirect_to_another_index_worktree_or_repository(self): + with mock.patch.dict(os.environ, {"GIT_DIR": str(self.base / "does-not-exist"), + "GIT_WORK_TREE": "/not-a-real-owner-file", + "GIT_INDEX_FILE": "/not-a-real-owner-file", + "GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "alias.cat-file", + "GIT_CONFIG_VALUE_0": "!false"}): + output, _ = self.build("git-env") + self.assertEqual(json.loads(self.archive(output)["manifest.json"])["source_commit"], self.commit) + + def test_only_explicit_app_launcher_helper_is_included_and_regular(self): + helper = "mseries/dictation/enable-app-input.py" + self.write(helper, "# Synthetic committed app launcher helper\n") + self.write("mseries/dictation/enable-some-other-app.py", "# Not approved for this package\n") + commit = self.save() + destination, _ = self.build("launcher", commit) + files = self.archive(destination) + self.assertEqual(files[helper], b"# Synthetic committed app launcher helper\n") + self.assertNotIn("mseries/dictation/enable-some-other-app.py", files) + self.assertEqual(len(package.runtime_names(self.installer().encode())), 16) + (self.repo / helper).unlink() + (self.repo / helper).symlink_to("/not-a-real-owner-file") + with self.assertRaises(package.PackageError): + self.build("launcher-link", self.save()) + + +if __name__ == "__main__": + unittest.main() diff --git a/mseries/releases/v0.0.1/README.md b/mseries/releases/v0.0.1/README.md new file mode 100644 index 000000000..77de2e924 --- /dev/null +++ b/mseries/releases/v0.0.1/README.md @@ -0,0 +1,113 @@ +# v0.0.1 — English F5 speech to text + +**Release candidate: physical F5 in the currently running ChatGPT app failed.** +The key reaches the speech service; the input guard refuses before microphone +capture because the app does not expose its editable field. The staged launcher +compatibility fix needs a full owner-controlled ChatGPT relaunch and a new human +check. A working public release is not declared until that gate passes. + +This is a small desktop update for an **already configured elementary OS M-Series +M1 Pro j314s with 32 GB RAM**, Python 3.14, the existing pinned CPU speech runtime, +IBus bridge and F5 remap. It is not a fresh-install ISO. The check below refuses +unsupported or incomplete installations before changing files. + +The package contains source code. No model, native binary, recording, private +settings, kernel, voice conversation or troubleshooting agent is bundled. +English words are inserted as the recognizer commits them. No language model, +grammar rewrite, clipboard or translation is used by the release transcript UI. + +## Verify and install + +Download the `.tar.gz`, `provenance.json` and `SHA256SUMS` from the same GitHub +release. Verify their checksums before extracting: + +```sh +sha256sum -c SHA256SUMS +tar -xzf elementaryos-mseries-f5-0.0.1.tar.gz +cd elementaryos-mseries-f5-0.0.1 +./install.sh --check +``` + +The package's complete file manifest is verified before host inspection or +installation. It is an integrity check; obtain the files from the trusted +repository release. Do not run the installer with sudo. + +Reuse the verified model already installed, or provide its local path: + +```sh +./install.sh --install --model-source /path/to/nemotron-speech-streaming-en-0.6b-Q8_0.gguf +``` + +If the pinned model is absent, this explicit alternative downloads it from its +pinned Hugging Face revision and checks SHA256. Review the model license first: + +```sh +./install.sh --install --download-model +``` + +The model filename is case-sensitive: uppercase `Q8_0`. Model, runtime and source +pins are in `manifest.json`. The installer uses a private, versioned rollback +checkpoint and preserves the existing hold/toggle preference and native input +selection. Only owned speech/input services reload. It does not restart apps, +IBus, the menu bar or the audio stack. An already active version checkpoint must +be rolled back before reapplying; later owner edits cause a safe refusal. + +## Use F5 + +Focus a normal editable field. In toggle mode, press F5, wait for Listening, +speak English, then press F5 again. In hold mode, hold F5 until Listening and +release to finish. Escape cancels; typing ends dictation. The installer preserves +the mode already chosen on your desktop. + +Open **Dictation** for an English transcript field. The release UI contains no +grammar or voice-reply controls. Password, unknown, inactive and inaccessible +fields are refused before microphone capture. Stable words stream; speculative +draft composition is suppressed because Chromium could commit it on focus loss. + +For ChatGPT's Electron app, prepare its user launcher: + +```sh +./install.sh --enable-chatgpt +``` + +This preserves the desktop entry's other fields and URLs, enables renderer +accessibility and selects the IBus environment. It does not alter the running +app. Fully quit ChatGPT and reopen it from Applications when convenient; closing +one window or launching a second instance may leave the old process running. +The current ChatGPT configuration remains an unresolved release gate above. + +## Rollback + +```sh +./install.sh --rollback +``` + +This restores the immediate pre-v0.0.1 payload, launcher and engine when their +tracked files still match. Private backups and previous models remain. An +independent ChatGPT launcher override has its own rollback: + +```sh +python3 -B mseries/dictation/enable-app-input.py --app chatgpt --rollback +``` + +## Qualification and limits + +The retained CPU/public-file results include two resident 62.61-second trials, +finishing 0.048/0.062 seconds after audio. Installed routes passed GTK4 public-file +ASR; GTK3/Qt6/Brave fixtures; and Code/Newelle/Tasks lease/cancel fixtures, with +positive real-app ASR in separate pinned cohorts. These are not seven new ASR +benchmarks. Three initial fixture argument failures and the sustained CPU +sensor-summary failure remain recorded; completed tests were not rerun to hide +them. Installed samples retained at least 10.48 GiB available at no more than +2530 RPM / 49.01°C. RPM is a noise proxy, not acoustic measurement. + +Speech preserves a six-GiB desktop reserve and stops at 2700 RPM / 63°C. +Native ANE and MLX research, high-capability diagnosis, broader app coverage, +natural voice, factory boot and power/battery qualification remain separate. +The permanent six-hour developer watcher continues discovering candidates; +release pins change only after new, separately identified qualification. + +Read the [canonical qualification ledger](https://github.com/techmore/ElementaryOS_MSeries/blob/main/mseries/dictation/QUALIFICATION-INDEX.md) +and [public results](https://techmore.github.io/ElementaryOS_MSeries/local-ai.html#english-f5) +before any continuation. Preserve successful and failed evidence. New popular +models and codebases are candidates, not automatic release upgrades. diff --git a/mseries/releases/v0.0.1/THIRD-PARTY.md b/mseries/releases/v0.0.1/THIRD-PARTY.md new file mode 100644 index 000000000..0a93a42c7 --- /dev/null +++ b/mseries/releases/v0.0.1/THIRD-PARTY.md @@ -0,0 +1,18 @@ +# Source and external runtime notices + +Repository source is distributed under the included GNU GPL version 3 license. +The exact repository commit and every packaged source digest are in manifest.json. + +The existing external dependencies retain their own licenses; this archive does +not redistribute model weights or native binaries: + +- [transcribe.cpp 0.4.0](https://github.com/handy-computer/transcribe.cpp/tree/v0.4.0): MIT. The qualified native library SHA256 is pinned in the manifest and checked before update. +- [NumPy](https://numpy.org/doc/stable/license.html): BSD license; existing qualified version 2.5.4. +- [Nemotron English streaming](https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b): NVIDIA Open Model License. +- [Pinned GGUF conversion and notices](https://huggingface.co/handy-computer/nemotron-speech-streaming-en-0.6b-gguf/tree/7df420cc3fb915a3b334560dac712af44fc41ef5): model license and NOTICE accompany the upstream artifact. The installer downloads it only when explicitly requested. +- GTK, GLib/GIO, AT-SPI, IBus, Python and ffmpeg are existing system dependencies. + +Synthetic qualification audio is public LibriSpeech (CC BY 4.0, +Panayotov/Chen/Povey/Khudanpur, https://www.openslr.org/12). No audio is in this +release archive. No private journal, transcript, model memory, task or account +settings are distributed. diff --git a/mseries/releases/v0.0.1/install.sh b/mseries/releases/v0.0.1/install.sh new file mode 100755 index 000000000..de72a04e3 --- /dev/null +++ b/mseries/releases/v0.0.1/install.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Source-only desktop update. Never boot, repartition or install system packages. +set -euo pipefail +package=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +if [[ -f $package/preflight.py && ! -f $package/manifest.json ]]; then + echo 'Use the generated release archive, not this source template.' >&2 + exit 2 +fi +preflight=$package/mseries/releases/v0.0.1/preflight.py +usage() { + cat <<'HELP' +English F5 v0.0.1 — update an already configured M1 Pro 32 GB desktop + ./install.sh --check + ./install.sh --install --model-source /path/to/pinned-Q8_0.gguf + ./install.sh --install --download-model + ./install.sh --rollback + ./install.sh --enable-chatgpt +HELP +} +action=${1:---help} +shift || true +case $action in + --check) + [[ $# == 0 ]] || { usage; exit 2; } + exec /usr/bin/python3 -B "$preflight" --package-root "$package" --check + ;; + --install) + args=() + while (( $# )); do + case $1 in + --download-model) args+=(--download-model); shift ;; + --model-source) + [[ $# -ge 2 && -n $2 ]] || { usage; exit 2; } + args+=(--model-source "$2"); shift 2 ;; + *) usage; exit 2 ;; + esac + done + /usr/bin/python3 -B "$preflight" --package-root "$package" --check + exec /usr/bin/python3 -B "$package/mseries/install-dictation-english.py" --activate nemotron --speech-only "${args[@]}" + ;; + --rollback) + [[ $# == 0 ]] || { usage; exit 2; } + /usr/bin/python3 -B "$preflight" --package-root "$package" --verify-package + exec /usr/bin/python3 -B "$package/mseries/install-dictation-english.py" --speech-only --rollback + ;; + --enable-chatgpt) + [[ $# == 0 ]] || { usage; exit 2; } + /usr/bin/python3 -B "$preflight" --package-root "$package" --verify-package + exec /usr/bin/python3 -B "$package/mseries/dictation/enable-app-input.py" --app chatgpt --stage + ;; + --help|-h) usage ;; + *) usage; exit 2 ;; +esac diff --git a/mseries/releases/v0.0.1/preflight.py b/mseries/releases/v0.0.1/preflight.py new file mode 100644 index 000000000..bbfbee35f --- /dev/null +++ b/mseries/releases/v0.0.1/preflight.py @@ -0,0 +1,326 @@ +#!/usr/bin/python3 +"""Read-only admission for the scoped English F5 v0.0.1 update. + +This is an update for an already working M1 Pro desktop, not a first-install or +hardware microphone test. No model/native library is loaded by these checks. +""" +import argparse +import ast +from contextlib import contextmanager +from email.parser import BytesParser +import hashlib +import importlib.util +import json +import os +from pathlib import Path, PurePosixPath +import re +import stat +import subprocess +import sys + +# Verifying or inspecting a package must not create unlisted cache files in it. +sys.dont_write_bytecode = True + +VERSION = '0.0.1' +GIB = 1024 ** 3 +NATIVE_SHA256 = '9f1f839d89a6d3a874c939ecf3108007cc0964a338596a35adb47c7e6c5c921d' +STREAM_UNIT = 'mseries-stream-dictate.service' +INPUT_UNIT = 'mseries-dictation-input.service' +NATIVE = 'native/transcribe-native-linux-aarch64-cpu-vulkan/libtranscribe.so' +PACKAGE_LABEL = 'Package files match the complete v0.0.1 manifest' +PYTHON_QUERY = '''import json, sys, gi +gi.require_version('Gtk', '4.0') +gi.require_version('IBus', '1.0') +gi.require_version('Atspi', '2.0') +from gi.repository import Gtk, IBus, Atspi +print(json.dumps({'python': list(sys.version_info[:2]), 'bindings': True}))''' +BRIDGE_QUERY = '''import gi +from gi.repository import Gio +bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) +reply = bus.call_sync('org.techmore.DictationInput', '/org/techmore/DictationInput', + 'org.techmore.DictationInput', 'Ready', None, None, + Gio.DBusCallFlags.NO_AUTO_START, 3000, None) +print('ready' if reply.unpack() == (True,) else 'not-ready')''' + + +def run(args): + """Only bounded read-only commands are passed by inspect().""" + return subprocess.run(args, capture_output=True, text=True, timeout=5, + stdin=subprocess.DEVNULL, check=False) + + +def bounded(path, limit=65536): + with path.open('rb') as stream: + data = stream.read(limit + 1) + if len(data) > limit: + raise ValueError('Bound exceeded') + return data + + +def digest(path): + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +@contextmanager +def package_file(root, name): + """Open through owned directory descriptors without following any link.""" + directory = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + parts = PurePosixPath(name).parts + for part in parts[:-1]: + child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory) + os.close(directory) + directory = child + fd = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory) + with os.fdopen(fd, 'rb') as stream: + if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode): + raise ValueError('Package payload is not a regular file') + yield stream + finally: + os.close(directory) + + +def result(checks): + return {'passed': all(checks.values()), 'checks': checks, + 'failures': [label for label, passed in checks.items() if not passed]} + + +def verify_package(package_root): + """Reject links, escapes, special files, missing/extra files and bad digests. + + The manifest describes integrity, not a detached authenticity signature. + Release download authenticity still depends on the published release digest. + """ + passed = False + try: + root = Path(package_root) + if root.is_symlink() or not root.is_dir(): + raise ValueError('Invalid package') + manifest_path = root / 'manifest.json' + if manifest_path.is_symlink() or not stat.S_ISREG(manifest_path.stat().st_mode): + raise ValueError('Invalid manifest') + def unique(pairs): + values = {} + for key, value in pairs: + if key in values: + raise ValueError('Duplicate manifest key') + values[key] = value + return values + with package_file(root, 'manifest.json') as stream: + raw = stream.read(2 * 1024 ** 2 + 1) + if len(raw) > 2 * 1024 ** 2: + raise ValueError('Manifest bound exceeded') + manifest = json.loads(raw, object_pairs_hook=unique) + if (not isinstance(manifest, dict) or manifest.get('version') != VERSION + or not isinstance(manifest.get('source_commit'), str) + or not re.fullmatch('[0-9a-f]{40}', manifest['source_commit'])): + raise ValueError('Invalid release identity') + files = manifest.get('files') + if not isinstance(files, dict) or not 1 <= len(files) <= 4096: + raise ValueError('Invalid file manifest') + for name, expected in files.items(): + if (not isinstance(name, str) or len(name) > 512 or '\\' in name + or '\x00' in name or name == 'manifest.json' + or PurePosixPath(name).is_absolute() + or any(part in ('', '.', '..') for part in name.split('/')) + or not isinstance(expected, str) or not re.fullmatch('[0-9a-f]{64}', expected)): + raise ValueError('Invalid manifest entry') + actual = set() + directories = 0 + def walk_error(error): + raise error + for parent, dirs, names in os.walk(root, followlinks=False, onerror=walk_error): + directories += 1 + if directories > 4096: + raise ValueError('Package directory bound exceeded') + for name in dirs: + if not stat.S_ISDIR((Path(parent) / name).lstat().st_mode): + raise ValueError('Linked or special directory') + for name in names: + path = Path(parent) / name + if not stat.S_ISREG(path.lstat().st_mode): + raise ValueError('Linked or special file') + actual.add(path.relative_to(root).as_posix()) + if len(actual) > 4097: + raise ValueError('Package file bound exceeded') + if actual != set(files) | {'manifest.json'}: + raise ValueError('Missing or unlisted package file') + for name, expected in files.items(): + with package_file(root, name) as stream: + before = os.fstat(stream.fileno()) + actual_digest = hashlib.file_digest(stream, 'sha256').hexdigest() + after = os.fstat(stream.fileno()) + if (actual_digest != expected or (before.st_size, before.st_mtime_ns, before.st_ctime_ns) + != (after.st_size, after.st_mtime_ns, after.st_ctime_ns)): + raise ValueError('Package digest mismatch or concurrent edit') + passed = True + except (OSError, ValueError, TypeError, RecursionError): + pass + return result({PACKAGE_LABEL: passed}) + + +def inspect(root=Path('/'), home=None, runner=run): + """Inspect an existing desktop; injectable filesystem/commands for tests. + + No raw paths, command output, journal, environment or settings leave this + function. Failed checks return only the fixed friendly labels below. + """ + root = Path(root) + home = Path.home() if home is None else Path(home) + base = home / '.local/share/mseries/stream' + checks = {} + def check(label, function): + try: + checks[label] = bool(function()) + except (OSError, ValueError, TypeError, KeyError, IndexError, SyntaxError, + RecursionError, OverflowError, + subprocess.SubprocessError, ImportError, AttributeError): + checks[label] = False + return checks[label] + def query(args): + completed = runner(args) + if completed.returncode != 0 or not isinstance(completed.stdout, str) or len(completed.stdout) > 8192: + raise ValueError('Command unavailable') + return completed.stdout.strip() + uid = None + try: + uid = int(query(['id', '-u'])) + except (OSError, ValueError, TypeError, subprocess.SubprocessError): + pass + check('Run as a normal desktop user', lambda: uid is not None and 0 < uid < 2 ** 31) + check('Linux aarch64 desktop', lambda: query(['uname', '-s']) == 'Linux' and query(['uname', '-m']) == 'aarch64') + def compatible(): + names = bounded(root / 'sys/firmware/devicetree/base/compatible', 4096).split(b'\x00') + return b'apple,j314s' in names and b'apple,t6000' in names + check('Apple M1 Pro j314s machine', compatible) + python = {} + def python_bindings(): + python.update(json.loads(query(['/usr/bin/python3', '-I', '-c', PYTHON_QUERY]))) + return python.get('python') == [3, 14] and python.get('bindings') is True + check('Python 3.14 with GTK4 IBus and accessibility bindings', python_bindings) + def venv(): + config = bounded(base / 'venv/pyvenv.cfg', 8192).decode() + versions = [line.split('=', 1)[1].strip() for line in config.splitlines() + if line.partition('=')[0].strip() == 'version'] + return (base / 'venv/bin/python').is_file() and len(versions) == 1 and versions[0].startswith('3.14.') + check('Existing Python 3.14 speech virtual environment', venv) + def metadata(name, version): + found = [] + for path in (base / 'venv/lib/python3.14/site-packages').glob('*.dist-info/METADATA'): + if path.is_symlink(): + continue + message = BytesParser().parsebytes(bounded(path, 128 * 1024), headersonly=True) + normalized = re.sub('[-_.]+', '-', message.get('Name', '').lower()) + if normalized == name: + found.append(message.get('Version')) + return found == [version] + check('Existing transcribe.cpp 0.4.0 metadata', lambda: metadata('transcribe-cpp', '0.4.0')) + check('Existing NumPy 2.5.4 metadata', lambda: metadata('numpy', '2.5.4')) + check('Qualified native speech library digest', lambda: digest(base / NATIVE) == NATIVE_SHA256) + check('Existing F5 controller and Unicode bridge files', lambda: all((base / name).is_file() + for name in ('stream-dictate.py', 'dictation-input.py', 'dictation-input-client.py'))) + for unit, label in [(STREAM_UNIT, 'Existing CPU F5 listener'), (INPUT_UNIT, 'Existing IBus output bridge')]: + check(label + ' is active', lambda unit=unit: query(['systemctl', '--user', 'is-active', unit]) == 'active') + check(label + ' is enabled', lambda unit=unit: query(['systemctl', '--user', 'is-enabled', unit]) == 'enabled') + check('Existing Unicode bridge is ready without service activation', + lambda: query(['/usr/bin/python3', '-I', '-c', BRIDGE_QUERY]) == 'ready') + def native_input(): + source_text = query(['gsettings', 'get', 'org.gnome.desktop.input-sources', 'sources']) + # GVariant renders an empty source array with its explicit type. + if source_text.startswith('@a(ss) '): + source_text = source_text[7:] + sources = ast.literal_eval(source_text) + current = query(['gsettings', 'get', 'org.gnome.desktop.input-sources', 'current']) + index = int(current.removeprefix('uint32 ')) + if not isinstance(sources, list) or not 0 <= index < len(sources): + return False + kind, name = sources[index] + if not isinstance(name, str) or not 0 < len(name) <= 256: + return False + expected = 'xkb:us::eng' if kind == 'xkb' else name if kind == 'ibus' else None + engine = query(['ibus', 'engine']) + return expected is not None and engine == expected and engine not in ('dummy', 'mseries-dictation') + check('Selected native IBus input remains valid', native_input) + check('Desktop toolkit accessibility is enabled', lambda: query( + ['gsettings', 'get', 'org.gnome.desktop.interface', 'toolkit-accessibility']) == 'true') + check('Desktop GTK input module is IBus', lambda: query( + ['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-im-module']) == "'ibus'") + def remap(): + config = bounded(root / 'etc/udev/hwdb.d/91-mseries-dictation.hwdb').decode().splitlines() + return ('evdev:name:Apple SPI [Kk]eyboard:*' in config + and 'evdev:name:Apple MTP [Kk]eyboard:*' in config + and any(line.strip() == 'KEYBOARD_KEY_7003e=f14' for line in config)) + check('Existing F5 remap configuration metadata (not a physical key test)', remap) + def idle(): + if uid is None: + return False + pid = int(query(['systemctl', '--user', 'show', STREAM_UNIT, '-p', 'MainPID', '--value'])) + if pid <= 0: + return False + status_path = root / ('run/user/' + str(uid)) / 'mseries-dictation/status.json' + info = status_path.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_uid != uid or stat.S_IMODE(info.st_mode) & 0o077: + return False + row = json.loads(bounded(status_path, 4096)) + if (row.get('pid') != pid or row.get('phase') != 'ready' + or row.get('output') != 'ibus' or row.get('backend') != 'cpu'): + return False + process = root / 'proc' / str(pid) + if bounded(process / 'stat', 8192).decode().rpartition(')')[2].split()[0] in ('Z', 'X'): + return False + environment = bounded(process / 'environ', 128 * 1024).split(b'\x00') + if b'STREAM_BACKEND=cpu' not in environment or b'STREAM_OUTPUT=ibus' not in environment: + return False + tasks = list((process / 'task').glob('*/children')) + if not tasks: + return False + for children in tasks: + for child in bounded(children, 65536).decode().split(): + if not child.isdigit(): + return False + state = bounded(root / 'proc' / child / 'stat', 8192).decode().rpartition(')')[2].split()[0] + if state not in ('Z', 'X'): + return False + return True + check('F5 is idle in the existing CPU and IBus listener', idle) + sample = {} + def telemetry(): + path = Path(__file__).resolve().parents[2] / 'local-ai/machine.py' + spec = importlib.util.spec_from_file_location('f5_release_machine', path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sample.update(module.snapshot(root=root)) + return True + sampled = check('Read-only memory fan and temperature telemetry is available', telemetry) + check('Installed memory is between 28 and 36 GiB', lambda: sampled and 28 * GIB <= sample['total_bytes'] <= 36 * GIB) + check('At least 8 GiB memory is currently available', lambda: sampled and sample['available_bytes'] >= 8 * GIB) + check('All measured fans are below 2700 RPM', lambda: sampled and bool(sample['fans_rpm']) + and all(0 <= value < 2700 for value in sample['fans_rpm'].values())) + check('All measured temperatures are below 60 C', lambda: sampled and bool(sample['temperatures_c']) + and all(0 < value < 60 for value in sample['temperatures_c'].values())) + return result(checks) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true', help='verify package, then inspect existing desktop prerequisites') + parser.add_argument('--verify-package', action='store_true', help='verify the complete package without inspecting the desktop') + parser.add_argument('--package-root', type=Path, default=Path(__file__).resolve().parents[3]) + parser.add_argument('--json', action='store_true', help='print only fixed labels and boolean check results') + args = parser.parse_args(argv) + if not (args.check or args.verify_package): + parser.error('Choose --check or --verify-package') + report = verify_package(args.package_root) + if report['passed'] and args.check: + report = result({**report['checks'], **inspect()['checks']}) + if args.json: + print(json.dumps(report, indent=2)) + else: + for label, passed in report['checks'].items(): + print(('PASS: ' if passed else 'FAIL: ') + label) + return 0 if report['passed'] else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/mseries/releases/v0.0.1/receipts/chatgpt-human-v1.json b/mseries/releases/v0.0.1/receipts/chatgpt-human-v1.json new file mode 100644 index 000000000..d19c23a45 --- /dev/null +++ b/mseries/releases/v0.0.1/receipts/chatgpt-human-v1.json @@ -0,0 +1,30 @@ +{ + "schema": 1, + "observed_at_utc": "2026-10-18T19:01:19+00:00", + "complete": true, + "passed": false, + "classification": "physical-F5 app-input failure, before microphone capture; not ASR quality", + "target_app": "ChatGPT Linux desktop", + "owner_report": "No words or an unexpected prompt; target was this ChatGPT app", + "generic_controller_events": { + "f5_received": 2, + "input_refused_before_capture": 2 + }, + "metadata_only_observation": { + "app_accessible_nodes_seen": 4, + "focused_editable_found": false, + "renderer_accessibility_flag_present": false, + "ibus_environment_present": false + }, + "model_inference": false, + "microphone_opened_by_these_attempts": false, + "private_words_collected": false, + "raw_log_published": false, + "private_generic_log_sha256": "d27318649d6be3a3f77723c47a02b23d3049f5421a0bc8d312b77662bbc7b730", + "kernel": "7.1.12-2-20261017.20-sep-ARCH", + "installed_source_sha256": { + "stream-dictate.py": "9b5a4ae68d739829004495813805135eaf62e5d26589e5cf8ef9ef2b8ebea113", + "dictation-input.py": "1a04d5a15cc8f4f121ef37d5efe35c1f6b108b416943af7f6fb6ff446b93a6c5", + "dictation-launch.py": "62cb332684b24140385d8e11f45d21538534649309933456af79c9e9e790167c" + } +} diff --git a/mseries/releases/v0.0.1/receipts/chatgpt-launcher-v1.json b/mseries/releases/v0.0.1/receipts/chatgpt-launcher-v1.json new file mode 100644 index 000000000..d201db299 --- /dev/null +++ b/mseries/releases/v0.0.1/receipts/chatgpt-launcher-v1.json @@ -0,0 +1,27 @@ +{ + "schema": 1, + "observed_at_utc": "2026-10-18T19:01:19+00:00", + "complete": true, + "passed": true, + "classification": "launcher staging only; running ChatGPT and physical F5 remain unverified", + "helper_sha256": "1deb4e851474ae3a206cdfb0ec716716963d9274828868c7a7964a43f3f1c1a9", + "source_helper_matches": true, + "desktop_resolves_to_user_override": true, + "url_field_code_preserved": true, + "override_sha256": "5dffc6d8dcecfaa0a91f018103434a3f3c92a39c18b7e300c4b9367107383eef", + "private_backup_retained": true, + "flags": [ + "--force-renderer-accessibility" + ], + "input_environment": [ + "GTK_IM_MODULE=ibus", + "QT_IM_MODULE=ibus", + "XMODIFIERS=@im=ibus" + ], + "owner_app_restarted": false, + "services_preserved": true, + "model_inference": false, + "microphone_recorded": false, + "activation": "full owner-controlled quit and launch from Applications required", + "human_acceptance": "pending after activation" +} diff --git a/mseries/releases/v0.0.1/test_preflight.py b/mseries/releases/v0.0.1/test_preflight.py new file mode 100644 index 000000000..85450f94e --- /dev/null +++ b/mseries/releases/v0.0.1/test_preflight.py @@ -0,0 +1,306 @@ +#!/usr/bin/python3 +"""Synthetic filesystem/command preflight contracts; no live host inspection.""" +import contextlib +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.dont_write_bytecode = True +spec = importlib.util.spec_from_file_location('f5_release_preflight', Path(__file__).with_name('preflight.py')) +preflight = importlib.util.module_from_spec(spec) +spec.loader.exec_module(preflight) + + +class PackageTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='mseries-preflight-package-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.payload = self.root / 'mseries/example.py' + self.payload.parent.mkdir() + self.payload.write_bytes(b'print("synthetic package")\n') + self.manifest = {'version': '0.0.1', 'source_commit': 'a' * 40, + 'files': {'mseries/example.py': hashlib.sha256(self.payload.read_bytes()).hexdigest()}} + self.save() + + def save(self): + (self.root / 'manifest.json').write_text(json.dumps(self.manifest)) + + def valid(self): + return preflight.verify_package(self.root)['passed'] + + def test_exact_files_and_digests(self): + before = sorted(p.relative_to(self.root).as_posix() for p in self.root.rglob('*')) + self.assertTrue(self.valid()) + self.assertEqual(before, sorted(p.relative_to(self.root).as_posix() for p in self.root.rglob('*'))) + self.payload.write_bytes(b'changed') + self.assertFalse(self.valid()) + self.payload.unlink() + self.assertFalse(self.valid()) + + def test_extra_unlisted_payload_and_special_file(self): + (self.root / 'extra.txt').write_text('unlisted') + self.assertFalse(self.valid()) + (self.root / 'extra.txt').unlink() + os.mkfifo(self.root / 'pipe') + self.assertFalse(self.valid()) + + def test_links_cannot_escape_or_hide_as_directories(self): + self.payload.unlink() + self.payload.symlink_to('/etc/passwd') + self.assertFalse(self.valid()) + self.payload.unlink() + self.payload.write_text('safe') + self.manifest['files']['mseries/example.py'] = hashlib.sha256(self.payload.read_bytes()).hexdigest() + self.save() + (self.root / 'linked-directory').symlink_to(self.root / 'mseries', target_is_directory=True) + self.assertFalse(self.valid()) + (self.root / 'linked-directory').unlink() + manifest = self.root / 'manifest.json' + manifest.rename(self.root / 'manifest-real') + manifest.symlink_to(self.root / 'manifest-real') + self.assertFalse(self.valid()) + + def test_manifest_identity_paths_digests_and_duplicates(self): + for update in ({'version': '0.0.2'}, {'source_commit': 'branch'}, {'files': {}}, + {'files': {'../escape': 'a' * 64}}, {'files': {'/absolute': 'a' * 64}}, + {'files': {'mseries//example.py': 'a' * 64}}, + {'files': {'mseries/./example.py': 'a' * 64}}, + {'files': {'mseries\\example.py': 'a' * 64}}, + {'files': {'manifest.json': 'a' * 64}}, + {'files': {'mseries/example.py': 'not-a-digest'}}): + with self.subTest(update=update): + original = dict(self.manifest) + self.manifest.update(update) + self.save() + self.assertFalse(self.valid()) + self.manifest = original + (self.root / 'manifest.json').write_text('{"version":"0.0.1","version":"0.0.1"}') + self.assertFalse(self.valid()) + + def test_link_swap_after_inventory_is_refused(self): + original_open = preflight.package_file + def swapped(root, name): + if name == 'mseries/example.py': + self.payload.unlink() + self.payload.symlink_to('/etc/passwd') + return original_open(root, name) + with patch.object(preflight, 'package_file', swapped): + self.assertFalse(self.valid()) + + def test_integrity_failure_never_inspects_host_or_exposes_paths(self): + self.payload.write_text('tampered') + output = io.StringIO() + with patch.object(preflight, 'inspect', side_effect=AssertionError('Host must not be inspected')), contextlib.redirect_stdout(output): + self.assertEqual(preflight.main(['--check', '--json', '--package-root', str(self.root)]), 1) + report = json.loads(output.getvalue()) + self.assertEqual(report['failures'], [preflight.PACKAGE_LABEL]) + self.assertNotIn(str(self.root), output.getvalue()) + + def test_verify_only_never_invokes_host_checks(self): + output = io.StringIO() + with patch.object(preflight, 'inspect', side_effect=AssertionError('No host check')), contextlib.redirect_stdout(output): + self.assertEqual(preflight.main(['--verify-package', '--json', '--package-root', str(self.root)]), 0) + self.assertTrue(json.loads(output.getvalue())['passed']) + + def test_real_verify_only_cli_is_bounded_and_keeps_package_unchanged(self): + command = [sys.executable, str(Path(__file__).with_name('preflight.py')), + '--verify-package', '--json', '--package-root', str(self.root)] + before = sorted(p.relative_to(self.root).as_posix() for p in self.root.rglob('*')) + completed = subprocess.run(command, capture_output=True, text=True, timeout=5) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertTrue(json.loads(completed.stdout)['passed']) + self.assertNotIn(str(self.root), completed.stdout + completed.stderr) + self.assertEqual(before, sorted(p.relative_to(self.root).as_posix() for p in self.root.rglob('*'))) + + +class ReleaseIdentityTests(unittest.TestCase): + def test_qualified_native_digest_and_release_identity(self): + self.assertEqual(preflight.VERSION, '0.0.1') + self.assertEqual(preflight.NATIVE_SHA256, + '9f1f839d89a6d3a874c939ecf3108007cc0964a338596a35adb47c7e6c5c921d') + + +class InspectionTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='mseries-preflight-inspect-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) / 'root' + self.home = Path(self.temp.name) / 'synthetic-private-home' + self.base = self.home / '.local/share/mseries/stream' + self.uid = os.getuid() or 1000 + self.calls = [] + self.overrides = {} + self.write('sys/firmware/devicetree/base/compatible', b'apple,j314s\x00apple,t6000\x00apple,arm-platform\x00') + self.write('proc/meminfo', b'MemTotal: 33554432 kB\nMemAvailable: 10485760 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\n') + self.write('sys/class/hwmon/hwmon0/fan1_input', b'2400\n') + self.write('sys/class/hwmon/hwmon0/temp1_input', b'43000\n') + self.write('etc/udev/hwdb.d/91-mseries-dictation.hwdb', b'evdev:name:Apple SPI [Kk]eyboard:*\nevdev:name:Apple MTP [Kk]eyboard:*\n KEYBOARD_KEY_7003e=f14\n') + self.status = {'pid': 1234, 'phase': 'ready', 'output': 'ibus', 'backend': 'cpu', 'updated_at': 1.0} + self.status_path = self.root / ('run/user/' + str(self.uid)) / 'mseries-dictation/status.json' + self.save_status() + self.write('proc/1234/stat', b'1234 (synthetic listener) S 1 2 3\n') + self.write('proc/1234/environ', b'STREAM_BACKEND=cpu\x00STREAM_OUTPUT=ibus\x00PRIVATE_TOKEN=do-not-publish\x00') + self.write('proc/1234/task/1234/children', b'') + for name in ('stream-dictate.py', 'dictation-input.py', 'dictation-input-client.py', 'venv/bin/python'): + self.base_write(name, b'synthetic fixture\n') + self.base_write('venv/pyvenv.cfg', b'version = 3.14.0\n') + self.base_write('venv/lib/python3.14/site-packages/transcribe_cpp-0.4.0.dist-info/METADATA', b'Name: transcribe_cpp\nVersion: 0.4.0\n') + self.base_write('venv/lib/python3.14/site-packages/numpy-2.5.4.dist-info/METADATA', b'Name: numpy\nVersion: 2.5.4\n') + self.base_write(preflight.NATIVE, b'synthetic native library - never loaded') + expected = hashlib.sha256((self.base / preflight.NATIVE).read_bytes()).hexdigest() + self.native_patch = patch.object(preflight, 'NATIVE_SHA256', expected) + self.native_patch.start() + self.addCleanup(self.native_patch.stop) + + def write(self, relative, data): + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + def base_write(self, relative, data): + path = self.base / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + def save_status(self): + self.status_path.parent.mkdir(parents=True, exist_ok=True) + self.status_path.write_text(json.dumps(self.status)) + self.status_path.chmod(0o600) + if os.getuid() == 0: + os.chown(self.status_path, self.uid, -1) + + def runner(self, args): + command = tuple(args) + self.calls.append(command) + if command in self.overrides: + override = self.overrides[command] + if isinstance(override, BaseException): + raise override + return subprocess.CompletedProcess(args, 0, override, 'PRIVATE command diagnostic') + if args == ['id', '-u']: + value = str(self.uid) + elif args == ['uname', '-s']: + value = 'Linux' + elif args == ['uname', '-m']: + value = 'aarch64' + elif args == ['/usr/bin/python3', '-I', '-c', preflight.PYTHON_QUERY]: + value = json.dumps({'python': [3, 14], 'bindings': True}) + elif args == ['/usr/bin/python3', '-I', '-c', preflight.BRIDGE_QUERY]: + value = 'ready' + elif args[:3] == ['systemctl', '--user', 'is-active']: + value = 'active' + elif args[:3] == ['systemctl', '--user', 'is-enabled']: + value = 'enabled' + elif args == ['systemctl', '--user', 'show', preflight.STREAM_UNIT, '-p', 'MainPID', '--value']: + value = '1234' + elif args == ['gsettings', 'get', 'org.gnome.desktop.input-sources', 'sources']: + value = "[('xkb', 'us')]" + elif args == ['gsettings', 'get', 'org.gnome.desktop.input-sources', 'current']: + value = 'uint32 0' + elif args == ['gsettings', 'get', 'org.gnome.desktop.interface', 'toolkit-accessibility']: + value = 'true' + elif args == ['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-im-module']: + value = "'ibus'" + elif args == ['ibus', 'engine']: + value = 'xkb:us::eng' + else: + raise AssertionError('Unexpected command: ' + repr(args)) + return subprocess.CompletedProcess(args, 0, value + '\n', '') + + def inspect(self): + return preflight.inspect(root=self.root, home=self.home, runner=self.runner) + + def test_supported_fixture_is_read_only_and_private(self): + before = {str(path): path.read_bytes() for path in Path(self.temp.name).rglob('*') if path.is_file()} + report = self.inspect() + self.assertTrue(report['passed'], report['failures']) + self.assertEqual(before, {str(path): path.read_bytes() for path in Path(self.temp.name).rglob('*') if path.is_file()}) + public = json.dumps(report) + for secret in (str(self.home), 'do-not-publish', 'PRIVATE', 'xkb:us::eng'): + self.assertNotIn(secret, public) + self.assertIn('NO_AUTO_START', preflight.BRIDGE_QUERY) + self.assertTrue(all('start' not in args and 'restart' not in args and 'set' not in args for args in self.calls)) + + def test_memory_and_quiet_boundary_checks(self): + self.write('proc/meminfo', b'MemTotal: 33554432 kB\nMemAvailable: 8388608 kB\n') + self.assertTrue(self.inspect()['passed']) + self.write('proc/meminfo', b'MemTotal: 33554432 kB\nMemAvailable: 8388607 kB\n') + self.assertIn('At least 8 GiB memory is currently available', self.inspect()['failures']) + self.write('proc/meminfo', b'MemTotal: 29360128 kB\nMemAvailable: 10485760 kB\n') + self.assertTrue(self.inspect()['passed']) + self.write('proc/meminfo', b'MemTotal: 37748736 kB\nMemAvailable: 10485760 kB\n') + self.assertTrue(self.inspect()['passed']) + self.write('sys/class/hwmon/hwmon0/fan1_input', b'2700\n') + self.write('sys/class/hwmon/hwmon0/temp1_input', b'60000\n') + self.assertIn('All measured fans are below 2700 RPM', self.inspect()['failures']) + self.assertIn('All measured temperatures are below 60 C', self.inspect()['failures']) + self.write('sys/class/hwmon/hwmon0/fan1_input', b'nan\n') + self.assertIn('All measured fans are below 2700 RPM', self.inspect()['failures']) + + def test_missing_telemetry_and_wrong_hardware_fail_closed(self): + self.write('sys/firmware/devicetree/base/compatible', b'apple,j413\x00apple,t8112\x00') + (self.root / 'sys/class/hwmon/hwmon0/fan1_input').unlink() + (self.root / 'sys/class/hwmon/hwmon0/temp1_input').unlink() + report = self.inspect() + self.assertFalse(report['passed']) + for label in ('Apple M1 Pro j314s machine', 'All measured fans are below 2700 RPM', 'All measured temperatures are below 60 C'): + self.assertIn(label, report['failures']) + + def test_runtime_version_and_library_pins(self): + self.base_write('venv/pyvenv.cfg', b'version = 3.13.9\n') + self.base_write(preflight.NATIVE, b'wrong library') + self.base_write('venv/lib/python3.14/site-packages/numpy-2.5.4.dist-info/METADATA', b'Name: numpy\nVersion: 2.5.5\n') + self.overrides[('/usr/bin/python3', '-I', '-c', preflight.PYTHON_QUERY)] = '{"python":[3,13],"bindings":true}' + report = self.inspect() + for label in ('Existing Python 3.14 speech virtual environment', 'Qualified native speech library digest', + 'Existing NumPy 2.5.4 metadata', 'Python 3.14 with GTK4 IBus and accessibility bindings'): + self.assertIn(label, report['failures']) + + def test_capture_status_pid_process_and_child_guards(self): + for status in ({'phase': 'listening'}, {'phase': 'finishing'}, {'pid': 999}, {'backend': 'ane'}): + with self.subTest(status=status): + original = dict(self.status) + self.status.update(status) + self.save_status() + self.assertIn('F5 is idle in the existing CPU and IBus listener', self.inspect()['failures']) + self.status = original + self.save_status() + self.write('proc/1234/task/1234/children', b'2345') + self.write('proc/2345/stat', b'2345 (synthetic capture) S 1 2\n') + self.assertIn('F5 is idle in the existing CPU and IBus listener', self.inspect()['failures']) + self.write('proc/2345/stat', b'2345 (synthetic child) Z 1 2\n') + self.assertTrue(self.inspect()['passed']) + self.status_path.chmod(0o644) + self.assertIn('F5 is idle in the existing CPU and IBus listener', self.inspect()['failures']) + + def test_commands_fail_closed_without_raw_output(self): + self.overrides[('id', '-u')] = '0' + self.overrides[('systemctl', '--user', 'is-active', preflight.STREAM_UNIT)] = 'inactive' + self.overrides[('/usr/bin/python3', '-I', '-c', preflight.BRIDGE_QUERY)] = subprocess.TimeoutExpired('PRIVATE COMMAND', 3) + self.overrides[('gsettings', 'get', 'org.gnome.desktop.input-sources', 'sources')] = 'PRIVATE malformed settings [' + report = self.inspect() + self.assertFalse(report['passed']) + self.assertNotIn('PRIVATE', json.dumps(report)) + self.assertIn('Selected native IBus input remains valid', report['failures']) + + def test_input_accessibility_and_remap_checks(self): + self.overrides[('ibus', 'engine')] = 'mseries-dictation' + self.overrides[('gsettings', 'get', 'org.gnome.desktop.interface', 'toolkit-accessibility')] = 'false' + self.write('etc/udev/hwdb.d/91-mseries-dictation.hwdb', b'KEYBOARD_KEY_7003e=micmute\n') + report = self.inspect() + self.assertIn('Selected native IBus input remains valid', report['failures']) + self.assertIn('Desktop toolkit accessibility is enabled', report['failures']) + self.assertIn('Existing F5 remap configuration metadata (not a physical key test)', report['failures']) + + +if __name__ == '__main__': + unittest.main() From 13a8b847ef3c754cbe37fe68686463bb2ec9f269 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 15:05:20 -0400 Subject: [PATCH 024/798] Retain failed ChatGPT retry and admit cleaned-up idle speech states --- mseries/OS9-HANDOFF.md | 6 ++-- mseries/ROADMAP.md | 6 ++-- mseries/dictation/QUALIFICATION-INDEX.md | 3 +- mseries/dictation/qualification-index.json | 10 ++++++- mseries/releases/v0.0.1/README.md | 4 ++- mseries/releases/v0.0.1/preflight.py | 7 ++++- .../releases/v0.0.1/receipts/build-rc1.json | 30 +++++++++++++++++++ .../v0.0.1/receipts/chatgpt-human-v2.json | 17 +++++++++++ mseries/releases/v0.0.1/test_preflight.py | 27 ++++++++++++++++- 9 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 mseries/releases/v0.0.1/receipts/build-rc1.json create mode 100644 mseries/releases/v0.0.1/receipts/chatgpt-human-v2.json diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 44eddb57c..90380c74e 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -15,8 +15,10 @@ environment at next full app launch. Seventeen launcher contracts pass, includin independent GLib quoting/spacing, URL/action preservation, owner-edit refusal and private rollback. Desktop resolution selects that override. ChatGPT, listener, input bridge and everyday4B were not restarted. No microphone/model was used. -Full owner-controlled Quit, reopen from Applications, then physical F5 in the chat -composer is the remaining acceptance gate. Preserve password/active-field guards. +A later owner retry also failed; read-only process metadata still shows the +original ChatGPT process without the new flags/environment. The fix has not +activated. Fully Quit, reopen from Applications, verify new process flags, then +physical F5 in the chat composer is the remaining acceptance gate. Preserve password/active-field guards. The owner permits OS-level dictation integration; it is not a reason to type into an unknown field. Native compositor input/broker work remains future development. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 6d6fbfbc0..e553ba2ea 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -15,8 +15,10 @@ environment at next full app launch. Seventeen launcher contracts pass, includin independent GLib quoting/spacing, URL/action preservation, owner-edit refusal and private rollback. Desktop resolution selects that override. ChatGPT, listener, input bridge and everyday4B were not restarted. No microphone/model was used. -Full owner-controlled Quit, reopen from Applications, then physical F5 in the chat -composer is the remaining acceptance gate. Preserve password/active-field guards. +A later owner retry also failed; read-only process metadata still shows the +original ChatGPT process without the new flags/environment. The fix has not +activated. Fully Quit, reopen from Applications, verify new process flags, then +physical F5 in the chat composer is the remaining acceptance gate. Preserve password/active-field guards. The owner permits OS-level dictation integration; it is not a reason to type into an unknown field. Native compositor input/broker work remains future development. diff --git a/mseries/dictation/QUALIFICATION-INDEX.md b/mseries/dictation/QUALIFICATION-INDEX.md index 708668c71..345741554 100644 --- a/mseries/dictation/QUALIFICATION-INDEX.md +++ b/mseries/dictation/QUALIFICATION-INDEX.md @@ -19,7 +19,8 @@ focused editable field. This app-input failure is retained in the [release receipts](../releases/v0.0.1/receipts/). A per-user accessibility/IBus launcher correction is staged, with 17 passing contracts and verified desktop-entry resolution. It requires a full owner-controlled app quit/relaunch and a fresh -physical F5 check. Staging is not a pass. The source-only English STT release +physical F5 check. A later retry failed with the original process and no new flags still loaded; +activation is unproven. Staging is not a pass. The source-only English STT release remains a candidate; no model benchmark was repeated and no v0.0.1 was published. ## Current decision and limits diff --git a/mseries/dictation/qualification-index.json b/mseries/dictation/qualification-index.json index f506cc238..a1e36f528 100644 --- a/mseries/dictation/qualification-index.json +++ b/mseries/dictation/qualification-index.json @@ -2224,13 +2224,21 @@ "release_readiness": { "version": "0.0.1", "scope": "existing M1 Pro 32 GB desktop, English F5 STT only", - "status": "candidate; physical ChatGPT F5 failed; launcher correction staged and awaiting full app relaunch/manual check", + "status": "candidate; two owner reports failed; original ChatGPT process lacks staged settings, activation pending", "model_cohorts_repeated": false, "receipts": [ + { + "path": "../releases/v0.0.1/receipts/build-rc1.json", + "sha256": "4118c9803f0ba43f9a43b457e2d06978aaf85170f8bf3a638f3a07c8ec5b999f" + }, { "path": "../releases/v0.0.1/receipts/chatgpt-human-v1.json", "sha256": "5c100124ac4b6f2c99f4f82a7ec3a2184bad70366e710c9e389de50577b39192" }, + { + "path": "../releases/v0.0.1/receipts/chatgpt-human-v2.json", + "sha256": "34206a1d561e3a125d1918153a551f9091116ee2d642d90258accc33c469f59c" + }, { "path": "../releases/v0.0.1/receipts/chatgpt-launcher-v1.json", "sha256": "62729514f7e2769a060b8c534861a428d9dd523dd4d7737fde0f388bb7e988e5" diff --git a/mseries/releases/v0.0.1/README.md b/mseries/releases/v0.0.1/README.md index 77de2e924..b562a6044 100644 --- a/mseries/releases/v0.0.1/README.md +++ b/mseries/releases/v0.0.1/README.md @@ -74,7 +74,9 @@ This preserves the desktop entry's other fields and URLs, enables renderer accessibility and selects the IBus environment. It does not alter the running app. Fully quit ChatGPT and reopen it from Applications when convenient; closing one window or launching a second instance may leave the old process running. -The current ChatGPT configuration remains an unresolved release gate above. +A later retry also failed while the original process was still running without +the new flags/environment. The fix has not activated. The current ChatGPT +configuration remains an unresolved release gate above. ## Rollback diff --git a/mseries/releases/v0.0.1/preflight.py b/mseries/releases/v0.0.1/preflight.py index bbfbee35f..7619effb7 100644 --- a/mseries/releases/v0.0.1/preflight.py +++ b/mseries/releases/v0.0.1/preflight.py @@ -26,6 +26,11 @@ NATIVE_SHA256 = '9f1f839d89a6d3a874c939ecf3108007cc0964a338596a35adb47c7e6c5c921d' STREAM_UNIT = 'mseries-stream-dictate.service' INPUT_UNIT = 'mseries-dictation-input.service' +# prepare() emits these after capture/output cleanup and active=False. An error +# is not proof of healthy dictation; it can still be an idle listener. The PID, +# CPU/IBus environment and no-live-child checks below remain mandatory because +# insertion_failed() can also emit error before the capture has been reaped. +IDLE_PHASES = frozenset(('ready', 'error', 'cancelled', 'waiting')) NATIVE = 'native/transcribe-native-linux-aarch64-cpu-vulkan/libtranscribe.so' PACKAGE_LABEL = 'Package files match the complete v0.0.1 manifest' PYTHON_QUERY = '''import json, sys, gi @@ -263,7 +268,7 @@ def idle(): if not stat.S_ISREG(info.st_mode) or info.st_uid != uid or stat.S_IMODE(info.st_mode) & 0o077: return False row = json.loads(bounded(status_path, 4096)) - if (row.get('pid') != pid or row.get('phase') != 'ready' + if (row.get('pid') != pid or row.get('phase') not in IDLE_PHASES or row.get('output') != 'ibus' or row.get('backend') != 'cpu'): return False process = root / 'proc' / str(pid) diff --git a/mseries/releases/v0.0.1/receipts/build-rc1.json b/mseries/releases/v0.0.1/receipts/build-rc1.json new file mode 100644 index 000000000..7c01aa4ed --- /dev/null +++ b/mseries/releases/v0.0.1/receipts/build-rc1.json @@ -0,0 +1,30 @@ +{ + "schema": 1, + "observed_at_utc": "2026-10-18T19:04:15+00:00", + "complete": true, + "passed": false, + "classification": "archive integrity/reproducibility passed; host preflight refused terminal input-error status", + "source_commit": "2f3fb13f909327a0fcc83518c3efe71e07805846", + "archive": { + "filename": "elementaryos-mseries-f5-0.0.1.tar.gz", + "sha256": "efe5922dc1cb8071aa82106460cda7f270de903dee4514a0217b9ea5191a1139", + "size": 75033 + }, + "all_three_artifacts_reproducible": true, + "outer_checksums_verified": true, + "complete_package_manifest_verified": true, + "host_checks_passed": 24, + "host_checks_failed": [ + "F5 is idle in the existing CPU and IBus listener" + ], + "terminal_controller_phase": "error", + "failure_preserved": true, + "setup_mistakes_retained": [ + "Initial install.sh command used repository cwd and did not execute", + "Initial SHA256SUMS check used extraction cwd, not artifact cwd; corrected artifact-directory check passed" + ], + "installer_run": false, + "model_inference": false, + "microphone_recorded": false, + "published": false +} diff --git a/mseries/releases/v0.0.1/receipts/chatgpt-human-v2.json b/mseries/releases/v0.0.1/receipts/chatgpt-human-v2.json new file mode 100644 index 000000000..0f62f308d --- /dev/null +++ b/mseries/releases/v0.0.1/receipts/chatgpt-human-v2.json @@ -0,0 +1,17 @@ +{ + "schema": 1, + "observed_at_utc": "2026-10-18T19:04:15+00:00", + "complete": true, + "passed": false, + "classification": "owner retry failed; staged configuration not activated, no corrected-runtime acceptance established", + "target_app": "ChatGPT Linux desktop", + "owner_report": "Still no words or an unexpected prompt", + "original_main_process_still_running": true, + "renderer_accessibility_flag_present": false, + "ibus_environment_present": false, + "staged_override_still_matches": true, + "new_model_trial": false, + "spoken_contents_collected": false, + "owner_app_restarted_by_agent": false, + "next_gate": "full owner-controlled app quit/relaunch from Applications, verify flags before accepting another microphone result" +} diff --git a/mseries/releases/v0.0.1/test_preflight.py b/mseries/releases/v0.0.1/test_preflight.py index 85450f94e..b8f9feefb 100644 --- a/mseries/releases/v0.0.1/test_preflight.py +++ b/mseries/releases/v0.0.1/test_preflight.py @@ -266,7 +266,9 @@ def test_runtime_version_and_library_pins(self): self.assertIn(label, report['failures']) def test_capture_status_pid_process_and_child_guards(self): - for status in ({'phase': 'listening'}, {'phase': 'finishing'}, {'pid': 999}, {'backend': 'ane'}): + for status in ({'phase': 'loading'}, {'phase': 'listening'}, {'phase': 'finishing'}, + {'phase': 'stopped'}, {'phase': 'unavailable'}, {'phase': 'unknown'}, + {'pid': 999}, {'backend': 'ane'}): with self.subTest(status=status): original = dict(self.status) self.status.update(status) @@ -282,6 +284,29 @@ def test_capture_status_pid_process_and_child_guards(self): self.status_path.chmod(0o644) self.assertIn('F5 is idle in the existing CPU and IBus listener', self.inspect()['failures']) + def test_terminal_idle_phases_still_require_no_capture_and_matching_runtime(self): + self.assertEqual(preflight.IDLE_PHASES, {'ready', 'error', 'cancelled', 'waiting'}) + label = 'F5 is idle in the existing CPU and IBus listener' + for phase in sorted(preflight.IDLE_PHASES): + with self.subTest(phase=phase): + self.status.update(phase=phase, pid=1234) + self.save_status() + self.assertTrue(self.inspect()['passed']) + # An asynchronous insertion failure may expose phase=error + # before cleanup; a live capture/output child must still deny. + self.write('proc/1234/task/1234/children', b'2345') + self.write('proc/2345/stat', b'2345 (synthetic capture) S 1 2\n') + self.assertIn(label, self.inspect()['failures']) + self.write('proc/1234/task/1234/children', b'') + self.status['pid'] = 999 + self.save_status() + self.assertIn(label, self.inspect()['failures']) + self.status['pid'] = 1234 + self.save_status() + self.write('proc/1234/environ', b'STREAM_BACKEND=ane\x00STREAM_OUTPUT=ibus\x00') + self.assertIn(label, self.inspect()['failures']) + self.write('proc/1234/environ', b'STREAM_BACKEND=cpu\x00STREAM_OUTPUT=ibus\x00') + def test_commands_fail_closed_without_raw_output(self): self.overrides[('id', '-u')] = '0' self.overrides[('systemctl', '--user', 'is-active', preflight.STREAM_UNIT)] = 'inactive' From 0e0b1953f76a02ef810ce198f56ade6fca3f3abd Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 15:09:57 -0400 Subject: [PATCH 025/798] Record checked STT candidate deployment and open human F5 gate --- docs/local-ai.html | 2 +- mseries/OS9-HANDOFF.md | 16 ++++++-- mseries/ROADMAP.md | 16 ++++++-- mseries/dictation/qualification-index.json | 15 ++++++- mseries/releases/v0.0.1/README.md | 6 +++ .../v0.0.1/receipts/activation-rc2.json | 39 +++++++++++++++++++ .../releases/v0.0.1/receipts/build-rc2.json | 16 ++++++++ 7 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 mseries/releases/v0.0.1/receipts/activation-rc2.json create mode 100644 mseries/releases/v0.0.1/receipts/build-rc2.json diff --git a/docs/local-ai.html b/docs/local-ai.html index da17ef6f8..e3cc59528 100644 --- a/docs/local-ai.html +++ b/docs/local-ai.html @@ -6,7 +6,7 @@

    Local AI on the M1 Pro 32 GB

    Measured October 16, 2026 on the working elementary OS 9 desktop. Qwen3.5 4B through private Linux MLX is the practical default: it passed 9/15 short tasks, 9/9 corrected MCP workflows and real Newelle/memory/coding checks. Qwen9 scored 12/15 short tasks but later crossed the desktop RAM reserve; it now requires a larger optional budget.

    Newelle provides local desktop chat and personal summary memory. The coding client uses a separate 16K profile. A six-GiB desktop memory reserve and fan/temperature limits take priority over completing an inference request. The real coding edit passed in 87.31 seconds, leaving at least 8.51 GiB available at no more than 2513 fan RPM. Bonsai completed a resumed 12/15 quality composite and 9/9 corrected tool workflows; native generation averaged 5.57 tokens/s. The resumed task sessions include a fan-limit interruption and are not a quiet-policy pass.

    English F5 integration and sustained tests · October 18

    -

    v0.0.1 remains a release candidate. The owner subsequently tried physical F5 in ChatGPT and got no words or an unexpected prompt. The key reached the listener; input was refused before microphone capture because the app exposed no focused editable field. A per-user launcher correction enables renderer accessibility and IBus on the next full app launch. Seventeen launcher contracts and desktop-entry resolution pass; post-relaunch human F5 acceptance is pending. No model benchmark was repeated or working v0.0.1 release declared. The candidate contains only English speech-to-text source for the existing configured M1 Pro desktop. Candidate instructions, scope and rollback · Retained failure and staging evidence.

    +

    v0.0.1 remains a release candidate. The owner subsequently tried physical F5 in ChatGPT and got no words or an unexpected prompt. The key reached the listener; input was refused before microphone capture because the app exposed no focused editable field. A per-user launcher correction enables renderer accessibility and IBus on the next full app launch. Seventeen launcher contracts and desktop-entry resolution pass; post-relaunch human F5 acceptance is pending. No model benchmark was repeated or working v0.0.1 release declared. The source-only STT candidate is installed after seven exact-head CI jobs passed. All25 read-only host/package checks and installed-byte/readiness/preservation checks pass; this does not close the ChatGPT human gate. The candidate contains only English speech-to-text source for the existing configured M1 Pro desktop. Candidate instructions, scope and rollback · Retained failure and staging evidence.

    Merged and installed. PR #155 passed all six head-specific CI checks and merged as 5c26ff6. Installed runtime/model hashes, strict live CPU/IBus settings, enabled readiness, preserved toggle mode/input settings and the unchanged everyday 4B process passed read-only verification. Native ANE/MLX assets are retained; the optional Voice Assistant is installed with automatic replies off. All seven installed routes passed: GTK4 with public-file ASR, GTK3/Qt6/Brave with no-inference input fixtures, and Code/Newelle/Tasks with no-inference field-discovery/lease/cancel checks. The last three do not repeat positive transcription; earlier pinned real-app ASR receipts supply that evidence. The three initial missing-argument setup failures are retained; only their unstarted fixtures resumed. The installed queues retained at least10.48GiB available at≤2530RPM/49.01°C, with no guard interruption. These tests used isolated app state. No fresh owner microphone or physical-F5 acceptance is claimed.

    The current integration selects English Nemotron CPU streaming, with native ANE and Parakeet MLX alternatives and configurable0–900second residency. Exact English words arrived during speech in actual elementary Code (1.671s), Newelle (1.839s) and Tasks (1.885s), plus GTK3/GTK4/Qt6/Brave fixtures. Times include controller/input preparation. No message-send or task-save action was requested. A synthetic Gmail-style rich compose/textarea regression passes; authenticated Gmail remains untested.

    Testing exposed Chromium confirming speculative draft composition on focus loss. Those failures remain retained. App input now uses only stable committed words; the original transcript is preserved and English grammar suggestions stay separate. Native worker deadlines, cancellation, field leases, pressure reclamation and private rollback are covered. No owner microphone or window was used.

    diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 90380c74e..597137031 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -22,6 +22,17 @@ physical F5 in the chat composer is the remaining acceptance gate. Preserve pass The owner permits OS-level dictation integration; it is not a reason to type into an unknown field. Native compositor input/broker work remains future development. +RC2 archive `d5c0e9b2…` is built from exact source `13a8b847ef3c754cbe37fe68686463bb2ec9f269`. +All 25 extracted-package/host checks passed. That exact head passed all seven CI +jobs in run `39157224321`. The source-only STT payload is now installed: all 16 +runtime hashes and managers match, the transcript-only launcher is active for +its next invocation, strict CPU/IBus is ready, all seven debug overrides are +absent, native input preferences and everyday4B PID are preserved. One post-install +sample had 13,683,703,808 bytes available, maximum 2,489 RPM / 42.331 C. This was +static/readiness verification without microphone/model inference. It is not +physical F5 acceptance in ChatGPT. RC1's terminal-error idle refusal was fixed +without changing its retained receipt or any live status to obtain a pass. + Release worktree: `ElementaryOS_MSeries-f5-v001`, branch `release/f5-v0.0.1`. Source-only deterministic Git-blob packaging, strict read-only preflight, versioned rollback and a transcript-only UI are prepared. Native/ANE/MLX assets @@ -31,9 +42,8 @@ stay available on the host but are not bundled or requalified. See Advanced diagnostic work is isolated on `feat/quiet-diagnostic-agent`, disabled and not qualified; never add it to the STT artifact or use everyday4B for repairs. The existing six-hour developer watcher continues; do not duplicate it or rerun -finished ASR benchmarks. Next: commit/push candidate, verify exact-commit archive -and host preflight, record the owner's post-relaunch F5 result, then release only -when that result passes. Preserve all owner main changes; one Git writer. +finished ASR benchmarks. Next: verify ChatGPT actually restarts with the staged flags, record the owner's +post-relaunch F5 result, then publish v0.0.1 only when that result passes. Preserve all owner main changes; one Git writer. ## CodexBar weekly quota beside top-row tokens — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index e553ba2ea..8892df35e 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -22,6 +22,17 @@ physical F5 in the chat composer is the remaining acceptance gate. Preserve pass The owner permits OS-level dictation integration; it is not a reason to type into an unknown field. Native compositor input/broker work remains future development. +RC2 archive `d5c0e9b2…` is built from exact source `13a8b847ef3c754cbe37fe68686463bb2ec9f269`. +All 25 extracted-package/host checks passed. That exact head passed all seven CI +jobs in run `39157224321`. The source-only STT payload is now installed: all 16 +runtime hashes and managers match, the transcript-only launcher is active for +its next invocation, strict CPU/IBus is ready, all seven debug overrides are +absent, native input preferences and everyday4B PID are preserved. One post-install +sample had 13,683,703,808 bytes available, maximum 2,489 RPM / 42.331 C. This was +static/readiness verification without microphone/model inference. It is not +physical F5 acceptance in ChatGPT. RC1's terminal-error idle refusal was fixed +without changing its retained receipt or any live status to obtain a pass. + Release worktree: `ElementaryOS_MSeries-f5-v001`, branch `release/f5-v0.0.1`. Source-only deterministic Git-blob packaging, strict read-only preflight, versioned rollback and a transcript-only UI are prepared. Native/ANE/MLX assets @@ -31,9 +42,8 @@ stay available on the host but are not bundled or requalified. See Advanced diagnostic work is isolated on `feat/quiet-diagnostic-agent`, disabled and not qualified; never add it to the STT artifact or use everyday4B for repairs. The existing six-hour developer watcher continues; do not duplicate it or rerun -finished ASR benchmarks. Next: commit/push candidate, verify exact-commit archive -and host preflight, record the owner's post-relaunch F5 result, then release only -when that result passes. Preserve all owner main changes; one Git writer. +finished ASR benchmarks. Next: verify ChatGPT actually restarts with the staged flags, record the owner's +post-relaunch F5 result, then publish v0.0.1 only when that result passes. Preserve all owner main changes; one Git writer. ## CodexBar weekly quota beside top-row tokens — 2026-10-18 diff --git a/mseries/dictation/qualification-index.json b/mseries/dictation/qualification-index.json index a1e36f528..9ce1b0ec9 100644 --- a/mseries/dictation/qualification-index.json +++ b/mseries/dictation/qualification-index.json @@ -2227,10 +2227,18 @@ "status": "candidate; two owner reports failed; original ChatGPT process lacks staged settings, activation pending", "model_cohorts_repeated": false, "receipts": [ + { + "path": "../releases/v0.0.1/receipts/activation-rc2.json", + "sha256": "3430533589105c1331979b35617241b2ec9fe21ff051fad3db8c932240b0088e" + }, { "path": "../releases/v0.0.1/receipts/build-rc1.json", "sha256": "4118c9803f0ba43f9a43b457e2d06978aaf85170f8bf3a638f3a07c8ec5b999f" }, + { + "path": "../releases/v0.0.1/receipts/build-rc2.json", + "sha256": "7ba3c75b61d74d494afb530b4635cc950873e5fe9ecd245a8a8abd123d5fdfc0" + }, { "path": "../releases/v0.0.1/receipts/chatgpt-human-v1.json", "sha256": "5c100124ac4b6f2c99f4f82a7ec3a2184bad70366e710c9e389de50577b39192" @@ -2243,6 +2251,11 @@ "path": "../releases/v0.0.1/receipts/chatgpt-launcher-v1.json", "sha256": "62729514f7e2769a060b8c534861a428d9dd523dd4d7737fde0f388bb7e988e5" } - ] + ], + "candidate_source_commit": "13a8b847ef3c754cbe37fe68686463bb2ec9f269", + "candidate_ci_run": 39157224321, + "candidate_ci": "all seven jobs success", + "candidate_deployed": true, + "physical_chatgpt_f5": "pending activation of next-launch settings; failed reports preserved" } } diff --git a/mseries/releases/v0.0.1/README.md b/mseries/releases/v0.0.1/README.md index b562a6044..60549ff6b 100644 --- a/mseries/releases/v0.0.1/README.md +++ b/mseries/releases/v0.0.1/README.md @@ -94,6 +94,12 @@ python3 -B mseries/dictation/enable-app-input.py --app chatgpt --rollback ## Qualification and limits +The RC2 source was deployed after all seven exact-head CI checks passed. All 25 +read-only package/host checks and installed-byte/readiness/preservation checks +pass. These do not establish physical F5 in ChatGPT; the unresolved gate above +remains mandatory. [Sanitized candidate receipts](receipts/) retain both failures +and the corrected installer admission. + The retained CPU/public-file results include two resident 62.61-second trials, finishing 0.048/0.062 seconds after audio. Installed routes passed GTK4 public-file ASR; GTK3/Qt6/Brave fixtures; and Code/Newelle/Tasks lease/cancel fixtures, with diff --git a/mseries/releases/v0.0.1/receipts/activation-rc2.json b/mseries/releases/v0.0.1/receipts/activation-rc2.json new file mode 100644 index 000000000..66a9cf33d --- /dev/null +++ b/mseries/releases/v0.0.1/receipts/activation-rc2.json @@ -0,0 +1,39 @@ +{ + "schema": 1, + "observed_at_utc": "2026-10-18T19:08:38+00:00", + "complete": true, + "passed": true, + "classification": "RC2 source-only STT candidate activation/static readiness; no physical-F5 acceptance", + "source_commit": "13a8b847ef3c754cbe37fe68686463bb2ec9f269", + "archive_sha256": "d5c0e9b2b36f257edf6cba6e74bb39e5870205fa2ccb6645f39e7bff66d166a5", + "all_16_runtime_files_match": true, + "installer_and_manager_match": true, + "machine_helper_matches": true, + "transcript_only_launcher": true, + "strict_cpu_ibus_ready": true, + "unverified_fallback_disabled": true, + "test_capture_overrides_absent": true, + "input_preferences_preserved": true, + "everyday_4b_process_preserved": true, + "chatgpt_not_restarted": true, + "speech_unit_restarted_by_owned_installer": true, + "versioned_private_rollback": true, + "resources": { + "available_bytes": 13683703808, + "max_fan_rpm": 2489.0, + "max_temp_c": 42.331 + }, + "microphone_recorded": false, + "asr_model_inference": false, + "physical_f5": "still pending full ChatGPT relaunch", + "published_release": false, + "unset_debug_environment_names": [ + "STREAM_KEEP", + "STREAM_DRY", + "STREAM_TEST_WAV", + "STREAM_TEST_RAW", + "STREAM_TEST_INSERT", + "STREAM_FORCE_FALLBACK", + "MSERIES_INPUT_TEST_SESSION" + ] +} diff --git a/mseries/releases/v0.0.1/receipts/build-rc2.json b/mseries/releases/v0.0.1/receipts/build-rc2.json new file mode 100644 index 000000000..d19c2270b --- /dev/null +++ b/mseries/releases/v0.0.1/receipts/build-rc2.json @@ -0,0 +1,16 @@ +{ + "schema": 1, + "complete": true, + "passed": true, + "scope": "extracted committed RC2 read-only prerequisites; not microphone/F5 acceptance", + "source_commit": "13a8b847ef3c754cbe37fe68686463bb2ec9f269", + "archive_sha256": "d5c0e9b2b36f257edf6cba6e74bb39e5870205fa2ccb6645f39e7bff66d166a5", + "host_check_count": 25, + "package_manifest_verified": true, + "outer_checksums_verified": true, + "prior_rc1_refusal_retained": true, + "services_changed": false, + "model_inference": false, + "microphone_recorded": false, + "published": false +} From 3b859e57e8c2bcd66fef17be1d099122582e3372 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 15:11:08 -0400 Subject: [PATCH 026/798] Document approved OS input direction and portable evidence link --- mseries/releases/v0.0.1/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mseries/releases/v0.0.1/README.md b/mseries/releases/v0.0.1/README.md index 60549ff6b..bcb9286d7 100644 --- a/mseries/releases/v0.0.1/README.md +++ b/mseries/releases/v0.0.1/README.md @@ -78,6 +78,18 @@ A later retry also failed while the original process was still running without the new flags/environment. The fix has not activated. The current ChatGPT configuration remains an unresolved release gate above. +## OS integration + +F5 is handled by the desktop speech service through the existing hardware +microphone-key remap and IBus input bridge. Dictation does not require an app's +own voice feature. The owner permits an OS-level override; the approved next +step is a native Gala/session dictation broker with one shortcut owner, visible +capture state, lock-screen suppression and focused text-purpose checks. +That compositor integration is future work. The current safe route needs an +app to expose its focused editable field; use the Dictation transcript field +when an app does not. No blind keystroke injection or audio-stack change is +needed for the staged ChatGPT compatibility fix. + ## Rollback ```sh @@ -97,7 +109,7 @@ python3 -B mseries/dictation/enable-app-input.py --app chatgpt --rollback The RC2 source was deployed after all seven exact-head CI checks passed. All 25 read-only package/host checks and installed-byte/readiness/preservation checks pass. These do not establish physical F5 in ChatGPT; the unresolved gate above -remains mandatory. [Sanitized candidate receipts](receipts/) retain both failures +remains mandatory. [Sanitized candidate receipts](https://github.com/techmore/ElementaryOS_MSeries/tree/main/mseries/releases/v0.0.1/receipts) retain both failures and the corrected installer admission. The retained CPU/public-file results include two resident 62.61-second trials, From 0cc2e85137ef7f9e2c7838d989b1d46f0810c89e Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 16:41:55 -0400 Subject: [PATCH 027/798] Share eyes: move into the Quick Settings popover (top-right menu); drop the standalone indicator Co-Authored-By: Claude Sonnet 5.5 --- mseries/install-quick-settings-brightness.sh | 10 +++- .../{Indicator.vala => SharePanel.vala} | 52 +++++-------------- mseries/share-eyes-wingpanel/meson.build | 33 ++++-------- .../probe/render-probe.vala | 9 ++-- 4 files changed, 36 insertions(+), 68 deletions(-) rename mseries/share-eyes-wingpanel/{Indicator.vala => SharePanel.vala} (91%) diff --git a/mseries/install-quick-settings-brightness.sh b/mseries/install-quick-settings-brightness.sh index 485f586fb..ffc4e56fe 100755 --- a/mseries/install-quick-settings-brightness.sh +++ b/mseries/install-quick-settings-brightness.sh @@ -20,6 +20,13 @@ git -C "$work/source" apply "$repo/mseries/patches/quick-settings-brightness.pat install -m644 "$repo/mseries/quick-settings/ScreenBrightness.vala" "$work/source/src/Widgets/ScreenBrightness.vala" install -m644 "$repo/mseries/quick-settings/KeyboardBrightness.vala" "$work/source/src/Widgets/KeyboardBrightness.vala" install -m644 "$repo/mseries/quick-settings/ChargeLimit.vala" "$work/source/src/Widgets/ChargeLimit.vala" +# Screen-share section (the pair of eyes): status, watchers, links and login inside the top-right menu. +for f in ShareState ShareClient SharePanel; do install -m644 "$repo/mseries/share-eyes-wingpanel/$f.vala" "$work/source/src/Widgets/$f.vala"; done +sed -i "s| 'Widgets' / 'ChargeLimit.vala',| 'Widgets' / 'ChargeLimit.vala',\n 'Widgets' / 'ShareState.vala',\n 'Widgets' / 'ShareClient.vala',\n 'Widgets' / 'SharePanel.vala',|; s| accountsservice_dep\$| accountsservice_dep,\n dependency('json-glib-1.0'),\n dependency('libsoup-3.0')|" "$work/source/src/meson.build" +sed -i 's| main_box.append (new ChargeLimit ());| main_box.append (new ChargeLimit ());\n main_box.append (new MSeries.SharePanel ());|' "$work/source/src/PopoverWidget.vala" +grep -q SharePanel "$work/source/src/PopoverWidget.vala" && grep -q libsoup "$work/source/src/meson.build" && grep -q "'SharePanel.vala'" "$work/source/src/meson.build" \ + || { echo 'could not wire the share section into Quick Settings' >&2; exit 1; } +install -m644 "$repo/mseries/share-eyes-wingpanel/icons/eyes.svg" "$work/mseries-share-eyes-symbolic.svg" meson setup "$work/build" "$work/source" --prefix=/usr --libdir=lib/aarch64-linux-gnu --buildtype=release ninja -C "$work/build" if ldd "$work/build/src/libquick-settings.so" | grep -q 'not found'; then @@ -28,4 +35,5 @@ if ldd "$work/build/src/libquick-settings.so" | grep -q 'not found'; then fi mseries_backup "$plugin" io.elementary.quick-settings sudo install -m644 "$work/build/src/libquick-settings.so" "$plugin" -echo 'Installed keyboard brightness, screen brightness and the charge-limit switch below the gear-menu toggles (the switch needs install-power.sh for its polkit action). Reload Wingpanel to activate.' +sudo install -m644 "$work/mseries-share-eyes-symbolic.svg" /usr/share/icons/hicolor/scalable/apps/mseries-share-eyes-symbolic.svg +echo 'Installed the screen-share section (shown only while the share demo runs), keyboard brightness, screen brightness and the charge-limit switch below the gear-menu toggles (the switch needs install-power.sh for its polkit action). Reload Wingpanel to activate.' diff --git a/mseries/share-eyes-wingpanel/Indicator.vala b/mseries/share-eyes-wingpanel/SharePanel.vala similarity index 91% rename from mseries/share-eyes-wingpanel/Indicator.vala rename to mseries/share-eyes-wingpanel/SharePanel.vala index 0354b44c7..8d7b6612c 100644 --- a/mseries/share-eyes-wingpanel/Indicator.vala +++ b/mseries/share-eyes-wingpanel/SharePanel.vala @@ -1,12 +1,10 @@ -/* Screen share in Wingpanel: a pair of eyes whose popover is the whole interaction - * (login to copy, viewer link, approve or remove watchers, stop). See ShareState for the launcher contract. */ +/* Screen share section for the Quick Settings popover (top-right menu): status, approve/remove watchers, stop, + * links and login. Hidden unless the share launcher is running. See ShareState for the launcher contract. */ using Gtk; -using Wingpanel; namespace MSeries { - public class ShareEyesIndicator : Wingpanel.Indicator { + public class SharePanel : Gtk.Box { private Gtk.Image eyes; - private Gtk.Box menu; private Gtk.Box content; private ShareState state = new ShareState (); private ShareClient client = new ShareClient (); @@ -17,29 +15,26 @@ namespace MSeries { private bool busy = false; private bool details_open = false; - public ShareEyesIndicator () { - GLib.Object (code_name: "mseries-share-eyes"); - } - construct { eyes = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 18 }; eyes.add_css_class ("mseries-eyes"); eyes.set_tooltip_text (state.tooltip ()); - this.visible = false; + orientation = Gtk.Orientation.VERTICAL; + visible = false; + add_css_class ("mseries-share"); + margin_top = 6; margin_bottom = 6; margin_start = 6; margin_end = 6; content = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); install_style (content.get_display ()); - menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 0) { - margin_top = 8, margin_bottom = 8, margin_start = 8, margin_end = 8, width_request = 320 - }; - menu.add_css_class ("mseries-share"); - menu.append (content); + append (content); + // The list is rebuilt as soon as the menu is shown, and polled while it stays open. + map.connect (() => { last_digest = ""; tick.begin (); }); tick.begin (); poll_source = GLib.Timeout.add_seconds (2, () => { tick.begin (); return GLib.Source.CONTINUE; }); } - ~ShareEyesIndicator () { + ~SharePanel () { if (poll_source != 0) { GLib.Source.remove (poll_source); } @@ -57,7 +52,7 @@ namespace MSeries { text = null; } state.parse (text, GLib.get_real_time () / 1000000); - this.visible = state.running; + visible = state.running; if (state.running) { client.configure (state.local_url, state.email, state.password); yield client.refresh (); @@ -298,28 +293,5 @@ namespace MSeries { more.notify["expanded"].connect (() => { details_open = more.expanded; }); content.append (more); } - - public override Gtk.Widget get_display_widget () { - return eyes; - } - - public override Gtk.Widget? get_widget () { - return menu; - } - - public override void opened () { - last_digest = ""; - tick.begin (); - } - - public override void closed () { - } - } -} - -public Wingpanel.Indicator? get_indicator (Module module, Wingpanel.ServerType server_type) { - if (server_type != Wingpanel.ServerType.SESSION) { - return null; } - return new MSeries.ShareEyesIndicator (); } diff --git a/mseries/share-eyes-wingpanel/meson.build b/mseries/share-eyes-wingpanel/meson.build index b2e5d37e7..0446c72aa 100644 --- a/mseries/share-eyes-wingpanel/meson.build +++ b/mseries/share-eyes-wingpanel/meson.build @@ -1,34 +1,23 @@ -project('share-eyes-wingpanel', 'vala', version: '0.1.0', meson_version: '>= 0.59.0') +project('share-eyes-wingpanel', 'vala', version: '0.2.0', meson_version: '>= 0.59.0') -libwingpanel_dep = dependency('wingpanel-9') -granite_dep = dependency('granite-7', version: '>= 7.7.0') +# The share section lives inside the (patched) Quick Settings popover: install-quick-settings-brightness.sh +# copies these sources in. This project only builds and tests them standalone. json_dep = dependency('json-glib-1.0') soup_dep = dependency('libsoup-3.0') -indicators_dir = libwingpanel_dep.get_variable(pkgconfig: 'indicatorsdir') - -shared_module( - 'shareeyes', - 'Indicator.vala', - 'ShareState.vala', - 'ShareClient.vala', - dependencies: [libwingpanel_dep, granite_dep, json_dep, soup_dep], - install: true, - install_dir: indicators_dir, -) +gtk_dep = dependency('gtk4') share_state_test = executable('test-share-state', 'ShareState.vala', 'test-share-state.vala', dependencies: json_dep) test('share-state', share_state_test) +executable( + 'render-probe', + 'SharePanel.vala', 'ShareState.vala', 'ShareClient.vala', 'probe/render-probe.vala', + dependencies: [gtk_dep, json_dep, soup_dep], + build_by_default: false, +) + install_data( 'icons/eyes.svg', install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps'), rename: 'mseries-share-eyes-symbolic.svg', ) - -# Headless render probe (not installed): see probe/run-probe.sh. -executable( - 'render-probe', - 'Indicator.vala', 'ShareState.vala', 'ShareClient.vala', 'probe/render-probe.vala', - dependencies: [libwingpanel_dep, granite_dep, json_dep, soup_dep], - build_by_default: false, -) diff --git a/mseries/share-eyes-wingpanel/probe/render-probe.vala b/mseries/share-eyes-wingpanel/probe/render-probe.vala index 8310e5e80..a2cdc0872 100644 --- a/mseries/share-eyes-wingpanel/probe/render-probe.vala +++ b/mseries/share-eyes-wingpanel/probe/render-probe.vala @@ -1,17 +1,16 @@ /* Headless: shows the indicator (panel widget + popover content) and snapshots both to PNGs. args: out-prefix [create-link] */ int main (string[] args) { Gtk.init (); - var ind = new MSeries.ShareEyesIndicator (); + var ind = new MSeries.SharePanel (); var w = new Gtk.Window (); var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) { margin_start = 20, margin_end = 20, margin_top = 10, margin_bottom = 10 }; - box.append (ind.get_display_widget ()); - box.append (ind.get_widget ()); + + box.append (ind); w.child = box; w.present (); - ind.opened (); var loop = new MainLoop (); GLib.Timeout.add (4500, () => { - print ("visible=%s tooltip=%s\n", ind.visible.to_string (), ind.get_display_widget ().get_tooltip_text ()); + print ("visible=%s\n", ind.visible.to_string ()); var paintable = new Gtk.WidgetPaintable (box); var snap = new Gtk.Snapshot (); paintable.snapshot (snap, w.get_width (), w.get_height ()); From 773df674758dc6cb23552264fc03763d6dfe4fe5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 16:53:50 -0400 Subject: [PATCH 028/798] Share eyes: viewer links use /v/ Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/ShareClient.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mseries/share-eyes-wingpanel/ShareClient.vala b/mseries/share-eyes-wingpanel/ShareClient.vala index 6785fc87d..3d27ef4ea 100644 --- a/mseries/share-eyes-wingpanel/ShareClient.vala +++ b/mseries/share-eyes-wingpanel/ShareClient.vala @@ -118,7 +118,7 @@ namespace MSeries { } public string? viewer_link (string base_for_viewers) { - return link_token == null ? null : base_for_viewers + "/v#" + link_token; + return link_token == null ? null : base_for_viewers + "/v/" + link_token; } public async bool create_link () { From 5b72acd645252531048cedca689d256c13fd29cc Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 19:31:40 -0400 Subject: [PATCH 029/798] Share eyes: dock app (eyes in the dock while a share is offered, badge for waiting viewers, quits when the offer ends) Co-Authored-By: Claude Sonnet 5.5 --- mseries/install-quick-settings-brightness.sh | 8 ++++- mseries/share-eyes-wingpanel/ShareClient.vala | 4 ++- mseries/share-eyes-wingpanel/SharePanel.vala | 29 +++++++++++++++++++ mseries/share-eyes-wingpanel/meson.build | 9 ++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/mseries/install-quick-settings-brightness.sh b/mseries/install-quick-settings-brightness.sh index ffc4e56fe..ee3c788c0 100755 --- a/mseries/install-quick-settings-brightness.sh +++ b/mseries/install-quick-settings-brightness.sh @@ -23,7 +23,7 @@ install -m644 "$repo/mseries/quick-settings/ChargeLimit.vala" "$work/source/src/ # Screen-share section (the pair of eyes): status, watchers, links and login inside the top-right menu. for f in ShareState ShareClient SharePanel; do install -m644 "$repo/mseries/share-eyes-wingpanel/$f.vala" "$work/source/src/Widgets/$f.vala"; done sed -i "s| 'Widgets' / 'ChargeLimit.vala',| 'Widgets' / 'ChargeLimit.vala',\n 'Widgets' / 'ShareState.vala',\n 'Widgets' / 'ShareClient.vala',\n 'Widgets' / 'SharePanel.vala',|; s| accountsservice_dep\$| accountsservice_dep,\n dependency('json-glib-1.0'),\n dependency('libsoup-3.0')|" "$work/source/src/meson.build" -sed -i 's| main_box.append (new ChargeLimit ());| main_box.append (new ChargeLimit ());\n main_box.append (new MSeries.SharePanel ());|' "$work/source/src/PopoverWidget.vala" +sed -i 's| main_box.append (new ChargeLimit ());| main_box.append (new ChargeLimit ());\n main_box.append (new MSeries.SharePanel () { spawn_dock_app = true });|' "$work/source/src/PopoverWidget.vala" grep -q SharePanel "$work/source/src/PopoverWidget.vala" && grep -q libsoup "$work/source/src/meson.build" && grep -q "'SharePanel.vala'" "$work/source/src/meson.build" \ || { echo 'could not wire the share section into Quick Settings' >&2; exit 1; } install -m644 "$repo/mseries/share-eyes-wingpanel/icons/eyes.svg" "$work/mseries-share-eyes-symbolic.svg" @@ -35,5 +35,11 @@ if ldd "$work/build/src/libquick-settings.so" | grep -q 'not found'; then fi mseries_backup "$plugin" io.elementary.quick-settings sudo install -m644 "$work/build/src/libquick-settings.so" "$plugin" +# Dock app (eyes in the dock while a share is offered): per-user install, no sudo. +meson setup "$work/share-app" "$repo/mseries/share-eyes-wingpanel" --prefix="$HOME/.local" --buildtype=release >/dev/null +ninja -C "$work/share-app" >/dev/null +install -Dm755 "$work/share-app/mseries-share-eyes" "$HOME/.local/bin/mseries-share-eyes" +install -Dm644 "$repo/mseries/share-eyes-wingpanel/io.github.techmore.ShareEyes.desktop" "$HOME/.local/share/applications/io.github.techmore.ShareEyes.desktop" +install -Dm644 "$repo/mseries/share-eyes-wingpanel/icons/eyes.svg" "$HOME/.local/share/icons/hicolor/scalable/apps/mseries-share-eyes-symbolic.svg" sudo install -m644 "$work/mseries-share-eyes-symbolic.svg" /usr/share/icons/hicolor/scalable/apps/mseries-share-eyes-symbolic.svg echo 'Installed the screen-share section (shown only while the share demo runs), keyboard brightness, screen brightness and the charge-limit switch below the gear-menu toggles (the switch needs install-power.sh for its polkit action). Reload Wingpanel to activate.' diff --git a/mseries/share-eyes-wingpanel/ShareClient.vala b/mseries/share-eyes-wingpanel/ShareClient.vala index 3d27ef4ea..bac72fe86 100644 --- a/mseries/share-eyes-wingpanel/ShareClient.vala +++ b/mseries/share-eyes-wingpanel/ShareClient.vala @@ -15,6 +15,7 @@ namespace MSeries { public ViewerInfo[] viewers = {}; public bool live = false; public bool reachable = false; + public int link_count = 0; public string? link_id = null; public string? link_token = null; public int64 link_expires = 0; @@ -97,6 +98,7 @@ namespace MSeries { } viewers = vs; bool link_alive = false; + link_count = s.has_member ("links") ? (int) s.get_array_member ("links").get_length () : 0; if (s.has_member ("links")) { s.get_array_member ("links").foreach_element ((a, i, n) => { var o = n.get_object (); @@ -112,7 +114,7 @@ namespace MSeries { public string digest () { var sb = new StringBuilder (); - sb.append ("%s|%s|%s|".printf (reachable.to_string (), live.to_string (), link_id ?? "")); + sb.append ("%s|%s|%s|%d|".printf (reachable.to_string (), live.to_string (), link_id ?? "", link_count)); foreach (var v in viewers) sb.append ("%s:%s;".printf (v.vid, v.state)); return sb.str; } diff --git a/mseries/share-eyes-wingpanel/SharePanel.vala b/mseries/share-eyes-wingpanel/SharePanel.vala index 8d7b6612c..de5406813 100644 --- a/mseries/share-eyes-wingpanel/SharePanel.vala +++ b/mseries/share-eyes-wingpanel/SharePanel.vala @@ -14,6 +14,13 @@ namespace MSeries { private uint poll_source = 0; private bool busy = false; private bool details_open = false; + private int tick_count = 0; + private bool dock_check_busy = false; + + /* Fired after every poll: what a dock badge or a notification needs to know. offering = a link exists or a share is live. */ + public signal void status_changed (bool running, bool offering, int waiting, int watching); + /* In Quick Settings this is true: while a share is offered it starts the dock app (eyes in the dock). The app itself sets it false. */ + public bool spawn_dock_app { get; set; default = false; } construct { eyes = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 18 }; @@ -68,6 +75,9 @@ namespace MSeries { : watching == 0 ? "Sharing your screen · nobody watching yet" : "Sharing your screen · %d people watching".printf (watching)) : "Screen share ready · not sharing yet"; + bool offering = state.running && client.reachable && (client.live || client.link_count > 0); + status_changed (state.running, offering, waiting, watching); + if (spawn_dock_app && offering && tick_count++ % 3 == 0) ensure_dock_app.begin (); eyes.set_tooltip_text (tip); eyes.remove_css_class ("live"); eyes.remove_css_class ("ask"); @@ -80,6 +90,25 @@ namespace MSeries { busy = false; } + /* Starts the dock app when nothing owns its bus name. It is not started twice: a second launch would raise its window. */ + private async void ensure_dock_app () { + if (dock_check_busy) return; + dock_check_busy = true; + try { + var bus = yield GLib.Bus.get (GLib.BusType.SESSION); + var r = yield bus.call ("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameHasOwner", + new GLib.Variant ("(s)", "io.github.techmore.ShareEyes"), new GLib.VariantType ("(b)"), GLib.DBusCallFlags.NONE, 2000); + bool owned; + r.get ("(b)", out owned); + if (!owned) { + new GLib.Subprocess.newv ({ "mseries-share-eyes" }, GLib.SubprocessFlags.STDOUT_SILENCE | GLib.SubprocessFlags.STDERR_SILENCE); + } + } catch (GLib.Error e) { + warning ("share dock app: %s", e.message); + } + dock_check_busy = false; + } + private void copy (string text, string what) { eyes.get_clipboard ().set_text (text); toast = what + " copied"; diff --git a/mseries/share-eyes-wingpanel/meson.build b/mseries/share-eyes-wingpanel/meson.build index 0446c72aa..8ba6a2518 100644 --- a/mseries/share-eyes-wingpanel/meson.build +++ b/mseries/share-eyes-wingpanel/meson.build @@ -21,3 +21,12 @@ install_data( install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps'), rename: 'mseries-share-eyes-symbolic.svg', ) + +executable( + 'mseries-share-eyes', + 'app/ShareApp.vala', 'SharePanel.vala', 'ShareState.vala', 'ShareClient.vala', + dependencies: [gtk_dep, json_dep, soup_dep], + install: true, + install_dir: join_paths(get_option('prefix'), 'bin'), +) +install_data('io.github.techmore.ShareEyes.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) From ae3d75383d46d124476e91efccbead4afffc7c22 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 19:32:42 -0400 Subject: [PATCH 030/798] Share eyes: Internet and Local network viewer links in the menu Co-Authored-By: Claude Sonnet 5.5 --- mseries/share-eyes-wingpanel/SharePanel.vala | 20 +++++++++++------- mseries/share-eyes-wingpanel/ShareState.vala | 21 ++++++++++++++++--- .../test-share-state.vala | 6 ++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/mseries/share-eyes-wingpanel/SharePanel.vala b/mseries/share-eyes-wingpanel/SharePanel.vala index de5406813..37b36de22 100644 --- a/mseries/share-eyes-wingpanel/SharePanel.vala +++ b/mseries/share-eyes-wingpanel/SharePanel.vala @@ -295,13 +295,19 @@ namespace MSeries { // Links: one flat menu-style item each, so the card reads as a short list. var links = card (null); - var link = client.viewer_link (state.viewer_base); - if (link != null) { - var cv = item ("insert-link-symbolic", "Copy viewer link", - "until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%H:%M"), "Copy the link for your viewer"); - cv.clicked.connect (() => copy (link, "Viewer link")); - links.append (cv); - } else { + var bases = state.link_bases (); + for (int bi = 0; bi < bases.length[0]; bi++) { + var label = bases[bi, 0]; + var origin = bases[bi, 1]; + var link = client.viewer_link (origin); + if (link != null) { + var cv = item ("insert-link-symbolic", "Copy %s link".printf (label.down ()), + bi == 0 ? "until " + new GLib.DateTime.from_unix_local (client.link_expires).format ("%H:%M") : null, "Copy the " + label.down () + " link for your viewer"); + cv.clicked.connect (() => copy (link, label + " link")); + links.append (cv); + } + } + if (client.viewer_link (state.viewer_base) == null) { var mk = item ("list-add-symbolic", "Create viewer link", null, "Make a link to send to your viewer"); mk.clicked.connect (() => { client.create_link.begin ((o, r) => { client.create_link.end (r); act (); }); }); links.append (mk); diff --git a/mseries/share-eyes-wingpanel/ShareState.vala b/mseries/share-eyes-wingpanel/ShareState.vala index 528b3d673..26e201fd8 100644 --- a/mseries/share-eyes-wingpanel/ShareState.vala +++ b/mseries/share-eyes-wingpanel/ShareState.vala @@ -12,6 +12,7 @@ namespace MSeries { public string email { get; private set; default = ""; } public string password { get; private set; default = ""; } public string public_url { get; private set; default = ""; } + public string lan_url { get; private set; default = ""; } public static string default_path () { return GLib.Path.build_filename (GLib.Environment.get_user_runtime_dir (), "rupert-share", "state.json"); @@ -21,11 +22,24 @@ namespace MSeries { public string share_page { owned get { return local_url + "/share"; } } public string viewer_base { owned get { return public_url != "" ? public_url : local_url; } } + /* The addresses a viewer can use: (label, base). Internet first when one is configured (https), then the local network. */ + public string[,] link_bases () { + bool internet = public_url.has_prefix ("https://"); + string local = internet ? (lan_url != "" ? lan_url : "") : (public_url != "" ? public_url : local_url); + if (internet && local != "") { + return { { "Internet", public_url }, { "Local network", local } }; + } + if (internet) { + return { { "Internet", public_url } }; + } + return { { "Local network", local } }; + } + /* Returns true when anything the indicator shows changed. */ public bool parse (string? text, int64 now) { bool n_running = false, n_live = false; int n_viewers = 0, n_port = 0; - string n_email = "", n_password = "", n_public = ""; + string n_email = "", n_password = "", n_public = "", n_lan = ""; if (text != null) { try { var parser = new Json.Parser (); @@ -44,6 +58,7 @@ namespace MSeries { if (o.has_member ("email")) n_email = o.get_string_member ("email"); if (o.has_member ("password")) n_password = o.get_string_member ("password"); if (o.has_member ("public_url")) n_public = o.get_string_member ("public_url"); + if (o.has_member ("lan_url")) n_lan = o.get_string_member ("lan_url"); } } } catch (GLib.Error e) { @@ -51,9 +66,9 @@ namespace MSeries { } } bool changed = n_running != running || n_live != live || n_viewers != viewers || n_port != port - || n_email != email || n_password != password || n_public != public_url; + || n_email != email || n_password != password || n_public != public_url || n_lan != lan_url; running = n_running; live = n_live; viewers = n_viewers; port = n_port; - email = n_email; password = n_password; public_url = n_public; + email = n_email; password = n_password; public_url = n_public; lan_url = n_lan; return changed; } diff --git a/mseries/share-eyes-wingpanel/test-share-state.vala b/mseries/share-eyes-wingpanel/test-share-state.vala index e10c19a65..800ec97fc 100644 --- a/mseries/share-eyes-wingpanel/test-share-state.vala +++ b/mseries/share-eyes-wingpanel/test-share-state.vala @@ -16,5 +16,11 @@ void main () { assert (s.tooltip () == "Sharing your screen · 1 person watching"); s.parse ("{\"live\":true,\"viewers\":0,\"updated\":1000,\"port\":1}", now); assert (s.tooltip ().contains ("nobody")); + s.parse ("{\"live\":true,\"viewers\":0,\"updated\":1000,\"port\":1,\"public_url\":\"https://tm-remote.stoverparc.org\",\"lan_url\":\"http://192.168.1.5:8799\"}", now); + var b = s.link_bases (); + assert (b.length[0] == 2 && b[0, 0] == "Internet" && b[0, 1] == "https://tm-remote.stoverparc.org" && b[1, 0] == "Local network" && b[1, 1] == "http://192.168.1.5:8799"); + s.parse ("{\"live\":true,\"viewers\":0,\"updated\":1000,\"port\":1,\"public_url\":\"http://192.168.1.5:8799\"}", now); + b = s.link_bases (); + assert (b.length[0] == 1 && b[0, 0] == "Local network" && b[0, 1] == "http://192.168.1.5:8799"); print ("share-state: ok\n"); } From 313377f113b4e463e6e24a3b828bd1cb668029a7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 19:35:19 -0400 Subject: [PATCH 031/798] Share eyes: desktop notification when a viewer starts waiting (tested on a private bus; found and fixed a malformed variant) Co-Authored-By: Claude Sonnet 5.5 --- mseries/install-quick-settings-brightness.sh | 2 +- mseries/share-eyes-wingpanel/SharePanel.vala | 26 +++++++++++++++++++ .../probe/render-probe.vala | 4 +-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/mseries/install-quick-settings-brightness.sh b/mseries/install-quick-settings-brightness.sh index ee3c788c0..d67e42bc5 100755 --- a/mseries/install-quick-settings-brightness.sh +++ b/mseries/install-quick-settings-brightness.sh @@ -23,7 +23,7 @@ install -m644 "$repo/mseries/quick-settings/ChargeLimit.vala" "$work/source/src/ # Screen-share section (the pair of eyes): status, watchers, links and login inside the top-right menu. for f in ShareState ShareClient SharePanel; do install -m644 "$repo/mseries/share-eyes-wingpanel/$f.vala" "$work/source/src/Widgets/$f.vala"; done sed -i "s| 'Widgets' / 'ChargeLimit.vala',| 'Widgets' / 'ChargeLimit.vala',\n 'Widgets' / 'ShareState.vala',\n 'Widgets' / 'ShareClient.vala',\n 'Widgets' / 'SharePanel.vala',|; s| accountsservice_dep\$| accountsservice_dep,\n dependency('json-glib-1.0'),\n dependency('libsoup-3.0')|" "$work/source/src/meson.build" -sed -i 's| main_box.append (new ChargeLimit ());| main_box.append (new ChargeLimit ());\n main_box.append (new MSeries.SharePanel () { spawn_dock_app = true });|' "$work/source/src/PopoverWidget.vala" +sed -i 's| main_box.append (new ChargeLimit ());| main_box.append (new ChargeLimit ());\n main_box.append (new MSeries.SharePanel () { spawn_dock_app = true, notify_waiting = true });|' "$work/source/src/PopoverWidget.vala" grep -q SharePanel "$work/source/src/PopoverWidget.vala" && grep -q libsoup "$work/source/src/meson.build" && grep -q "'SharePanel.vala'" "$work/source/src/meson.build" \ || { echo 'could not wire the share section into Quick Settings' >&2; exit 1; } install -m644 "$repo/mseries/share-eyes-wingpanel/icons/eyes.svg" "$work/mseries-share-eyes-symbolic.svg" diff --git a/mseries/share-eyes-wingpanel/SharePanel.vala b/mseries/share-eyes-wingpanel/SharePanel.vala index 37b36de22..46325ef1c 100644 --- a/mseries/share-eyes-wingpanel/SharePanel.vala +++ b/mseries/share-eyes-wingpanel/SharePanel.vala @@ -21,6 +21,10 @@ namespace MSeries { public signal void status_changed (bool running, bool offering, int waiting, int watching); /* In Quick Settings this is true: while a share is offered it starts the dock app (eyes in the dock). The app itself sets it false. */ public bool spawn_dock_app { get; set; default = false; } + /* In Quick Settings: a desktop notification when a viewer starts waiting, so it is noticed without opening the menu. */ + public bool notify_waiting { get; set; default = false; } + private int last_waiting = 0; + private uint32 notification_id = 0; construct { eyes = new Gtk.Image.from_icon_name ("mseries-share-eyes-symbolic") { pixel_size = 18 }; @@ -77,6 +81,8 @@ namespace MSeries { : "Screen share ready · not sharing yet"; bool offering = state.running && client.reachable && (client.live || client.link_count > 0); status_changed (state.running, offering, waiting, watching); + if (notify_waiting && waiting > last_waiting) notify_viewer.begin (); + last_waiting = waiting; if (spawn_dock_app && offering && tick_count++ % 3 == 0) ensure_dock_app.begin (); eyes.set_tooltip_text (tip); eyes.remove_css_class ("live"); @@ -90,6 +96,26 @@ namespace MSeries { busy = false; } + private async void notify_viewer () { + try { + var bus = yield GLib.Bus.get (GLib.BusType.SESSION); + var args = new GLib.VariantBuilder (new GLib.VariantType ("(susssasa{sv}i)")); + args.add ("s", "Screen Share"); + args.add ("u", notification_id); + args.add ("s", "mseries-share-eyes-symbolic"); + args.add ("s", "Someone wants to watch your screen"); + args.add ("s", "Open the eyes menu to allow or deny."); + args.add_value (new GLib.Variant.array (new GLib.VariantType ("s"), {})); + args.add_value (new GLib.Variant.array (new GLib.VariantType ("{sv}"), {})); + args.add ("i", 15000); + var r = yield bus.call ("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "Notify", + args.end (), new GLib.VariantType ("(u)"), GLib.DBusCallFlags.NONE, 2000); + r.get ("(u)", out notification_id); + } catch (GLib.Error e) { + warning ("notification: %s", e.message); + } + } + /* Starts the dock app when nothing owns its bus name. It is not started twice: a second launch would raise its window. */ private async void ensure_dock_app () { if (dock_check_busy) return; diff --git a/mseries/share-eyes-wingpanel/probe/render-probe.vala b/mseries/share-eyes-wingpanel/probe/render-probe.vala index a2cdc0872..64eed2454 100644 --- a/mseries/share-eyes-wingpanel/probe/render-probe.vala +++ b/mseries/share-eyes-wingpanel/probe/render-probe.vala @@ -1,7 +1,7 @@ /* Headless: shows the indicator (panel widget + popover content) and snapshots both to PNGs. args: out-prefix [create-link] */ int main (string[] args) { Gtk.init (); - var ind = new MSeries.SharePanel (); + var ind = new MSeries.SharePanel () { notify_waiting = GLib.Environment.get_variable ("SHARE_NOTIFY") == "1" }; var w = new Gtk.Window (); var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12) { margin_start = 20, margin_end = 20, margin_top = 10, margin_bottom = 10 }; @@ -9,7 +9,7 @@ int main (string[] args) { w.child = box; w.present (); var loop = new MainLoop (); - GLib.Timeout.add (4500, () => { + GLib.Timeout.add (int.parse (GLib.Environment.get_variable ("PROBE_MS") ?? "4500"), () => { print ("visible=%s\n", ind.visible.to_string ()); var paintable = new Gtk.WidgetPaintable (box); var snap = new Gtk.Snapshot (); From 30b77caa852fe8c9e35a425fd8064936b5058ba0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 21:42:52 -0400 Subject: [PATCH 032/798] RepoBar: readable worktree names in Checkouts; Pinned/Recently updated dashboard headings Co-Authored-By: Claude Opus 5.5 --- mseries/OS9-HANDOFF.md | 8 +++++ mseries/REPOBAR-PORT.md | 3 ++ mseries/repobar-wingpanel/Indicator.vala | 43 +++++++++++++++++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e06d41064..4b7fcf5c5 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,13 @@ # OS 9 implementation / Claude Code handoff +## RepoBar checkout names and dashboard groups — 2026-10-18 + +Details → Checkouts drops the repository-name prefix from worktree folders +(`ElementaryOS_MSeries-repobar-ui` shows as `repobar-ui`, the plain clone as +"main checkout"); the full path and branch are in the tooltip. When pins exist, +the dashboard labels its two groups "Pinned" and "Recently updated". Verified by +headless renders (real snapshot, light; fixture with a pin, dark) only. + ## RepoBar compact contribution graph — 2026-10-18 The contribution card now starts collapsed to a single line: one accent-toned diff --git a/mseries/REPOBAR-PORT.md b/mseries/REPOBAR-PORT.md index 054fc69e5..f24a3155d 100644 --- a/mseries/REPOBAR-PORT.md +++ b/mseries/REPOBAR-PORT.md @@ -31,6 +31,9 @@ implementation; no macOS source or binary is copied into this project. - With a projects folder set, cards show every local checkout and worktree (Details → Checkouts) with branch and sync state, open them in Files or Terminal, and offer Clone for repositories you have not cloned yet. +- With pins, the dashboard shows them under a "Pinned" heading above + "Recently updated". Checkout lists show worktree names without the + repository prefix. - A contribution graph sits at the top of the dashboard. It starts collapsed to one line (a toned mark per week plus this week's count); click its heading for GitHub's day-by-day calendar (remembered). RepoBar Settings picks the style: diff --git a/mseries/repobar-wingpanel/Indicator.vala b/mseries/repobar-wingpanel/Indicator.vala index ee7a6dfc6..490981b53 100644 --- a/mseries/repobar-wingpanel/Indicator.vala +++ b/mseries/repobar-wingpanel/Indicator.vala @@ -261,6 +261,7 @@ namespace MSeries { ".mseries-repobar .detail-list { margin-top: 2px; padding-top: 4px; border-top: 1px solid alpha(currentColor, 0.1); } " + ".mseries-repobar .pinned-star { color: #d4a300; } " + ".mseries-repobar .section-title { font-weight: 700; font-size: 8pt; opacity: 0.75; } " + + ".mseries-repobar list.repo-list > row .group-heading { margin-top: 2px; margin-bottom: -4px; } " + ".mseries-repobar .for-you { background: alpha(@accent_color, 0.08); } " + ".mseries-repobar .contribution-graph { color: @accent_color; } " + ".mseries-repobar button.site-link { opacity: 0.6; padding: 0 2px; } " + @@ -551,8 +552,22 @@ namespace MSeries { if (scope == 2) visible.sort_with_data (compare_severity); else if (scope != 0 && sort_order != "activity") visible.sort_with_data (compare_repos); int shown = (int) visible.length; + // Dashboard with pins: label the two groups so pinned repositories read as a deliberate list. + int pinned_shown = 0; + if (scope == 0) { + for (int i = 0; i < shown; i++) { + if (visible[i].get_string_member_with_default ("repoState", "visible") == "pinned") pinned_shown++; + } + } + bool grouped = pinned_shown > 0 && pinned_shown < shown; for (int i = 0; i < shown; i++) { var repo = visible[i]; + if (grouped && (i == 0 || i == pinned_shown)) { + var heading = new Gtk.Label (i == 0 ? "Pinned" : "Recently updated") { xalign = 0, margin_start = 4 }; + heading.add_css_class ("section-title"); + heading.add_css_class ("group-heading"); + repo_list.append (new Gtk.ListBoxRow () { child = heading, activatable = false, focusable = false }); + } var row = new Gtk.ListBoxRow () { child = build_card (repo) }; row.set_data ("url", repo.get_string_member_with_default ("url", "")); row.update_property (Gtk.AccessibleProperty.LABEL, repo.get_string_member_with_default ("name", ""), -1); @@ -1171,7 +1186,21 @@ namespace MSeries { } /* One local checkout: folder name and branch, its state, and a terminal button; the row opens the folder. */ - private static Gtk.Widget checkout_row (Json.Object checkout) { + /* Worktrees are usually named "-"; show just the topic so the names fit and stay distinct. */ + private static string checkout_label (string path, string repo_name) { + string folder = GLib.Path.get_basename (path); + string repo = repo_name.contains ("/") ? repo_name.substring (repo_name.last_index_of ("/") + 1) : repo_name; + string lower = folder.casefold (); + string prefix = repo.casefold (); + if (lower == prefix) return "main checkout"; + if (lower.has_prefix (prefix) && folder.length > repo.length + 1) { + unichar separator = folder.get_char (repo.length); + if (separator == '-' || separator == '_' || separator == '.') return folder.substring (repo.length + 1); + } + return folder; + } + + private static Gtk.Widget checkout_row (Json.Object checkout, string repo_name) { string path = checkout.get_string_member_with_default ("path", ""); int dirty = (int) checkout.get_int_member_with_default ("dirtyFileCount", 0); int behind = (int) checkout.get_int_member_with_default ("behind", 0); @@ -1179,7 +1208,11 @@ namespace MSeries { var line = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 4); var content = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); content.append (new Gtk.Image.from_icon_name ("folder-symbolic") { pixel_size = 12, opacity = 0.7 }); - var folder = new Gtk.Label (GLib.Path.get_basename (path)) { xalign = 0, ellipsize = Pango.EllipsizeMode.MIDDLE, max_width_chars = 22 }; + string label = checkout_label (path, repo_name); + var folder = new Gtk.Label (label) { + xalign = 0, ellipsize = Pango.EllipsizeMode.END, max_width_chars = 18, width_chars = 12 + }; + if (label == "main checkout") folder.add_css_class ("dim-label"); folder.add_css_class ("caption"); content.append (folder); string branch_name = checkout.get_boolean_member_with_default ("detached", false) ? "detached" : @@ -1192,7 +1225,9 @@ namespace MSeries { if (behind > 0) content.append (status_text ("↓%d".printf (behind), "text-low")); if (ahead > 0) content.append (status_text ("↑%d".printf (ahead), "dim-label")); if (dirty == 0 && behind == 0 && ahead == 0) content.append (status_text ("clean", "dim-label")); - var open = new Gtk.Button () { child = content, hexpand = true, tooltip_text = "Open %s in Files".printf (path) }; + var open = new Gtk.Button () { + child = content, hexpand = true, tooltip_text = "%s\n%s\nOpen in Files".printf (path, branch_name) + }; open.add_css_class ("flat"); open.add_css_class ("link-row"); open.clicked.connect (() => open_folder (path, open)); @@ -1346,7 +1381,7 @@ namespace MSeries { if (!chip.get_active ()) return; list.set_visible (true); clear_box (list); - for (uint i = 0; i < checkouts.get_length (); i++) list.append (checkout_row (checkouts.get_object_element (i))); + for (uint i = 0; i < checkouts.get_length (); i++) list.append (checkout_row (checkouts.get_object_element (i), name)); }); chips.append (chip); } From 85c19e8632570c81ebf3294f9eaee553bf254a9e Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:04:36 -0400 Subject: [PATCH 033/798] Branch model: dev is default and what the owner runs; main is frozen stable Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 ++- mseries/BRANCHING.md | 8 ++++---- mseries/OS9-HANDOFF.md | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4ad4b11c8..a01dd381b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,8 @@ status pages current as work lands. Record concrete changes, commands, verification results, blockers, and the next task. - Keep the owner’s menu bar stable: stage routine indicator updates without restarting Wingpanel. Reload only when the owner explicitly requests it or to recover from a confirmed panel defect. -- **Branches:** `main` is stable (the v0.0.1 line); `dev` is integration. Branch from +- **Branches:** `dev` is the default branch and what the owner runs; `main` is the + frozen stable line (v0.0.1). Branch from `origin/dev` and open PRs with `--base dev`; only release PRs (`dev` → `main`) and urgent hotfixes go to `main`. See [mseries/BRANCHING.md](mseries/BRANCHING.md). - Commit and push completed, reviewed batches to GitHub. Use one agent for diff --git a/mseries/BRANCHING.md b/mseries/BRANCHING.md index 352049088..da465ee38 100644 --- a/mseries/BRANCHING.md +++ b/mseries/BRANCHING.md @@ -4,8 +4,8 @@ Two long-lived branches: | Branch | Role | Who merges into it | | --- | --- | --- | -| `main` | **Stable.** For now `main` is the `v0.0.1` stable line: what the owner runs and what release builds are cut from. | Only release PRs from `dev`, plus urgent hotfixes. | -| `dev` | **Integration.** Every finished feature/fix branch lands here first. | Feature PRs (`claude/*`, `codex/*`, `deepseek/*`, …). | +| `dev` | **Default branch, what the owner runs.** Every finished feature/fix branch lands here. | Feature PRs (`claude/*`, `codex/*`, `deepseek/*`, …). | +| `main` | **Stable, effectively frozen.** The `v0.0.1` line: a known-good snapshot that release builds are cut from. It moves only on a deliberate release. | Only release PRs from `dev`, plus urgent hotfixes. | ## Day-to-day @@ -17,8 +17,8 @@ Two long-lived branches: ## Promoting to stable -When `dev` has a batch that has been running on the owner's machine without -regressions, open a release PR `dev` → `main` titled `Release v0.0.N`, list what +`main` stays frozen between releases. When `dev` has been running on the owner's +machine without regressions and the owner asks for a release, open a release PR `dev` → `main` titled `Release v0.0.N`, list what changed and what was verified (and what was not), merge it with a merge commit, then tag `main`: `git tag -a v0.0.N -m "v0.0.N" origin/main && git push origin v0.0.N`. diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 68f58a97b..9c39ac8cc 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -4,7 +4,8 @@ `main` is now the stable (v0.0.1) line and `dev` (cut from `main` after this lands) is where feature PRs merge. Release PRs `dev` → `main` promote batches and get a -`v0.0.N` tag. `proposed-9.0.yml` now also runs on PRs to `dev`. Rules in +`v0.0.N` tag. `dev` is the GitHub default branch and is what the owner runs; +`main` is effectively frozen between releases. `proposed-9.0.yml` now also runs on PRs to `dev`. Rules in `mseries/BRANCHING.md`; AGENTS.md points to it. ## RepoBar compact contribution graph — 2026-10-18 From 1091c1031f76b8f3c85da5f355914e1ed52f9bc9 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:12:19 -0400 Subject: [PATCH 034/798] Start nested Rust compositor prototype for Pantheon --- mseries/OS9-HANDOFF.md | 31 + mseries/ROADMAP.md | 17 + mseries/rust-mutter/README.md | 72 + rust/Cargo.lock | 2159 +++++++++++++++++ rust/Cargo.toml | 2 +- rust/pantheon-compositor/Cargo.toml | 20 + rust/pantheon-compositor/README.md | 35 + rust/pantheon-compositor/SMITHAY-LICENSE.txt | 21 + rust/pantheon-compositor/THIRD-PARTY.md | 5 + rust/pantheon-compositor/src/grabs/mod.rs | 5 + .../src/grabs/move_grab.rs | 160 ++ .../src/grabs/resize_grab.rs | 365 +++ .../src/handlers/compositor.rs | 57 + rust/pantheon-compositor/src/handlers/mod.rs | 90 + .../src/handlers/xdg_shell.rs | 214 ++ rust/pantheon-compositor/src/input.rs | 144 ++ rust/pantheon-compositor/src/main.rs | 58 + rust/pantheon-compositor/src/state.rs | 168 ++ rust/pantheon-compositor/src/winit.rs | 110 + 19 files changed, 3732 insertions(+), 1 deletion(-) create mode 100644 mseries/rust-mutter/README.md create mode 100644 rust/pantheon-compositor/Cargo.toml create mode 100644 rust/pantheon-compositor/README.md create mode 100644 rust/pantheon-compositor/SMITHAY-LICENSE.txt create mode 100644 rust/pantheon-compositor/THIRD-PARTY.md create mode 100644 rust/pantheon-compositor/src/grabs/mod.rs create mode 100644 rust/pantheon-compositor/src/grabs/move_grab.rs create mode 100644 rust/pantheon-compositor/src/grabs/resize_grab.rs create mode 100644 rust/pantheon-compositor/src/handlers/compositor.rs create mode 100644 rust/pantheon-compositor/src/handlers/mod.rs create mode 100644 rust/pantheon-compositor/src/handlers/xdg_shell.rs create mode 100644 rust/pantheon-compositor/src/input.rs create mode 100644 rust/pantheon-compositor/src/main.rs create mode 100644 rust/pantheon-compositor/src/state.rs create mode 100644 rust/pantheon-compositor/src/winit.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e631c94e2..466bbc5ed 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5271,3 +5271,34 @@ Fresh physical-F5/microphone, authenticated Gmail, natural/full-duplex voice, Calendar GUI/reminders, indefinite soak, power/battery and factory boot remain separate work. Six-hour developer tracking remains the existing workflow; the old completion heartbeat is already PAUSED. No duplicate automation was created. + +## Rust Mutter replacement for Pantheon — 2026-10-18 + +Started branch `codex/rust-mutter-pantheon` for a Rust compositor intended to +replace the Gala/Mutter display-server role in the Pantheon session. The source +audit found Gala 8.6.2 directly uses Mutter's `Meta` objects and Clutter/Cogl/Mtk +APIs and is itself started as the Wayland display server by its session unit. +Therefore, serving Wayland clients alone is not a swap-in; Gala's dependency +boundary and the complete Pantheon session contract need to move as well. +Recorded nested-compositor, shell-contract, hardware/input, parallel-session, +and swap-in gates in `mseries/rust-mutter/README.md`. + +Public source check found no first-party Rust Mutter/Gala replacement to reuse: +elementary's public [Gala repository](https://github.com/elementary/gala) remains +the libmutter-based Vala implementation. Independent Smithay projects are useful +references, but they do not provide Pantheon session compatibility. + +Added `rust/pantheon-compositor`, an opt-in nested prototype based on Smithay's +smallvil at pinned revision `023b78b1c5a05ca512767cacc07efca55fae8be4`. Its +workspace manifest and lockfile are wired; it uses Smithay's Winit backend, +needs an existing X11/Wayland session, and can launch a requested Wayland client +on its nested display. It does not open DRM/KMS or alter the session. It has +not been built or tested. + +No compositor has been installed and no desktop configuration has changed. No +active desktop, kernel, bootloader, or audio-stack changes were made. The +WD22TB4 HDMI G3 link-training blocker remains on the DCP/kernel path; it must be +tracked separately from Rust compositor output and hotplug qualification. +Next: compile and run the nested prototype with a Wayland client, then inventory +the Gala/Pantheon shell contract for the next implementation milestone. Do not +switch the login session at this stage. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index e7e2a7dc9..c37b342b7 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,5 +1,22 @@ # Roadmap, features and follow-ups (MacBook Pro 14" M1 Pro, elementary OS 9 on Asahi) +## Rust Mutter replacement for Pantheon — started 2026-10-18 + +Work begins on branch `codex/rust-mutter-pantheon`. Gala is the Wayland +display server and directly depends on Mutter `Meta`, Clutter, Cogl, and Mtk; +the replacement needs to cover the Pantheon session contract, not just draw +Wayland client surfaces. The source audit and staged acceptance gates are in +[`rust-mutter/README.md`](rust-mutter/README.md). First implementation gate: +an opt-in nested Smithay compositor that can be exercised without replacing +the active desktop. Prototype code is in `rust/pantheon-compositor/`, pinned to +a Smithay revision, and has not been compiled or hardware qualified. A public +search found the first-party [Gala repository](https://github.com/elementary/gala) +still based on libmutter, but no first-party Rust port to reuse. Independent +Smithay compositors are references, not Pantheon replacements. The WD22TB4 +dock's G3 link-training blocker remains in the kernel/DCP workstream and is not +a compositor acceptance result. + + ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 Removed “local” from the seven-day panel/card labels and moved counting scope into diff --git a/mseries/rust-mutter/README.md b/mseries/rust-mutter/README.md new file mode 100644 index 000000000..ebc4141d1 --- /dev/null +++ b/mseries/rust-mutter/README.md @@ -0,0 +1,72 @@ +# Rust Mutter for Pantheon + +This branch starts the work toward a Rust compositor for the elementary OS +Pantheon stack. The target is a session-level replacement for Gala/Mutter that +can be enabled and rolled back as one package. It is not a dock-firmware fix. + +## Current boundary + +Gala is both Pantheon's window manager and the Wayland display server. In the +current Gala 8.6.2 source it is compiled against `libmutter-14` through +`libmutter-18`, and directly uses Mutter's `Meta` types, Clutter actors, Cogl, +and Mtk geometry. Its Wayland session unit starts `gala` as the display server +and treats a failure as a session failure. Replacing `gala` with an unrelated +compositor would therefore break the session even if ordinary Wayland clients +could still render. + +The first milestone is a Rust compositor that can run nested and prove its +Wayland client, rendering, focus, input, and output contracts without taking +over the user's desktop. After that, move Pantheon window-management and shell +behavior behind Wayland protocols or a small, versioned Pantheon control +protocol. Do not promise a drop-in replacement until Gala's Mutter/Clutter +dependencies have been removed or an explicitly maintained compatibility +layer covers every API Gala uses. + +## Delivery gates + +1. **Nested compositor:** Rust process starts as a nested Wayland client, serves + a second Wayland display, presents a client, and exits without changing + the host session. The first prototype now lives in + [`rust/pantheon-compositor`](../../rust/pantheon-compositor/README.md). It is + based on Smithay's smallvil at pinned revision + `023b78b1c5a05ca512767cacc07efca55fae8be4`. It has not been built or + hardware-qualified yet. +2. **Pantheon shell contract:** enumerate Gala and Pantheon consumers, then + implement window activation, close/minimize/maximize, workspaces, placement, + panel/dock reservations, shell hotkeys, lock-screen handoff, and session + lifecycle. Define only the compatibility protocols actually required by + this stack. +3. **Desktop hardware and input:** DRM/KMS outputs, multi-monitor changes, + libinput, keyboard layouts, accessibility, IBus/input-method protocols, + screencast/remote-desktop portals, clipboard, drag-and-drop, and Xwayland. + Exercise these on supported M-series hardware before changing the default. +4. **Parallel session qualification:** start the new compositor from an + opt-in session entry with the existing Gala session intact as rollback. + Qualify login/logout, apps, panel, dock, settings, lock/unlock, suspend/resume, + hotplug, screen sharing, accessibility, input methods, and crash recovery. +5. **Swap-in:** only after the compatibility matrix is green, package a + reversible session change and collect owner acceptance on hardware. Keep + Gala available until the new session has passed that acceptance. + +## Hardware blocker separation + +The WD22TB4 HDMI goal is tracked in +[`../fairydust/usb4/PRIMARY-GOAL.md`](../fairydust/usb4/PRIMARY-GOAL.md). Its +current G3 gate is DCP link training. A Rust compositor may eventually need to +handle output hotplug and modesets correctly, but it cannot make firmware train +a link that the kernel/DCP path has not trained. Track compositor output +qualification separately from G3 through G5. + +## Source audit + +The initial interface audit used the repository's pinned Gala source at +`elementary/gala` commit `f0791434` (Gala 8.6.2). Its build requires Mutter 46 +through 50 (`libmutter-14` through `libmutter-18`) and corresponding Mutter +Clutter/Cogl/Mtk libraries. Gala's `WindowManager` implementation subclasses +`Meta.Plugin`, manages `Meta.Window`/`Meta.Workspace`, and builds its shell UI +from Clutter actors. Its Wayland desktop entry starts `gala --wayland +--display-server`; the systemd user unit also owns display-server startup and +session-failure handling. + +This branch has not changed the active desktop, kernel, bootloader, or audio +stack. Nothing here claims a running or hardware-qualified replacement yet. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5b0fa0c31..5b299e2d1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,6 +2,2165 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "appendlist" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags", + "polling", + "rustix", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.33.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags", + "serde", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "mseries-power" version = "0.1.0" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f140a738f180203d7912dfd369936fff2fb0ad6f426c0aa007ed3e2b443e6b" +dependencies = [ + "libredox", +] + +[[package]] +name = "pantheon-compositor" +version = "0.1.0" +dependencies = [ + "bitflags", + "smithay", + "tracing-subscriber", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_event" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5018d583d6d2f5499352aea8d177e9067d1eb03ab17c78169d5ba7a30001b15" +dependencies = [ + "bitflags", + "libredox", +] + +[[package]] +name = "redox_syscall" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reis" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b967ec6489a42067a20724f11987bb96178ea796873cbe1afd25fb93ee6f85f" +dependencies = [ + "calloop", + "enumflags2", + "log", + "rustix", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "smithay" +version = "0.7.0" +source = "git+https://github.com/Smithay/smithay.git?rev=023b78b1c5a05ca512767cacc07efca55fae8be4#023b78b1c5a05ca512767cacc07efca55fae8be4" +dependencies = [ + "appendlist", + "bitflags", + "calloop", + "cursor-icon", + "downcast-rs", + "drm-fourcc", + "errno", + "gl_generator", + "glam", + "indexmap", + "libc", + "libloading", + "portable-atomic", + "profiling", + "rand", + "reis", + "rustix", + "siphasher", + "smallvec", + "tempfile", + "thiserror 2.0.21", + "tracing", + "wayland-client", + "wayland-cursor", + "wayland-egl", + "wayland-protocols", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-server", + "winit", + "xkbcommon", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dc9ee14b0fdcb535f9556141bacac070c994a977d2240ee455d7438a617f00" +dependencies = [ + "bitflags", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "log", + "memmap2", + "rustix", + "thiserror 2.0.21", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-egl" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b97bdb7c49e5bd9b7562f38ff84f0dad47079fdc9e926f691a787f6dbc05451" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20251230.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c6c3e7178e553d093d46999032bc79aafea9dedd71dd702c8ace7bbc25cf13" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde9c29be0f723a573977de51ee455bf3dfa03652730a74f9dd3b337e374d75" +dependencies = [ + "bitflags", + "downcast-rs", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winit" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f431156a3e4f0f1b35eaaf37674e55838e3652908c8088f570466274d1902e2b" +dependencies = [ + "bitflags", + "cfg_aliases", + "cursor-icon", + "dpi", + "libc", + "raw-window-handle", + "rustix", + "smol_str", + "tracing", + "winit-android", + "winit-appkit", + "winit-common", + "winit-core", + "winit-orbital", + "winit-uikit", + "winit-wayland", + "winit-web", + "winit-win32", + "winit-x11", +] + +[[package]] +name = "winit-android" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d60d944a564986b9311e325334cc22e7e4cf726dd0ff7475fd925404215af4" +dependencies = [ + "android-activity", + "bitflags", + "dpi", + "ndk", + "raw-window-handle", + "smol_str", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-appkit" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23343ec0ea7ac6aa72f38009a6ff66fb63bcf6395e0622fba72794d2a762ab42" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "dpi", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation", + "raw-window-handle", + "smol_str", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-common" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90422ff1c04502aaaac1dad1d099791c2bbb3fbb3f823019e0ce5cdeee04025" +dependencies = [ + "block2", + "dpi", + "memmap2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "smol_str", + "tracing", + "winit-core", + "x11-dl", + "xkbcommon-dl", +] + +[[package]] +name = "winit-core" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8af6307236c51847199950c15cf80e0481a250bb96ce9132f5026df4ad3d0f9" +dependencies = [ + "bitflags", + "cursor-icon", + "dpi", + "keyboard-types", + "raw-window-handle", + "smol_str", + "url", + "web-time", +] + +[[package]] +name = "winit-orbital" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278a8ae837902a3bc95dd2fd696d680da33f32130eecc29905358c9089cf6c50" +dependencies = [ + "bitflags", + "dpi", + "libredox", + "orbclient", + "raw-window-handle", + "redox_event", + "smol_str", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-uikit" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83315a3ea1739c8466cf23d35e3b0a2cb20c9515d8857666b633afef484740b5" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "dpi", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "raw-window-handle", + "smol_str", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-wayland" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b64ceed06de0c45886eef05102106abe1413e703384e337bc57c7e95b71f803" +dependencies = [ + "bitflags", + "calloop", + "cursor-icon", + "dpi", + "foldhash", + "libc", + "memmap2", + "percent-encoding", + "raw-window-handle", + "rustix", + "smithay-client-toolkit", + "smol_str", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-web" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f539be45f95cc297bdfb80569aef38d925d64aec4133852a4d3fc5cb3660f1c9" +dependencies = [ + "atomic-waker", + "bitflags", + "concurrent-queue", + "cursor-icon", + "dpi", + "js-sys", + "pin-project", + "raw-window-handle", + "smol_str", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winit-core", +] + +[[package]] +name = "winit-win32" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fb480c41df01d351f656cbf3ad74013f8a9d22bb5b2dcab59f9167e7a0b6ec" +dependencies = [ + "bitflags", + "cursor-icon", + "dpi", + "raw-window-handle", + "smol_str", + "tracing", + "unicode-segmentation", + "url", + "windows-sys", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-x11" +version = "0.31.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5746cd907e514b7895491a1c6f7d0cb70077f9e16928cd74ae6f987ffd96aa" +dependencies = [ + "bitflags", + "bytemuck", + "calloop", + "cursor-icon", + "dpi", + "libc", + "percent-encoding", + "raw-window-handle", + "rustix", + "smol_str", + "tracing", + "winit-common", + "winit-core", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index d056b238d..4a21ed76a 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["mseries-power"] +members = ["mseries-power", "pantheon-compositor"] resolver = "2" diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml new file mode 100644 index 000000000..912df283a --- /dev/null +++ b/rust/pantheon-compositor/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pantheon-compositor" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" +description = "Nested Rust Wayland compositor prototype for elementary OS" + +[[bin]] +name = "pantheon-compositor" +path = "src/main.rs" + +[dependencies] +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +bitflags = "2.2.1" + +[dependencies.smithay] +git = "https://github.com/Smithay/smithay.git" +rev = "023b78b1c5a05ca512767cacc07efca55fae8be4" +default-features = false +features = ["backend_winit", "wayland_frontend", "desktop"] diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md new file mode 100644 index 000000000..6ccc36825 --- /dev/null +++ b/rust/pantheon-compositor/README.md @@ -0,0 +1,35 @@ +# pantheon-compositor + +First implementation milestone for the Rust Mutter replacement: an opt-in, +nested Wayland compositor prototype using Smithay's Winit backend. It runs in a +window on an existing Wayland or X11 session and serves its own Wayland socket. +It does not open DRM/KMS, become a login session, or replace Gala. + +## Run + +From the repository's `rust/` directory, with Smithay's native build dependencies +installed: + +```sh +cargo run -p pantheon-compositor -- --run weston-terminal +``` + +Replace `weston-terminal` with a Wayland client installed on the development +system. With no `--run` argument, the nested compositor starts without launching +a client. `WAYLAND_DISPLAY` is changed only inside the compositor process after +the host Winit backend has initialized; the requested child uses the nested +socket. The active session's services and configuration are not touched. + +## Current limits + +This is a nested proof of rendering and basic interactive Wayland surfaces, not +a Mutter port or a Pantheon replacement yet. It currently has one nested output, +basic keyboard/pointer handling, xdg-shell windows, move/resize grabs, clipboard +and drag-and-drop. Touch input is not implemented. DRM/KMS, libinput device +management, multi-monitor configuration, accessibility, IBus, Xwayland, screen +sharing/remote-desktop portals, lock screen integration, Pantheon panel/dock +protocols, and production crash recovery are not implemented here. + +The code is based on Smithay's `smallvil` learning compositor at pinned commit +`023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the +upstream revision pinned until an explicit upgrade is reviewed. diff --git a/rust/pantheon-compositor/SMITHAY-LICENSE.txt b/rust/pantheon-compositor/SMITHAY-LICENSE.txt new file mode 100644 index 000000000..5a8cb6738 --- /dev/null +++ b/rust/pantheon-compositor/SMITHAY-LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Victor Berger and Victoria Brekenfeld + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rust/pantheon-compositor/THIRD-PARTY.md b/rust/pantheon-compositor/THIRD-PARTY.md new file mode 100644 index 000000000..6724e2f69 --- /dev/null +++ b/rust/pantheon-compositor/THIRD-PARTY.md @@ -0,0 +1,5 @@ +# Third-party source + +`src/` adapts Smithay's `smallvil` example compositor from +. +The upstream license is included as `SMITHAY-LICENSE.txt`. diff --git a/rust/pantheon-compositor/src/grabs/mod.rs b/rust/pantheon-compositor/src/grabs/mod.rs new file mode 100644 index 000000000..5756e1bea --- /dev/null +++ b/rust/pantheon-compositor/src/grabs/mod.rs @@ -0,0 +1,5 @@ +pub mod move_grab; +pub use move_grab::MoveSurfaceGrab; + +pub mod resize_grab; +pub use resize_grab::ResizeSurfaceGrab; diff --git a/rust/pantheon-compositor/src/grabs/move_grab.rs b/rust/pantheon-compositor/src/grabs/move_grab.rs new file mode 100644 index 000000000..a55bec9bb --- /dev/null +++ b/rust/pantheon-compositor/src/grabs/move_grab.rs @@ -0,0 +1,160 @@ +//! Move grab is the state of a composer during which the client window is being dragged around. +//! +//! eg. Usually whenever a user clicks on the app's titlebar and starts dragging, the compositors +//! enters a MoveSurfaceGrab state. + +use crate::PantheonCompositor; +use smithay::{ + desktop::Window, + input::pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{Logical, Point}, +}; + +pub struct MoveSurfaceGrab { + pub start_data: PointerGrabStartData, + pub window: Window, + pub initial_window_location: Point, +} + +impl PointerGrab for MoveSurfaceGrab { + fn motion( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + _focus: Option<(WlSurface, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + let delta = event.location - self.start_data.location; + let new_location = self.initial_window_location.to_f64() + delta; + data.space + .map_element(self.window.clone(), new_location.to_i32_round(), true); + } + + fn relative_motion( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + focus: Option<(WlSurface, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &ButtonEvent, + ) { + handle.button(data, event); + + // The button is a button code as defined in the + // Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT. + const BTN_LEFT: u32 = 0x110; + + if !handle.current_pressed().contains(&BTN_LEFT) { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn axis( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame(&mut self, data: &mut PantheonCompositor, handle: &mut PointerInnerHandle<'_, PantheonCompositor>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut PantheonCompositor) {} +} diff --git a/rust/pantheon-compositor/src/grabs/resize_grab.rs b/rust/pantheon-compositor/src/grabs/resize_grab.rs new file mode 100644 index 000000000..aa72c03ea --- /dev/null +++ b/rust/pantheon-compositor/src/grabs/resize_grab.rs @@ -0,0 +1,365 @@ +//! Resize grab is the state of a composer during which the client window is being resized. +//! +//! eg. Usually whenever a user clicks on the app's border and starts dragging, the compositors +//! enters a ResizeSurfaceGrab state. + +use crate::PantheonCompositor; +use smithay::{ + desktop::{Space, Window}, + input::pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::protocol::wl_surface::WlSurface, + }, + utils::{Logical, Point, Rectangle, Size}, + wayland::{compositor, shell::xdg::SurfaceCachedState}, +}; +use std::cell::RefCell; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct ResizeEdge: u32 { + const TOP = 0b0001; + const BOTTOM = 0b0010; + const LEFT = 0b0100; + const RIGHT = 0b1000; + + const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits(); + const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits(); + + const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits(); + const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits(); + } +} + +impl From for ResizeEdge { + #[inline] + fn from(x: xdg_toplevel::ResizeEdge) -> Self { + Self::from_bits(x as u32).unwrap() + } +} + +pub struct ResizeSurfaceGrab { + start_data: PointerGrabStartData, + window: Window, + + edges: ResizeEdge, + + initial_rect: Rectangle, + last_window_size: Size, +} + +impl ResizeSurfaceGrab { + pub fn start( + start_data: PointerGrabStartData, + window: Window, + edges: ResizeEdge, + initial_window_rect: Rectangle, + ) -> Self { + let initial_rect = initial_window_rect; + + ResizeSurfaceState::with(window.toplevel().unwrap().wl_surface(), |state| { + *state = ResizeSurfaceState::Resizing { edges, initial_rect }; + }); + + Self { + start_data, + window, + edges, + initial_rect, + last_window_size: initial_rect.size, + } + } +} + +impl PointerGrab for ResizeSurfaceGrab { + fn motion( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + _focus: Option<(WlSurface, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + let mut delta = event.location - self.start_data.location; + + let mut new_window_width = self.initial_rect.size.w; + let mut new_window_height = self.initial_rect.size.h; + + if self.edges.intersects(ResizeEdge::LEFT | ResizeEdge::RIGHT) { + if self.edges.intersects(ResizeEdge::LEFT) { + delta.x = -delta.x; + } + + new_window_width = (self.initial_rect.size.w as f64 + delta.x) as i32; + } + + if self.edges.intersects(ResizeEdge::TOP | ResizeEdge::BOTTOM) { + if self.edges.intersects(ResizeEdge::TOP) { + delta.y = -delta.y; + } + + new_window_height = (self.initial_rect.size.h as f64 + delta.y) as i32; + } + + let (min_size, max_size) = + compositor::with_states(self.window.toplevel().unwrap().wl_surface(), |states| { + let mut guard = states.cached_state.get::(); + let data = guard.current(); + (data.min_size, data.max_size) + }); + + let min_width = min_size.w.max(1); + let min_height = min_size.h.max(1); + + let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w }; + let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h }; + + self.last_window_size = Size::from(( + new_window_width.max(min_width).min(max_width), + new_window_height.max(min_height).min(max_height), + )); + + let xdg = self.window.toplevel().unwrap(); + xdg.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + + xdg.send_pending_configure(); + } + + fn relative_motion( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + focus: Option<(WlSurface, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &ButtonEvent, + ) { + handle.button(data, event); + + // The button is a button code as defined in the + // Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT. + const BTN_LEFT: u32 = 0x110; + + if !handle.current_pressed().contains(&BTN_LEFT) { + // No more buttons are pressed, release the grab. + handle.unset_grab(self, data, event.serial, event.time, true); + + let xdg = self.window.toplevel().unwrap(); + xdg.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + + xdg.send_pending_configure(); + + ResizeSurfaceState::with(xdg.wl_surface(), |state| { + *state = ResizeSurfaceState::WaitingForLastCommit { + edges: self.edges, + initial_rect: self.initial_rect, + }; + }); + } + } + + fn axis( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame(&mut self, data: &mut PantheonCompositor, handle: &mut PointerInnerHandle<'_, PantheonCompositor>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + &self.start_data + } + + fn unset(&mut self, _data: &mut PantheonCompositor) {} +} + +/// State of the resize operation. +/// +/// It is stored inside of WlSurface, +/// and can be accessed using [`ResizeSurfaceState::with`] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +enum ResizeSurfaceState { + #[default] + Idle, + Resizing { + edges: ResizeEdge, + /// The initial window size and location. + initial_rect: Rectangle, + }, + /// Resize is done, we are now waiting for last commit, to do the final move + WaitingForLastCommit { + edges: ResizeEdge, + /// The initial window size and location. + initial_rect: Rectangle, + }, +} + +impl ResizeSurfaceState { + fn with(surface: &WlSurface, cb: F) -> T + where + F: FnOnce(&mut Self) -> T, + { + compositor::with_states(surface, |states| { + states.data_map.insert_if_missing(RefCell::::default); + let state = states.data_map.get::>().unwrap(); + + cb(&mut state.borrow_mut()) + }) + } + + fn commit(&mut self) -> Option<(ResizeEdge, Rectangle)> { + match *self { + Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)), + Self::WaitingForLastCommit { edges, initial_rect } => { + // The resize is done, let's go back to idle + *self = Self::Idle; + + Some((edges, initial_rect)) + } + Self::Idle => None, + } + } +} + +/// Should be called on `WlSurface::commit` +pub fn handle_commit(space: &mut Space, surface: &WlSurface) -> Option<()> { + let window = space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == surface) + .cloned()?; + + let mut window_loc = space.element_location(&window)?; + let geometry = window.geometry(); + + let new_loc: Point, Logical> = ResizeSurfaceState::with(surface, |state| { + state + .commit() + .and_then(|(edges, initial_rect)| { + // If the window is being resized by top or left, its location must be adjusted + // accordingly. + edges.intersects(ResizeEdge::TOP_LEFT).then(|| { + let new_x = edges + .intersects(ResizeEdge::LEFT) + .then_some(initial_rect.loc.x + (initial_rect.size.w - geometry.size.w)); + + let new_y = edges + .intersects(ResizeEdge::TOP) + .then_some(initial_rect.loc.y + (initial_rect.size.h - geometry.size.h)); + + (new_x, new_y).into() + }) + }) + .unwrap_or_default() + }); + + if let Some(new_x) = new_loc.x { + window_loc.x = new_x; + } + if let Some(new_y) = new_loc.y { + window_loc.y = new_y; + } + + if new_loc.x.is_some() || new_loc.y.is_some() { + // If TOP or LEFT side of the window got resized, we have to move it + space.map_element(window, window_loc, false); + } + + Some(()) +} diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs new file mode 100644 index 000000000..ba4d8fd91 --- /dev/null +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -0,0 +1,57 @@ +use crate::{PantheonCompositor, grabs::resize_grab, state::ClientState}; +use smithay::{ + backend::renderer::utils::on_commit_buffer_handler, + reexports::wayland_server::{ + Client, + protocol::{wl_buffer, wl_surface::WlSurface}, + }, + wayland::{ + buffer::BufferHandler, + compositor::{ + CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface, + }, + shm::{ShmHandler, ShmState}, + }, +}; + +use super::xdg_shell; + +impl CompositorHandler for PantheonCompositor { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + &client.get_data::().unwrap().compositor_state + } + + fn commit(&mut self, surface: &WlSurface) { + on_commit_buffer_handler::(surface); + if !is_sync_subsurface(surface) { + let mut root = surface.clone(); + while let Some(parent) = get_parent(&root) { + root = parent; + } + if let Some(window) = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == &root) + { + window.on_commit(); + } + }; + + xdg_shell::handle_commit(&mut self.popups, &self.space, surface); + resize_grab::handle_commit(&mut self.space, surface); + } +} + +impl BufferHandler for PantheonCompositor { + fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {} +} + +impl ShmHandler for PantheonCompositor { + fn shm_state(&self) -> &ShmState { + &self.shm_state + } +} diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs new file mode 100644 index 000000000..9373ac100 --- /dev/null +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -0,0 +1,90 @@ +mod compositor; +mod xdg_shell; + +use crate::PantheonCompositor; + +// +// Wl Seat +// + +use smithay::input::dnd::{DnDGrab, DndGrabHandler, GrabType, Source}; +use smithay::input::pointer::Focus; +use smithay::input::{Seat, SeatHandler, SeatState}; +use smithay::reexports::wayland_server::Resource; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::Serial; +use smithay::wayland::output::OutputHandler; +use smithay::wayland::pointer_constraints::PointerConstraintsHandler; +use smithay::wayland::selection::SelectionHandler; +use smithay::wayland::selection::data_device::{ + DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, +}; + +impl SeatHandler for PantheonCompositor { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn cursor_image(&mut self, _seat: &Seat, _image: smithay::input::pointer::CursorImageStatus) {} + + fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { + let dh = &self.display_handle; + let client = focused.and_then(|s| dh.get_client(s.id()).ok()); + set_data_device_focus(dh, seat, client); + } +} + +impl PointerConstraintsHandler for PantheonCompositor {} + +// +// Wl Data Device +// + +impl SelectionHandler for PantheonCompositor { + type SelectionUserData = (); +} + +impl DataDeviceHandler for PantheonCompositor { + fn data_device_state(&mut self) -> &mut DataDeviceState { + &mut self.data_device_state + } +} + +impl DndGrabHandler for PantheonCompositor {} +impl WaylandDndGrabHandler for PantheonCompositor { + fn dnd_requested( + &mut self, + source: S, + _icon: Option, + seat: Seat, + serial: Serial, + type_: GrabType, + ) { + match type_ { + GrabType::Pointer => { + let ptr = seat.get_pointer().unwrap(); + let start_data = ptr.grab_start_data().unwrap(); + + // create a dnd grab to start the operation + let grab = DnDGrab::new_pointer(&self.display_handle, start_data, source, seat); + ptr.set_grab(self, grab, serial, Focus::Keep); + } + GrabType::Touch => { + // pantheon-compositor lacks touch handling + source.cancel(); + } + } + } +} + +// +// Wl Output & Xdg Output +// + +impl OutputHandler for PantheonCompositor {} + +smithay::delegate_dispatch2!(PantheonCompositor); diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs new file mode 100644 index 000000000..9c93c72f6 --- /dev/null +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -0,0 +1,214 @@ +use smithay::{ + desktop::{PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords}, + input::{ + Seat, + pointer::{Focus, GrabStartData as PointerGrabStartData}, + }, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel, + wayland_server::{ + Resource, + protocol::{wl_seat, wl_surface::WlSurface}, + }, + }, + utils::{Rectangle, Serial}, + wayland::{ + compositor::with_states, + shell::xdg::{ + PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, + XdgToplevelSurfaceData, + }, + }, +}; + +use crate::{ + PantheonCompositor, + grabs::{MoveSurfaceGrab, ResizeSurfaceGrab}, +}; + +impl XdgShellHandler for PantheonCompositor { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.xdg_shell_state + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + let window = Window::new_wayland_window(surface); + self.space.map_element(window, (0, 0), false); + } + + fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { + self.unconstrain_popup(&surface); + let _ = self.popups.track_popup(PopupKind::Xdg(surface)); + } + + fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { + surface.with_pending_state(|state| { + let geometry = positioner.get_geometry(); + state.geometry = geometry; + state.positioner = positioner; + }); + self.unconstrain_popup(&surface); + surface.send_repositioned(token); + } + + fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) { + let seat = Seat::from_resource(&seat).unwrap(); + + let wl_surface = surface.wl_surface(); + + if let Some(start_data) = check_grab(&seat, wl_surface, serial) { + let pointer = seat.get_pointer().unwrap(); + + let window = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == wl_surface) + .unwrap() + .clone(); + let initial_window_location = self.space.element_location(&window).unwrap(); + + let grab = MoveSurfaceGrab { + start_data, + window, + initial_window_location, + }; + + pointer.set_grab(self, grab, serial, Focus::Clear); + } + } + + fn resize_request( + &mut self, + surface: ToplevelSurface, + seat: wl_seat::WlSeat, + serial: Serial, + edges: xdg_toplevel::ResizeEdge, + ) { + let seat = Seat::from_resource(&seat).unwrap(); + + let wl_surface = surface.wl_surface(); + + if let Some(start_data) = check_grab(&seat, wl_surface, serial) { + let pointer = seat.get_pointer().unwrap(); + + let window = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == wl_surface) + .unwrap() + .clone(); + let initial_window_location = self.space.element_location(&window).unwrap(); + let initial_window_size = window.geometry().size; + + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + }); + + surface.send_pending_configure(); + + let grab = ResizeSurfaceGrab::start( + start_data, + window, + edges.into(), + Rectangle::new(initial_window_location, initial_window_size), + ); + + pointer.set_grab(self, grab, serial, Focus::Clear); + } + } + + fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { + // TODO popup grabs + } +} + +fn check_grab( + seat: &Seat, + surface: &WlSurface, + serial: Serial, +) -> Option> { + let pointer = seat.get_pointer()?; + + // Check that this surface has a click grab. + if !pointer.has_grab(serial) { + return None; + } + + let start_data = pointer.grab_start_data()?; + + let (focus, _) = start_data.focus.as_ref()?; + // If the focus was for a different surface, ignore the request. + if !focus.id().same_client_as(&surface.id()) { + return None; + } + + Some(start_data) +} + +/// Should be called on `WlSurface::commit` +pub fn handle_commit(popups: &mut PopupManager, space: &Space, surface: &WlSurface) { + // Handle toplevel commits. + if let Some(window) = space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == surface) + .cloned() + { + let initial_configure_sent = with_states(surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + + if !initial_configure_sent { + window.toplevel().unwrap().send_configure(); + } + } + + // Handle popup commits. + popups.commit(surface); + if let Some(popup) = popups.find_popup(surface) { + match popup { + PopupKind::Xdg(ref xdg) => { + if !xdg.is_initial_configure_sent() { + // NOTE: This should never fail as the initial configure is always + // allowed. + xdg.send_configure().expect("initial configure failed"); + } + } + PopupKind::InputMethod(ref _input_method) => {} + } + } +} + +impl PantheonCompositor { + fn unconstrain_popup(&self, popup: &PopupSurface) { + let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else { + return; + }; + let Some(window) = self + .space + .elements() + .find(|w| w.toplevel().unwrap().wl_surface() == &root) + else { + return; + }; + + let output = self.space.outputs().next().unwrap(); + let output_geo = self.space.output_geometry(output).unwrap(); + let window_geo = self.space.element_geometry(window).unwrap(); + + // The target geometry for the positioner should be relative to its parent's geometry, so + // we will compute that here. + let mut target = output_geo; + target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())); + target.loc -= window_geo.loc; + + popup.with_pending_state(|state| { + state.geometry = state.positioner.get_unconstrained_geometry(target); + }); + } +} diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs new file mode 100644 index 000000000..99590c1f1 --- /dev/null +++ b/rust/pantheon-compositor/src/input.rs @@ -0,0 +1,144 @@ +use smithay::{ + backend::input::{ + AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, + KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, + }, + input::{ + keyboard::FilterResult, + pointer::{AxisFrame, ButtonEvent, MotionEvent}, + }, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::SERIAL_COUNTER, +}; + +use crate::state::PantheonCompositor; + +impl PantheonCompositor { + pub fn process_input_event(&mut self, event: InputEvent) { + match event { + InputEvent::Keyboard { event, .. } => { + let serial = SERIAL_COUNTER.next_serial(); + let time = Event::time(&event); + + self.seat.get_keyboard().unwrap().input::<(), _>( + self, + event.key_code(), + event.state(), + serial, + time, + |_, _, _| FilterResult::Forward, + ); + } + InputEvent::PointerMotion { .. } => {} + InputEvent::PointerMotionAbsolute { event, .. } => { + let output = self.space.outputs().next().unwrap(); + + let output_geo = self.space.output_geometry(output).unwrap(); + + let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); + + let serial = SERIAL_COUNTER.next_serial(); + + let pointer = self.seat.get_pointer().unwrap(); + + let under = self.surface_under(pos); + + pointer.motion( + self, + under, + &MotionEvent { + location: pos, + serial, + time: event.time(), + }, + ); + pointer.frame(self); + } + InputEvent::PointerButton { event, .. } => { + let pointer = self.seat.get_pointer().unwrap(); + let keyboard = self.seat.get_keyboard().unwrap(); + + let serial = SERIAL_COUNTER.next_serial(); + + let button = event.button_code(); + + let button_state = event.state(); + + if ButtonState::Pressed == button_state && !pointer.is_grabbed() { + if let Some((window, _loc)) = self + .space + .element_under(pointer.current_location()) + .map(|(w, l)| (w.clone(), l)) + { + self.space.raise_element(&window, true); + keyboard.set_focus( + self, + Some(window.toplevel().unwrap().wl_surface().clone()), + serial, + ); + self.space.elements().for_each(|window| { + window.toplevel().unwrap().send_pending_configure(); + }); + } else { + self.space.elements().for_each(|window| { + window.set_activated(false); + window.toplevel().unwrap().send_pending_configure(); + }); + keyboard.set_focus(self, Option::::None, serial); + } + }; + + pointer.button( + self, + &ButtonEvent { + button, + state: button_state, + serial, + time: event.time(), + }, + ); + pointer.frame(self); + } + InputEvent::PointerAxis { event, .. } => { + let source = event.source(); + + let horizontal_amount = event + .amount(Axis::Horizontal) + .unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.); + let vertical_amount = event + .amount(Axis::Vertical) + .unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.); + let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal); + let vertical_amount_discrete = event.amount_v120(Axis::Vertical); + + let mut frame = AxisFrame::new(event.time()).source(source); + if horizontal_amount != 0.0 { + frame = frame.value(Axis::Horizontal, horizontal_amount); + if let Some(discrete) = horizontal_amount_discrete { + frame = frame.v120(Axis::Horizontal, discrete as i32); + } + } + if vertical_amount != 0.0 { + frame = frame.value(Axis::Vertical, vertical_amount); + if let Some(discrete) = vertical_amount_discrete { + frame = frame.v120(Axis::Vertical, discrete as i32); + } + } + + if source == AxisSource::Finger { + if event.amount(Axis::Horizontal) == Some(0.0) { + frame = frame.stop(Axis::Horizontal); + } + if event.amount(Axis::Vertical) == Some(0.0) { + frame = frame.stop(Axis::Vertical); + } + } + + let pointer = self.seat.get_pointer().unwrap(); + pointer.axis(self, frame); + pointer.frame(self); + } + _ => {} + } + } +} diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs new file mode 100644 index 000000000..a3c940f84 --- /dev/null +++ b/rust/pantheon-compositor/src/main.rs @@ -0,0 +1,58 @@ +#![allow(irrefutable_let_patterns)] + +mod handlers; +mod grabs; +mod input; +mod state; +mod winit; + +use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; +pub use state::PantheonCompositor; + +fn main() -> Result<(), Box> { + init_logging(); + + if std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none() { + return Err("nested prototype requires an existing Wayland or X11 session".into()); + } + + let command = parse_command()?; + let mut event_loop: EventLoop = EventLoop::try_new()?; + let display: Display = Display::new()?; + let mut state = PantheonCompositor::new(&mut event_loop, display); + + // This prototype deliberately uses the nested winit backend. It does not + // open DRM/KMS devices or replace the active desktop session. + crate::winit::init_winit(&mut event_loop, &mut state)?; + + // The host backend is already initialized from the original environment. + // Launch requested clients against the nested display only. + unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; + + if let Some((program, args)) = command { + std::process::Command::new(program).args(args).spawn()?; + } + + event_loop.run(None, &mut state, move |_| {})?; + Ok(()) +} + +fn init_logging() { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + } else { + tracing_subscriber::fmt().init(); + } +} + +fn parse_command() -> Result)>, Box> { + let mut args = std::env::args().skip(1); + match args.next().as_deref() { + None => Ok(None), + Some("--run") => { + let program = args.next().ok_or("--run requires a program")?; + Ok(Some((program, args.collect()))) + } + Some(_) => Err("usage: pantheon-compositor [--run PROGRAM [ARGS...]]".into()), + } +} diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs new file mode 100644 index 000000000..a61e59939 --- /dev/null +++ b/rust/pantheon-compositor/src/state.rs @@ -0,0 +1,168 @@ +use std::{ffi::OsString, sync::Arc}; + +use smithay::{ + desktop::{PopupManager, Space, Window, WindowSurfaceType}, + input::{Seat, SeatState}, + reexports::{ + calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, + wayland_server::{ + Display, DisplayHandle, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::wl_surface::WlSurface, + }, + }, + utils::{Logical, Point}, + wayland::{ + compositor::{CompositorClientState, CompositorState}, + output::OutputManagerState, + selection::data_device::DataDeviceState, + shell::xdg::XdgShellState, + shm::ShmState, + socket::ListeningSocketSource, + }, +}; + +pub struct PantheonCompositor { + pub start_time: std::time::Instant, + pub socket_name: OsString, + pub display_handle: DisplayHandle, + + pub space: Space, + pub loop_signal: LoopSignal, + + // Smithay State + pub compositor_state: CompositorState, + pub xdg_shell_state: XdgShellState, + pub shm_state: ShmState, + pub output_manager_state: OutputManagerState, + pub seat_state: SeatState, + pub data_device_state: DataDeviceState, + pub popups: PopupManager, + + pub seat: Seat, +} + +impl PantheonCompositor { + pub fn new(event_loop: &mut EventLoop, display: Display) -> Self { + let start_time = std::time::Instant::now(); + + let dh = display.handle(); + + // Here we initialize implementations of some wayland protocols + // Some of them require us to implement traits on the PantheonCompositor state, + // you can find those implementations in the `crate::handlers` module + + // Initialize protocols needed for displaying windows + let compositor_state = CompositorState::new::(&dh); + let xdg_shell_state = XdgShellState::new::(&dh); + let shm_state = ShmState::new::(&dh, vec![]); + let popups = PopupManager::default(); + + let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + + // Data device is responsible for clipboard and drag-and-drop + let data_device_state = DataDeviceState::new::(&dh); + + // A seat is a group of keyboards, pointer and touch devices. + // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. + let mut seat_state = SeatState::new(); + let mut seat: Seat = seat_state.new_wl_seat(&dh, "winit"); + + // Notify clients that we have a keyboard, for the sake of the example we assume that keyboard is always present. + // You may want to track keyboard hot-plug in real compositor. + seat.add_keyboard(Default::default(), 200, 25).unwrap(); + + // Notify clients that we have a pointer (mouse) + // Here we assume that there is always pointer plugged in + seat.add_pointer(); + + // A space represents a two-dimensional plane. Windows and Outputs can be mapped onto it. + // + // Windows get a position and stacking order through mapping. + // Outputs become views of a part of the Space and can be rendered via Space::render_output. + let space = Space::default(); + + // Setup a wayland socket that will be used to accept clients + let socket_name = Self::init_wayland_listener(display, event_loop); + + // Get the loop signal, used to stop the event loop + let loop_signal = event_loop.get_signal(); + + Self { + start_time, + display_handle: dh, + + space, + loop_signal, + socket_name, + + compositor_state, + xdg_shell_state, + shm_state, + output_manager_state, + seat_state, + data_device_state, + popups, + seat, + } + } + + fn init_wayland_listener(display: Display, event_loop: &mut EventLoop) -> OsString { + // Creates a new listening socket, automatically choosing the next available `wayland` socket name. + let listening_socket = ListeningSocketSource::new_auto().unwrap(); + + // Get the name of the listening socket. + // Clients will connect to this socket. + let socket_name = listening_socket.socket_name().to_os_string(); + + let loop_handle = event_loop.handle(); + + loop_handle + .insert_source(listening_socket, move |client_stream, _, state| { + // Inside the callback, you should insert the client into the display. + // + // You may also associate some data with the client when inserting the client. + state + .display_handle + .insert_client(client_stream, Arc::new(ClientState::default())) + .unwrap(); + }) + .expect("Failed to init the wayland event source."); + + // You also need to add the display itself to the event loop, so that client events will be processed by wayland-server. + loop_handle + .insert_source( + Generic::new(display, Interest::READ, Mode::Level), + |_, display, state| { + // Safety: we don't drop the display + unsafe { + display.get_mut().dispatch_clients(state).unwrap(); + } + Ok(PostAction::Continue) + }, + ) + .unwrap(); + + socket_name + } + + pub fn surface_under(&self, pos: Point) -> Option<(WlSurface, Point)> { + self.space.element_under(pos).and_then(|(window, location)| { + window + .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL) + .map(|(s, p)| (s, (p + location).to_f64())) + }) + } +} + +/// Data associated with a wayland client that connects to PantheonCompositor. +/// One instance of this type per client. +#[derive(Default)] +pub struct ClientState { + pub compositor_state: CompositorClientState, +} + +impl ClientData for ClientState { + fn initialized(&self, _client_id: ClientId) {} + fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} +} diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs new file mode 100644 index 000000000..2eef311ec --- /dev/null +++ b/rust/pantheon-compositor/src/winit.rs @@ -0,0 +1,110 @@ +use std::time::Duration; + +use smithay::{ + backend::{ + renderer::{ + damage::OutputDamageTracker, element::surface::WaylandSurfaceRenderElement, gles::GlesRenderer, + }, + winit::{self, WinitEvent}, + }, + output::{Mode, Output, PhysicalProperties, Subpixel}, + reexports::calloop::EventLoop, + utils::{Rectangle, Transform}, +}; + +use crate::PantheonCompositor; + +pub fn init_winit( + event_loop: &mut EventLoop, + state: &mut PantheonCompositor, +) -> Result<(), Box> { + let (mut backend, winit) = winit::init()?; + + let mode = Mode { + size: backend.window_size(), + refresh: 60_000, + }; + + let output = Output::new( + "winit".to_string(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "Smithay".into(), + model: "Winit".into(), + serial_number: "Unknown".into(), + }, + ); + let _global = output.create_global::(&state.display_handle); + output.change_current_state(Some(mode), Some(Transform::Flipped180), None, Some((0, 0).into())); + output.set_preferred(mode); + + state.space.map_output(&output, (0, 0)); + + let mut damage_tracker = OutputDamageTracker::from_output(&output); + + event_loop.handle().insert_source(winit, move |event, _, state| { + match event { + WinitEvent::Resized { size, .. } => { + output.change_current_state( + Some(Mode { + size, + refresh: 60_000, + }), + None, + None, + None, + ); + } + WinitEvent::Input(event) => state.process_input_event(event), + WinitEvent::Redraw => { + let size = backend.window_size(); + let damage = Rectangle::from_size(size); + + { + let (renderer, mut framebuffer) = backend.bind().unwrap(); + smithay::desktop::space::render_output::< + _, + WaylandSurfaceRenderElement, + _, + _, + >( + &output, + renderer, + &mut framebuffer, + 1.0, + 0, + [&state.space], + &[], + &mut damage_tracker, + [0.1, 0.1, 0.1, 1.0], + ) + .unwrap(); + } + backend.submit(Some(&[damage])).unwrap(); + + state.space.elements().for_each(|window| { + window.send_frame( + &output, + state.start_time.elapsed(), + Some(Duration::ZERO), + |_, _| Some(output.clone()), + ) + }); + + state.space.refresh(); + state.popups.cleanup(); + let _ = state.display_handle.flush_clients(); + + // Ask for redraw to schedule new frame. + backend.window().request_redraw(); + } + WinitEvent::CloseRequested => { + state.loop_signal.stop(); + } + _ => (), + }; + })?; + + Ok(()) +} From 24a787d7852943257a26a40a8b2b83314b9dc4ef Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:17:27 -0400 Subject: [PATCH 035/798] Define M-series minimal Rust compositor scope --- mseries/OS9-HANDOFF.md | 42 ++++++---- mseries/ROADMAP.md | 32 ++++---- .../EXPERIENCE-REQUIREMENTS.md | 81 +++++++++++++++++++ mseries/pantheon-rust-compositor/README.md | 39 +++++++++ mseries/rust-mutter/README.md | 72 ----------------- rust/pantheon-compositor/README.md | 18 +++-- 6 files changed, 176 insertions(+), 108 deletions(-) create mode 100644 mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md create mode 100644 mseries/pantheon-rust-compositor/README.md delete mode 100644 mseries/rust-mutter/README.md diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 466bbc5ed..9faa11e67 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5272,16 +5272,22 @@ Calendar GUI/reminders, indefinite soak, power/battery and factory boot remain separate work. Six-hour developer tracking remains the existing workflow; the old completion heartbeat is already PAUSED. No duplicate automation was created. -## Rust Mutter replacement for Pantheon — 2026-10-18 - -Started branch `codex/rust-mutter-pantheon` for a Rust compositor intended to -replace the Gala/Mutter display-server role in the Pantheon session. The source -audit found Gala 8.6.2 directly uses Mutter's `Meta` objects and Clutter/Cogl/Mtk -APIs and is itself started as the Wayland display server by its session unit. -Therefore, serving Wayland clients alone is not a swap-in; Gala's dependency -boundary and the complete Pantheon session contract need to move as well. -Recorded nested-compositor, shell-contract, hardware/input, parallel-session, -and swap-in gates in `mseries/rust-mutter/README.md`. +## Mutter-minimal viable Rust for elementaryOS M-series — 2026-10-18 + +Started branch `codex/rust-mutter-pantheon`. The working title is +`Mutter-Minimal-Viable-Rust_elementaryos-mseries`. **Scope clarified by the +owner:** identify the features needed for the M-series/Pantheon experience and +implement those; do not port Mutter or target full feature parity. Requirements +and decision gates are documented in +`mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`. + +Gala 8.6.2 directly uses Mutter `Meta` objects and Clutter/Cogl/Mtk APIs and is +started as the Wayland display server by its session unit. The source audit +found 79 Vala files using `Meta.*` across 24,534 lines, which demonstrates that +replacing Gala underneath with a small shim is not a drop-in. This is migration +context, not a list of features to port. The initial scope derives from our +documented notch-aware panel, dock work-area behavior, display profiles and +hotplug, snapping, input, and session reliability. Public source check found no first-party Rust Mutter/Gala replacement to reuse: elementary's public [Gala repository](https://github.com/elementary/gala) remains @@ -5292,13 +5298,19 @@ Added `rust/pantheon-compositor`, an opt-in nested prototype based on Smithay's smallvil at pinned revision `023b78b1c5a05ca512767cacc07efca55fae8be4`. Its workspace manifest and lockfile are wired; it uses Smithay's Winit backend, needs an existing X11/Wayland session, and can launch a requested Wayland client -on its nested display. It does not open DRM/KMS or alter the session. It has -not been built or tested. +on its nested display. It does not open DRM/KMS or alter the session. `cargo +check -p pantheon-compositor` passes on the development host; runtime/client +behavior and M-series hardware remain unqualified. The prototype is a +development harness, not a finished session. Conditional needs such as +Xwayland, IBus, accessibility, screencasting and multi-GPU are not automatic +port targets; each needs a demonstrated workflow. Mutter upstream is a feature +reference: its CTM Night Light change is relevant only if the M-series driver +exposes CTM, which the current handoff says it did not. No compositor has been installed and no desktop configuration has changed. No active desktop, kernel, bootloader, or audio-stack changes were made. The WD22TB4 HDMI G3 link-training blocker remains on the DCP/kernel path; it must be tracked separately from Rust compositor output and hotplug qualification. -Next: compile and run the nested prototype with a Wayland client, then inventory -the Gala/Pantheon shell contract for the next implementation milestone. Do not -switch the login session at this stage. +Next: complete nested runtime/client smoke, then implement the first +experience-derived requirement with a narrow Pantheon shell protocol and +work-area behavior. Do not switch the login session at this stage. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index c37b342b7..70fd509c0 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,20 +1,22 @@ # Roadmap, features and follow-ups (MacBook Pro 14" M1 Pro, elementary OS 9 on Asahi) -## Rust Mutter replacement for Pantheon — started 2026-10-18 - -Work begins on branch `codex/rust-mutter-pantheon`. Gala is the Wayland -display server and directly depends on Mutter `Meta`, Clutter, Cogl, and Mtk; -the replacement needs to cover the Pantheon session contract, not just draw -Wayland client surfaces. The source audit and staged acceptance gates are in -[`rust-mutter/README.md`](rust-mutter/README.md). First implementation gate: -an opt-in nested Smithay compositor that can be exercised without replacing -the active desktop. Prototype code is in `rust/pantheon-compositor/`, pinned to -a Smithay revision, and has not been compiled or hardware qualified. A public -search found the first-party [Gala repository](https://github.com/elementary/gala) -still based on libmutter, but no first-party Rust port to reuse. Independent -Smithay compositors are references, not Pantheon replacements. The WD22TB4 -dock's G3 link-training blocker remains in the kernel/DCP workstream and is not -a compositor acceptance result. +## Mutter-minimal viable Rust for elementaryOS M-series — started 2026-10-18 + +Working title: `Mutter-Minimal-Viable-Rust_elementaryos-mseries`. The owner +clarified that the goal is to identify and implement the compositor features +needed by the M-series/Pantheon experience, not port Mutter or reproduce all +its features. The scoped requirements and upstream feature radar are in +[`pantheon-rust-compositor/README.md`](pantheon-rust-compositor/README.md) and +[`EXPERIENCE-REQUIREMENTS.md`](pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). +The list is grounded in this repo's notch-aware panel, dock work area, display +profiles/hotplug, window snapping, input and session work. Features such as +Xwayland, IBus, accessibility, screencasting and multi-GPU are conditional on +an actual supported workflow. The opt-in Smithay nested prototype in +`rust/pantheon-compositor/` passes `cargo check`; nested runtime and M-series +hardware remain unqualified. A public search found elementary's +[Gala](https://github.com/elementary/gala) still based on libmutter, with no +first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker +remains a separate workstream. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md new file mode 100644 index 000000000..89cb96d33 --- /dev/null +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -0,0 +1,81 @@ +# Mutter-minimal viable Rust: M-series experience requirements + +This is the working name for a **minimal viable Rust compositor for elementaryOS +on M-series Macs**. Mutter is a source of proven behavior and implementation +ideas. The deliverable is a Pantheon session that supports the workflows we +actually use, not a Rust port of Mutter or a feature-for-feature clone of Gala. + +Requirements below come from the M-series handoff and implemented desktop work +in this repository. Priority means needed for a useful first Pantheon session; +it does not imply that the current prototype supports it. + +## Experience-derived scope + +| Priority | User-visible need | Evidence in this repository | Compositor work | +| --- | --- | --- | --- | +| First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, focus/stacking, basic window state, startup readiness and orderly failure/restart behavior | +| M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | +| Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | +| Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | +| Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | + +## Conditional work, driven by an observed need + +Add these only when an M-series workflow depends on them, and keep each as a +separate acceptance item: Xwayland for a required X11 app; IBus/input-method +support for the installed input method; accessibility protocols/actions used by +the owner's setup; screencast/remote-desktop portal support for screen sharing; +touch/gestures if the selected hardware and workflow use them; multi-GPU buffer +sharing if a supported M-series configuration actually traverses that path. +They are important capabilities in a general-purpose desktop, but their +presence in Mutter is not by itself a reason to reproduce them in the first +cut. + +Keep notification UI, app launchers, panel/dock rendering, settings surfaces, +wallpaper and other shell UI in their existing Pantheon components unless a +documented requirement shows they cannot work through the compositor contract. + +## Explicitly separate workstreams + +- **WD22TB4 HDMI picture:** the tracked G3 blocker is Apple DCP/kernel link + training ([primary goal](../fairydust/usb4/PRIMARY-GOAL.md)). The compositor + cannot create a display output the kernel does not expose. Qualify hotplug and + modesetting only after the output exists in DRM. +- **Mutter parity:** there is no goal to implement every Mutter API, effect, + hardware path or private Gala integration. Gala's broad Meta/Clutter usage is + a migration constraint; it is not the product requirements list. +- **Other hardware:** don't add generic multi-GPU, virtual-display or unusual + cursor paths until a supported M-series setup or user-visible workflow needs + them. + +## Upstream feature radar + +Use Mutter releases and merge requests as a source of ideas and regression +knowledge. For example, [Mutter !5205](https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/5205) +adds a Night Light color-transform path for hardware such as Apple DCP that has +CTM but no gamma LUT. The current M-series handoff reports that this machine's +exposed path did not advertise CTM, so this is a conditional candidate rather +than a compositor v1 requirement. Cursor-overlay and secondary-GPU damage fixes +are similarly conditional on those output paths being used. Recheck upstream +before adopting a fix; do not treat every upstream change as a porting task. + +The initial public tag check found Mutter 51.1 as the newest stable tag and no +52 beta tag. This is a dated observation, not a promise about current upstream; +check the [Mutter tag list](https://gitlab.gnome.org/GNOME/mutter/-/tags) before +planning against a release. + +## Build-up plan + +1. Keep the Smithay prototype nested and opt-in. Turn it into a clean minimal + Wayland session with basic window behavior and usable diagnostics. +2. Inventory the installed Pantheon clients' actual protocols and session + expectations. Add the narrow panel/dock protocol, work areas, outputs and + input behaviors required by the evidence above. +3. Run the session beside the existing Gala session as a reversible opt-in. + Qualify login, ordinary apps, panel/dock, monitor changes, snapping, + lock/unlock and crash recovery on the M1 Pro. +4. Add conditional capabilities only against named apps/workflows and explicit + acceptance checks. Keep Gala available as rollback until owner acceptance. + +The prototype currently only passes `cargo check`; it has not yet run with a +client or on M-series hardware. A check is not session or hardware qualification. diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md new file mode 100644 index 000000000..5e4828a3b --- /dev/null +++ b/mseries/pantheon-rust-compositor/README.md @@ -0,0 +1,39 @@ +# Mutter-minimal viable Rust for elementaryOS M-series + +Working title: **Mutter-Minimal-Viable-Rust_elementaryos-mseries**. The goal is +a focused Rust compositor for the Pantheon experience on our M-series machines. +Mutter is the proven reference; porting Mutter itself or reproducing all of its +features is explicitly out of scope. + +The requirements come from the actual desktop work recorded in this repo: +normal app/window behavior, the M1 Pro's full-height/notch-aware panel, dock +visibility and work-area handling, supported display changes, window snapping, +input, and a reliable Pantheon login/lock/recovery path. Existing apps should +keep providing panel, dock and settings UI through a narrow shell contract. +Anything beyond those needs is conditional on a named app or workflow. The +prioritized list and upstream feature radar are in +[`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md). + +The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across +24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That +explains why swapping out Mutter underneath Gala is not a drop-in operation. +It does not mean all those APIs need to be ported: replace only the behavior +required by the M-series/Pantheon session, in independently qualified slices. + +## Current prototype + +[`rust/pantheon-compositor`](../../rust/pantheon-compositor/README.md) is an +opt-in nested Smithay/Winit prototype. It serves a separate Wayland socket from +an existing desktop and does not own DRM/KMS or change the login session. It is +the first development harness, not a finished compositor or a session swap-in. + +## Dock HDMI boundary + +The WD22TB4 HDMI goal remains in +[`../fairydust/usb4/PRIMARY-GOAL.md`](../fairydust/usb4/PRIMARY-GOAL.md). The +G3 gate is DCP/kernel link training; a compositor cannot fix an output the +kernel has not exposed. Keep compositor modeset/hotplug qualification separate. + +No change here qualifies an M-series session or hardware. Gala remains the +production session and rollback until the scoped experience passes on-device +acceptance. diff --git a/mseries/rust-mutter/README.md b/mseries/rust-mutter/README.md deleted file mode 100644 index ebc4141d1..000000000 --- a/mseries/rust-mutter/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Rust Mutter for Pantheon - -This branch starts the work toward a Rust compositor for the elementary OS -Pantheon stack. The target is a session-level replacement for Gala/Mutter that -can be enabled and rolled back as one package. It is not a dock-firmware fix. - -## Current boundary - -Gala is both Pantheon's window manager and the Wayland display server. In the -current Gala 8.6.2 source it is compiled against `libmutter-14` through -`libmutter-18`, and directly uses Mutter's `Meta` types, Clutter actors, Cogl, -and Mtk geometry. Its Wayland session unit starts `gala` as the display server -and treats a failure as a session failure. Replacing `gala` with an unrelated -compositor would therefore break the session even if ordinary Wayland clients -could still render. - -The first milestone is a Rust compositor that can run nested and prove its -Wayland client, rendering, focus, input, and output contracts without taking -over the user's desktop. After that, move Pantheon window-management and shell -behavior behind Wayland protocols or a small, versioned Pantheon control -protocol. Do not promise a drop-in replacement until Gala's Mutter/Clutter -dependencies have been removed or an explicitly maintained compatibility -layer covers every API Gala uses. - -## Delivery gates - -1. **Nested compositor:** Rust process starts as a nested Wayland client, serves - a second Wayland display, presents a client, and exits without changing - the host session. The first prototype now lives in - [`rust/pantheon-compositor`](../../rust/pantheon-compositor/README.md). It is - based on Smithay's smallvil at pinned revision - `023b78b1c5a05ca512767cacc07efca55fae8be4`. It has not been built or - hardware-qualified yet. -2. **Pantheon shell contract:** enumerate Gala and Pantheon consumers, then - implement window activation, close/minimize/maximize, workspaces, placement, - panel/dock reservations, shell hotkeys, lock-screen handoff, and session - lifecycle. Define only the compatibility protocols actually required by - this stack. -3. **Desktop hardware and input:** DRM/KMS outputs, multi-monitor changes, - libinput, keyboard layouts, accessibility, IBus/input-method protocols, - screencast/remote-desktop portals, clipboard, drag-and-drop, and Xwayland. - Exercise these on supported M-series hardware before changing the default. -4. **Parallel session qualification:** start the new compositor from an - opt-in session entry with the existing Gala session intact as rollback. - Qualify login/logout, apps, panel, dock, settings, lock/unlock, suspend/resume, - hotplug, screen sharing, accessibility, input methods, and crash recovery. -5. **Swap-in:** only after the compatibility matrix is green, package a - reversible session change and collect owner acceptance on hardware. Keep - Gala available until the new session has passed that acceptance. - -## Hardware blocker separation - -The WD22TB4 HDMI goal is tracked in -[`../fairydust/usb4/PRIMARY-GOAL.md`](../fairydust/usb4/PRIMARY-GOAL.md). Its -current G3 gate is DCP link training. A Rust compositor may eventually need to -handle output hotplug and modesets correctly, but it cannot make firmware train -a link that the kernel/DCP path has not trained. Track compositor output -qualification separately from G3 through G5. - -## Source audit - -The initial interface audit used the repository's pinned Gala source at -`elementary/gala` commit `f0791434` (Gala 8.6.2). Its build requires Mutter 46 -through 50 (`libmutter-14` through `libmutter-18`) and corresponding Mutter -Clutter/Cogl/Mtk libraries. Gala's `WindowManager` implementation subclasses -`Meta.Plugin`, manages `Meta.Window`/`Meta.Workspace`, and builds its shell UI -from Clutter actors. Its Wayland desktop entry starts `gala --wayland ---display-server`; the systemd user unit also owns display-server startup and -session-failure handling. - -This branch has not changed the active desktop, kernel, bootloader, or audio -stack. Nothing here claims a running or hardware-qualified replacement yet. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 6ccc36825..70b3242a9 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -1,9 +1,10 @@ # pantheon-compositor -First implementation milestone for the Rust Mutter replacement: an opt-in, -nested Wayland compositor prototype using Smithay's Winit backend. It runs in a -window on an existing Wayland or X11 session and serves its own Wayland socket. -It does not open DRM/KMS, become a login session, or replace Gala. +First development harness for the Mutter-minimal viable Rust effort: an +opt-in, nested Wayland compositor prototype using Smithay's Winit backend. It +runs in a window on an existing Wayland or X11 session and serves its own +Wayland socket. It does not open DRM/KMS, become a login session, or replace +Gala. ## Run @@ -23,7 +24,10 @@ socket. The active session's services and configuration are not touched. ## Current limits This is a nested proof of rendering and basic interactive Wayland surfaces, not -a Mutter port or a Pantheon replacement yet. It currently has one nested output, +a Mutter port or a Pantheon replacement yet. The target is only the +M-series/Pantheon behavior documented in +[`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). +It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows, move/resize grabs, clipboard and drag-and-drop. Touch input is not implemented. DRM/KMS, libinput device management, multi-monitor configuration, accessibility, IBus, Xwayland, screen @@ -32,4 +36,6 @@ protocols, and production crash recovery are not implemented here. The code is based on Smithay's `smallvil` learning compositor at pinned commit `023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the -upstream revision pinned until an explicit upgrade is reviewed. +upstream revision pinned until an explicit upgrade is reviewed. `cargo check +-p pantheon-compositor` passes on the development host; the runtime path has not +yet been exercised in a nested display or on M-series hardware. From 0b7ef976db77eadc073577925720ca06510c13bf Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:24:24 -0400 Subject: [PATCH 036/798] Implement Pantheon panel protocol in nested compositor --- .gitignore | 1 + mseries/OS9-HANDOFF.md | 20 +- mseries/ROADMAP.md | 5 +- rust/Cargo.lock | 3 + rust/pantheon-compositor/Cargo.toml | 3 + rust/pantheon-compositor/README.md | 26 +- rust/pantheon-compositor/THIRD-PARTY.md | 7 + .../protocol/pantheon-desktop-shell-v1.xml | 199 +++++++++ .../src/handlers/compositor.rs | 3 +- .../src/handlers/xdg_shell.rs | 31 +- rust/pantheon-compositor/src/input.rs | 13 +- rust/pantheon-compositor/src/main.rs | 2 + .../pantheon-compositor/src/pantheon_shell.rs | 414 ++++++++++++++++++ rust/pantheon-compositor/src/protocols.rs | 14 + rust/pantheon-compositor/src/state.rs | 19 +- 15 files changed, 739 insertions(+), 21 deletions(-) create mode 100644 rust/pantheon-compositor/protocol/pantheon-desktop-shell-v1.xml create mode 100644 rust/pantheon-compositor/src/pantheon_shell.rs create mode 100644 rust/pantheon-compositor/src/protocols.rs diff --git a/.gitignore b/.gitignore index 510129ac8..d426caf37 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ local-build-records/ *.efi __pycache__/ *.pyc +/rust/target/ mseries/gala-drawers/build/ mkosi.packages/ /mkosi.images/sysupdate/mkosi.extra/efi/m1n1/boot.bin diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 9faa11e67..558f8c137 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5307,10 +5307,24 @@ port targets; each needs a demonstrated workflow. Mutter upstream is a feature reference: its CTM Night Light change is relevant only if the M-series driver exposes CTM, which the current handoff says it did not. +Implemented the first Pantheon shell integration slice in the prototype: +vendored elementary's desktop-shell protocol XML with the repository's v2 +`set_offset` extension, advertised it from Smithay, and handled panel creation, +anchor, size, hide-mode, focus and offset requests. Panel windows are placed on +the nested output; xdg maximize configures the work area after exclusive panel +reservation, while fullscreen uses the full output. Panel clicks do not take +keyboard focus unless the panel explicitly requests it. `cargo check +-p pantheon-compositor` passes after the change. Autohide/overlap tracking, +blur, reflow of already maximized windows, display/session startup and other +Pantheon shell behaviors remain incomplete. Wingpanel and Dock have not yet +been run against the Rust compositor. The nested event loop stayed up for an +8-second Xvfb smoke run on the local build host; no Wayland client connected, +and this is not M-series hardware qualification. + No compositor has been installed and no desktop configuration has changed. No active desktop, kernel, bootloader, or audio-stack changes were made. The WD22TB4 HDMI G3 link-training blocker remains on the DCP/kernel path; it must be tracked separately from Rust compositor output and hotplug qualification. -Next: complete nested runtime/client smoke, then implement the first -experience-derived requirement with a narrow Pantheon shell protocol and -work-area behavior. Do not switch the login session at this stage. +Next: connect a Wayland protocol probe, then qualify the Pantheon protocol +against Wingpanel and Dock and add overlap-based autohide. Do not switch the +login session at this stage. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 70fd509c0..d6d6f47f8 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -13,7 +13,10 @@ profiles/hotplug, window snapping, input and session work. Features such as Xwayland, IBus, accessibility, screencasting and multi-GPU are conditional on an actual supported workflow. The opt-in Smithay nested prototype in `rust/pantheon-compositor/` passes `cargo check`; nested runtime and M-series -hardware remain unqualified. A public search found elementary's +hardware remain unqualified. It now advertises Pantheon's protocol v2, places +panel surfaces using anchor/size/offset, and applies exclusive panel geometry +when configuring maximized windows. Autohide, blur, dynamic reflow of already +maximized windows and session integration remain incomplete. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5b299e2d1..0dced88f9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -839,6 +839,9 @@ dependencies = [ "bitflags", "smithay", "tracing-subscriber", + "wayland-backend", + "wayland-scanner", + "wayland-server", ] [[package]] diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index 912df283a..a712715aa 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -12,6 +12,9 @@ path = "src/main.rs" [dependencies] tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } bitflags = "2.2.1" +wayland-scanner = "0.31.11" +wayland-backend = "0.3.17" +wayland-server = "0.31.14" [dependencies.smithay] git = "https://github.com/Smithay/smithay.git" diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 70b3242a9..f135a8e83 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -29,13 +29,25 @@ M-series/Pantheon behavior documented in [`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows, move/resize grabs, clipboard -and drag-and-drop. Touch input is not implemented. DRM/KMS, libinput device -management, multi-monitor configuration, accessibility, IBus, Xwayland, screen -sharing/remote-desktop portals, lock screen integration, Pantheon panel/dock -protocols, and production crash recovery are not implemented here. +and drag-and-drop. It now advertises the existing Pantheon desktop-shell +protocol v2: panel surfaces can set their anchor, size, hide mode and offset; +the compositor places those surfaces and uses exclusive panel geometry for +xdg-toplevel maximize requests. The Dock's M-series offset request is therefore +available to protocol-v2 clients. + +This is only the first shell integration slice. Autohide/overlap tracking, +animated hiding, blur, overview behavior, full work-area updates for already +maximized windows, and session/lock integration are not complete. Widget, +extended-behavior and greeter protocol requests are accepted but currently have +no behavior. Touch input, DRM/KMS, libinput device management, multi-monitor +configuration, accessibility, IBus, Xwayland, screen-sharing portals and +production crash recovery also remain unimplemented. The code is based on Smithay's `smallvil` learning compositor at pinned commit `023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the -upstream revision pinned until an explicit upgrade is reviewed. `cargo check --p pantheon-compositor` passes on the development host; the runtime path has not -yet been exercised in a nested display or on M-series hardware. +upstream revision pinned until an explicit upgrade is reviewed. The Pantheon +protocol XML is based on elementary/Gala commit `f0791434` (LGPL-2.1-or-later) +with the repository's v2 `set_offset` extension; see `THIRD-PARTY.md`. +`cargo check -p pantheon-compositor` passes on the local build host. The nested +event loop stayed up in an 8-second Xvfb smoke run; no Wayland client connected. +Wingpanel/Dock behavior and M-series hardware remain unqualified. diff --git a/rust/pantheon-compositor/THIRD-PARTY.md b/rust/pantheon-compositor/THIRD-PARTY.md index 6724e2f69..aa736cf97 100644 --- a/rust/pantheon-compositor/THIRD-PARTY.md +++ b/rust/pantheon-compositor/THIRD-PARTY.md @@ -3,3 +3,10 @@ `src/` adapts Smithay's `smallvil` example compositor from . The upstream license is included as `SMITHAY-LICENSE.txt`. + +`protocol/pantheon-desktop-shell-v1.xml` is based on elementary/Gala's +`protocol/pantheon-desktop-shell-v1.xml` at commit `f0791434` +(), released under +LGPL-2.1-or-later. This copy advertises protocol version 2 and includes the +M-series `set_offset` request already used by this repository's Gala/Dock +patches. The source XML retains its copyright and SPDX notice. diff --git a/rust/pantheon-compositor/protocol/pantheon-desktop-shell-v1.xml b/rust/pantheon-compositor/protocol/pantheon-desktop-shell-v1.xml new file mode 100644 index 000000000..5147382f5 --- /dev/null +++ b/rust/pantheon-compositor/protocol/pantheon-desktop-shell-v1.xml @@ -0,0 +1,199 @@ + + + + + SPDX-License-Identifier: LGPL-2.1-or-later + ]]> + + + + This interface is used by the Pantheon Wayland shell to communicate with + the compositor. + + + + + Create a panel surface from an existing surface. + + + + + + + + Create a desktop widget surface from an existing surface. + + + + + + + + Create a desktop-specific surface from an existing surface. + + + + + + + + Create a greeter surface from an existing surface. + + + + + + + + + + + + The anchor is a placement hint to the compositor. + + + + + + + + + + How the shell should handle the window. + + + + + + + + + + + Tell the shell which side of the screen the panel is + located. This is so that new windows do not overlap the panel + and maximized windows maximize properly. + + + + + + + + Request keyboard focus, taking it away from any other window. + Keyboard focus must always be manually be requested and is + - in contrast to normal windows - never automatically granted + by the compositor. + + + + + + The given size is only used for exclusive zones and + collision tracking for auto hide. By default and if set + to -1 the size of the surface is used. + + + + + + + + + Tell the shell when to hide the panel. + + + + + + + + Tell the shell that the panel would like to be visible in the multitasking view. + + + + + + Tell the window manager to add background blur. + + + + + + + + + + + + Tell the window manager to remove blur that was set in set_blur_region. + + + + + + Move the panel by the given number of logical pixels along its edge. + For top and bottom anchors, positive offsets move right. Zero centers it. + + + + + + + + + + + + + + Tell the shell to keep the surface above on all workspaces + + + + + + Request to keep the surface centered. This will cause keyboard focus + to not be granted automatically but having to be requested via focus. + + + + + + Request keyboard focus, taking it away from any other window. + Keyboard focus must always be manually be requested and is + - in contrast to normal windows - never automatically granted + by the compositor. + + + + + + This will block all user input outside the surface and most system shortcuts. + + + + + + + + Request to make the surface a monitor label for the given monitor. The surface will be placed + in the top left corner of the monitor and will be kept above other surfaces. + + + + + + + + + + + + Request to make the surface a Greeter window, which will be displayed bellow other windows + when running in greeter compositor mode. + + + + diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index ba4d8fd91..975ec356a 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -1,4 +1,4 @@ -use crate::{PantheonCompositor, grabs::resize_grab, state::ClientState}; +use crate::{PantheonCompositor, grabs::resize_grab, pantheon_shell, state::ClientState}; use smithay::{ backend::renderer::utils::on_commit_buffer_handler, reexports::wayland_server::{ @@ -42,6 +42,7 @@ impl CompositorHandler for PantheonCompositor { }; xdg_shell::handle_commit(&mut self.popups, &self.space, surface); + pantheon_shell::handle_surface_commit(self, surface); resize_grab::handle_commit(&mut self.space, surface); } } diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 9c93c72f6..5cc68997d 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -1,5 +1,7 @@ use smithay::{ - desktop::{PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords}, + desktop::{ + PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords, + }, input::{ Seat, pointer::{Focus, GrabStartData as PointerGrabStartData}, @@ -41,7 +43,12 @@ impl XdgShellHandler for PantheonCompositor { let _ = self.popups.track_popup(PopupKind::Xdg(surface)); } - fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { + fn reposition_request( + &mut self, + surface: PopupSurface, + positioner: PositionerState, + token: u32, + ) { surface.with_pending_state(|state| { let geometry = positioner.get_geometry(); state.geometry = geometry; @@ -117,6 +124,26 @@ impl XdgShellHandler for PantheonCompositor { } } + fn maximize_request(&mut self, surface: ToplevelSurface) { + self.configure_window_mode(surface, true, false); + } + + fn unmaximize_request(&mut self, surface: ToplevelSurface) { + self.configure_window_mode(surface, false, false); + } + + fn fullscreen_request( + &mut self, + surface: ToplevelSurface, + _output: Option, + ) { + self.configure_window_mode(surface, false, true); + } + + fn unfullscreen_request(&mut self, surface: ToplevelSurface) { + self.configure_window_mode(surface, false, false); + } + fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { // TODO popup grabs } diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 99590c1f1..6c507678d 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -70,12 +70,13 @@ impl PantheonCompositor { .element_under(pointer.current_location()) .map(|(w, l)| (w.clone(), l)) { - self.space.raise_element(&window, true); - keyboard.set_focus( - self, - Some(window.toplevel().unwrap().wl_surface().clone()), - serial, - ); + let surface = window.toplevel().unwrap().wl_surface().clone(); + if self.is_panel_surface(&surface) { + self.space.raise_element(&window, false); + } else { + self.space.raise_element(&window, true); + keyboard.set_focus(self, Some(surface), serial); + } self.space.elements().for_each(|window| { window.toplevel().unwrap().send_pending_configure(); }); diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index a3c940f84..bbc55a231 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -3,6 +3,8 @@ mod handlers; mod grabs; mod input; +mod pantheon_shell; +mod protocols; mod state; mod winit; diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs new file mode 100644 index 000000000..9c6676051 --- /dev/null +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -0,0 +1,414 @@ +use smithay::{ + desktop::Window, + reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::GlobalId, protocol::wl_surface::WlSurface, + }, + utils::{Logical, Point, Rectangle, SERIAL_COUNTER, Size}, + wayland::shell::xdg::ToplevelSurface, +}; + +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; + +use crate::{ + PantheonCompositor, protocols::pantheon_desktop_shell_v1::server as protocol, + state::ManagedWindow, +}; + +pub struct PantheonShellState { + _global: GlobalId, +} + +impl PantheonShellState { + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch< + protocol::io_elementary_pantheon_shell_v1::IoElementaryPantheonShellV1, + (), + > + 'static, + { + let global = display.create_global::(2, ()); + Self { _global: global } + } +} + +#[derive(Clone)] +pub struct ShellSurfaceData { + pub surface: WlSurface, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct PanelState { + /// Pantheon protocol values: top=0, bottom=1, left=2, right=3. + pub anchor: Option, + /// Non-positive dimensions use the committed surface size. + pub width: i32, + pub height: i32, + pub offset: i32, + /// Pantheon protocol values: never=0, maximized-focused=1, + /// overlapping-focused=2, overlapping-any=3, always=4. + pub hide_mode: u32, + pub visible_in_multitasking_view: bool, +} + +#[derive(Clone)] +pub struct PanelSurfaceData { + pub surface: WlSurface, +} + +impl GlobalDispatch + for PantheonCompositor +{ + fn bind( + _state: &mut Self, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &(), + data_init: &mut DataInit<'_, Self>, + ) { + data_init.init(resource, ()); + } +} + +impl Dispatch + for PantheonCompositor +{ + fn request( + state: &mut Self, + _client: &Client, + _resource: &protocol::io_elementary_pantheon_shell_v1::IoElementaryPantheonShellV1, + request: protocol::io_elementary_pantheon_shell_v1::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + match request { + protocol::io_elementary_pantheon_shell_v1::Request::GetPanel { output, surface } => { + state.add_panel(surface.clone()); + data_init.init(output, PanelSurfaceData { surface }); + } + protocol::io_elementary_pantheon_shell_v1::Request::GetWidget { output, surface } => { + data_init.init(output, ShellSurfaceData { surface }); + } + protocol::io_elementary_pantheon_shell_v1::Request::GetExtendedBehavior { + output, + surface, + } => { + data_init.init(output, ShellSurfaceData { surface }); + } + protocol::io_elementary_pantheon_shell_v1::Request::GetGreeter { output, surface } => { + data_init.init(output, ShellSurfaceData { surface }); + } + } + } +} + +impl + Dispatch< + protocol::io_elementary_pantheon_panel_v1::IoElementaryPantheonPanelV1, + PanelSurfaceData, + > for PantheonCompositor +{ + fn request( + state: &mut Self, + _client: &Client, + _resource: &protocol::io_elementary_pantheon_panel_v1::IoElementaryPantheonPanelV1, + request: protocol::io_elementary_pantheon_panel_v1::Request, + data: &PanelSurfaceData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + let surface = &data.surface; + match request { + protocol::io_elementary_pantheon_panel_v1::Request::Destroy => {} + protocol::io_elementary_pantheon_panel_v1::Request::SetAnchor { anchor } => { + state.update_panel(surface, |panel| panel.anchor = Some(anchor.into())); + } + protocol::io_elementary_pantheon_panel_v1::Request::Focus => { + if state.find_window_for_surface(surface).is_some() { + if let Some(keyboard) = state.seat.get_keyboard() { + keyboard.set_focus( + state, + Some(surface.clone()), + SERIAL_COUNTER.next_serial(), + ); + } + } + } + protocol::io_elementary_pantheon_panel_v1::Request::SetSize { width, height } => { + state.update_panel(surface, |panel| { + panel.width = width; + panel.height = height; + }); + } + protocol::io_elementary_pantheon_panel_v1::Request::SetHideMode { hide_mode } => { + state.update_panel(surface, |panel| panel.hide_mode = hide_mode.into()); + } + protocol::io_elementary_pantheon_panel_v1::Request::RequestVisibleInMultitaskingView => { + state.update_panel(surface, |panel| panel.visible_in_multitasking_view = true); + } + protocol::io_elementary_pantheon_panel_v1::Request::AddBlur { .. } + | protocol::io_elementary_pantheon_panel_v1::Request::RemoveBlur => { + // The nested prototype has no blur renderer yet. + } + protocol::io_elementary_pantheon_panel_v1::Request::SetOffset { offset } => { + state.update_panel(surface, |panel| panel.offset = offset); + } + } + } + + fn destroyed( + state: &mut Self, + _client: smithay::reexports::wayland_server::backend::ClientId, + _resource: &protocol::io_elementary_pantheon_panel_v1::IoElementaryPantheonPanelV1, + data: &PanelSurfaceData, + ) { + state.remove_panel(&data.surface); + } +} + +impl + Dispatch< + protocol::io_elementary_pantheon_widget_v1::IoElementaryPantheonWidgetV1, + ShellSurfaceData, + > for PantheonCompositor +{ + fn request( + _state: &mut Self, + _client: &Client, + _resource: &protocol::io_elementary_pantheon_widget_v1::IoElementaryPantheonWidgetV1, + _request: protocol::io_elementary_pantheon_widget_v1::Request, + _data: &ShellSurfaceData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +impl Dispatch + for PantheonCompositor +{ + fn request( + _state: &mut Self, + _client: &Client, + _resource: &protocol::io_elementary_pantheon_extended_behavior_v1::IoElementaryPantheonExtendedBehaviorV1, + _request: protocol::io_elementary_pantheon_extended_behavior_v1::Request, + _data: &ShellSurfaceData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +impl + Dispatch< + protocol::io_elementary_pantheon_greeter_v1::IoElementaryPantheonGreeterV1, + ShellSurfaceData, + > for PantheonCompositor +{ + fn request( + _state: &mut Self, + _client: &Client, + _resource: &protocol::io_elementary_pantheon_greeter_v1::IoElementaryPantheonGreeterV1, + _request: protocol::io_elementary_pantheon_greeter_v1::Request, + _data: &ShellSurfaceData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + } +} + +impl PantheonCompositor { + pub fn add_panel(&mut self, surface: WlSurface) { + self.panels.insert(surface.id(), PanelState::default()); + self.position_panel(&surface); + } + + pub fn remove_panel(&mut self, surface: &WlSurface) { + self.panels.remove(&surface.id()); + } + + pub fn update_panel(&mut self, surface: &WlSurface, update: impl FnOnce(&mut PanelState)) { + let panel = self.panels.entry(surface.id()).or_default(); + update(panel); + self.position_panel(surface); + } + + pub fn is_panel_surface(&self, surface: &WlSurface) -> bool { + self.panels.contains_key(&surface.id()) + } + + pub fn work_area_for_output( + &self, + output_geometry: Rectangle, + ) -> Rectangle { + let (mut top, mut bottom, mut left, mut right) = (0, 0, 0, 0); + for (surface_id, panel) in &self.panels { + if panel.anchor.is_none() || panel.hide_mode != 0 { + continue; + } + let Some(window) = self.space.elements().find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface().id() == *surface_id) + }) else { + continue; + }; + let rect = self.space.element_geometry(window).unwrap_or_else(|| { + let geometry = window.geometry(); + Rectangle::new( + self.space.element_location(window).unwrap_or_default(), + geometry.size, + ) + }); + let Some(intersection) = rect.intersection(output_geometry) else { + continue; + }; + match panel.anchor.unwrap() { + 0 => top = top.max(intersection.size.h), + 1 => bottom = bottom.max(intersection.size.h), + 2 => left = left.max(intersection.size.w), + 3 => right = right.max(intersection.size.w), + _ => {} + } + } + + let left = left.min(output_geometry.size.w); + let right = right.min(output_geometry.size.w - left); + let top = top.min(output_geometry.size.h); + let bottom = bottom.min(output_geometry.size.h - top); + Rectangle::new( + Point::from((output_geometry.loc.x + left, output_geometry.loc.y + top)), + Size::from(( + output_geometry.size.w - left - right, + output_geometry.size.h - top - bottom, + )), + ) + } + + fn find_window_for_surface(&self, surface: &WlSurface) -> Option { + self.space + .elements() + .find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == surface) + }) + .cloned() + } + + fn position_panel(&mut self, surface: &WlSurface) { + let Some(panel) = self.panels.get(&surface.id()).copied() else { + return; + }; + let Some(anchor) = panel.anchor else { + return; + }; + let Some(window) = self.find_window_for_surface(surface) else { + return; + }; + let Some(output) = self.space.outputs().next().cloned() else { + return; + }; + let Some(output_geometry) = self.space.output_geometry(&output) else { + return; + }; + + let surface_size = window.geometry().size; + let width = if panel.width > 0 { + panel.width + } else { + surface_size.w + }; + let height = if panel.height > 0 { + panel.height + } else { + surface_size.h + }; + let offset = panel.offset; + let location = match anchor { + 0 => ( + output_geometry.loc.x + (output_geometry.size.w - width) / 2 + offset, + output_geometry.loc.y, + ), + 1 => ( + output_geometry.loc.x + (output_geometry.size.w - width) / 2 + offset, + output_geometry.loc.y + output_geometry.size.h - height, + ), + 2 => ( + output_geometry.loc.x, + output_geometry.loc.y + (output_geometry.size.h - height) / 2 + offset, + ), + 3 => ( + output_geometry.loc.x + output_geometry.size.w - width, + output_geometry.loc.y + (output_geometry.size.h - height) / 2 + offset, + ), + _ => return, + }; + self.space.map_element(window.clone(), location, false); + self.space.raise_element(&window, false); + } + + pub fn configure_window_mode( + &mut self, + surface: ToplevelSurface, + maximized: bool, + fullscreen: bool, + ) { + let wl_surface = surface.wl_surface(); + let Some(window) = self.find_window_for_surface(wl_surface) else { + return; + }; + + let surface_id = wl_surface.id(); + let restore = *self + .managed_windows + .entry(surface_id.clone()) + .or_insert_with(|| ManagedWindow { + restore_location: self.space.element_location(&window).unwrap_or_default(), + restore_size: window.geometry().size, + }); + + let Some(output) = self.space.outputs().next().cloned() else { + return; + }; + let Some(output_geometry) = self.space.output_geometry(&output) else { + return; + }; + + let target = if fullscreen { + output_geometry + } else if maximized { + self.work_area_for_output(output_geometry) + } else { + Rectangle::new(restore.restore_location, restore.restore_size) + }; + + surface.with_pending_state(|state| { + if maximized { + state.states.set(xdg_toplevel::State::Maximized); + } else { + state.states.unset(xdg_toplevel::State::Maximized); + } + if fullscreen { + state.states.set(xdg_toplevel::State::Fullscreen); + } else { + state.states.unset(xdg_toplevel::State::Fullscreen); + } + state.size = Some(target.size); + }); + surface.send_pending_configure(); + self.space.map_element(window, target.loc, false); + + if !maximized && !fullscreen { + self.managed_windows.remove(&surface_id); + } + } +} + +pub fn handle_surface_commit(state: &mut PantheonCompositor, surface: &WlSurface) { + if state.is_panel_surface(surface) { + state.update_panel(surface, |_| {}); + } +} diff --git a/rust/pantheon-compositor/src/protocols.rs b/rust/pantheon-compositor/src/protocols.rs new file mode 100644 index 000000000..1ba5471c3 --- /dev/null +++ b/rust/pantheon-compositor/src/protocols.rs @@ -0,0 +1,14 @@ +pub mod pantheon_desktop_shell_v1 { + pub mod server { + use smithay::reexports::wayland_server; + use wayland_server::protocol::*; + + pub mod __interfaces { + use wayland_server::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("protocol/pantheon-desktop-shell-v1.xml"); + } + + use self::__interfaces::*; + wayland_scanner::generate_server_code!("protocol/pantheon-desktop-shell-v1.xml"); + } +} diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index a61e59939..d44a75c48 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::{ffi::OsString, sync::Arc}; use smithay::{ @@ -11,7 +12,7 @@ use smithay::{ protocol::wl_surface::WlSurface, }, }, - utils::{Logical, Point}, + utils::{Logical, Point, Size}, wayland::{ compositor::{CompositorClientState, CompositorState}, output::OutputManagerState, @@ -22,6 +23,8 @@ use smithay::{ }, }; +use crate::pantheon_shell::{PanelState, PantheonShellState}; + pub struct PantheonCompositor { pub start_time: std::time::Instant, pub socket_name: OsString, @@ -37,11 +40,21 @@ pub struct PantheonCompositor { pub output_manager_state: OutputManagerState, pub seat_state: SeatState, pub data_device_state: DataDeviceState, + pub pantheon_shell_state: PantheonShellState, + pub panels: HashMap, + pub managed_windows: + HashMap, pub popups: PopupManager, pub seat: Seat, } +#[derive(Clone, Copy)] +pub struct ManagedWindow { + pub restore_location: Point, + pub restore_size: Size, +} + impl PantheonCompositor { pub fn new(event_loop: &mut EventLoop, display: Display) -> Self { let start_time = std::time::Instant::now(); @@ -62,6 +75,7 @@ impl PantheonCompositor { // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); + let pantheon_shell_state = PantheonShellState::new::(&dh); // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. @@ -102,6 +116,9 @@ impl PantheonCompositor { output_manager_state, seat_state, data_device_state, + pantheon_shell_state, + panels: HashMap::new(), + managed_windows: HashMap::new(), popups, seat, } From bc6097ab8d72d13ac0e4efae1f2797104176a632 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:26:32 -0400 Subject: [PATCH 037/798] Reflow maximized windows with Pantheon panel work area --- mseries/OS9-HANDOFF.md | 12 ++++ mseries/ROADMAP.md | 6 +- .../EXPERIENCE-REQUIREMENTS.md | 7 ++- rust/pantheon-compositor/README.md | 8 ++- .../pantheon-compositor/src/pantheon_shell.rs | 59 ++++++++++++++++++- rust/pantheon-compositor/src/state.rs | 4 ++ 6 files changed, 86 insertions(+), 10 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 558f8c137..8dcfbb705 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,17 @@ # OS 9 implementation / Claude Code handoff +## Mutter-minimal viable Rust compositor progress — 2026-10-18 + +On branch `codex/rust-mutter-pantheon`, the nested Smithay compositor now +reconfigures existing maximized windows when a committed Pantheon panel change +alters its exclusive work area. It caches the last work-area rectangle so normal +panel repaints do not trigger redundant xdg configure events. `cargo check +-p pantheon-compositor` passes on the local build host. This remains unqualified +with a Wayland panel client or on M-series DRM/KMS hardware. Next: connect an +isolated Wayland client probe, then qualify Wingpanel/Dock protocol behavior +before implementing overlap-based autohide. The WD22TB4 HDMI G3 DCP/kernel issue +remains separate. Scope and limitations: `mseries/pantheon-rust-compositor/`. + ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 Removed “local” from the seven-day panel/card labels and moved counting scope into diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index d6d6f47f8..ec017bb5f 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -15,8 +15,10 @@ an actual supported workflow. The opt-in Smithay nested prototype in `rust/pantheon-compositor/` passes `cargo check`; nested runtime and M-series hardware remain unqualified. It now advertises Pantheon's protocol v2, places panel surfaces using anchor/size/offset, and applies exclusive panel geometry -when configuring maximized windows. Autohide, blur, dynamic reflow of already -maximized windows and session integration remain incomplete. A public search found elementary's +when configuring maximized windows. A changed panel work area now reconfigures +already maximized windows, while unchanged panel repaints do not send redundant +configures. Autohide, blur, multi-output reflow and session integration remain +incomplete. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 89cb96d33..2c33da156 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -77,5 +77,8 @@ planning against a release. 4. Add conditional capabilities only against named apps/workflows and explicit acceptance checks. Keep Gala available as rollback until owner acceptance. -The prototype currently only passes `cargo check`; it has not yet run with a -client or on M-series hardware. A check is not session or hardware qualification. +The nested prototype serves the Pantheon shell protocol v2, places a panel from +its anchor/size/offset, reserves exclusive panel geometry, and updates already +maximized windows when that work area changes. The last local `cargo check` +passes. It has not yet run with a Wayland client or on M-series hardware. A +build check is not session or hardware qualification. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index f135a8e83..e06788989 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -33,11 +33,13 @@ and drag-and-drop. It now advertises the existing Pantheon desktop-shell protocol v2: panel surfaces can set their anchor, size, hide mode and offset; the compositor places those surfaces and uses exclusive panel geometry for xdg-toplevel maximize requests. The Dock's M-series offset request is therefore -available to protocol-v2 clients. +available to protocol-v2 clients. When panel commits change the exclusive work +area, already maximized windows are reconfigured to its new bounds; unchanged +panel repaints do not issue redundant configures. This is only the first shell integration slice. Autohide/overlap tracking, -animated hiding, blur, overview behavior, full work-area updates for already -maximized windows, and session/lock integration are not complete. Widget, +animated hiding, blur, overview behavior, multi-output work-area updates for +already maximized windows, and session/lock integration are not complete. Widget, extended-behavior and greeter protocol requests are accepted but currently have no behavior. Touch input, DRM/KMS, libinput device management, multi-monitor configuration, accessibility, IBus, Xwayland, screen-sharing portals and diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 9c6676051..25dfe0ed9 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -233,6 +233,9 @@ impl PantheonCompositor { let panel = self.panels.entry(surface.id()).or_default(); update(panel); self.position_panel(surface); + if self.refresh_work_area() { + self.reconfigure_managed_windows(); + } } pub fn is_panel_surface(&self, surface: &WlSurface) -> bool { @@ -287,6 +290,21 @@ impl PantheonCompositor { ) } + fn refresh_work_area(&mut self) -> bool { + let Some(output) = self.space.outputs().next().cloned() else { + return false; + }; + let Some(output_geometry) = self.space.output_geometry(&output) else { + return false; + }; + let work_area = self.work_area_for_output(output_geometry); + if self.last_work_area == Some(work_area) { + return false; + } + self.last_work_area = Some(work_area); + true + } + fn find_window_for_surface(&self, surface: &WlSurface) -> Option { self.space .elements() @@ -368,8 +386,30 @@ impl PantheonCompositor { .or_insert_with(|| ManagedWindow { restore_location: self.space.element_location(&window).unwrap_or_default(), restore_size: window.geometry().size, + maximized: false, + fullscreen: false, }); + if let Some(managed) = self.managed_windows.get_mut(&surface_id) { + managed.maximized = maximized; + managed.fullscreen = fullscreen; + } + + self.apply_window_mode(&surface, &window, restore, maximized, fullscreen); + + if !maximized && !fullscreen { + self.managed_windows.remove(&surface_id); + } + } + + fn apply_window_mode( + &mut self, + surface: &ToplevelSurface, + window: &Window, + restore: ManagedWindow, + maximized: bool, + fullscreen: bool, + ) { let Some(output) = self.space.outputs().next().cloned() else { return; }; @@ -399,10 +439,23 @@ impl PantheonCompositor { state.size = Some(target.size); }); surface.send_pending_configure(); - self.space.map_element(window, target.loc, false); + self.space.map_element(window.clone(), target.loc, false); + } - if !maximized && !fullscreen { - self.managed_windows.remove(&surface_id); + fn reconfigure_managed_windows(&mut self) { + let mut managed = Vec::new(); + for window in self.space.elements() { + let Some(surface) = window.toplevel().cloned() else { + continue; + }; + let Some(state) = self.managed_windows.get(&surface.wl_surface().id()) else { + continue; + }; + managed.push((surface, (*window).clone(), *state)); + } + + for (surface, window, state) in managed { + self.apply_window_mode(&surface, &window, state, state.maximized, state.fullscreen); } } } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index d44a75c48..46f3a7268 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -42,6 +42,7 @@ pub struct PantheonCompositor { pub data_device_state: DataDeviceState, pub pantheon_shell_state: PantheonShellState, pub panels: HashMap, + pub last_work_area: Option>, pub managed_windows: HashMap, pub popups: PopupManager, @@ -53,6 +54,8 @@ pub struct PantheonCompositor { pub struct ManagedWindow { pub restore_location: Point, pub restore_size: Size, + pub maximized: bool, + pub fullscreen: bool, } impl PantheonCompositor { @@ -118,6 +121,7 @@ impl PantheonCompositor { data_device_state, pantheon_shell_state, panels: HashMap::new(), + last_work_area: None, managed_windows: HashMap::new(), popups, seat, From f8d9e55ffb4b4fd01f216ccebee50adeee072340 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:30:43 -0400 Subject: [PATCH 038/798] Add Pantheon shell protocol integration probe --- mseries/OS9-HANDOFF.md | 25 ++++--- mseries/ROADMAP.md | 14 ++-- .../EXPERIENCE-REQUIREMENTS.md | 8 ++- rust/Cargo.lock | 1 + rust/pantheon-compositor/Cargo.toml | 5 ++ rust/pantheon-compositor/README.md | 28 +++++++- .../src/bin/pantheon-shell-probe.rs | 67 +++++++++++++++++++ rust/pantheon-compositor/src/protocols.rs | 13 ++++ 8 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 rust/pantheon-compositor/src/bin/pantheon-shell-probe.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 8dcfbb705..0b96ab775 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -2,15 +2,22 @@ ## Mutter-minimal viable Rust compositor progress — 2026-10-18 -On branch `codex/rust-mutter-pantheon`, the nested Smithay compositor now -reconfigures existing maximized windows when a committed Pantheon panel change -alters its exclusive work area. It caches the last work-area rectangle so normal -panel repaints do not trigger redundant xdg configure events. `cargo check --p pantheon-compositor` passes on the local build host. This remains unqualified -with a Wayland panel client or on M-series DRM/KMS hardware. Next: connect an -isolated Wayland client probe, then qualify Wingpanel/Dock protocol behavior -before implementing overlap-based autohide. The WD22TB4 HDMI G3 DCP/kernel issue -remains separate. Scope and limitations: `mseries/pantheon-rust-compositor/`. +On branch `codex/rust-mutter-pantheon`, commit `bc6097a` reconfigures already +maximized windows when a committed Pantheon panel change alters its exclusive +work area; unchanged panel repaints do not trigger redundant xdg configure +events. Added `pantheon-shell-probe`, which binds shell/panel v2 and sends +`get_panel`, anchor, size, hide-mode, and offset requests. Local `cargo check` +and `cargo build --bins` pass. The 12-second Xvfb nested run completed the probe +over Wayland using Mesa llvmpipe. Then installed Wingpanel 8.0.4 ran against the +nested socket under isolated D-Bus/Xvfb and forced Wayland. Its client trace +shows `get_panel`, `set_anchor(top)`, `set_hide_mode(never)`, and attached SHM +buffers. It also reports missing `org.gnome.Mutter.DisplayConfig` and GNOME +Shell keybinding/sound services; implement only the compatibility surface +Wingpanel needs, based on the client API, rather than Mutter itself. Dock +behavior and M-series DRM/KMS remain unqualified. Next: identify and implement +the minimal display-config query used by Wingpanel, then run Dock and panel +geometry checks against the nested output. The WD22TB4 HDMI G3 DCP/kernel issue +remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ec017bb5f..d08ef7c90 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -12,13 +12,19 @@ The list is grounded in this repo's notch-aware panel, dock work area, display profiles/hotplug, window snapping, input and session work. Features such as Xwayland, IBus, accessibility, screencasting and multi-GPU are conditional on an actual supported workflow. The opt-in Smithay nested prototype in -`rust/pantheon-compositor/` passes `cargo check`; nested runtime and M-series -hardware remain unqualified. It now advertises Pantheon's protocol v2, places +`rust/pantheon-compositor/` passes local `cargo check` and `cargo build`. It +advertises Pantheon's protocol v2, places panel surfaces using anchor/size/offset, and applies exclusive panel geometry when configuring maximized windows. A changed panel work area now reconfigures already maximized windows, while unchanged panel repaints do not send redundant -configures. Autohide, blur, multi-output reflow and session integration remain -incomplete. A public search found elementary's +configures. Its v2 protocol probe completed a nested Xvfb run and sent panel +requests over the Wayland socket. Installed Wingpanel 8.0.4 also connected in an +isolated nested Wayland run, sent `get_panel`, anchor and hide-mode requests, +and attached SHM buffers. It reports the absent `org.gnome.Mutter.DisplayConfig` +service (plus GNOME Shell keybinding/sound hooks), so the next slice is to +identify and provide only the display-config query Wingpanel requires. +Autohide, blur, multi-output reflow, Dock behavior, full session integration +and M-series hardware remain unqualified. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 2c33da156..4e4a614af 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -80,5 +80,9 @@ planning against a release. The nested prototype serves the Pantheon shell protocol v2, places a panel from its anchor/size/offset, reserves exclusive panel geometry, and updates already maximized windows when that work area changes. The last local `cargo check` -passes. It has not yet run with a Wayland client or on M-series hardware. A -build check is not session or hardware qualification. +and `cargo build` pass. Its protocol probe completed under Xvfb over a nested +Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested +Wayland session and rendered SHM buffers, but reports that +`org.gnome.Mutter.DisplayConfig` is unavailable. Dock integration and M-series +hardware remain unqualified; this is not full session or hardware +qualification. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0dced88f9..5ed32c878 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -840,6 +840,7 @@ dependencies = [ "smithay", "tracing-subscriber", "wayland-backend", + "wayland-client", "wayland-scanner", "wayland-server", ] diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index a712715aa..2b893c650 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -9,11 +9,16 @@ description = "Nested Rust Wayland compositor prototype for elementary OS" name = "pantheon-compositor" path = "src/main.rs" +[[bin]] +name = "pantheon-shell-probe" +path = "src/bin/pantheon-shell-probe.rs" + [dependencies] tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } bitflags = "2.2.1" wayland-scanner = "0.31.11" wayland-backend = "0.3.17" +wayland-client = "0.31.11" wayland-server = "0.31.14" [dependencies.smithay] diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index e06788989..53b98e29f 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -21,6 +21,20 @@ a client. `WAYLAND_DISPLAY` is changed only inside the compositor process after the host Winit backend has initialized; the requested child uses the nested socket. The active session's services and configuration are not touched. +To exercise the Pantheon protocol against the nested socket, build both binaries +and start the compositor with its probe client: + +```sh +cargo build -p pantheon-compositor --bins +cargo run -p pantheon-compositor -- --run target/debug/pantheon-shell-probe +``` + +The probe binds the shell and panel v2 interfaces, creates a Wayland surface, +and sends `get_panel`, anchor, size, hide-mode, and offset requests. It has +passed under Xvfb with Mesa llvmpipe. It does not attach a buffer or xdg role, so +it validates protocol dispatch rather than panel rendering or Wingpanel/Dock +integration. + ## Current limits This is a nested proof of rendering and basic interactive Wayland surfaces, not @@ -50,6 +64,14 @@ The code is based on Smithay's `smallvil` learning compositor at pinned commit upstream revision pinned until an explicit upgrade is reviewed. The Pantheon protocol XML is based on elementary/Gala commit `f0791434` (LGPL-2.1-or-later) with the repository's v2 `set_offset` extension; see `THIRD-PARTY.md`. -`cargo check -p pantheon-compositor` passes on the local build host. The nested -event loop stayed up in an 8-second Xvfb smoke run; no Wayland client connected. -Wingpanel/Dock behavior and M-series hardware remain unqualified. +Local `cargo check -p pantheon-compositor --bin pantheon-shell-probe` and +`cargo build -p pantheon-compositor --bins` pass. The compositor stayed alive +for the 12-second Xvfb integration smoke and the probe completed its protocol +requests. Installed Wingpanel 8.0.4 also connected in an isolated D-Bus/Xvfb +run forced to Wayland. Its client trace confirms it bound the shell global, +sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated +and attached shared-memory buffers. It logged that +`org.gnome.Mutter.DisplayConfig` is unavailable; its GNOME Shell keybinding and +sound hooks also had no service on the isolated bus. This is evidence of panel +client compatibility at the protocol/rendering path, not full session +qualification. Dock behavior and M-series DRM/KMS remain unqualified. diff --git a/rust/pantheon-compositor/src/bin/pantheon-shell-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-shell-probe.rs new file mode 100644 index 000000000..65e12c350 --- /dev/null +++ b/rust/pantheon-compositor/src/bin/pantheon-shell-probe.rs @@ -0,0 +1,67 @@ +use wayland_client::protocol::{wl_compositor, wl_registry, wl_surface}; +use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, globals::registry_queue_init}; + +mod protocols { + include!("../protocols.rs"); +} + +use protocols::pantheon_desktop_shell_v1::client::{ + io_elementary_pantheon_panel_v1::{Anchor, HideMode, IoElementaryPantheonPanelV1}, + io_elementary_pantheon_shell_v1::IoElementaryPantheonShellV1, +}; + +struct Probe; + +impl Dispatch for Probe { + fn event( + _state: &mut Self, + _registry: &wl_registry::WlRegistry, + _event: wl_registry::Event, + _data: &wayland_client::globals::GlobalListContents, + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +wayland_client::delegate_noop!(Probe: ignore wl_compositor::WlCompositor); +wayland_client::delegate_noop!(Probe: ignore wl_surface::WlSurface); +wayland_client::delegate_noop!(Probe: ignore IoElementaryPantheonPanelV1); + +impl Dispatch for Probe { + fn event( + _state: &mut Self, + _shell: &IoElementaryPantheonShellV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +fn main() -> Result<(), Box> { + let connection = Connection::connect_to_env()?; + let (globals, mut queue) = registry_queue_init::(&connection)?; + let qh = queue.handle(); + let compositor: wl_compositor::WlCompositor = globals.bind(&qh, 1..=6, ())?; + let shell: IoElementaryPantheonShellV1 = globals.bind(&qh, 1..=2, ())?; + + let surface = compositor.create_surface(&qh, ()); + let panel = shell.get_panel(&surface, &qh, ()); + panel.set_anchor(Anchor::Top); + panel.set_size(600, 32); + panel.set_hide_mode(HideMode::Never); + panel.set_offset(0); + queue.roundtrip(&mut Probe)?; + + println!( + "Pantheon shell v{} accepted get_panel and panel v{} accepted anchor, size, hide mode, and offset", + shell.version(), + panel.version() + ); + panel.destroy(); + surface.destroy(); + connection.flush()?; + Ok(()) +} diff --git a/rust/pantheon-compositor/src/protocols.rs b/rust/pantheon-compositor/src/protocols.rs index 1ba5471c3..056f44644 100644 --- a/rust/pantheon-compositor/src/protocols.rs +++ b/rust/pantheon-compositor/src/protocols.rs @@ -1,4 +1,17 @@ pub mod pantheon_desktop_shell_v1 { + pub mod client { + use wayland_client; + use wayland_client::protocol::*; + + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("protocol/pantheon-desktop-shell-v1.xml"); + } + + use self::__interfaces::*; + wayland_scanner::generate_client_code!("protocol/pantheon-desktop-shell-v1.xml"); + } + pub mod server { use smithay::reexports::wayland_server; use wayland_server::protocol::*; From 19aa3f78312628b237fc1ec65d1329d230719014 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:35:41 -0400 Subject: [PATCH 039/798] Roadmap: v0.0.1 stages (F5 dictation parity first, support assistants after) Co-Authored-By: Claude Opus 5.5 --- mseries/OS9-HANDOFF.md | 15 +++++++++++++++ mseries/ROADMAP.md | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 6fb31b075..a52ac094c 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,20 @@ # OS 9 implementation / Claude Code handoff +## Release stages: F5 parity first, support assistants after — 2026-10-18 + +Owner's order: + +1. **Stage 1 (gates v0.0.1):** F5 speech-to-text with the ease of macOS dictation + ([#196](https://github.com/techmore/ElementaryOS_MSeries/issues/196)), plus the + native elementary OS / Granite app audit + ([#242](https://github.com/techmore/ElementaryOS_MSeries/issues/242)). Open + gate: physical F5 in ChatGPT still fails (see the v0.0.1 F5 entry below). +2. **Stage 2:** support assistants on the shared Pantheon MCP broker, starting + with troubleshooting ([#203](https://github.com/techmore/ElementaryOS_MSeries/issues/203), + draft PR #185, disabled). Speech and Share eyes become clients of that broker. + +v0.0.1 is cut (`dev` → `main`, see `mseries/BRANCHING.md`) only when stage 1 passes. + ## v0.0.1 English F5 candidate; ChatGPT input correction staged — 2026-10-18 The owner narrowed v0.0.1 to English F5 speech to text on the existing M1 Pro diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index eaca655ac..cca7857d4 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,5 +1,20 @@ # Roadmap, features and follow-ups (MacBook Pro 14" M1 Pro, elementary OS 9 on Asahi) +## Release stages: F5 parity first, support assistants after — 2026-10-18 + +Owner's order: + +1. **Stage 1 (gates v0.0.1):** F5 speech-to-text with the ease of macOS dictation + ([#196](https://github.com/techmore/ElementaryOS_MSeries/issues/196)), plus the + native elementary OS / Granite app audit + ([#242](https://github.com/techmore/ElementaryOS_MSeries/issues/242)). Open + gate: physical F5 in ChatGPT still fails (see the v0.0.1 F5 entry below). +2. **Stage 2:** support assistants on the shared Pantheon MCP broker, starting + with troubleshooting ([#203](https://github.com/techmore/ElementaryOS_MSeries/issues/203), + draft PR #185, disabled). Speech and Share eyes become clients of that broker. + +v0.0.1 is cut (`dev` → `main`, see `mseries/BRANCHING.md`) only when stage 1 passes. + ## v0.0.1 English F5 candidate; ChatGPT input correction staged — 2026-10-18 The owner narrowed v0.0.1 to English F5 speech to text on the existing M1 Pro From e57a42c2c5e4483875b97be248192aa17657711b Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:36:43 -0400 Subject: [PATCH 040/798] Serve Wingpanel display config query --- mseries/OS9-HANDOFF.md | 21 +- mseries/ROADMAP.md | 10 +- .../EXPERIENCE-REQUIREMENTS.md | 8 +- rust/Cargo.lock | 394 ++++++++++++++++++ rust/pantheon-compositor/Cargo.toml | 2 + rust/pantheon-compositor/README.md | 14 +- rust/pantheon-compositor/THIRD-PARTY.md | 8 + .../pantheon-compositor/src/display_config.rs | 158 +++++++ rust/pantheon-compositor/src/main.rs | 1 + rust/pantheon-compositor/src/state.rs | 4 + rust/pantheon-compositor/src/winit.rs | 30 +- 11 files changed, 625 insertions(+), 25 deletions(-) create mode 100644 rust/pantheon-compositor/src/display_config.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 0b96ab775..2b56d8f4d 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -2,7 +2,7 @@ ## Mutter-minimal viable Rust compositor progress — 2026-10-18 -On branch `codex/rust-mutter-pantheon`, commit `bc6097a` reconfigures already +On branch `codex/rust-mutter-pantheon`, recent changes reconfigure already maximized windows when a committed Pantheon panel change alters its exclusive work area; unchanged panel repaints do not trigger redundant xdg configure events. Added `pantheon-shell-probe`, which binds shell/panel v2 and sends @@ -11,13 +11,18 @@ and `cargo build --bins` pass. The 12-second Xvfb nested run completed the probe over Wayland using Mesa llvmpipe. Then installed Wingpanel 8.0.4 ran against the nested socket under isolated D-Bus/Xvfb and forced Wayland. Its client trace shows `get_panel`, `set_anchor(top)`, `set_hide_mode(never)`, and attached SHM -buffers. It also reports missing `org.gnome.Mutter.DisplayConfig` and GNOME -Shell keybinding/sound services; implement only the compatibility surface -Wingpanel needs, based on the client API, rather than Mutter itself. Dock -behavior and M-series DRM/KMS remain unqualified. Next: identify and implement -the minimal display-config query used by Wingpanel, then run Dock and panel -geometry checks against the nested output. The WD22TB4 HDMI G3 DCP/kernel issue -remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. +buffers. It also reported missing `org.gnome.Mutter.DisplayConfig` and GNOME +Shell keybinding/sound services. Added a read-only D-Bus +`org.gnome.Mutter.DisplayConfig.GetCurrentState` response built from the nested +output and logical layout, plus `MonitorsChanged` on nested-output resize. Local +`cargo check` and build pass; `gdbus call` returned one 1280×800@60 mode and +logical layout mode 1. A second isolated Wingpanel launch no longer reports the +missing DisplayConfig service and attaches SHM buffers. The GNOME Shell hooks +remain unresolved; applying monitor modes, Dock behavior and M-series DRM/KMS +remain unqualified. Next: trace the remaining Wingpanel Shell/Dock requirements +and implement needed behavior against the nested output. The WD22TB4 HDMI G3 +DCP/kernel issue remains separate. Scope and limits: +`mseries/pantheon-rust-compositor/`. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index d08ef7c90..ae3a6a46a 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -21,10 +21,12 @@ configures. Its v2 protocol probe completed a nested Xvfb run and sent panel requests over the Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland run, sent `get_panel`, anchor and hide-mode requests, and attached SHM buffers. It reports the absent `org.gnome.Mutter.DisplayConfig` -service (plus GNOME Shell keybinding/sound hooks), so the next slice is to -identify and provide only the display-config query Wingpanel requires. -Autohide, blur, multi-output reflow, Dock behavior, full session integration -and M-series hardware remain unqualified. A public search found elementary's +service. The prototype now returns its read-only `GetCurrentState` query using +the nested output and logical layout mode; Wingpanel connects without the +missing-service error. The GNOME Shell keybinding/sound hooks still need +session-level compatibility. Autohide, blur, multi-output reflow, monitor mode +application, Dock behavior, full session integration and M-series hardware +remain unqualified. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 4e4a614af..371e63c31 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -82,7 +82,7 @@ its anchor/size/offset, reserves exclusive panel geometry, and updates already maximized windows when that work area changes. The last local `cargo check` and `cargo build` pass. Its protocol probe completed under Xvfb over a nested Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested -Wayland session and rendered SHM buffers, but reports that -`org.gnome.Mutter.DisplayConfig` is unavailable. Dock integration and M-series -hardware remain unqualified; this is not full session or hardware -qualification. +Wayland session and rendered SHM buffers. The prototype serves the read-only +`org.gnome.Mutter.DisplayConfig.GetCurrentState` query Wingpanel uses, and +Wingpanel completes that call. Dock integration and M-series hardware remain +unqualified; this is not full session or hardware qualification. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5ed32c878..c4f1c0744 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -48,12 +48,149 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bitflags" version = "2.13.2" @@ -69,6 +206,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.8.1" @@ -244,6 +394,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + [[package]] name = "enumflags2" version = "0.7.12" @@ -251,6 +407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", + "serde", ] [[package]] @@ -280,6 +437,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.5.0" @@ -313,6 +490,25 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-task" version = "0.3.34" @@ -382,6 +578,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "icu_collections" version = "2.3.0" @@ -667,6 +869,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mseries-power" version = "0.1.0" @@ -832,19 +1043,37 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "pantheon-compositor" version = "0.1.0" dependencies = [ "bitflags", "smithay", + "tracing", "tracing-subscriber", "wayland-backend", "wayland-client", "wayland-scanner", "wayland-server", + "zbus", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -877,6 +1106,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.34" @@ -1129,6 +1369,17 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1144,6 +1395,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -1463,6 +1724,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys", +] + [[package]] name = "unicode-ident" version = "1.0.27" @@ -1493,6 +1765,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2115,6 +2398,76 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -2168,3 +2521,44 @@ dependencies = [ "quote", "syn 3.0.9", ] + +[[package]] +name = "zvariant" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.9", + "winnow", +] diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index 2b893c650..f2e64e883 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -15,11 +15,13 @@ path = "src/bin/pantheon-shell-probe.rs" [dependencies] tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +tracing = "0.1" bitflags = "2.2.1" wayland-scanner = "0.31.11" wayland-backend = "0.3.17" wayland-client = "0.31.11" wayland-server = "0.31.14" +zbus = "=5.19.0" [dependencies.smithay] git = "https://github.com/Smithay/smithay.git" diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 53b98e29f..46766155c 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -70,8 +70,12 @@ for the 12-second Xvfb integration smoke and the probe completed its protocol requests. Installed Wingpanel 8.0.4 also connected in an isolated D-Bus/Xvfb run forced to Wayland. Its client trace confirms it bound the shell global, sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated -and attached shared-memory buffers. It logged that -`org.gnome.Mutter.DisplayConfig` is unavailable; its GNOME Shell keybinding and -sound hooks also had no service on the isolated bus. This is evidence of panel -client compatibility at the protocol/rendering path, not full session -qualification. Dock behavior and M-series DRM/KMS remain unqualified. +and attached shared-memory buffers. Wingpanel requires +`org.gnome.Mutter.DisplayConfig`; the compositor now serves the read-only +`GetCurrentState` query it calls, with the current nested output and logical +layout mode, and emits `MonitorsChanged` when the output resizes. The isolated +Wingpanel run no longer reports a missing DisplayConfig service. GNOME Shell +keybinding and sound hooks still have no service on the isolated bus. This is +evidence of panel compatibility at the protocol/rendering path, not full +session qualification. Applying monitor modes, Dock behavior and M-series +DRM/KMS remain unqualified. diff --git a/rust/pantheon-compositor/THIRD-PARTY.md b/rust/pantheon-compositor/THIRD-PARTY.md index aa736cf97..7842542d0 100644 --- a/rust/pantheon-compositor/THIRD-PARTY.md +++ b/rust/pantheon-compositor/THIRD-PARTY.md @@ -10,3 +10,11 @@ The upstream license is included as `SMITHAY-LICENSE.txt`. LGPL-2.1-or-later. This copy advertises protocol version 2 and includes the M-series `set_offset` request already used by this repository's Gala/Dock patches. The source XML retains its copyright and SPDX notice. + +The read-only `org.gnome.Mutter.DisplayConfig.GetCurrentState` compatibility +reply follows the public D-Bus contract in GNOME Mutter's +`data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml` +(). +The Rust implementation is a fresh subset for Wingpanel's current read-only +layout query; it does not copy Mutter implementation code or provide mode +application. diff --git a/rust/pantheon-compositor/src/display_config.rs b/rust/pantheon-compositor/src/display_config.rs new file mode 100644 index 000000000..9fc92b8d6 --- /dev/null +++ b/rust/pantheon-compositor/src/display_config.rs @@ -0,0 +1,158 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex, mpsc}, + thread, + time::Duration, +}; + +use zbus::{blocking::Connection, interface, object_server::SignalEmitter, zvariant::OwnedValue}; + +const BUS_NAME: &str = "org.gnome.Mutter.DisplayConfig"; +const OBJECT_PATH: &str = "/org/gnome/Mutter/DisplayConfig"; +const INTERFACE: &str = "org.gnome.Mutter.DisplayConfig"; + +#[derive(Clone, Debug)] +pub struct OutputSnapshot { + pub connector: String, + pub vendor: String, + pub product: String, + pub serial: String, + pub width: i32, + pub height: i32, + pub refresh_millihz: i32, + pub scale: f64, +} + +impl Default for OutputSnapshot { + fn default() -> Self { + Self { + connector: "winit".into(), + vendor: "Smithay".into(), + product: "Winit".into(), + serial: "Nested".into(), + width: 1280, + height: 800, + refresh_millihz: 60_000, + scale: 1.0, + } + } +} + +#[derive(Clone)] +pub struct DisplayConfigHandle { + updates: mpsc::Sender, +} + +impl DisplayConfigHandle { + pub fn start() -> Self { + let (updates, receiver) = mpsc::channel(); + thread::Builder::new() + .name("pantheon-display-config-dbus".into()) + .spawn(move || serve_display_config(receiver)) + .expect("failed to start display config D-Bus thread"); + Self { updates } + } + + pub fn update(&self, snapshot: OutputSnapshot) { + let _ = self.updates.send(snapshot); + } +} + +struct DisplayConfigServer(Arc>); + +type Properties = HashMap; +type MonitorInfo = (String, String, String, String); +type Mode = (String, i32, i32, f64, f64, Vec, Properties); +type Monitor = (MonitorInfo, Vec, Properties); +type LogicalMonitor = (i32, i32, f64, u32, bool, Vec, Properties); + +#[interface(name = "org.gnome.Mutter.DisplayConfig")] +impl DisplayConfigServer { + fn get_current_state(&self) -> (u32, Vec, Vec, Properties) { + let output = self.0.lock().expect("display config lock poisoned").clone(); + let monitor_info = ( + output.connector.clone(), + output.vendor, + output.product, + output.serial, + ); + let mode_id = format!( + "{}x{}@{:.3}", + output.width, + output.height, + f64::from(output.refresh_millihz) / 1000.0 + ); + let mut mode_properties = Properties::new(); + mode_properties.insert("is-current".into(), OwnedValue::from(true)); + mode_properties.insert("is-preferred".into(), OwnedValue::from(true)); + let mode = ( + mode_id, + output.width, + output.height, + f64::from(output.refresh_millihz) / 1000.0, + output.scale, + vec![output.scale], + mode_properties, + ); + let monitor = (monitor_info.clone(), vec![mode], Properties::new()); + let logical_monitor = ( + 0, + 0, + output.scale, + 0, + true, + vec![monitor_info], + Properties::new(), + ); + let mut properties = Properties::new(); + properties.insert("layout-mode".into(), OwnedValue::from(1_u32)); + (1, vec![monitor], vec![logical_monitor], properties) + } + + #[zbus(signal)] + async fn monitors_changed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; +} + +fn serve_display_config(receiver: mpsc::Receiver) { + let connection = match Connection::session() { + Ok(connection) => connection, + Err(error) => { + tracing::warn!(%error, "cannot connect to the session bus; display config service is disabled"); + return; + } + }; + + let snapshot = Arc::new(Mutex::new(OutputSnapshot::default())); + if let Err(error) = connection + .object_server() + .at(OBJECT_PATH, DisplayConfigServer(snapshot.clone())) + { + tracing::warn!(%error, "cannot register the display config compatibility interface"); + return; + } + + if let Err(error) = connection.request_name(BUS_NAME) { + tracing::warn!(%error, "cannot own the Mutter display config compatibility name"); + return; + } + + tracing::info!("serving the read-only Mutter display config query used by Wingpanel"); + loop { + match receiver.recv_timeout(Duration::from_millis(100)) { + Ok(output) => { + *snapshot.lock().expect("display config lock poisoned") = output; + if let Err(error) = connection.emit_signal( + None::<&str>, + OBJECT_PATH, + INTERFACE, + "MonitorsChanged", + &(), + ) { + tracing::warn!(%error, "could not emit MonitorsChanged"); + } + } + Err(mpsc::RecvTimeoutError::Timeout) => {} + Err(mpsc::RecvTimeoutError::Disconnected) => return, + } + } +} diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index bbc55a231..e27783e25 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -2,6 +2,7 @@ mod handlers; mod grabs; +mod display_config; mod input; mod pantheon_shell; mod protocols; diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 46f3a7268..ebf6999c3 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -24,6 +24,7 @@ use smithay::{ }; use crate::pantheon_shell::{PanelState, PantheonShellState}; +use crate::display_config::DisplayConfigHandle; pub struct PantheonCompositor { pub start_time: std::time::Instant, @@ -41,6 +42,7 @@ pub struct PantheonCompositor { pub seat_state: SeatState, pub data_device_state: DataDeviceState, pub pantheon_shell_state: PantheonShellState, + pub display_config: DisplayConfigHandle, pub panels: HashMap, pub last_work_area: Option>, pub managed_windows: @@ -79,6 +81,7 @@ impl PantheonCompositor { // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); let pantheon_shell_state = PantheonShellState::new::(&dh); + let display_config = DisplayConfigHandle::start(); // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. @@ -120,6 +123,7 @@ impl PantheonCompositor { seat_state, data_device_state, pantheon_shell_state, + display_config, panels: HashMap::new(), last_work_area: None, managed_windows: HashMap::new(), diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 2eef311ec..fd7277075 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -13,6 +13,7 @@ use smithay::{ }; use crate::PantheonCompositor; +use crate::display_config::OutputSnapshot; pub fn init_winit( event_loop: &mut EventLoop, @@ -40,21 +41,42 @@ pub fn init_winit( output.set_preferred(mode); state.space.map_output(&output, (0, 0)); + state.display_config.update(OutputSnapshot { + connector: "winit".into(), + vendor: "Smithay".into(), + product: "Winit".into(), + serial: "Nested".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + scale: 1.0, + }); let mut damage_tracker = OutputDamageTracker::from_output(&output); event_loop.handle().insert_source(winit, move |event, _, state| { match event { WinitEvent::Resized { size, .. } => { + let mode = Mode { + size, + refresh: 60_000, + }; output.change_current_state( - Some(Mode { - size, - refresh: 60_000, - }), + Some(mode), None, None, None, ); + state.display_config.update(OutputSnapshot { + connector: "winit".into(), + vendor: "Smithay".into(), + product: "Winit".into(), + serial: "Nested".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + scale: 1.0, + }); } WinitEvent::Input(event) => state.process_input_event(event), WinitEvent::Redraw => { From e26c98af66447cff2f416ab5c7ce5b9a15ca9fe4 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:41:20 -0400 Subject: [PATCH 041/798] WIP: save uncommitted work before reboot (CodexBar reset credits, ANE dictation, Share Eyes app, repo-split secret scan) Work in progress from other sessions, committed as-is at the owner's request so a reboot cannot lose it. Not reviewed or tested as part of this commit. Co-Authored-By: Claude Opus 5.5 --- mseries/CODEXBAR-PORT.md | 1 + mseries/REPO-SPLIT-SECRET-SCAN-2026-10-18.md | 116 ++++++++ mseries/codexbar-opencode-login.py | 178 ++++++++++++ mseries/deploy-codexbar.sh | 2 +- mseries/dictation/ane-dictate.py | 262 ++++++++++++++++++ mseries/dictation/ane-worker-shim.py | 35 +++ mseries/dictation/test_ane_dictate.py | 38 +++ mseries/enhance.sh | 2 +- mseries/install-codexbar.sh | 1 + .../share-eyes-wingpanel/app/ShareApp.vala | 72 +++++ .../io.github.techmore.ShareEyes.desktop | 10 + .../share-eyes-wingpanel/probe/test-app.sh | 36 +++ .../share-eyes-wingpanel/probe/test-notify.sh | 27 ++ 13 files changed, 778 insertions(+), 2 deletions(-) create mode 100644 mseries/REPO-SPLIT-SECRET-SCAN-2026-10-18.md create mode 100644 mseries/codexbar-opencode-login.py create mode 100644 mseries/dictation/ane-dictate.py create mode 100644 mseries/dictation/ane-worker-shim.py create mode 100644 mseries/dictation/test_ane_dictate.py create mode 100644 mseries/share-eyes-wingpanel/app/ShareApp.vala create mode 100644 mseries/share-eyes-wingpanel/io.github.techmore.ShareEyes.desktop create mode 100755 mseries/share-eyes-wingpanel/probe/test-app.sh create mode 100755 mseries/share-eyes-wingpanel/probe/test-notify.sh diff --git a/mseries/CODEXBAR-PORT.md b/mseries/CODEXBAR-PORT.md index 3b8dd91a9..991fb0e40 100644 --- a/mseries/CODEXBAR-PORT.md +++ b/mseries/CODEXBAR-PORT.md @@ -14,6 +14,7 @@ ones.** Upgrade by re-pinning, re-installing upstream, and re-verifying each ite | Sessions helper | `codexbar-sessions.py` | none (reads local data) | | OpenCode local usage + account email | `codexbar-opencode-local.py` | none (reads `opencode.db` read-only) | | Codex multi-account switcher (upstream's is macOS-only) | `codexbar-accounts.py` | upstream `codexbar usage --provider codex` honoring `CODEX_HOME` | +| OpenCode Go sign-in (upstream imports opencode.ai cookies only on macOS): native WebKit window, saves the `auth`/`__Host-console_session` session as `providers[opencodego].cookieSource=manual` + `cookieHeader`; the panel's OpenCode **+** runs it | `codexbar-opencode-login.py` | config path resolution (`CodexBarConfigStore.defaultURL`), `OpenCodeWebCookieSupport` cookie names, `usage --source web` | | Claude accounts via claude-swap | `codexbar-claude.py`, venv `~/.local/share/claude-swap-venv`, `cswap` | claude-swap `list/status/switch --json`, `schemaVersion` 1 | | Installers / deploy / CI | `install-codexbar.sh`, `install-codexbar-wingpanel.sh`, `deploy-codexbar.sh`, `check-codexbar-port.sh`, `.github/workflows/validate-mseries.yml`, `enhance.sh` payload list | version pin `version=vX.Y.Z` | diff --git a/mseries/REPO-SPLIT-SECRET-SCAN-2026-10-18.md b/mseries/REPO-SPLIT-SECRET-SCAN-2026-10-18.md new file mode 100644 index 000000000..6d0bebfaa --- /dev/null +++ b/mseries/REPO-SPLIT-SECRET-SCAN-2026-10-18.md @@ -0,0 +1,116 @@ +# Repo split secret scan: orchard-incus and TM-Remote (2026-10-18) + +Purpose: inform the owner's decision on making `techmore/orchard-incus` and +`techmore/TM-Remote` public. Both repositories are still **private**; no +visibility was changed. Secret values are never reproduced here: credential-like +strings are described by file and kind only. Hostnames and IPs are listed +because deciding whether they may be published is the point of the review. + +## Scope + +Fresh `git clone --mirror` of each repo, scanning every ref (`--all`): all +branches, tags and GitHub `refs/pull/*`. + +| Repo | Refs | Commits | Heads at scan | +| --- | --- | --- | --- | +| orchard-incus | main, elementaryos-mseries-import, codex/wingpanel-indicator, pull/1, pull/2 | 13 | main `0fc9800` (merge of PR #2) | +| TM-Remote | main, elementaryos-mseries, dependabot/go_modules/golang.org/x/crypto-0.52.0, pull/1-8, tags v0.1.0, v0.2.0, v0.3.0, v0.3.127 | 78 | main `fb64501`, elementaryos-mseries `132be05` | + +## Tools + +| Tool | Version | Install | Integrity | +| --- | --- | --- | --- | +| gitleaks | 8.30.1 (linux_arm64) | `~/.local/bin/gitleaks` | sha256 `e4a487ee…b080` matched the release `gitleaks_8.30.1_checksums.txt` | +| trufflehog | 3.97.9 (linux_arm64; picked because it is more than two weeks old, the newest was 10 days old) | `~/.local/bin/trufflehog` | sha256 `372c5686…d705` matched `trufflehog_3.97.9_checksums.txt` (the cosign signature was not checked; cosign is not installed) | +| targeted greps | Python script over the added lines of `git log --all -p`, plus metadata, file-name and blob-size checks | scratchpad only | n/a | + +Commands: `gitleaks git --log-opts=--all --redact=100`; +`trufflehog git file:// --bare --no-verification --results=verified,unknown,unverified` +(run offline, so no candidate was sent to any provider for verification). + +## Results: scanners + +| Repo | gitleaks | trufflehog | +| --- | --- | --- | +| orchard-incus | 0 leaks (~0.93 MB) | 2 unverified `URI` hits: one line in `tests/model-smoke.vala`, found in two commits. Test fixture: a `user:pass@example.test` URL that the test expects `validate_endpoint` to reject. **Test value, not a secret.** | +| TM-Remote | 0 leaks (~3.4 MB) | 0 (1255 chunks) | + +## Results: targeted greps + +Patterns: PEM private keys and certificates; AWS/GitHub/OpenAI-style/Slack/Google/Tailscale +keys; JWTs; password/secret/token/api-key assignments; APNs/MDM tokens (PushMagic, +UnlockToken, BootstrapToken, `com.apple.mgmt.*` topics); IPv4; e-mail; hostnames; MAC/UDID/serials; +`/Users`/`/home` paths; SSH public keys and fingerprints; sensitive file names +(`.p12 .pem .key .mobileconfig .env .db` and the like); blobs over 300 KB. + +### orchard-incus + +| Finding | Where | Class | +| --- | --- | --- | +| No private keys, certificates, API keys or tokens | all history | none | +| `secret@example.test`, `example.internal`, `10.40.0.1/24` | tests, ResourceBrowser placeholder | test/example | +| `10.169.229.233` | `python/tests/test_backend.py` | sample Incus bridge address (RFC1918), test data | +| Owner's personal Gmail as commit author/committer | 16 commit-metadata entries (`sdolbec` and `Techmore` identities) | **real personal e-mail** (already public in ElementaryOS_MSeries history) | +| README mentions the live-test host had "Rupert and orchard-test instances" | README.md | internal instance names, harmless | + +### TM-Remote + +No credentials found. All token, password and PushMagic values sit in `*_test.go` +or `test/` files and are synthetic (`UDID-1`, `SERIAL123`, `11111111-2222-…` +UUIDs, `com.apple.mgmt.External.TEST`, `*-profile-password` PKCS#12 test identities generated at +test time). `turnserver.conf.example`, `environment.example` and the Caddyfile use +`REPLACE_WITH_…` placeholders. The `"Password":` plist field in `mdm_server.go` is +filled from a variable, not a literal. Committed `.pyc` files (`scripts/__pycache__`, +in history only, not on main) hold no absolute user paths. No `.p12/.pem/.key/.mobileconfig` files exist in any commit. + +What it does contain is **real infrastructure and personal data**: + +| Finding | Where (examples) | Class | +| --- | --- | --- | +| Tailnet name `tailf80972` with node names `techmore-2`, `csp-bfs-mini`, `node` (`*.tailf80972.ts.net`) | docs/COMPLETION-AUDIT.md, docs/MATT-SCREEN-MIRROR.md, docs/REMOTE-DESKTOP-ROADMAP.md, main_test.go, elementaryos-mseries/webrtc/deploy/gateway.py and its test | **real** hostnames | +| Tailscale CGNAT addresses of real nodes (`100.127.99.74`, `100.69.38.89`, `100.82.173.72`, `100.82.173.10`) | README.md, docs/*, scripts/install-control-plane-*.sh, scripts/install-linux-tailnet-routing.sh | **real** (reachable only inside the tailnet) | +| LAN addresses of named machines (`10.20.0.105/106/109/117`, `10.2.0.2`, `192.168.3.251`, `192.168.222.220/244`) | README.md, docs/*, hosts.example.json (also has the username `sdolbec`), scripts/test-screen-mirror.cjs, webrtc gateway.py | **real** internal topology | +| Staff names and workstation details: "Matt" and "Ryan" with SSH user, host ID, IP, OS and installed services | docs/MATT-SCREEN-MIRROR.md, docs/matt-screen-*.service, related docs | **real personal data about third parties** | +| Organisation domains `stoverparc.org` (`mdm.`, `relay.`, `remote.`; also `remote.stoverparc.com`) and `cybersecuritypilot.org` (`daedalus.`, `remote.`) | docs/PUBLIC-CONTROL-PLANE.md, elementaryos-mseries/webrtc/* | **real** hostnames of planned or running services | +| Real Apple TV identifiers (AirPlay `deviceid`, `btaddr`, `gid`, model) captured from a LAN device | discovery_test.go, testdata/appletv_airplay_unicast_response.hex | **real** device identifiers, low sensitivity | +| Two SSH host-key SHA256 fingerprints | docs | real but public by nature, low | +| `/Users/seandolbec` (14 lines), `/home/sdolbec` (9) | docs, scripts | real usernames, low | +| Owner's personal Gmail as committer (7 entries); 134 commits use the placeholder `sean@example.com` | commit metadata | **real personal e-mail** in 7 entries | +| Vendored noVNC author e-mails (`acme.com`, `widget.com`, `jalf.dk`) | web/vendor/novnc | third-party attribution, keep | +| Dependabot reports 18 open alerts on main (8 critical, 3 high, 7 moderate) | GitHub push notice | not a secret; becomes publicly visible as advisories once public | + +The WebRTC import (`elementaryos-mseries/`) adds nothing new to this exposure: +its stoverparc hostnames, `techmore-2.tailf80972.ts.net` and `192.168.3.251` +are already public in the public `techmore/ElementaryOS_MSeries` branch +`feat/webrtc`. `discovery-2026-10-17.json` lists branch names and SHAs of ten +techmore repositories, all of which are currently public. + +## Verdict + +- **orchard-incus: safe to make public.** No secrets, no customer data, test + fixtures only. The one optional decision is the owner's personal Gmail in commit + metadata, which is already public through ElementaryOS_MSeries. If it should + not appear here, rewrite with `git filter-repo --mailmap` before publishing. + That changes every SHA, including merge `0fc9800`. +- **TM-Remote: no secrets to rotate, but not ready to publish as-is.** Publishing + would disclose the tailnet name and node names, internal LAN/Tailscale topology, + staff names with their workstation access details, real device identifiers and + the owner's personal e-mail. Before making it public: + 1. Remove or anonymise `docs/MATT-SCREEN-MIRROR.md`, `docs/matt-screen-*.service` + and `docs/SER8-SCREEN-MIRROR.md`/`ser8-*` (or move them to a private ops repo). + 2. Replace real IPs/hostnames in README.md, docs/*, hosts.example.json, main_test.go, + scripts/* and `elementaryos-mseries/webrtc/` with RFC 5737/6761 placeholders + (`192.0.2.x`, `100.64.0.x`, `*.example`, `your-tailnet.ts.net`). + 3. Replace the captured Apple TV `deviceid`/`btaddr`/`gid` in discovery_test.go and + `testdata/appletv_airplay_unicast_response.hex` with synthetic values (regenerate the hex). + 4. Because all of this is in history (and in `refs/pull/*`, which a branch rewrite + does not change), clean history with `git filter-repo --replace-text` + (+ `--mailmap` for the Gmail) on a fresh mirror. Then force-push all + branches and tags, or publish a new repo from the cleaned mirror, since GitHub + keeps old PR refs. Re-run both scanners and these greps afterwards. + 5. Separately, triage the 18 Dependabot alerts before going public. + +If the owner decides the topology/names are acceptable to disclose (the tailnet +addresses are not internet-reachable and several hostnames are already public +via ElementaryOS_MSeries), TM-Remote holds no credentials that would need +rotating. Even then, item 1 (third-party staff details) should be done first. diff --git a/mseries/codexbar-opencode-login.py b/mseries/codexbar-opencode-login.py new file mode 100644 index 000000000..13b019a2b --- /dev/null +++ b/mseries/codexbar-opencode-login.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Sign in to OpenCode Go for CodexBar's quota. + +Upstream CodexBar reads OpenCode Go quota from an opencode.ai web session, but it only imports browser +cookies on macOS. On Linux it accepts the same session as a manual Cookie header, so this opens a small +native sign-in window, keeps only the opencode.ai session cookies upstream forwards, and saves them to +CodexBar's config (providers[opencodego]: cookieSource "manual", cookieHeader). + + codexbar-opencode-login.py login sign in, save the session, and check that quota now loads + codexbar-opencode-login.py status JSON: {"signedIn": bool} +""" + +from __future__ import annotations + +import contextlib +import fcntl +import json +import os +import pathlib +import subprocess +import sys +import tempfile +from typing import Any + +SIGN_IN_URL = "https://opencode.ai/auth" +# The cookies upstream forwards (OpenCodeWebCookieSupport.requestCookieNames); nothing else is kept. +SESSION_COOKIES = ("auth", "__Host-auth", "__Host-console_session") +PROVIDER = "opencodego" + + +def config_path() -> pathlib.Path: + """Same resolution as upstream's CodexBarConfigStore.defaultURL.""" + override = os.environ.get("CODEXBAR_CONFIG", "").strip() + if override: + return pathlib.Path(override).expanduser() + xdg = os.environ.get("XDG_CONFIG_HOME", "").strip() + if xdg and pathlib.Path(xdg).expanduser().is_absolute(): + return pathlib.Path(xdg).expanduser() / "codexbar" / "config.json" + home = pathlib.Path.home() + xdg_default = home / ".config" / "codexbar" / "config.json" + legacy = home / ".codexbar" / "config.json" + if not xdg_default.exists() and legacy.exists(): + return legacy + return xdg_default + + +def cookie_header(cookies: dict[str, str]) -> str: + return "; ".join(f"{name}={cookies[name]}" for name in SESSION_COOKIES if cookies.get(name)) + + +@contextlib.contextmanager +def locked(path: pathlib.Path): + path.parent.mkdir(parents=True, exist_ok=True) + with open(path.with_name(path.name + ".lock"), "a") as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(lock, fcntl.LOCK_UN) + + +def save_session(header: str, path: pathlib.Path | None = None) -> None: + path = path or config_path() + with locked(path): + try: + config: dict[str, Any] = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + config = {"version": 1, "providers": []} + providers = config.setdefault("providers", []) + entry = next((p for p in providers if isinstance(p, dict) and p.get("id") == PROVIDER), None) + if entry is None: + entry = {"id": PROVIDER} + providers.append(entry) + entry.update({"enabled": True, "cookieSource": "manual", "cookieHeader": header}) + with tempfile.NamedTemporaryFile("w", dir=path.parent, prefix=".config.", delete=False, + encoding="utf-8") as handle: + os.fchmod(handle.fileno(), 0o600) + json.dump(config, handle, indent=2) + handle.write("\n") + os.replace(handle.name, path) + + +def signed_in(path: pathlib.Path | None = None) -> bool: + try: + config = json.loads((path or config_path()).read_text(encoding="utf-8")) + except (OSError, ValueError): + return False + return any(isinstance(p, dict) and p.get("id") == PROVIDER and p.get("cookieSource") == "manual" + and p.get("cookieHeader") for p in config.get("providers", [])) + + +def check_quota() -> tuple[bool, str]: + cli = pathlib.Path.home() / ".local/bin/codexbar" + try: + result = subprocess.run([str(cli), "usage", "--provider", PROVIDER, "--source", "web", "--format", "json"], + capture_output=True, text=True, timeout=90, check=False) + rows = json.loads(result.stdout or "[]") + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + return False, f"Saved, but CodexBar couldn't be asked for quota: {error}" + row = rows[0] if isinstance(rows, list) and rows else {} + if isinstance(row.get("error"), dict): + return False, "Saved the sign-in, but opencode.ai rejected it: " + str(row["error"].get("message", "")) + return True, "OpenCode Go quota is now read from your opencode.ai account." + + +def sign_in() -> str | None: + """Show the opencode.ai sign-in page; return the session Cookie header once the user is signed in.""" + import gi + gi.require_version("Gtk", "4.0") + gi.require_version("WebKit", "6.0") + from gi.repository import GLib, Gtk, WebKit + + # A private, persistent profile: a later re-sign-in after the session expires is usually one click. + data = pathlib.Path(os.environ.get("XDG_DATA_HOME") or pathlib.Path.home() / ".local/share") / "codexbar-opencode-login" + cache = pathlib.Path(os.environ.get("XDG_CACHE_HOME") or pathlib.Path.home() / ".cache") / "codexbar-opencode-login" + for directory in (data, cache): + directory.mkdir(parents=True, exist_ok=True) + directory.chmod(0o700) + session = WebKit.NetworkSession.new(str(data), str(cache)) + session.get_cookie_manager().set_persistent_storage(str(data / "cookies.sqlite"), + WebKit.CookiePersistentStorage.SQLITE) + result: dict[str, str | None] = {"header": None} + app = Gtk.Application(application_id="io.github.mseries.CodexBarOpenCodeLogin") + + def activate(application: Gtk.Application) -> None: + view = WebKit.WebView(network_session=session) + window = Gtk.ApplicationWindow(application=application, title="Sign in to OpenCode", + default_width=480, default_height=720) + window.set_child(view) + + def collect(*_args: Any) -> None: + uri = view.get_uri() or "" + # Still on the sign-in or an identity-provider page: nothing to collect yet. + if (not uri.startswith("https://opencode.ai/") or uri.startswith(SIGN_IN_URL) + or "/login" in uri.split("?")[0]): + return + + def got(manager: Any, task: Any) -> None: + try: + cookies = {c.get_name(): c.get_value() for c in manager.get_cookies_finish(task)} + except GLib.Error: + return + header = cookie_header(cookies) + if header and result["header"] is None: + result["header"] = header + window.close() + + session.get_cookie_manager().get_cookies("https://opencode.ai/", None, got) + + view.connect("load-changed", lambda v, event: event == WebKit.LoadEvent.FINISHED and collect()) + view.load_uri(SIGN_IN_URL) + window.present() + + app.connect("activate", activate) + app.run([]) + return result["header"] + + +def main(arguments: list[str]) -> int: + command = arguments[0] if arguments else "" + if command == "status": + print(json.dumps({"signedIn": signed_in()})) + return 0 + if command == "login": + header = sign_in() + if not header: + print("Sign-in was closed before opencode.ai finished signing you in.") + return 1 + save_session(header) + ok, message = check_quota() + print(message) + return 0 if ok else 1 + print("usage: codexbar-opencode-login.py login | status", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/mseries/deploy-codexbar.sh b/mseries/deploy-codexbar.sh index 2255099a6..eeddba542 100755 --- a/mseries/deploy-codexbar.sh +++ b/mseries/deploy-codexbar.sh @@ -7,7 +7,7 @@ set -euo pipefail repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) lib="$HOME/.local/lib/codexbar-cli" mkdir -p "$lib" -for helper in codexbar-opencode-local.py codexbar-sessions.py codexbar-accounts.py codexbar-claude.py; do +for helper in codexbar-opencode-local.py codexbar-sessions.py codexbar-accounts.py codexbar-claude.py codexbar-opencode-login.py; do install -m 0755 "$repo/mseries/$helper" "$lib/$helper" done install -m 0755 "$repo/mseries/codexbar-opencode-wrapper.py" "$HOME/.local/bin/codexbar.new" diff --git a/mseries/dictation/ane-dictate.py b/mseries/dictation/ane-dictate.py new file mode 100644 index 000000000..475434f10 --- /dev/null +++ b/mseries/dictation/ane-dictate.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""Dictation on the Apple Neural Engine: hold F5 (KEY_F14 after the hwdb remap), speak, release. + +Parakeet TDT 0.6B v3 (Core ML reference, encoder on the ANE via omarchy-ane, mel/decoder on the +Honeykrisp GPU) transcribes the whole utterance after release and the text is typed into the +focused field. Whole-utterance decoding is what gives word-for-word output: the model sees all +of the audio, nothing is revised after typing. Nothing is stored; audio stays in RAM. + +Run as a service (needs the evdev keyboard and ydotool) or offline: + ane-dictate.py --wav FILE [--repeat N] transcribe a file and print text + timings +Env: ANE_PREFIX (default ~/.local/share/mlx-omarchy), ANE_VK_ICD (private Mesa 26.2.4 ICD), + STREAM_CARD/STREAM_GAIN/STREAM_KEY (same meaning as stream-dictate.py), STREAM_DRY=1 (print only). +""" +import argparse, math, os, queue, re, signal, struct, subprocess, sys, tempfile, threading, time, wave +import json + +PREFIX = os.path.expanduser(os.environ.get("ANE_PREFIX", "~/.local/share/mlx-omarchy")) +ICD = os.path.expanduser(os.environ.get( + "ANE_VK_ICD", "~/.local/share/mseries/ane/mesa-hk/share/vulkan/icd.d/asahi_icd.aarch64.json")) +CARD = os.environ.get("STREAM_CARD", "hw:1,0") +GAIN = os.environ.get("STREAM_GAIN", "0.2") +CHAIN = ("pan=mono|c0=0.333*c0+0.333*c1+0.333*c2,highpass=f=150,lowpass=f=7000," + f"volume={GAIN},alimiter=limit=0.9,aresample=async=1") +KEY = int(os.environ.get("STREAM_KEY", "184")) +DRY = bool(os.environ.get("STREAM_DRY")) +MAX_SECONDS = 600 # a held key longer than this is cut off (RAM bound) + + +def log(*a): + print(*a, file=sys.stderr, flush=True) + + +def clean(text): + """The model emits runs of '.' on trailing silence and the odd stray non-Latin token there. + Drop runs of 3+ dots, then a trailing fragment that contains no Latin letter or digit.""" + text = re.sub(r"(?<=[A-Za-z0-9])\.{2,}", ".", text) + text = re.sub(r"\.{2,}", " ", text) + words = text.split() + while words and not re.search(r"[A-Za-z0-9]", words[-1]): + words.pop() + return re.sub(r"\s+([,.;:!?])", r"\1", " ".join(words)).strip() + + +def split_points(pcm, window=1600, lo=15.0, hi=27.0, limit=29.0): + """Cut utterances longer than ~29 s at the quietest 100 ms window in [lo, hi] s so the model's + fixed 30 s chunking never lands inside a word. Returns byte offsets (f32le, 4 bytes/sample).""" + import array + a = array.array("f"); a.frombytes(pcm[: len(pcm) // 4 * 4]) + cuts, start, n = [], 0, len(a) + while n - start > limit * 16000: + best, at = None, None + for i in range(start + int(lo * 16000), start + int(hi * 16000), window): + e = sum(x * x for x in a[i:i + window]) + if best is None or e < best: + best, at = e, i + cuts.append(at * 4) + start = at + return cuts + + +def transcribe_long(worker, pcm): + cuts = split_points(pcm) + edges = [0, *cuts, len(pcm)] + return " ".join(clean(worker.transcribe(pcm[a:b])) for a, b in zip(edges, edges[1:])) + + +class Worker: + """The owned ANE worker process (parakeet_dictation --worker) with its framing protocol.""" + + def __init__(self): + site = os.path.join(PREFIX, "venv/lib/python3.14/site-packages") + env = dict(os.environ, OMARCHY_MLX_SYSTEM_PREFIX=PREFIX, PYTHONPATH=os.path.join(site, "mlx")) + if os.path.exists(ICD): + env["VK_DRIVER_FILES"] = ICD + self.p = subprocess.Popen( + [os.path.join(PREFIX, "venv/bin/python"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "ane-worker-shim.py")], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env) + self.n = 0 + self.lock = threading.Lock() + + def transcribe(self, pcm_f32, deadline_ms=90000): + with self.lock: + self.n += 1 + head = json.dumps({"op": "transcribe", "id": self.n, "sample_rate": 16000, + "deadline_ms": deadline_ms}).encode() + head = json.dumps({**json.loads(head), "payload_bytes": len(pcm_f32)}).encode() + self.p.stdin.write(struct.pack(" 29: + t0 = time.perf_counter() + print("split:", transcribe_long(w, pcm), f"({time.perf_counter() - t0:.2f}s)") + finally: + w.close() + + +def notify(msg): + subprocess.Popen(["notify-send", "-t", "2500", "-i", "audio-input-microphone", "-a", "Dictation", + "-h", "string:x-canonical-private-synchronous:mseries-dictation", msg], + stderr=subprocess.DEVNULL) + + +def tone(freq, ms, vol=0.25): + path = os.path.join(tempfile.gettempdir(), f"mseries-chime-{freq}.wav") + if not os.path.exists(path): + n = int(16000 * ms / 1000) + s = [int(32767 * vol * math.sin(2 * math.pi * freq * i / 16000) * min(1, i / 200, (n - i) / 400)) + for i in range(n)] + with wave.open(path, "wb") as f: + f.setnchannels(1); f.setsampwidth(2); f.setframerate(16000) + f.writeframes(struct.pack("<%dh" % n, *s)) + subprocess.Popen(["pw-play", path], stderr=subprocess.DEVNULL) + + +def keyboard_device(): + for b in open("/proc/bus/input/devices").read().split("\n\n"): + if "Apple SPI Keyboard" in b or "Apple MTP Keyboard" in b: + for tok in b.split(): + if tok.startswith("event"): + return "/dev/input/" + tok + raise SystemExit("no Apple keyboard found") + + +class Dictation: + def __init__(self): + self.worker = Worker() + self.active = False + self.stop_evt = threading.Event() + self.typeq = queue.Queue() + threading.Thread(target=self._typer, daemon=True).start() + threading.Thread(target=self._warm, daemon=True).start() + + def _warm(self): # the first call builds the encoder (tens of seconds once, then resident) + try: + self.worker.transcribe(bytes(16000 * 4)) + log("ANE worker warm") + except Exception as e: + log("warm-up failed:", e) + + def _typer(self): + while True: + text = self.typeq.get() + if DRY: + print("type:", repr(text), flush=True) + else: + subprocess.run(["ydotool", "type", "--key-delay", "2", "--", text], stderr=subprocess.DEVNULL) + + def start(self): + if self.active: + return + self.active = True + self.stop_evt.clear() + tone(880, 90) + notify("Listening…") + threading.Thread(target=self.run, daemon=True).start() + + def stop(self): + self.stop_evt.set() + + def run(self): + cmd = ["ffmpeg", "-nostdin", "-v", "error", "-f", "alsa", "-channels", "3", "-sample_rate", "48000", + "-i", CARD, "-af", CHAIN, "-ar", "16000", "-f", "f32le", "-"] + cap = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + buf = bytearray() + t_start = time.time() + try: + while time.time() - t_start < MAX_SECONDS: + data = cap.stdout.read1(6400) + if not data: + break + buf += data + if self.stop_evt.is_set(): + time.sleep(0.15) # short tail so the last word is not clipped + buf += cap.stdout.read1(6400) or b"" + break + finally: + cap.terminate() + try: + cap.wait(2) + except Exception: + cap.kill() + tone(440, 120) + buf = buf[: len(buf) // 4 * 4] + n_samples = len(buf) // 4 + if n_samples < 8000: # under half a second + self.active = False + return + t0 = time.perf_counter() + try: + text = transcribe_long(self.worker, bytes(buf)) + except Exception as e: + log("ANE transcription failed:", e) + notify("Dictation failed (ANE). See the service log.") + text = "" + finally: + self.active = False + log(f"{n_samples / 16000:.1f}s audio -> {len(text)} chars in {time.perf_counter() - t0:.2f}s") + if text: + self.typeq.put(text + " ") + else: + notify("Didn't catch that") + + +def service(): + d = Dictation() + dev = keyboard_device() + log("ready, keyboard", dev, "engine Parakeet TDT v3 on ANE") + signal.signal(signal.SIGTERM, lambda *a: (d.worker.close(), os._exit(0))) + signal.signal(signal.SIGUSR1, lambda *a: d.stop() if d.active else d.start()) + fmt = "llHHi" + size = struct.calcsize(fmt) + with open(dev, "rb", buffering=0) as f: + while True: + _, _, typ, code, val = struct.unpack(fmt, f.read(size)) + if typ == 1 and code == KEY: + d.start() if val == 1 else d.stop() if val == 0 else None + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--wav") + ap.add_argument("--repeat", type=int, default=3) + a = ap.parse_args() + offline(a.wav, a.repeat) if a.wav else service() diff --git a/mseries/dictation/ane-worker-shim.py b/mseries/dictation/ane-worker-shim.py new file mode 100644 index 000000000..9412f23a8 --- /dev/null +++ b/mseries/dictation/ane-worker-shim.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Run upstream's parakeet_dictation worker against the installed mlx-omarchy 0.7.33 CLI. + +Upstream drift (found 2026-10-18): parakeet_dictation calls cli._verify_assets(), which the +installed CLI no longer has, and builds AneIsland without the seal_assets that `transcribe` +passes. This shim restores the same guarantee without editing the installed wheel: the cache +is still verified against the lock by the module, and every byte the ANE loads is sealed +against pin["assets"] exactly as the CLI does. Env: PYTHONPATH must contain /.../mlx. +""" +import sys +from coreml import parakeet_dictation as pd + +_load = pd.load_cli_module + + +def _patched_cli(): # the module loads a fresh CLI copy on every call, so patch each one + cli = _load() + cli._verify_assets = lambda _pin: None # superseded by seal_assets below + return cli + + +pd.load_cli_module = _patched_cli +pin = _patched_cli()._load_pin() + +from coreml import vulkan_encoder as ve +_Island = ve.AneIsland + + +def _sealed(*a, **kw): + kw.setdefault("seal_assets", pin["assets"]) + return _Island(*a, **kw) + + +ve.AneIsland = _sealed +sys.exit(pd.main(["--worker"])) diff --git a/mseries/dictation/test_ane_dictate.py b/mseries/dictation/test_ane_dictate.py new file mode 100644 index 000000000..40eec3fc0 --- /dev/null +++ b/mseries/dictation/test_ane_dictate.py @@ -0,0 +1,38 @@ +import importlib.util, struct, unittest +spec = importlib.util.spec_from_file_location("ane", __file__.replace("test_ane_dictate", "ane-dictate")) +ane = importlib.util.module_from_spec(spec); spec.loader.exec_module(ane) + + +class Clean(unittest.TestCase): + def test_trailing_dots_and_stray_tokens(self): + self.assertEqual(ane.clean("peppered sauce........ Юн Ю"), "peppered sauce.") + + def test_double_dot(self): + self.assertEqual(ane.clean("with potatoes.."), "with potatoes.") + + def test_normal_text_untouched(self): + self.assertEqual(ane.clean("Hello, world. Fine."), "Hello, world. Fine.") + + def test_empty_and_only_garbage(self): + self.assertEqual(ane.clean("... Юн"), "") + + +class Split(unittest.TestCase): + def pcm(self, seconds, quiet_at): + n = int(seconds * 16000) + v = [0.5] * n + for i in range(int(quiet_at * 16000), int(quiet_at * 16000) + 3200): + v[i] = 0.0 + return struct.pack(f"<{n}f", *v) + + def test_short_audio_not_split(self): + self.assertEqual(ane.split_points(self.pcm(20, 18)), []) + + def test_cut_lands_in_the_quiet_gap(self): + cuts = ane.split_points(self.pcm(40, 22)) + self.assertEqual(len(cuts), 1) + self.assertAlmostEqual(cuts[0] / 4 / 16000, 22, delta=0.2) + + +if __name__ == "__main__": + unittest.main() diff --git a/mseries/enhance.sh b/mseries/enhance.sh index b45b9c53c..5765abe23 100755 --- a/mseries/enhance.sh +++ b/mseries/enhance.sh @@ -311,7 +311,7 @@ mod_payload() { install-network-throughput.sh install-quick-settings-brightness.sh \ install-sound-indicator-fix.sh install-keyboard-indicator-fix.sh \ install-files-portal-fix.sh install-gala-osk-fix.sh install-netusage-policy.sh \ - install-codexbar.sh install-brave-widevine.sh codexbar-opencode-local.py codexbar-accounts.py codexbar-claude.py deploy-codexbar.sh \ + install-codexbar.sh install-brave-widevine.sh codexbar-opencode-local.py codexbar-accounts.py codexbar-claude.py codexbar-opencode-login.py deploy-codexbar.sh \ codexbar-opencode-wrapper.py user-enhance.sh install-dark-mode.sh \ dark-mode/mseries-dark-mode dark-mode/mseries-dark-mode.service dark-mode/mseries-dark-mode.timer \ netspeed/netusage/install.sh netspeed/netusage/netusage.py \ diff --git a/mseries/install-codexbar.sh b/mseries/install-codexbar.sh index 801540b3f..f385ce17e 100755 --- a/mseries/install-codexbar.sh +++ b/mseries/install-codexbar.sh @@ -28,6 +28,7 @@ install -m 0755 "$repo/mseries/codexbar-opencode-local.py" "$HOME/.local/lib/cod install -m 0755 "$repo/mseries/codexbar-sessions.py" "$HOME/.local/lib/codexbar-cli/codexbar-sessions.py" install -m 0755 "$repo/mseries/codexbar-accounts.py" "$HOME/.local/lib/codexbar-cli/codexbar-accounts.py" install -m 0755 "$repo/mseries/codexbar-claude.py" "$HOME/.local/lib/codexbar-cli/codexbar-claude.py" +install -m 0755 "$repo/mseries/codexbar-opencode-login.py" "$HOME/.local/lib/codexbar-cli/codexbar-opencode-login.py" tar -xzf "CodexBarCLI-${version}-linux-aarch64.tar.gz" -C "$HOME/.local/lib/codexbar-cli" mv -f "$HOME/.local/lib/codexbar-cli/codexbar" "$HOME/.local/lib/codexbar-cli/codexbar-upstream" # Replace atomically so the panel never finds ~/.local/bin/codexbar missing mid-install. diff --git a/mseries/share-eyes-wingpanel/app/ShareApp.vala b/mseries/share-eyes-wingpanel/app/ShareApp.vala new file mode 100644 index 000000000..65a2aab1e --- /dev/null +++ b/mseries/share-eyes-wingpanel/app/ShareApp.vala @@ -0,0 +1,72 @@ +/* "Screen Share" dock app: a small elementary-style window around the same panel as the Quick Settings section. + * Quick Settings starts it when a share is offered; it sits minimized in the dock with the eyes icon, shows a badge + * when someone is waiting, and quits when no link and no live share is left. Clicking the dock icon opens it. */ +public class ShareApp : Gtk.Application { + private const string APP_ID = "io.github.techmore.ShareEyes"; + private MSeries.SharePanel panel; + private Gtk.ApplicationWindow? win = null; + private int idle_ticks = 0; + private DBusConnection? bus = null; + private int last_count = -1; + + public ShareApp () { + Object (application_id: APP_ID, flags: ApplicationFlags.DEFAULT_FLAGS); + } + + protected override void activate () { + if (win != null) { + win.unminimize (); + win.present (); + return; + } + panel = new MSeries.SharePanel (); + panel.margin_start = 6; panel.margin_end = 6; panel.margin_top = 6; panel.margin_bottom = 8; + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + win = new Gtk.ApplicationWindow (this) { + title = "Screen Share", + default_width = 360, + default_height = 520, + titlebar = header, + child = new Gtk.ScrolledWindow () { child = panel, hscrollbar_policy = Gtk.PolicyType.NEVER, propagate_natural_height = true } + }; + win.close_request.connect (() => { win.minimize (); return true; }); + try { bus = Bus.get_sync (BusType.SESSION); } catch (Error e) { bus = null; } + panel.status_changed.connect (on_status); + win.present (); + win.minimize (); // start in the dock, not on the screen + hold (); + } + + private void on_status (bool running, bool offering, int waiting, int watching) { + if (waiting != last_count) { + last_count = waiting; + set_badge (waiting); + } + // Quit a few polls after the offer ends, so a brief hiccup does not drop the dock icon. + idle_ticks = offering ? 0 : idle_ticks + 1; + if (idle_ticks >= 3) { + release (); + quit (); + } + } + + /* Unity LauncherEntry: the dock shows a count badge and marks the app urgent. */ + private void set_badge (int count) { + if (bus == null) return; + var props = new VariantBuilder (new VariantType ("a{sv}")); + props.add ("{sv}", "count", new Variant.int64 (count)); + props.add ("{sv}", "count-visible", new Variant.boolean (count > 0)); + props.add ("{sv}", "urgent", new Variant.boolean (count > 0)); + try { + bus.emit_signal (null, "/com/canonical/unity/launcherentry/1", "com.canonical.Unity.LauncherEntry", "Update", + new Variant ("(sa{sv})", "application://" + APP_ID + ".desktop", props)); + } catch (Error e) { + warning ("badge: %s", e.message); + } + } + + public static int main (string[] args) { + return new ShareApp ().run (args); + } +} diff --git a/mseries/share-eyes-wingpanel/io.github.techmore.ShareEyes.desktop b/mseries/share-eyes-wingpanel/io.github.techmore.ShareEyes.desktop new file mode 100644 index 000000000..99bbfb7a6 --- /dev/null +++ b/mseries/share-eyes-wingpanel/io.github.techmore.ShareEyes.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=Screen Share +Comment=Share a window with someone and approve who watches +Exec=mseries-share-eyes +Icon=mseries-share-eyes-symbolic +Categories=Network; +StartupNotify=false +NoDisplay=false +StartupWMClass=io.github.techmore.ShareEyes diff --git a/mseries/share-eyes-wingpanel/probe/test-app.sh b/mseries/share-eyes-wingpanel/probe/test-app.sh new file mode 100755 index 000000000..1f3caccd1 --- /dev/null +++ b/mseries/share-eyes-wingpanel/probe/test-app.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Headless test of the dock app: stays while a link exists, shows a badge for a waiting viewer, quits after the offer ends. +# Usage: test-app.sh BUILD_DIR SHARE_REPO_CORE_DIR +set -euo pipefail +build=$(realpath "$1"); core=$(realpath "$2") +home=$(mktemp -d); run="$home/run"; mkdir -p "$run"; port=8822 +env XDG_RUNTIME_DIR="$run" "$core/scripts/share-demo.sh" --port $port >"$home/demo.out" 2>&1 & demo=$! +cleanup() { kill -TERM "$demo" 2>/dev/null; wait "$demo" 2>/dev/null || true; rm -rf "$home" 2>/dev/null || true; } +trap cleanup EXIT +for _ in $(seq 1 120); do [ -s "$run/rupert-share/state.json" ] && break; sleep 0.5; done +field() { python3 -I -c "import json;print(json.load(open('$run/rupert-share/state.json'))['$1'])"; } +email=$(field email); pass=$(field password); jar="$home/jar" +curl -fs -c "$jar" "http://127.0.0.1:$port/auth/login" -H 'content-type: application/json' -d "{\"email\":\"$email\",\"password\":\"$pass\"}" >/dev/null +api() { curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d '{}' "http://127.0.0.1:$port/api/v1/share/$1"; } +link=$(api links); tok=$(echo "$link" | python3 -I -c "import json,sys;print(json.load(sys.stdin)['token'])"); lid=$(echo "$link" | python3 -I -c "import json,sys;print(json.load(sys.stdin)['id'])") +cat >"$home/inner.sh" < "$home/mon.txt" 2>&1 & +mon=\$! +sleep 1 +env XDG_RUNTIME_DIR="$run" GSK_RENDERER=cairo "$build/mseries-share-eyes" > "$home/app.out" 2>&1 & +app=\$! +sleep 5 +kill -0 \$app 2>/dev/null && echo "app-alive-with-link: yes" || echo "app-alive-with-link: NO" +curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d '{}' http://127.0.0.1:$port/api/v1/share/start >/dev/null +curl -fs -X POST -H "authorization: Bearer $tok" http://127.0.0.1:$port/api/v1/share/view/join >/dev/null +sleep 5 +grep -c "count" "$home/mon.txt" | sed 's/^/badge-signal-lines: /' +grep -E "int64 [1-9]" "$home/mon.txt" | head -1 | sed 's/^ */badge-count-seen: /' +curl -fs -b "$jar" -H 'x-rupert-share: 1' -X DELETE http://127.0.0.1:$port/api/v1/share/links/$lid >/dev/null +curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d '{}' http://127.0.0.1:$port/api/v1/share/stop >/dev/null +for i in \$(seq 1 20); do kill -0 \$app 2>/dev/null || { echo "app-quit-after-offer-ended: yes (\${i}s)"; break; }; sleep 1; done +kill -0 \$app 2>/dev/null && echo "app-quit-after-offer-ended: NO" +kill \$mon 2>/dev/null +INNER +env -i PATH="$PATH" HOME="$home" XDG_RUNTIME_DIR="$run" LANG=C.UTF-8 GDK_BACKEND=x11 xvfb-run -a -s "-screen 0 800x700x24" dbus-run-session -- bash "$home/inner.sh" 2>&1 | grep -v libEGL +cat "$home/app.out" | grep -v -i "egl\|dri3" | head -5 diff --git a/mseries/share-eyes-wingpanel/probe/test-notify.sh b/mseries/share-eyes-wingpanel/probe/test-notify.sh new file mode 100755 index 000000000..17653ce7e --- /dev/null +++ b/mseries/share-eyes-wingpanel/probe/test-notify.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Headless: a waiting viewer makes the Quick Settings panel send one desktop notification (captured on a private session bus). +# Usage: test-notify.sh BUILD_DIR SHARE_REPO_CORE_DIR +set -euo pipefail +build=$(realpath "$1"); core=$(realpath "$2") +home=$(mktemp -d); run="$home/run"; mkdir -p "$run"; port=8823 +env XDG_RUNTIME_DIR="$run" "$core/scripts/share-demo.sh" --port $port >"$home/demo.out" 2>&1 & demo=$! +trap 'kill -TERM "$demo" 2>/dev/null; wait "$demo" 2>/dev/null || true; rm -rf "$home" 2>/dev/null || true' EXIT +for _ in $(seq 1 120); do [ -s "$run/rupert-share/state.json" ] && break; sleep 0.5; done +field() { python3 -I -c "import json;print(json.load(open('$run/rupert-share/state.json'))['$1'])"; } +jar="$home/jar"; curl -fs -c "$jar" "http://127.0.0.1:$port/auth/login" -H 'content-type: application/json' -d "{\"email\":\"$(field email)\",\"password\":\"$(field password)\"}" >/dev/null +api() { curl -fs -b "$jar" -H 'x-rupert-share: 1' -H 'content-type: application/json' -d '{}' "http://127.0.0.1:$port/api/v1/share/$1"; } +tok=$(api links | python3 -I -c "import json,sys;print(json.load(sys.stdin)['token'])"); api start >/dev/null +cat >"$home/inner.sh" < "$home/mon.txt" 2>&1 & +mon=\$! +sleep 1 +env XDG_RUNTIME_DIR="$run" SHARE_NOTIFY=1 PROBE_MS=14000 GSK_RENDERER=cairo "$build/render-probe" "$home/x.png" > "$home/probe.out" 2>&1 & +sleep 4 +echo "before waiting viewer: \$(grep -c 'member=Notify' "$home/mon.txt") notifications" +curl -fs -X POST -H "authorization: Bearer $tok" http://127.0.0.1:$port/api/v1/share/view/join >/dev/null +sleep 5 +echo "after waiting viewer: \$(grep -c 'member=Notify' "$home/mon.txt") notifications" +grep -m1 'Someone wants to watch' "$home/mon.txt" | sed 's/^ */text: /' +grep -v -i "egl\|dri3" "$home/probe.out" | head -5; kill \$mon 2>/dev/null +INNER +env -i PATH="$PATH" HOME="$home" XDG_RUNTIME_DIR="$run" LANG=C.UTF-8 GDK_BACKEND=x11 xvfb-run -a -s "-screen 0 800x700x24" dbus-run-session -- bash "$home/inner.sh" 2>&1 From bff11b7f9ab168e7dfebd55a53575125f1c8d2ee Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:42:37 -0400 Subject: [PATCH 042/798] Add GNOME Shell shortcut compatibility --- mseries/OS9-HANDOFF.md | 14 +- mseries/ROADMAP.md | 10 +- .../EXPERIENCE-REQUIREMENTS.md | 7 +- rust/pantheon-compositor/README.md | 18 +- rust/pantheon-compositor/THIRD-PARTY.md | 7 + rust/pantheon-compositor/src/input.rs | 11 +- rust/pantheon-compositor/src/main.rs | 1 + rust/pantheon-compositor/src/shell_compat.rs | 245 ++++++++++++++++++ rust/pantheon-compositor/src/state.rs | 4 + 9 files changed, 300 insertions(+), 17 deletions(-) create mode 100644 rust/pantheon-compositor/src/shell_compat.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 2b56d8f4d..64f9c69ad 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -17,11 +17,15 @@ Shell keybinding/sound services. Added a read-only D-Bus output and logical layout, plus `MonitorsChanged` on nested-output resize. Local `cargo check` and build pass; `gdbus call` returned one 1280×800@60 mode and logical layout mode 1. A second isolated Wingpanel launch no longer reports the -missing DisplayConfig service and attaches SHM buffers. The GNOME Shell hooks -remain unresolved; applying monitor modes, Dock behavior and M-series DRM/KMS -remain unqualified. Next: trace the remaining Wingpanel Shell/Dock requirements -and implement needed behavior against the nested output. The WD22TB4 HDMI G3 -DCP/kernel issue remains separate. Scope and limits: +missing DisplayConfig service and attaches SHM buffers. Added the +`org.gnome.Shell` accelerator API used by Wingpanel and its sound indicator; +`Super+N` on the nested window produced `AcceleratorActivated` for Wingpanel's +registered action, and media-key registrations completed without a D-Bus +failure. `ShowOSD`, lock-screen modes and physical media-key activation remain +unimplemented/unqualified. Applying monitor modes, Dock behavior and M-series +DRM/KMS remain unqualified. Next: trace the Dock's shell protocol and window +interaction requirements against this output. The WD22TB4 HDMI G3 DCP/kernel +issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ae3a6a46a..25f6a5ea7 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -23,10 +23,12 @@ isolated nested Wayland run, sent `get_panel`, anchor and hide-mode requests, and attached SHM buffers. It reports the absent `org.gnome.Mutter.DisplayConfig` service. The prototype now returns its read-only `GetCurrentState` query using the nested output and logical layout mode; Wingpanel connects without the -missing-service error. The GNOME Shell keybinding/sound hooks still need -session-level compatibility. Autohide, blur, multi-output reflow, monitor mode -application, Dock behavior, full session integration and M-series hardware -remain unqualified. A public search found elementary's +missing-service error. The compositor now serves Wingpanel's `org.gnome.Shell` +accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during +an isolated nested run. `ShowOSD`, lock-screen action modes and physical media +key activation remain unimplemented or unqualified. Autohide, blur, +multi-output reflow, monitor mode application, Dock behavior, full session +integration and M-series hardware remain unqualified. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 371e63c31..3ec60c293 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -84,5 +84,8 @@ and `cargo build` pass. Its protocol probe completed under Xvfb over a nested Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland session and rendered SHM buffers. The prototype serves the read-only `org.gnome.Mutter.DisplayConfig.GetCurrentState` query Wingpanel uses, and -Wingpanel completes that call. Dock integration and M-series hardware remain -unqualified; this is not full session or hardware qualification. +Wingpanel completes that call. It also implements the `org.gnome.Shell` +accelerator registration and activation path: pressing the registered +`Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, +physical media-key activation and M-series hardware remain unqualified; this +is not full session or hardware qualification. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 46766155c..761bbd9e2 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -59,6 +59,12 @@ no behavior. Touch input, DRM/KMS, libinput device management, multi-monitor configuration, accessibility, IBus, Xwayland, screen-sharing portals and production crash recovery also remain unimplemented. +The session bus also exposes the small `org.gnome.Shell` accelerator API used +by Wingpanel and its sound indicator. Accelerator registrations are matched +through the compositor's active XKB state and reported with +`AcceleratorActivated`; `ShowOSD` is currently a no-op, and lock-screen action +modes are not implemented. + The code is based on Smithay's `smallvil` learning compositor at pinned commit `023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the upstream revision pinned until an explicit upgrade is reviewed. The Pantheon @@ -74,8 +80,10 @@ and attached shared-memory buffers. Wingpanel requires `org.gnome.Mutter.DisplayConfig`; the compositor now serves the read-only `GetCurrentState` query it calls, with the current nested output and logical layout mode, and emits `MonitorsChanged` when the output resizes. The isolated -Wingpanel run no longer reports a missing DisplayConfig service. GNOME Shell -keybinding and sound hooks still have no service on the isolated bus. This is -evidence of panel compatibility at the protocol/rendering path, not full -session qualification. Applying monitor modes, Dock behavior and M-series -DRM/KMS remain unqualified. +Wingpanel run no longer reports missing DisplayConfig or Shell keybinding +services. Pressing `Super+N` in the nested window produced the registered +`AcceleratorActivated` signal. The sound indicator's media-key registrations +also completed without its prior D-Bus error; physical media-key activation has +not been checked. This is evidence of panel compatibility at the +protocol/rendering path, not full session qualification. Applying monitor +modes, Dock behavior and M-series DRM/KMS remain unqualified. diff --git a/rust/pantheon-compositor/THIRD-PARTY.md b/rust/pantheon-compositor/THIRD-PARTY.md index 7842542d0..2d55c24a9 100644 --- a/rust/pantheon-compositor/THIRD-PARTY.md +++ b/rust/pantheon-compositor/THIRD-PARTY.md @@ -18,3 +18,10 @@ reply follows the public D-Bus contract in GNOME Mutter's The Rust implementation is a fresh subset for Wingpanel's current read-only layout query; it does not copy Mutter implementation code or provide mode application. + +The `org.gnome.Shell` accelerator compatibility surface follows the client +interfaces in elementary/Wingpanel `lib/KeybindingHelper.vala` at +`e737de88bc02ddc82a9f4d23f574cc3c04098c03` and +elementary/wingpanel-indicator-sound `src/Services/ShellKeyGrabber.vala` at +`115d1495b754e2692c112fa5b02f9f3b5860a314`. These files are API references; +the Rust D-Bus service and accelerator matcher are original implementation. diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 6c507678d..a66184f82 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -19,6 +19,7 @@ impl PantheonCompositor { InputEvent::Keyboard { event, .. } => { let serial = SERIAL_COUNTER.next_serial(); let time = Event::time(&event); + let key_state = event.state(); self.seat.get_keyboard().unwrap().input::<(), _>( self, @@ -26,7 +27,15 @@ impl PantheonCompositor { event.state(), serial, time, - |_, _, _| FilterResult::Forward, + |state, modifiers, keysym| { + if key_state == smithay::backend::input::KeyState::Pressed + && state.shell_compat.activate_key(keysym.modified_sym(), modifiers) + { + FilterResult::Intercept(()) + } else { + FilterResult::Forward + } + }, ); } InputEvent::PointerMotion { .. } => {} diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index e27783e25..2ee64c93a 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -6,6 +6,7 @@ mod display_config; mod input; mod pantheon_shell; mod protocols; +mod shell_compat; mod state; mod winit; diff --git a/rust/pantheon-compositor/src/shell_compat.rs b/rust/pantheon-compositor/src/shell_compat.rs new file mode 100644 index 000000000..a73bd6101 --- /dev/null +++ b/rust/pantheon-compositor/src/shell_compat.rs @@ -0,0 +1,245 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex, mpsc}, + thread, + time::Duration, +}; + +use smithay::input::keyboard::{Keysym, ModifiersState, xkb}; +use zbus::{blocking::Connection, interface, object_server::SignalEmitter, zvariant::OwnedValue}; + +const BUS_NAME: &str = "org.gnome.Shell"; +const OBJECT_PATH: &str = "/org/gnome/Shell"; +const INTERFACE: &str = "org.gnome.Shell"; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct AcceleratorModifiers { + ctrl: bool, + alt: bool, + shift: bool, + logo: bool, +} + +#[derive(Clone, Copy, Debug)] +struct ParsedAccelerator { + keysym: Keysym, + modifiers: AcceleratorModifiers, +} + +#[derive(Clone, Copy, Debug)] +struct KeyBinding { + id: u32, + accelerator: ParsedAccelerator, +} + +#[derive(Clone)] +pub struct ShellCompatHandle { + bindings: Arc>>, + activated: mpsc::Sender, +} + +impl ShellCompatHandle { + pub fn start() -> Self { + let bindings = Arc::new(Mutex::new(Vec::new())); + let (activated, receiver) = mpsc::channel(); + let service_bindings = bindings.clone(); + thread::Builder::new() + .name("pantheon-shell-dbus".into()) + .spawn(move || serve_shell_compat(service_bindings, receiver)) + .expect("failed to start shell compatibility D-Bus thread"); + Self { + bindings, + activated, + } + } + + pub fn activate_key(&self, keysym: Keysym, modifiers: &ModifiersState) -> bool { + let matched = { + let bindings = self.bindings.lock().expect("keybinding lock poisoned"); + bindings + .iter() + .filter(|binding| matches_accelerator(binding.accelerator, keysym, modifiers)) + .map(|binding| binding.id) + .collect::>() + }; + + if matched.is_empty() { + return false; + } + for id in matched { + let _ = self.activated.send(id); + } + true + } +} + +struct ShellKeyGrabber { + bindings: Arc>>, + next_id: u32, +} + +#[interface(name = "org.gnome.Shell")] +impl ShellKeyGrabber { + fn grab_accelerator(&mut self, accelerator: String, _mode_flags: u32, grab_flags: u32) -> u32 { + self.grab_one(&accelerator, grab_flags) + } + + fn grab_accelerators(&mut self, accelerators: Vec<(String, u32, u32)>) -> Vec { + accelerators + .into_iter() + .map(|(accelerator, _mode_flags, grab_flags)| self.grab_one(&accelerator, grab_flags)) + .collect() + } + + fn ungrab_accelerator(&mut self, action: u32) -> bool { + self.remove_bindings(&[action]) > 0 + } + + fn ungrab_accelerators(&mut self, actions: Vec) -> bool { + let removed = self.remove_bindings(&actions); + removed == actions.len() + } + + fn show_osd(&self, _parameters: HashMap) {} + + #[zbus(signal)] + async fn accelerator_activated( + emitter: &SignalEmitter<'_>, + action: u32, + parameters_dict: HashMap, + ) -> zbus::Result<()>; +} + +impl ShellKeyGrabber { + fn grab_one(&mut self, accelerator: &str, grab_flags: u32) -> u32 { + let Some(parsed) = parse_accelerator(accelerator, grab_flags) else { + return 0; + }; + let mut bindings = self.bindings.lock().expect("keybinding lock poisoned"); + if bindings + .iter() + .any(|binding| same_accelerator(binding.accelerator, parsed)) + { + return 0; + } + + let id = self.next_id; + self.next_id = self.next_id.wrapping_add(1).max(1); + bindings.push(KeyBinding { + id, + accelerator: parsed, + }); + id + } + + fn remove_bindings(&self, actions: &[u32]) -> usize { + let mut bindings = self.bindings.lock().expect("keybinding lock poisoned"); + let initial_len = bindings.len(); + bindings.retain(|binding| !actions.contains(&binding.id)); + initial_len - bindings.len() + } +} + +fn serve_shell_compat(bindings: Arc>>, receiver: mpsc::Receiver) { + let connection = match Connection::session() { + Ok(connection) => connection, + Err(error) => { + tracing::warn!(%error, "cannot connect to the session bus; Shell keybindings are disabled"); + return; + } + }; + + if let Err(error) = connection.object_server().at( + OBJECT_PATH, + ShellKeyGrabber { + bindings, + next_id: 1, + }, + ) { + tracing::warn!(%error, "cannot register the Shell keybinding compatibility interface"); + return; + } + if let Err(error) = connection.request_name(BUS_NAME) { + tracing::warn!(%error, "cannot own the Shell keybinding compatibility name"); + return; + } + + tracing::info!("serving the Wingpanel Shell accelerator compatibility interface"); + loop { + match receiver.recv_timeout(Duration::from_millis(100)) { + Ok(action) => { + let parameters = HashMap::::new(); + if let Err(error) = connection.emit_signal( + None::<&str>, + OBJECT_PATH, + INTERFACE, + "AcceleratorActivated", + &(action, parameters), + ) { + tracing::warn!(%error, "could not emit AcceleratorActivated"); + } + } + Err(mpsc::RecvTimeoutError::Timeout) => {} + Err(mpsc::RecvTimeoutError::Disconnected) => return, + } + } +} + +fn parse_accelerator(value: &str, _grab_flags: u32) -> Option { + let mut remaining = value.trim(); + let mut modifiers = AcceleratorModifiers::default(); + while remaining.starts_with('<') { + let end = remaining.find('>')?; + let modifier = remaining[1..end].to_ascii_lowercase(); + match modifier.as_str() { + "control" | "ctrl" | "primary" => modifiers.ctrl = true, + "alt" | "mod1" => modifiers.alt = true, + "shift" => modifiers.shift = true, + "super" | "meta" | "logo" | "mod4" => modifiers.logo = true, + _ => return None, + } + remaining = &remaining[end + 1..]; + } + + let key_name = remaining.trim_start_matches('+'); + if key_name.is_empty() { + return None; + } + let keysym = xkb::keysym_from_name(key_name, xkb::KEYSYM_CASE_INSENSITIVE); + if keysym == xkb::keysyms::KEY_NoSymbol.into() { + return None; + } + Some(ParsedAccelerator { keysym, modifiers }) +} + +fn matches_accelerator( + accelerator: ParsedAccelerator, + keysym: Keysym, + modifiers: &ModifiersState, +) -> bool { + let expected = accelerator.keysym.raw(); + let actual = keysym.raw(); + let expected_char = char::from_u32(expected); + let actual_char = char::from_u32(actual); + let ascii_letter = expected_char.is_some_and(|key| key.is_ascii_alphabetic()) + && actual_char.is_some_and(|key| key.is_ascii_alphabetic()); + let keys_match = if ascii_letter { + expected_char + .unwrap() + .eq_ignore_ascii_case(&actual_char.unwrap()) + } else { + expected == actual + }; + let shift_matches = accelerator.modifiers.shift == modifiers.shift + || (ascii_letter && !accelerator.modifiers.shift); + + keys_match + && accelerator.modifiers.ctrl == modifiers.ctrl + && accelerator.modifiers.alt == modifiers.alt + && accelerator.modifiers.logo == modifiers.logo + && shift_matches +} + +fn same_accelerator(left: ParsedAccelerator, right: ParsedAccelerator) -> bool { + left.keysym == right.keysym && left.modifiers == right.modifiers +} diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index ebf6999c3..22047ef90 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -25,6 +25,7 @@ use smithay::{ use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::display_config::DisplayConfigHandle; +use crate::shell_compat::ShellCompatHandle; pub struct PantheonCompositor { pub start_time: std::time::Instant, @@ -43,6 +44,7 @@ pub struct PantheonCompositor { pub data_device_state: DataDeviceState, pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, + pub shell_compat: ShellCompatHandle, pub panels: HashMap, pub last_work_area: Option>, pub managed_windows: @@ -82,6 +84,7 @@ impl PantheonCompositor { let data_device_state = DataDeviceState::new::(&dh); let pantheon_shell_state = PantheonShellState::new::(&dh); let display_config = DisplayConfigHandle::start(); + let shell_compat = ShellCompatHandle::start(); // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. @@ -124,6 +127,7 @@ impl PantheonCompositor { data_device_state, pantheon_shell_state, display_config, + shell_compat, panels: HashMap::new(), last_work_area: None, managed_windows: HashMap::new(), From 9322d577ad8ab4e27dae6b68b12840fadb437980 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:44:32 -0400 Subject: [PATCH 043/798] Route relative pointer motion to windows --- mseries/OS9-HANDOFF.md | 13 +++++--- mseries/ROADMAP.md | 14 ++++++--- rust/pantheon-compositor/README.md | 6 ++++ rust/pantheon-compositor/src/input.rs | 44 +++++++++++++++++++++++++-- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 64f9c69ad..9b891b7c3 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -22,10 +22,15 @@ missing DisplayConfig service and attaches SHM buffers. Added the `Super+N` on the nested window produced `AcceleratorActivated` for Wingpanel's registered action, and media-key registrations completed without a D-Bus failure. `ShowOSD`, lock-screen modes and physical media-key activation remain -unimplemented/unqualified. Applying monitor modes, Dock behavior and M-series -DRM/KMS remain unqualified. Next: trace the Dock's shell protocol and window -interaction requirements against this output. The WD22TB4 HDMI G3 DCP/kernel -issue remains separate. Scope and limits: +unimplemented/unqualified. Implemented relative pointer motion, pointer focus +updates, and dispatch through active window move/resize grabs; local `cargo +check --bins` and build pass. Applying monitor modes, Dock behavior and +M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` +on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` +are connected while `HDMI-A-1` is disconnected. No modeset was attempted. Next: +trace the Dock's shell protocol and window interaction requirements, then build +an opt-in native DRM path against the exposed M-series device stack. The WD22TB4 +HDMI G3 DCP/kernel issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 25f6a5ea7..f0ee92fda 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -26,12 +26,18 @@ the nested output and logical layout mode; Wingpanel connects without the missing-service error. The compositor now serves Wingpanel's `org.gnome.Shell` accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during an isolated nested run. `ShowOSD`, lock-screen action modes and physical media -key activation remain unimplemented or unqualified. Autohide, blur, -multi-output reflow, monitor mode application, Dock behavior, full session -integration and M-series hardware remain unqualified. A public search found elementary's +key activation remain unimplemented or unqualified. Relative pointer motion now +updates pointer focus and drives the compositor's existing move/resize grabs; +the Rust binaries build locally. Autohide, blur, multi-output reflow, monitor +mode application, Dock behavior, full session integration and M-series hardware +remain unqualified. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker -remains a separate workstream. +remains a separate workstream. Read-only enumeration on the current M-series +host found the Asahi GPU at `/dev/dri/card1`, Apple display controller at +`/dev/dri/card2`, and connected `eDP-1`/`DP-2` outputs; this is device presence, +not evidence of compositor modesetting. Next implementation work is an opt-in +native DRM path while keeping the nested backend available. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 761bbd9e2..9676c78d9 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -59,6 +59,12 @@ no behavior. Touch input, DRM/KMS, libinput device management, multi-monitor configuration, accessibility, IBus, Xwayland, screen-sharing portals and production crash recovery also remain unimplemented. +Relative pointer motion now updates Wayland pointer focus and dispatches motion +through active compositor grabs; coordinates are clamped to the mapped output +bounds. This allows the existing xdg move/resize grab implementations to receive +physical pointer motion. Multi-output bounds, pointer constraints, and hardware +input remain unqualified. + The session bus also exposes the small `org.gnome.Shell` accelerator API used by Wingpanel and its sound indicator. Accelerator registrations are matched through the compositor's active XKB state and reported with diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index a66184f82..30331fecd 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -1,11 +1,11 @@ use smithay::{ backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, - KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, + KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, }, input::{ keyboard::FilterResult, - pointer::{AxisFrame, ButtonEvent, MotionEvent}, + pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent}, }, reexports::wayland_server::protocol::wl_surface::WlSurface, utils::SERIAL_COUNTER, @@ -38,7 +38,45 @@ impl PantheonCompositor { }, ); } - InputEvent::PointerMotion { .. } => {} + InputEvent::PointerMotion { event, .. } => { + let pointer = self.seat.get_pointer().unwrap(); + let location = pointer.current_location(); + let output_bounds = self + .space + .outputs() + .filter_map(|output| self.space.output_geometry(output)) + .reduce(|bounds, output| bounds.merge(output)) + .unwrap(); + let max_x = (output_bounds.loc.x + output_bounds.size.w - 1) as f64; + let max_y = (output_bounds.loc.y + output_bounds.size.h - 1) as f64; + let target = location + event.delta(); + let new_location = smithay::utils::Point::from(( + target.x.clamp(output_bounds.loc.x as f64, max_x), + target.y.clamp(output_bounds.loc.y as f64, max_y), + )); + let serial = SERIAL_COUNTER.next_serial(); + let under = self.surface_under(location); + + pointer.relative_motion( + self, + under.clone(), + &RelativeMotionEvent { + delta: event.delta(), + delta_unaccel: event.delta_unaccel(), + time: event.time(), + }, + ); + pointer.motion( + self, + self.surface_under(new_location), + &MotionEvent { + location: new_location, + serial, + time: event.time(), + }, + ); + pointer.frame(self); + } InputEvent::PointerMotionAbsolute { event, .. } => { let output = self.space.outputs().next().unwrap(); From 562c2bc094209d37a5f27fac4fb798065cabd7ce Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:45:57 -0400 Subject: [PATCH 044/798] Support xdg popup grabs --- mseries/OS9-HANDOFF.md | 6 ++- mseries/ROADMAP.md | 6 ++- rust/pantheon-compositor/README.md | 9 +++- .../src/handlers/xdg_shell.rs | 46 +++++++++++++++++-- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 9b891b7c3..048401909 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -23,8 +23,10 @@ missing DisplayConfig service and attaches SHM buffers. Added the registered action, and media-key registrations completed without a D-Bus failure. `ShowOSD`, lock-screen modes and physical media-key activation remain unimplemented/unqualified. Implemented relative pointer motion, pointer focus -updates, and dispatch through active window move/resize grabs; local `cargo -check --bins` and build pass. Applying monitor modes, Dock behavior and +updates, and dispatch through active window move/resize grabs. Added xdg popup +keyboard/pointer grabs rooted at managed toplevels, including nested serial +validation; local `cargo check --bins` and `cargo build --bins` pass. Popup +behavior is not yet runtime-qualified. Applying monitor modes, Dock behavior and M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` are connected while `HDMI-A-1` is disconnected. No modeset was attempted. Next: diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index f0ee92fda..293276d24 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -28,8 +28,10 @@ accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during an isolated nested run. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; -the Rust binaries build locally. Autohide, blur, multi-output reflow, monitor -mode application, Dock behavior, full session integration and M-series hardware +xdg popup requests now install Smithay keyboard/pointer grabs for managed +toplevel roots. Both features build locally; popup behavior has not been +runtime-qualified. Autohide, blur, multi-output reflow, monitor mode +application, Dock behavior, full session integration and M-series hardware remain unqualified. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 9676c78d9..720b546f5 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -42,8 +42,8 @@ a Mutter port or a Pantheon replacement yet. The target is only the M-series/Pantheon behavior documented in [`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). It currently has one nested output, -basic keyboard/pointer handling, xdg-shell windows, move/resize grabs, clipboard -and drag-and-drop. It now advertises the existing Pantheon desktop-shell +basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize +grabs, clipboard and drag-and-drop. It now advertises the existing Pantheon desktop-shell protocol v2: panel surfaces can set their anchor, size, hide mode and offset; the compositor places those surfaces and uses exclusive panel geometry for xdg-toplevel maximize requests. The Dock's M-series offset request is therefore @@ -65,6 +65,11 @@ bounds. This allows the existing xdg move/resize grab implementations to receive physical pointer motion. Multi-output bounds, pointer constraints, and hardware input remain unqualified. +Xdg popup requests now resolve their root to a managed toplevel and install +Smithay's popup keyboard and pointer grabs, including the serial checks needed +to preserve valid nested grabs. Popup interaction has only been build-checked; +it has not yet been exercised with a real application menu. + The session bus also exposes the small `org.gnome.Shell` accelerator API used by Wingpanel and its sound indicator. Accelerator registrations are matched through the compositor's active XKB state and reported with diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 5cc68997d..c7099c4fd 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -1,6 +1,7 @@ use smithay::{ desktop::{ - PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords, + PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Space, + Window, find_popup_root_surface, get_popup_toplevel_coords, }, input::{ Seat, @@ -144,8 +145,47 @@ impl XdgShellHandler for PantheonCompositor { self.configure_window_mode(surface, false, false); } - fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { - // TODO popup grabs + fn grab(&mut self, surface: PopupSurface, seat_resource: wl_seat::WlSeat, serial: Serial) { + let seat = Seat::from_resource(&seat_resource).unwrap(); + let kind = PopupKind::Xdg(surface); + let Ok(root_surface) = find_popup_root_surface(&kind) else { + return; + }; + let Some(root) = self.space.elements().find_map(|window| { + window + .toplevel() + .filter(|toplevel| toplevel.wl_surface() == &root_surface) + .map(|toplevel| toplevel.wl_surface().clone()) + }) else { + return; + }; + + let Ok(mut grab) = self.popups.grab_popup(root, kind, &seat, serial) else { + return; + }; + + if let Some(keyboard) = seat.get_keyboard() { + if keyboard.is_grabbed() + && !(keyboard.has_grab(serial) + || keyboard.has_grab(grab.previous_serial().unwrap_or(serial))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + keyboard.set_focus(self, grab.current_grab(), serial); + keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); + } + + if let Some(pointer) = seat.get_pointer() { + if pointer.is_grabbed() + && !(pointer.has_grab(serial) + || pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial()))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep); + } } } From bf8386f8041574bb2c609310c7351680242c10bf Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:47:43 -0400 Subject: [PATCH 045/798] Stage optional native DRM build dependencies --- mseries/OS9-HANDOFF.md | 9 +- mseries/ROADMAP.md | 5 +- rust/Cargo.lock | 393 +++++++++++++++++++++++++--- rust/pantheon-compositor/Cargo.toml | 15 ++ rust/pantheon-compositor/README.md | 6 + 5 files changed, 392 insertions(+), 36 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 048401909..4f1307949 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -30,8 +30,13 @@ behavior is not yet runtime-qualified. Applying monitor modes, Dock behavior and M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` are connected while `HDMI-A-1` is disconnected. No modeset was attempted. Next: -trace the Dock's shell protocol and window interaction requirements, then build -an opt-in native DRM path against the exposed M-series device stack. The WD22TB4 +added an opt-in `native-drm` Cargo feature to stage Smithay's DRM/GBM, +libinput/udev/libseat, EGL and multi-GPU dependencies. The +`cargo check --features native-drm --bins` command currently stops at +`libseat-sys`: this host lacks +the `libseat.pc` development package. The normal Winit binaries still build; +native startup is not wired. Next: satisfy the host dependency and implement an +opt-in native DRM path against the exposed M-series device stack. The WD22TB4 HDMI G3 DCP/kernel issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 293276d24..0167935bb 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -39,7 +39,10 @@ remains a separate workstream. Read-only enumeration on the current M-series host found the Asahi GPU at `/dev/dri/card1`, Apple display controller at `/dev/dri/card2`, and connected `eDP-1`/`DP-2` outputs; this is device presence, not evidence of compositor modesetting. Next implementation work is an opt-in -native DRM path while keeping the nested backend available. +native DRM path while keeping the nested backend available. The opt-in +`native-drm` Smithay dependency feature is staged, but its local build currently +needs the missing `libseat.pc` development package; no native session code is +wired yet. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index c4f1c0744..e9a5578c9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "android-activity" version = "0.6.1" @@ -99,9 +105,9 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.5", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -130,7 +136,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix", + "rustix 1.1.5", ] [[package]] @@ -156,10 +162,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 1.1.5", "signal-hook-registry", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -240,6 +246,20 @@ name = "bytemuck" version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] [[package]] name = "bytes" @@ -255,7 +275,7 @@ checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" dependencies = [ "bitflags", "polling", - "rustix", + "rustix 1.1.5", "tracing", ] @@ -266,7 +286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ "calloop", - "rustix", + "rustix 1.1.5", "wayland-backend", "wayland-client", ] @@ -388,12 +408,46 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.5", +] + [[package]] name = "drm-fourcc" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + [[package]] name = "endi" version = "1.1.1" @@ -434,7 +488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -527,13 +581,35 @@ dependencies = [ "slab", ] +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + [[package]] name = "gethostname" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix", + "rustix 1.1.5", "windows-link", ] @@ -572,6 +648,12 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.5.3" @@ -698,6 +780,35 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "input" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9793345a65d71317763a33066b5d8351f8760dde8d4930fe9e39b5f14a7959d" +dependencies = [ + "bitflags", + "input-sys", + "libc", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eee07d8e02bd95bf52b2e642cf13d33701b94c6e4b04fbf1d1fb07e9cb19e7" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -827,6 +938,48 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libseat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6656c69b6e0366ffb83faa232f3e4fda5fc91161722d140f2c95ce2e90b1ef31" +dependencies = [ + "errno", + "libseat-sys", + "log", +] + +[[package]] +name = "libseat-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a484fa48be5f62a8d3ffed767779d0ab808cfead91de98ef4362d469097dfb" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -918,7 +1071,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1137,10 +1290,10 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.3", "pin-project-lite", - "rustix", - "windows-sys", + "rustix 1.1.5", + "windows-sys 0.61.2", ] [[package]] @@ -1287,7 +1440,7 @@ dependencies = [ "calloop", "enumflags2", "log", - "rustix", + "rustix 1.1.5", ] [[package]] @@ -1299,6 +1452,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.5" @@ -1308,8 +1474,8 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", - "windows-sys", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -1444,28 +1610,38 @@ name = "smithay" version = "0.7.0" source = "git+https://github.com/Smithay/smithay.git?rev=023b78b1c5a05ca512767cacc07efca55fae8be4#023b78b1c5a05ca512767cacc07efca55fae8be4" dependencies = [ + "aliasable", "appendlist", "bitflags", "calloop", + "cc", "cursor-icon", "downcast-rs", + "drm", + "drm-ffi", "drm-fourcc", + "drm-sys", "errno", + "gbm", "gl_generator", "glam", "indexmap", + "input", "libc", "libloading", + "libseat", + "pkg-config", "portable-atomic", "profiling", "rand", "reis", - "rustix", + "rustix 1.1.5", "siphasher", "smallvec", "tempfile", "thiserror 2.0.21", "tracing", + "udev", "wayland-client", "wayland-cursor", "wayland-egl", @@ -1489,7 +1665,7 @@ dependencies = [ "cursor-icon", "log", "memmap2", - "rustix", + "rustix 1.1.5", "thiserror 2.0.21", "wayland-client", "wayland-csd-frame", @@ -1560,8 +1736,8 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix", - "windows-sys", + "rustix 1.1.5", + "windows-sys 0.61.2", ] [[package]] @@ -1724,6 +1900,18 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "uds_windows" version = "1.2.1" @@ -1732,7 +1920,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1856,7 +2044,7 @@ checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 1.1.5", "scoped-tls", "smallvec", "wayland-sys", @@ -1869,7 +2057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ "bitflags", - "rustix", + "rustix 1.1.5", "wayland-backend", "wayland-scanner", ] @@ -1891,7 +2079,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "rustix", + "rustix 1.1.5", "wayland-client", "xcursor", ] @@ -1992,7 +2180,7 @@ checksum = "0dde9c29be0f723a573977de51ee455bf3dfa03652730a74f9dd3b337e374d75" dependencies = [ "bitflags", "downcast-rs", - "rustix", + "rustix 1.1.5", "wayland-backend", "wayland-scanner", ] @@ -2035,7 +2223,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2044,6 +2232,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2053,6 +2259,127 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winit" version = "0.31.0-beta.3" @@ -2065,7 +2392,7 @@ dependencies = [ "dpi", "libc", "raw-window-handle", - "rustix", + "rustix 1.1.5", "smol_str", "tracing", "winit-android", @@ -2207,7 +2534,7 @@ dependencies = [ "memmap2", "percent-encoding", "raw-window-handle", - "rustix", + "rustix 1.1.5", "smithay-client-toolkit", "smol_str", "tracing", @@ -2256,7 +2583,7 @@ dependencies = [ "tracing", "unicode-segmentation", "url", - "windows-sys", + "windows-sys 0.61.2", "winit-common", "winit-core", ] @@ -2275,7 +2602,7 @@ dependencies = [ "libc", "percent-encoding", "raw-window-handle", - "rustix", + "rustix 1.1.5", "smol_str", "tracing", "winit-common", @@ -2322,7 +2649,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix", + "rustix 1.1.5", "x11rb-protocol", "xcursor", ] @@ -2420,13 +2747,13 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix", + "rustix 1.1.5", "serde", "serde_repr", "tracing", "uds_windows", "uuid", - "windows-sys", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index f2e64e883..29b0a3630 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -5,6 +5,21 @@ edition = "2024" license = "GPL-3.0-only" description = "Nested Rust Wayland compositor prototype for elementary OS" +[features] +default = [] +# Dependencies for the opt-in native M-series DRM/session backend. The current +# executable still starts through Winit; native session startup is being added +# separately so routine development cannot take over the active display. +native-drm = [ + "smithay/backend_drm", + "smithay/backend_gbm", + "smithay/backend_libinput", + "smithay/backend_udev", + "smithay/backend_session_libseat", + "smithay/renderer_gl", + "smithay/renderer_multi", +] + [[bin]] name = "pantheon-compositor" path = "src/main.rs" diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 720b546f5..7613c01aa 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -59,6 +59,12 @@ no behavior. Touch input, DRM/KMS, libinput device management, multi-monitor configuration, accessibility, IBus, Xwayland, screen-sharing portals and production crash recovery also remain unimplemented. +`native-drm` is an opt-in Cargo feature that stages Smithay's DRM, GBM, +libinput, udev, libseat, EGL, and multi-GPU dependencies. The current executable +still starts through Winit; the native session/backend wiring is not implemented +yet. Building this feature also requires the host `libseat` development package +(`libseat.pc`), which is absent on the current host. + Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output bounds. This allows the existing xdg move/resize grab implementations to receive From 8455e82854554fa5e6daf323f845cfd201b8c3fe Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:53:28 -0400 Subject: [PATCH 046/798] Record native Smithay feature build --- mseries/OS9-HANDOFF.md | 13 ++++++------- mseries/ROADMAP.md | 5 ++--- rust/pantheon-compositor/README.md | 5 +++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4f1307949..e102569ef 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -29,13 +29,12 @@ validation; local `cargo check --bins` and `cargo build --bins` pass. Popup behavior is not yet runtime-qualified. Applying monitor modes, Dock behavior and M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` -are connected while `HDMI-A-1` is disconnected. No modeset was attempted. Next: -added an opt-in `native-drm` Cargo feature to stage Smithay's DRM/GBM, -libinput/udev/libseat, EGL and multi-GPU dependencies. The -`cargo check --features native-drm --bins` command currently stops at -`libseat-sys`: this host lacks -the `libseat.pc` development package. The normal Winit binaries still build; -native startup is not wired. Next: satisfy the host dependency and implement an +are connected while `HDMI-A-1` is disconnected. No modeset was attempted. Added +an opt-in `native-drm` Cargo feature to stage Smithay's DRM/GBM, +libinput/udev/libseat, EGL and multi-GPU dependencies. Installed `libseat-dev` +0.9.2-2 on the host; both `cargo check --features native-drm --bins` and +`cargo build --features native-drm --bins` pass. The default Winit binary also +builds. No native session startup is wired or tested yet. Next: implement an opt-in native DRM path against the exposed M-series device stack. The WD22TB4 HDMI G3 DCP/kernel issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 0167935bb..c704c5e9a 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -40,9 +40,8 @@ host found the Asahi GPU at `/dev/dri/card1`, Apple display controller at `/dev/dri/card2`, and connected `eDP-1`/`DP-2` outputs; this is device presence, not evidence of compositor modesetting. Next implementation work is an opt-in native DRM path while keeping the nested backend available. The opt-in -`native-drm` Smithay dependency feature is staged, but its local build currently -needs the missing `libseat.pc` development package; no native session code is -wired yet. +`native-drm` Smithay dependency feature now compiles locally after installing +`libseat-dev`; no native session code is wired yet. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 7613c01aa..f9ccf750b 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -62,8 +62,9 @@ production crash recovery also remain unimplemented. `native-drm` is an opt-in Cargo feature that stages Smithay's DRM, GBM, libinput, udev, libseat, EGL, and multi-GPU dependencies. The current executable still starts through Winit; the native session/backend wiring is not implemented -yet. Building this feature also requires the host `libseat` development package -(`libseat.pc`), which is absent on the current host. +yet. The feature-enabled `cargo check` and `cargo build` pass locally after +installing the host `libseat-dev` package; neither command initializes a native +session. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output From d9dde2ba80a32780eb9266df98f74688638c1abe Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:53:28 -0400 Subject: [PATCH 047/798] Record enh1 diagnostic boot result and post-reboot crash triage The one-shot enh1 diagnostic entry reached the desktop (no failed units, dock authorized, full-height panel, udmabuf/dma-heap present). GRUB next_entry is consumed; the next normal boot returns to .20. Today's real Gala SEGVs both predate the #244 libmutter fix, which the running Gala now loads. Co-Authored-By: Claude Opus 5.5 --- mseries/OS9-HANDOFF.md | 28 +++++++++++++++++++ ...2026-10-18-j314s-enh1-diagnostic-boot.json | 27 ++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 mseries/aurora/hardware-tests/2026-10-18-j314s-enh1-diagnostic-boot.json diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index a52ac094c..bb3424581 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,33 @@ # OS 9 implementation / Claude Code handoff +## enh1 diagnostic boot reached the desktop; crash triage after reboot — 2026-10-18 (Claude Code) + +**Kernel now:** `7.1.12-elementary-aurora20-enh1` via the one-shot +`elementary-aurora20-enh1-diagnostic` entry (kernel and initrd hashes match staging). +GRUB consumed `next_entry`, which is now empty, so the **next normal boot is +`mseries-full-height` on `.20`**. The earlier enh1 hang did not reproduce: initramfs +log shows every hook ran, no failed units, graphical target at 5.2 s, dock authorized, +panel 3024x1964 full height, `/dev/udmabuf` + `/dev/dma_heap/system` present (the +intended config delta), SEP/fprintd and speakers up. Kernel errors match the previous +`.20` boot apart from deferred dp-phy probes. [Receipt](aurora/hardware-tests/2026-10-18-j314s-enh1-diagnostic-boot.json). +One boot only: candidate stays unqualified, original hang cause unknown, no +suspend or repeat-boot test. Promotion is DeepSeek's/owner's call; no boot change made. + +**Crash triage across today's boots:** +- Boot ending 19:53: Gala SEGV at 19:52 (`clutter_sprite_invalidate_cursor`, same + stack as #240), then the owner's power-button forced shutdown; the workqueue WARN + and DCP endpoint timeouts are in the shutdown path, after the crash. +- Boot 19:57–22:48: real-session Gala SEGV at 21:43 with the same stack, before the + #244 libmutter fix was installed. Ended with a clean reboot. +- Other cores that boot were our own headless tests (nested Gala, a Wingpanel started + from an agent session, test-agenda), not the owner's desktop. +- Running Gala now maps `/usr/local/lib/mseries/gala/mutter-fix/libmutter-18.so.0`: + this is the first real session with the fix. Not yet proven over time. +- WIP from before the reboot is on `origin/wip/reboot-save-20261018` (`e26c98a`). + +Next: watch `coredumpctl list gala` this session; the next reboot returns to `.20` +unless the owner arms enh1 again. + ## Release stages: F5 parity first, support assistants after — 2026-10-18 Owner's order: diff --git a/mseries/aurora/hardware-tests/2026-10-18-j314s-enh1-diagnostic-boot.json b/mseries/aurora/hardware-tests/2026-10-18-j314s-enh1-diagnostic-boot.json new file mode 100644 index 000000000..4390bfbcd --- /dev/null +++ b/mseries/aurora/hardware-tests/2026-10-18-j314s-enh1-diagnostic-boot.json @@ -0,0 +1,27 @@ +{ + "schema": 1, + "recorded_at_utc": "2026-10-19T02:55:00+00:00", + "status": "diagnostic one-shot boot reached the desktop; candidate still unqualified", + "entry_id": "elementary-aurora20-enh1-diagnostic", + "running_kernel": "7.1.12-elementary-aurora20-enh1", + "kernel_sha256": "21ee66cf7b7afd0a999c8e9087411aef4b121d1ecffab88989ba5cacf1dfdaa7", + "diagnostic_initrd_sha256": "b7c6a3e74e1c90d4d5925551cdfa44d41e38f1e0f83d456c4a5eb1bea9dd8c35", + "cmdline_extra": ["initcall_debug", "ignore_loglevel", "log_buf_len=8M", "plymouth.enable=0", "rd.log=kmsg|console|file", "appledrm.show_notch=1"], + "boot_id": "db83b1a40d7f48cdbd8733b2eaca9d0f", + "boot_started": "2026-10-18T22:48:55-04:00", + "previous_boot_end": "clean systemd reboot at 2026-10-18T22:48:05-04:00 from .20", + "grub_next_entry_after_boot": "", + "next_normal_boot": "mseries-full-height (7.1.12-2-20261017.20-sep-ARCH)", + "startup": "17.705s kernel + 7.168s userspace; graphical.target at 5.236s", + "initramfs_log": "all hooks ran; encrypted root fsck + mount; late asahi firmware copy; no hook failure", + "failed_system_units": 0, + "failed_user_units": 0, + "dock": "WD22TB4 attached and authorized", + "panel": "eDP-1 3024x1964 (full height, show_notch=Y)", + "intended_config_delta_present": {"CONFIG_UDMABUF": "y, /dev/udmabuf", "CONFIG_DMABUF_HEAPS_SYSTEM": "y, /dev/dma_heap/system"}, + "drivers_seen": ["apple_sep loaded, fprintd sees the SEP sensor (no fingers enrolled)", "snd_soc_macaudio + speakersafetyd active, j314 convolver default sink", "appledrm"], + "kernel_errors_vs_previous_dot20_boot": "same set (dcp set_active_lane_count lane 4, brcmfmac p2p -52, tas2764 3-003c DT parse, pcieport of_irq -22) plus 3 deferred 'Failed to get dp-phy: -517' (probe deferral)", + "failed_initcalls": "only -ENODEV/-ENOSYS feature probes (cpufreq_dt, SM4, esrt, efi_shutdown)", + "not_tested": ["suspend/resume", "dock HDMI picture (G3)", "non-diagnostic enh1 entry", "repeat boots"], + "original_hang_cause": "still unknown; this boot did not reproduce it" +} From 4040e43c5d41f687045dfb29cd89bdd6523dde33 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:54:34 -0400 Subject: [PATCH 048/798] Probe M-series EGL and DRM topology --- mseries/OS9-HANDOFF.md | 10 +++-- mseries/ROADMAP.md | 5 ++- rust/pantheon-compositor/Cargo.toml | 5 +++ rust/pantheon-compositor/README.md | 7 ++++ .../src/bin/pantheon-drm-probe.rs | 37 +++++++++++++++++++ 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e102569ef..0029c4982 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -34,9 +34,13 @@ an opt-in `native-drm` Cargo feature to stage Smithay's DRM/GBM, libinput/udev/libseat, EGL and multi-GPU dependencies. Installed `libseat-dev` 0.9.2-2 on the host; both `cargo check --features native-drm --bins` and `cargo build --features native-drm --bins` pass. The default Winit binary also -builds. No native session startup is wired or tested yet. Next: implement an -opt-in native DRM path against the exposed M-series device stack. The WD22TB4 -HDMI G3 DCP/kernel issue remains separate. Scope and limits: +builds. Added and ran `pantheon-drm-probe`, which only enumerates EGL devices. +It identifies Asahi `/dev/dri/card1` plus `/dev/dri/renderD128` for rendering; +the Apple display-controller `/dev/dri/card2` has no EGL device. Native startup +will therefore need the multi-GPU import path across the renderer and KMS +devices. No native session startup is wired or tested yet. Next: implement an +opt-in DRM path with this split-device topology. The WD22TB4 HDMI G3 DCP/kernel +issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index c704c5e9a..87a8516ea 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -41,7 +41,10 @@ host found the Asahi GPU at `/dev/dri/card1`, Apple display controller at not evidence of compositor modesetting. Next implementation work is an opt-in native DRM path while keeping the nested backend available. The opt-in `native-drm` Smithay dependency feature now compiles locally after installing -`libseat-dev`; no native session code is wired yet. +`libseat-dev`. The new non-invasive `pantheon-drm-probe` reports the Asahi +renderer on card1/renderD128 and no EGL device for the Apple display controller +on card2. Native session code must bridge rendering to KMS across these devices; +no session startup is wired yet. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index 29b0a3630..e29a88f80 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -28,6 +28,11 @@ path = "src/main.rs" name = "pantheon-shell-probe" path = "src/bin/pantheon-shell-probe.rs" +[[bin]] +name = "pantheon-drm-probe" +path = "src/bin/pantheon-drm-probe.rs" +required-features = ["native-drm"] + [dependencies] tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } tracing = "0.1" diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index f9ccf750b..073a4b929 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -66,6 +66,13 @@ yet. The feature-enabled `cargo check` and `cargo build` pass locally after installing the host `libseat-dev` package; neither command initializes a native session. +With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices +without opening a session or changing display modes. On the current M-series +host it finds EGL/GBM rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, +Asahi), while the Apple display-controller `/dev/dri/card2` has no EGL device. +The native backend therefore needs Smithay's explicit multi-GPU render/import +path to connect the Asahi renderer to Apple KMS; single-node setup is not enough. + Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output bounds. This allows the existing xdg move/resize grab implementations to receive diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs new file mode 100644 index 000000000..4de7d5d56 --- /dev/null +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -0,0 +1,37 @@ +use smithay::backend::egl::EGLDevice; + +fn main() -> Result<(), Box> { + let devices = EGLDevice::enumerate()?; + let mut count = 0; + + for (index, device) in devices.enumerate() { + count += 1; + let drm_path = device + .drm_device_path() + .map(|path| path.display().to_string()) + .unwrap_or_else(|error| format!("unavailable ({error})")); + let render_path = device + .render_device_path() + .map(|path| path.display().to_string()) + .unwrap_or_else(|error| format!("unavailable ({error})")); + let render_node = device + .try_get_render_node() + .map(|node| { + node.map(|node| node.to_string()) + .unwrap_or_else(|| "unavailable".to_owned()) + }) + .unwrap_or_else(|error| format!("unavailable ({error})")); + + println!("EGL device {index}:"); + println!(" DRM node: {drm_path}"); + println!(" render node: {render_path}"); + println!(" Smithay node selection: {render_node}"); + println!(" software renderer: {}", device.is_software()); + } + + if count == 0 { + println!("No EGL devices found."); + } + + Ok(()) +} From 9035b4f64d778aa606a4767330caf25da86cb9a7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:56:23 -0400 Subject: [PATCH 049/798] Inventory M-series KMS connectors read-only --- mseries/OS9-HANDOFF.md | 13 ++-- mseries/ROADMAP.md | 7 +- rust/pantheon-compositor/README.md | 14 ++-- .../src/bin/pantheon-drm-probe.rs | 72 +++++++++++++++++++ 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 0029c4982..2bcc9b751 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -36,11 +36,14 @@ libinput/udev/libseat, EGL and multi-GPU dependencies. Installed `libseat-dev` `cargo build --features native-drm --bins` pass. The default Winit binary also builds. Added and ran `pantheon-drm-probe`, which only enumerates EGL devices. It identifies Asahi `/dev/dri/card1` plus `/dev/dri/renderD128` for rendering; -the Apple display-controller `/dev/dri/card2` has no EGL device. Native startup -will therefore need the multi-GPU import path across the renderer and KMS -devices. No native session startup is wired or tested yet. Next: implement an -opt-in DRM path with this split-device topology. The WD22TB4 HDMI G3 DCP/kernel -issue remains separate. Scope and limits: +read-only KMS queries there return `Operation not supported`. DRM card2 owns the +connected `eDP-1` (3024×1964@120 Hz) and `DP-2` (up to 3840×2160@30 Hz); its +`HDMI-A-1` connector is disconnected. Card2 has no EGL device, so native startup +must bridge the renderer and display controller. The probe uses read-only file +descriptors, does not acquire DRM master, and does not force-probe connectors or +change modes. No native session startup is wired or tested yet. Next: implement +an opt-in DRM path with this split-device topology. The WD22TB4 HDMI G3 +DCP/kernel issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 87a8516ea..ad772e1a2 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -42,9 +42,10 @@ not evidence of compositor modesetting. Next implementation work is an opt-in native DRM path while keeping the nested backend available. The opt-in `native-drm` Smithay dependency feature now compiles locally after installing `libseat-dev`. The new non-invasive `pantheon-drm-probe` reports the Asahi -renderer on card1/renderD128 and no EGL device for the Apple display controller -on card2. Native session code must bridge rendering to KMS across these devices; -no session startup is wired yet. +renderer on card1/renderD128, with KMS resource queries unsupported there. Card2 +owns `eDP-1` and `DP-2`, while `HDMI-A-1` is disconnected; card2 has no EGL +device. The native backend must bridge rendering to KMS across these devices; +the read-only probe did not change modes. No session startup is wired yet. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 073a4b929..663b8db19 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -67,11 +67,15 @@ installing the host `libseat-dev` package; neither command initializes a native session. With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices -without opening a session or changing display modes. On the current M-series -host it finds EGL/GBM rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, -Asahi), while the Apple display-controller `/dev/dri/card2` has no EGL device. -The native backend therefore needs Smithay's explicit multi-GPU render/import -path to connect the Asahi renderer to Apple KMS; single-node setup is not enough. +without acquiring DRM master or changing display modes. On the current M-series +host it finds EGL rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, Asahi) +but KMS resource queries there return `Operation not supported`. The Apple +display-controller `/dev/dri/card2` owns KMS: `eDP-1` is connected at +3024×1964@120 Hz and `DP-2` is connected with 3840×2160@30 Hz and 1920×1080@60 +Hz modes; `HDMI-A-1` is disconnected. Card2 has no EGL device. The native backend +therefore needs Smithay's multi-GPU render/import path from Asahi to Apple KMS; +single-node setup is not enough. This probe reads connector state and modes but +does not force a connector reprobe or test modesetting. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs index 4de7d5d56..1b8845d15 100644 --- a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -1,5 +1,21 @@ +use std::{ + fs::File, + os::fd::{AsFd, BorrowedFd}, +}; + use smithay::backend::egl::EGLDevice; +struct ReadOnlyDrmDevice(File); + +impl AsFd for ReadOnlyDrmDevice { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } +} + +impl smithay::reexports::drm::Device for ReadOnlyDrmDevice {} +impl smithay::reexports::drm::control::Device for ReadOnlyDrmDevice {} + fn main() -> Result<(), Box> { let devices = EGLDevice::enumerate()?; let mut count = 0; @@ -33,5 +49,61 @@ fn main() -> Result<(), Box> { println!("No EGL devices found."); } + inspect_kms_connectors()?; + + Ok(()) +} + +fn inspect_kms_connectors() -> Result<(), Box> { + use smithay::reexports::drm::control::Device as _; + + let mut cards = std::fs::read_dir("/dev/dri")? + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .and_then(|name| name.strip_prefix("card")) + .is_some_and(|suffix| { + !suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_digit()) + }) + }) + .collect::>(); + cards.sort(); + + for path in cards { + println!("KMS device {}:", path.display()); + let device = match File::open(&path) { + Ok(file) => ReadOnlyDrmDevice(file), + Err(error) => { + println!(" cannot open read-only: {error}"); + continue; + } + }; + let resources = match device.resource_handles() { + Ok(resources) => resources, + Err(error) => { + println!(" cannot query resources: {error}"); + continue; + } + }; + + for handle in resources.connectors() { + match device.get_connector(*handle, false) { + Ok(connector) => { + println!(" {}: {:?}", connector, connector.state()); + for mode in connector.modes().iter().take(8) { + let (width, height) = mode.size(); + println!(" {width}x{height}@{}Hz", mode.vrefresh()); + } + if connector.modes().len() > 8 { + println!(" ... {} more modes", connector.modes().len() - 8); + } + } + Err(error) => println!(" connector {:?}: {error}", handle), + } + } + } + Ok(()) } From 39bc014ab5e5033041619723e60ed2aa6d3e6edd Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 22:59:17 -0400 Subject: [PATCH 050/798] Require cross-GPU output bridge on M-series --- mseries/OS9-HANDOFF.md | 13 +++++++++++++ mseries/ROADMAP.md | 12 ++++++++---- .../EXPERIENCE-REQUIREMENTS.md | 6 ++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 2bcc9b751..05a88ac2c 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5374,3 +5374,16 @@ tracked separately from Rust compositor output and hotplug qualification. Next: connect a Wayland protocol probe, then qualify the Pantheon protocol against Wingpanel and Dock and add overlap-based autohide. Do not switch the login session at this stage. + +### Native M-series topology requirement — 2026-10-18 + +Reconciled the read-only DRM inventory with the compositor requirements. This +M1 Pro cannot use a single GPU for both rendering and scanout: Asahi card1 has +the EGL render node but no KMS resources; Apple display card2 owns connected +eDP/DP outputs but has no EGL device. Cross-device rendered-buffer import plus +scanout allocation on card2 is therefore a first-session requirement for this +machine. This changes the earlier conditional treatment of multi-GPU support; +it does not change the separate DCP/kernel diagnosis for dock HDMI G3. Native +session startup remains unwired and no modesetting has been attempted. Next: +build the optional native path around Smithay's multi-GPU renderer and validate +it without taking over the active desktop. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ad772e1a2..1beb72d71 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -10,8 +10,10 @@ its features. The scoped requirements and upstream feature radar are in [`EXPERIENCE-REQUIREMENTS.md`](pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). The list is grounded in this repo's notch-aware panel, dock work area, display profiles/hotplug, window snapping, input and session work. Features such as -Xwayland, IBus, accessibility, screencasting and multi-GPU are conditional on -an actual supported workflow. The opt-in Smithay nested prototype in +Xwayland, IBus, accessibility and screencasting are conditional on an actual +supported workflow. The measured M1 Pro requires cross-device rendering: +Asahi `/dev/dri/card1` supplies EGL/renderD128 but no KMS resources, while Apple +`/dev/dri/card2` owns connected displays and has no EGL device. The opt-in Smithay nested prototype in `rust/pantheon-compositor/` passes local `cargo check` and `cargo build`. It advertises Pantheon's protocol v2, places panel surfaces using anchor/size/offset, and applies exclusive panel geometry @@ -44,8 +46,10 @@ native DRM path while keeping the nested backend available. The opt-in `libseat-dev`. The new non-invasive `pantheon-drm-probe` reports the Asahi renderer on card1/renderD128, with KMS resource queries unsupported there. Card2 owns `eDP-1` and `DP-2`, while `HDMI-A-1` is disconnected; card2 has no EGL -device. The native backend must bridge rendering to KMS across these devices; -the read-only probe did not change modes. No session startup is wired yet. +device. Therefore cross-device buffer import, display-controller scanout +allocation and failure handling are first-session requirements for this M1 Pro, +not optional general multi-GPU support. The read-only probe did not change +modes. No session startup is wired yet. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 3ec60c293..283541951 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -16,6 +16,7 @@ it does not imply that the current prototype supports it. | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, focus/stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | +| M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | @@ -25,8 +26,9 @@ Add these only when an M-series workflow depends on them, and keep each as a separate acceptance item: Xwayland for a required X11 app; IBus/input-method support for the installed input method; accessibility protocols/actions used by the owner's setup; screencast/remote-desktop portal support for screen sharing; -touch/gestures if the selected hardware and workflow use them; multi-GPU buffer -sharing if a supported M-series configuration actually traverses that path. +touch/gestures if the selected hardware and workflow use them. The measured M1 +Pro topology already requires cross-device buffer sharing for native display +output; additional GPU topologies remain conditional. They are important capabilities in a general-purpose desktop, but their presence in Mutter is not by itself a reason to reproduce them in the first cut. From 916fe24b4f6f46262eb8a2e7f04655c999a9a0c5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:00:41 -0400 Subject: [PATCH 051/798] Map absolute pointer motion across outputs --- mseries/OS9-HANDOFF.md | 11 ++++++ .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/pantheon-compositor/src/input.rs | 34 +++++++++++++------ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 05a88ac2c..4c85d7022 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5387,3 +5387,14 @@ it does not change the separate DCP/kernel diagnosis for dock HDMI G3. Native session startup remains unwired and no modesetting has been attempted. Next: build the optional native path around Smithay's multi-GPU renderer and validate it without taking over the active desktop. + +### Absolute pointer mapping — 2026-10-18 + +Absolute pointer events previously mapped normalized coordinates against only +the first output in Smithay `Space`. They now map against the union of logical +output geometries, matching relative-pointer desktop bounds and allowing +coordinates to reach secondary outputs. `rustfmt --edition 2024` on the changed +file, `cargo check --bins`, and `cargo check --features native-drm --bins` pass +on the M1 Pro build host. This is compile evidence only; no absolute-input +device or multi-output runtime check was run. The pinned Smithay input and +multi-GPU paths remain the next focus; native session startup is still absent. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 283541951..8921f0bc5 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -15,7 +15,7 @@ it does not imply that the current prototype supports it. | --- | --- | --- | --- | | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, focus/stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | -| Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | +| Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 30331fecd..627dda4b4 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -29,7 +29,9 @@ impl PantheonCompositor { time, |state, modifiers, keysym| { if key_state == smithay::backend::input::KeyState::Pressed - && state.shell_compat.activate_key(keysym.modified_sym(), modifiers) + && state + .shell_compat + .activate_key(keysym.modified_sym(), modifiers) { FilterResult::Intercept(()) } else { @@ -78,11 +80,21 @@ impl PantheonCompositor { pointer.frame(self); } InputEvent::PointerMotionAbsolute { event, .. } => { - let output = self.space.outputs().next().unwrap(); - - let output_geo = self.space.output_geometry(output).unwrap(); + // Absolute devices report normalized coordinates. Map them over + // the complete logical desktop so a multi-output layout does + // not constrain the device to whichever output happens to be + // first in Space's iterator. + let Some(desktop_bounds) = self + .space + .outputs() + .filter_map(|output| self.space.output_geometry(output)) + .reduce(|bounds, output| bounds.merge(output)) + else { + return; + }; - let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); + let pos = + event.position_transformed(desktop_bounds.size) + desktop_bounds.loc.to_f64(); let serial = SERIAL_COUNTER.next_serial(); @@ -150,12 +162,12 @@ impl PantheonCompositor { InputEvent::PointerAxis { event, .. } => { let source = event.source(); - let horizontal_amount = event - .amount(Axis::Horizontal) - .unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.); - let vertical_amount = event - .amount(Axis::Vertical) - .unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.); + let horizontal_amount = event.amount(Axis::Horizontal).unwrap_or_else(|| { + event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120. + }); + let vertical_amount = event.amount(Axis::Vertical).unwrap_or_else(|| { + event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120. + }); let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal); let vertical_amount_discrete = event.amount_v120(Axis::Vertical); From b91f98a8f2eba3775663ee47a99dcf89fb101270 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:04:22 -0400 Subject: [PATCH 052/798] Place and focus new Pantheon windows --- mseries/OS9-HANDOFF.md | 10 +++ mseries/ROADMAP.md | 7 +- .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/pantheon-compositor/README.md | 6 +- .../src/handlers/compositor.rs | 5 +- .../src/handlers/xdg_shell.rs | 81 ++++++++++++++++--- rust/pantheon-compositor/src/state.rs | 28 ++++--- 7 files changed, 115 insertions(+), 24 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4c85d7022..60abc766c 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5398,3 +5398,13 @@ file, `cargo check --bins`, and `cargo check --features native-drm --bins` pass on the M1 Pro build host. This is compile evidence only; no absolute-input device or multi-output runtime check was run. The pinned Smithay input and multi-GPU paths remain the next focus; native session startup is still absent. + +### Initial toplevel placement and focus — 2026-10-18 + +New toplevels are centered on the first available output when their first +non-empty buffer commits. Later windows are offset in a bounded cascade, the +new window is activated and receives keyboard focus, and toplevel destruction +clears placement/window-mode tracking and unmaps it from `Space`. Default and +`native-drm` feature `cargo check --bins` both pass locally. Runtime window +launch/focus/close behavior remains unverified; this does not add native DRM +startup, and native session work remains the next major milestone. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 1beb72d71..7b49e77c2 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -19,8 +19,11 @@ advertises Pantheon's protocol v2, places panel surfaces using anchor/size/offset, and applies exclusive panel geometry when configuring maximized windows. A changed panel work area now reconfigures already maximized windows, while unchanged panel repaints do not send redundant -configures. Its v2 protocol probe completed a nested Xvfb run and sent panel -requests over the Wayland socket. Installed Wingpanel 8.0.4 also connected in an +configures. New committed toplevels are centered and cascaded on the first +output, activated, and focused; absolute pointer events map to all logical +outputs. These new behaviors are compile-checked but not runtime-qualified. The +v2 protocol probe completed a nested Xvfb run and sent panel requests over the +Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland run, sent `get_panel`, anchor and hide-mode requests, and attached SHM buffers. It reports the absent `org.gnome.Mutter.DisplayConfig` service. The prototype now returns its read-only `GetCurrentState` query using diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 8921f0bc5..d52b38f91 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -13,7 +13,7 @@ it does not imply that the current prototype supports it. | Priority | User-visible need | Evidence in this repository | Compositor work | | --- | --- | --- | --- | -| First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, focus/stacking, basic window state, startup readiness and orderly failure/restart behavior | +| First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 663b8db19..eb4618e1d 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -43,7 +43,11 @@ M-series/Pantheon behavior documented in [`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize -grabs, clipboard and drag-and-drop. It now advertises the existing Pantheon desktop-shell +grabs, clipboard and drag-and-drop. On the first non-empty toplevel commit, +windows are centered and cascaded on the first output, activated, and given +keyboard focus; toplevel destruction removes the placement tracking. Absolute +pointer input maps to the complete logical output bounds. These behaviors have +compile checks but no new runtime qualification. It now advertises the existing Pantheon desktop-shell protocol v2: panel surfaces can set their anchor, size, hide mode and offset; the compositor places those surfaces and uses exclusive panel geometry for xdg-toplevel maximize requests. The Dock's M-series offset request is therefore diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index 975ec356a..2d8a1110c 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -8,7 +8,8 @@ use smithay::{ wayland::{ buffer::BufferHandler, compositor::{ - CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface, + CompositorClientState, CompositorHandler, CompositorState, get_parent, + is_sync_subsurface, }, shm::{ShmHandler, ShmState}, }, @@ -41,7 +42,7 @@ impl CompositorHandler for PantheonCompositor { } }; - xdg_shell::handle_commit(&mut self.popups, &self.space, surface); + xdg_shell::handle_commit(self, surface); pantheon_shell::handle_surface_commit(self, surface); resize_grab::handle_commit(&mut self.space, surface); } diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index c7099c4fd..cddbea1fd 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -1,7 +1,7 @@ use smithay::{ desktop::{ - PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Space, - Window, find_popup_root_surface, get_popup_toplevel_coords, + PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, Window, + find_popup_root_surface, get_popup_toplevel_coords, }, input::{ Seat, @@ -145,6 +145,26 @@ impl XdgShellHandler for PantheonCompositor { self.configure_window_mode(surface, false, false); } + fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { + let wl_surface = surface.wl_surface(); + let id = wl_surface.id(); + self.initially_positioned.remove(&id); + self.managed_windows.remove(&id); + + let window = self + .space + .elements() + .find(|window| { + window + .toplevel() + .is_some_and(|top| top.wl_surface() == wl_surface) + }) + .cloned(); + if let Some(window) = window { + self.space.unmap_elem(&window); + } + } + fn grab(&mut self, surface: PopupSurface, seat_resource: wl_seat::WlSeat, serial: Serial) { let seat = Seat::from_resource(&seat_resource).unwrap(); let kind = PopupKind::Xdg(surface); @@ -212,14 +232,15 @@ fn check_grab( Some(start_data) } -/// Should be called on `WlSurface::commit` -pub fn handle_commit(popups: &mut PopupManager, space: &Space, surface: &WlSurface) { +/// Should be called on `WlSurface::commit` after `Window::on_commit`. +pub fn handle_commit(state: &mut PantheonCompositor, surface: &WlSurface) { // Handle toplevel commits. - if let Some(window) = space + let window = state + .space .elements() .find(|w| w.toplevel().unwrap().wl_surface() == surface) - .cloned() - { + .cloned(); + if let Some(window) = window { let initial_configure_sent = with_states(surface, |states| { states .data_map @@ -233,11 +254,53 @@ pub fn handle_commit(popups: &mut PopupManager, space: &Space, surface: if !initial_configure_sent { window.toplevel().unwrap().send_configure(); } + + // The first committed buffer makes the requested window size known. + // Center new app windows on the first available output and offset later + // launches so they remain visible and easy to find. + let surface_id = surface.id(); + let size = window.geometry().size; + if size.w > 0 && size.h > 0 && !state.initially_positioned.contains(&surface_id) { + let cascade = (state.space.elements().count().saturating_sub(1) % 10) as i32 * 24; + let placement = state + .space + .outputs() + .next() + .and_then(|output| state.space.output_geometry(output)) + .map(|output_geometry| { + let max_x = output_geometry.loc.x + (output_geometry.size.w - size.w).max(0); + let max_y = output_geometry.loc.y + (output_geometry.size.h - size.h).max(0); + let x = + (output_geometry.loc.x + (output_geometry.size.w - size.w) / 2 + cascade) + .clamp(output_geometry.loc.x, max_x); + let y = + (output_geometry.loc.y + (output_geometry.size.h - size.h) / 2 + cascade) + .clamp(output_geometry.loc.y, max_y); + (x, y) + }); + + if let Some(location) = placement { + state.space.map_element(window.clone(), location, true); + state.initially_positioned.insert(surface_id); + + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + state + .seat + .get_keyboard() + .unwrap() + .set_focus(state, Some(surface.clone()), serial); + state.space.elements().for_each(|window| { + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } + }); + } + } } // Handle popup commits. - popups.commit(surface); - if let Some(popup) = popups.find_popup(surface) { + state.popups.commit(surface); + if let Some(popup) = state.popups.find_popup(surface) { match popup { PopupKind::Xdg(ref xdg) => { if !xdg.is_initial_configure_sent() { diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 22047ef90..17f33c1f8 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::{ffi::OsString, sync::Arc}; use smithay::{ @@ -23,8 +23,8 @@ use smithay::{ }, }; -use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::display_config::DisplayConfigHandle; +use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::shell_compat::ShellCompatHandle; pub struct PantheonCompositor { @@ -49,6 +49,7 @@ pub struct PantheonCompositor { pub last_work_area: Option>, pub managed_windows: HashMap, + pub initially_positioned: HashSet, pub popups: PopupManager, pub seat: Seat, @@ -131,12 +132,16 @@ impl PantheonCompositor { panels: HashMap::new(), last_work_area: None, managed_windows: HashMap::new(), + initially_positioned: HashSet::new(), popups, seat, } } - fn init_wayland_listener(display: Display, event_loop: &mut EventLoop) -> OsString { + fn init_wayland_listener( + display: Display, + event_loop: &mut EventLoop, + ) -> OsString { // Creates a new listening socket, automatically choosing the next available `wayland` socket name. let listening_socket = ListeningSocketSource::new_auto().unwrap(); @@ -175,12 +180,17 @@ impl PantheonCompositor { socket_name } - pub fn surface_under(&self, pos: Point) -> Option<(WlSurface, Point)> { - self.space.element_under(pos).and_then(|(window, location)| { - window - .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL) - .map(|(s, p)| (s, (p + location).to_f64())) - }) + pub fn surface_under( + &self, + pos: Point, + ) -> Option<(WlSurface, Point)> { + self.space + .element_under(pos) + .and_then(|(window, location)| { + window + .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL) + .map(|(s, p)| (s, (p + location).to_f64())) + }) } } From 65b423bd01b39967a617b5347ebe8ac9cd0fbc7c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:05:45 -0400 Subject: [PATCH 053/798] Restore focus after closing active window --- mseries/OS9-HANDOFF.md | 11 +++-- rust/pantheon-compositor/README.md | 4 +- .../src/handlers/xdg_shell.rs | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 60abc766c..5efd8c094 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5404,7 +5404,10 @@ multi-GPU paths remain the next focus; native session startup is still absent. New toplevels are centered on the first available output when their first non-empty buffer commits. Later windows are offset in a bounded cascade, the new window is activated and receives keyboard focus, and toplevel destruction -clears placement/window-mode tracking and unmaps it from `Space`. Default and -`native-drm` feature `cargo check --bins` both pass locally. Runtime window -launch/focus/close behavior remains unverified; this does not add native DRM -startup, and native session work remains the next major milestone. +clears placement/window-mode tracking and unmaps it from `Space`. When the +destroyed toplevel had keyboard focus, focus moves to the topmost remaining +non-panel window (or clears if none remains); destroying a background window +does not steal focus. Default and `native-drm` feature `cargo check --bins` +both pass locally. Runtime window launch/focus/close behavior remains +unverified; this does not add native DRM startup, and native session work +remains the next major milestone. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index eb4618e1d..759fc0111 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -45,7 +45,9 @@ It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize grabs, clipboard and drag-and-drop. On the first non-empty toplevel commit, windows are centered and cascaded on the first output, activated, and given -keyboard focus; toplevel destruction removes the placement tracking. Absolute +keyboard focus; when the focused toplevel is destroyed, focus moves to the +topmost remaining app window. Toplevel destruction removes placement tracking. +Absolute pointer input maps to the complete logical output bounds. These behaviors have compile checks but no new runtime qualification. It now advertises the existing Pantheon desktop-shell protocol v2: panel surfaces can set their anchor, size, hide mode and offset; diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index cddbea1fd..72ef0b207 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -148,6 +148,11 @@ impl XdgShellHandler for PantheonCompositor { fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { let wl_surface = surface.wl_surface(); let id = wl_surface.id(); + let was_focused = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .is_some_and(|focused| focused == *wl_surface); self.initially_positioned.remove(&id); self.managed_windows.remove(&id); @@ -163,6 +168,41 @@ impl XdgShellHandler for PantheonCompositor { if let Some(window) = window { self.space.unmap_elem(&window); } + + if was_focused { + let next_window = self + .space + .elements() + .rev() + .find(|window| { + window + .toplevel() + .is_some_and(|top| !self.is_panel_surface(top.wl_surface())) + }) + .cloned(); + if let Some(window) = next_window { + self.space.raise_element(&window, true); + if let Some(surface) = window.toplevel().map(|top| top.wl_surface().clone()) { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.seat + .get_keyboard() + .unwrap() + .set_focus(self, Some(surface), serial); + self.space.elements().for_each(|window| { + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } + }); + } + } else { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.seat.get_keyboard().unwrap().set_focus( + self, + Option::::None, + serial, + ); + } + } } fn grab(&mut self, surface: PopupSurface, seat_resource: wl_seat::WlSeat, serial: Serial) { From 3b6fc104a71fb096a56c51f056522edcd3321185 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:05:51 -0400 Subject: [PATCH 054/798] Dock: halve icon size to 24 (dconf), note in handoff Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 5374738de..106a9a5b3 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,13 @@ # OS 9 implementation / Claude Code handoff +## Dock icons halved — 2026-10-18 (Claude Code) + +Owner asked for the applications tray icons at half size. Ran +`gsettings set io.elementary.dock icon-size 24` (was 48, the dock's setting; dconf only, no repo +files). Verified by reading the key back (24). Not visually checked on screen. To revert: +`gsettings reset io.elementary.dock icon-size`. If "applications tray" meant the Applications +menu or drawers instead, that is still at its default size. + ## Official Discord via FEX in a 4 KB-page microVM — 2026-10-18 (Claude Code) Owner required official Discord (amd64 only). FEX alone aborts here (16 KB pages, `: Unsupported system From 638c4a451260c019f3305fb406a4e388c92bb7f6 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:06:27 -0400 Subject: [PATCH 055/798] Allow backend-specific Wayland seat names --- mseries/OS9-HANDOFF.md | 6 ++++++ rust/pantheon-compositor/src/main.rs | 6 +++--- rust/pantheon-compositor/src/state.rs | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 5efd8c094..dd3ea3ec1 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5388,6 +5388,12 @@ session startup remains unwired and no modesetting has been attempted. Next: build the optional native path around Smithay's multi-GPU renderer and validate it without taking over the active desktop. +The compositor state constructor now accepts the seat name supplied by the +backend; nested startup continues to advertise `winit`. Both default and +`native-drm` `cargo check --bins` pass after this seam change. This prepares the +Wayland seat identity for a libseat-backed session but does not initialize or +control a native session yet. + ### Absolute pointer mapping — 2026-10-18 Absolute pointer events previously mapped normalized coordinates against only diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 2ee64c93a..43ef8c56b 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -1,8 +1,8 @@ #![allow(irrefutable_let_patterns)] -mod handlers; -mod grabs; mod display_config; +mod grabs; +mod handlers; mod input; mod pantheon_shell; mod protocols; @@ -23,7 +23,7 @@ fn main() -> Result<(), Box> { let command = parse_command()?; let mut event_loop: EventLoop = EventLoop::try_new()?; let display: Display = Display::new()?; - let mut state = PantheonCompositor::new(&mut event_loop, display); + let mut state = PantheonCompositor::new(&mut event_loop, display, "winit"); // This prototype deliberately uses the nested winit backend. It does not // open DRM/KMS devices or replace the active desktop session. diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 17f33c1f8..fbe5a9d80 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -64,7 +64,7 @@ pub struct ManagedWindow { } impl PantheonCompositor { - pub fn new(event_loop: &mut EventLoop, display: Display) -> Self { + pub fn new(event_loop: &mut EventLoop, display: Display, seat_name: &str) -> Self { let start_time = std::time::Instant::now(); let dh = display.handle(); @@ -90,7 +90,7 @@ impl PantheonCompositor { // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. let mut seat_state = SeatState::new(); - let mut seat: Seat = seat_state.new_wl_seat(&dh, "winit"); + let mut seat: Seat = seat_state.new_wl_seat(&dh, seat_name); // Notify clients that we have a keyboard, for the sake of the example we assume that keyboard is always present. // You may want to track keyboard hot-plug in real compositor. From 116fbbb802fb35813146af832e5e0f39b69a55c7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:08:47 -0400 Subject: [PATCH 056/798] Report all active outputs through DisplayConfig --- mseries/OS9-HANDOFF.md | 12 ++ mseries/ROADMAP.md | 3 +- rust/pantheon-compositor/README.md | 6 +- .../pantheon-compositor/src/display_config.rs | 131 ++++++++++++------ rust/pantheon-compositor/src/winit.rs | 6 + 5 files changed, 112 insertions(+), 46 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index dd3ea3ec1..386a3070f 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5394,6 +5394,18 @@ backend; nested startup continues to advertise `winit`. Both default and Wayland seat identity for a libseat-backed session but does not initialize or control a native session yet. +### Multi-output DisplayConfig state — 2026-10-18 + +The compatibility `org.gnome.Mutter.DisplayConfig.GetCurrentState` response now +tracks outputs by connector, reports their current modes, scales, logical x/y +positions, and primary flags, and removes disconnected outputs. Each output +change advances the state serial and emits `MonitorsChanged`. The D-Bus +interface remains read-only: it does not apply monitor configuration. Nested +startup continues to publish one `winit` output. Default and `native-drm` +`cargo check --bins` pass; multi-output D-Bus runtime behavior remains +unverified. This prepares the native connector lifecycle to report hotplug +state without claiming mode-setting support. + ### Absolute pointer mapping — 2026-10-18 Absolute pointer events previously mapped normalized coordinates against only diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 7b49e77c2..ffee49b11 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -27,7 +27,8 @@ Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland run, sent `get_panel`, anchor and hide-mode requests, and attached SHM buffers. It reports the absent `org.gnome.Mutter.DisplayConfig` service. The prototype now returns its read-only `GetCurrentState` query using -the nested output and logical layout mode; Wingpanel connects without the +all tracked output modes and logical positions, and increments the state serial +on output add/update/remove before emitting `MonitorsChanged`; Wingpanel connects without the missing-service error. The compositor now serves Wingpanel's `org.gnome.Shell` accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during an isolated nested run. `ShowOSD`, lock-screen action modes and physical media diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 759fc0111..8ad0c2c06 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -113,8 +113,10 @@ run forced to Wayland. Its client trace confirms it bound the shell global, sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated and attached shared-memory buffers. Wingpanel requires `org.gnome.Mutter.DisplayConfig`; the compositor now serves the read-only -`GetCurrentState` query it calls, with the current nested output and logical -layout mode, and emits `MonitorsChanged` when the output resizes. The isolated +`GetCurrentState` query it calls, with every tracked output's current mode, +logical position, scale, and primary status. Output add/update/remove changes +increment the state serial and emit `MonitorsChanged`. The service remains +read-only and does not apply monitor modes or layouts. The isolated Wingpanel run no longer reports missing DisplayConfig or Shell keybinding services. Pressing `Super+N` in the nested window produced the registered `AcceleratorActivated` signal. The sound indicator's media-key registrations diff --git a/rust/pantheon-compositor/src/display_config.rs b/rust/pantheon-compositor/src/display_config.rs index 9fc92b8d6..4c11e2af0 100644 --- a/rust/pantheon-compositor/src/display_config.rs +++ b/rust/pantheon-compositor/src/display_config.rs @@ -21,6 +21,9 @@ pub struct OutputSnapshot { pub height: i32, pub refresh_millihz: i32, pub scale: f64, + pub x: i32, + pub y: i32, + pub primary: bool, } impl Default for OutputSnapshot { @@ -34,13 +37,21 @@ impl Default for OutputSnapshot { height: 800, refresh_millihz: 60_000, scale: 1.0, + x: 0, + y: 0, + primary: true, } } } #[derive(Clone)] pub struct DisplayConfigHandle { - updates: mpsc::Sender, + updates: mpsc::Sender, +} + +enum DisplayConfigUpdate { + Upsert(OutputSnapshot), + Remove(String), } impl DisplayConfigHandle { @@ -54,11 +65,17 @@ impl DisplayConfigHandle { } pub fn update(&self, snapshot: OutputSnapshot) { - let _ = self.updates.send(snapshot); + let _ = self.updates.send(DisplayConfigUpdate::Upsert(snapshot)); + } + + pub fn remove(&self, connector: impl Into) { + let _ = self + .updates + .send(DisplayConfigUpdate::Remove(connector.into())); } } -struct DisplayConfigServer(Arc>); +struct DisplayConfigServer(Arc)>>); type Properties = HashMap; type MonitorInfo = (String, String, String, String); @@ -69,51 +86,63 @@ type LogicalMonitor = (i32, i32, f64, u32, bool, Vec, Properties); #[interface(name = "org.gnome.Mutter.DisplayConfig")] impl DisplayConfigServer { fn get_current_state(&self) -> (u32, Vec, Vec, Properties) { - let output = self.0.lock().expect("display config lock poisoned").clone(); - let monitor_info = ( - output.connector.clone(), - output.vendor, - output.product, - output.serial, - ); - let mode_id = format!( - "{}x{}@{:.3}", - output.width, - output.height, - f64::from(output.refresh_millihz) / 1000.0 - ); - let mut mode_properties = Properties::new(); - mode_properties.insert("is-current".into(), OwnedValue::from(true)); - mode_properties.insert("is-preferred".into(), OwnedValue::from(true)); - let mode = ( - mode_id, - output.width, - output.height, - f64::from(output.refresh_millihz) / 1000.0, - output.scale, - vec![output.scale], - mode_properties, - ); - let monitor = (monitor_info.clone(), vec![mode], Properties::new()); - let logical_monitor = ( - 0, - 0, - output.scale, - 0, - true, - vec![monitor_info], - Properties::new(), - ); + let (serial, outputs) = self.0.lock().expect("display config lock poisoned").clone(); + let mut outputs = outputs.into_values().collect::>(); + outputs.sort_by(|a, b| (a.x, a.y, &a.connector).cmp(&(b.x, b.y, &b.connector))); + if !outputs.iter().any(|output| output.primary) + && let Some(first) = outputs.first_mut() + { + first.primary = true; + } + + let mut monitors = Vec::with_capacity(outputs.len()); + let mut logical_monitors = Vec::with_capacity(outputs.len()); + for output in outputs { + let monitor_info = ( + output.connector.clone(), + output.vendor, + output.product, + output.serial, + ); + let mode_id = format!( + "{}x{}@{:.3}", + output.width, + output.height, + f64::from(output.refresh_millihz) / 1000.0 + ); + let mut mode_properties = Properties::new(); + mode_properties.insert("is-current".into(), OwnedValue::from(true)); + mode_properties.insert("is-preferred".into(), OwnedValue::from(true)); + let mode = ( + mode_id, + output.width, + output.height, + f64::from(output.refresh_millihz) / 1000.0, + output.scale, + vec![output.scale], + mode_properties, + ); + monitors.push((monitor_info.clone(), vec![mode], Properties::new())); + logical_monitors.push(( + output.x, + output.y, + output.scale, + 0, + output.primary, + vec![monitor_info], + Properties::new(), + )); + } let mut properties = Properties::new(); properties.insert("layout-mode".into(), OwnedValue::from(1_u32)); - (1, vec![monitor], vec![logical_monitor], properties) + (serial, monitors, logical_monitors, properties) } #[zbus(signal)] async fn monitors_changed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; } -fn serve_display_config(receiver: mpsc::Receiver) { +fn serve_display_config(receiver: mpsc::Receiver) { let connection = match Connection::session() { Ok(connection) => connection, Err(error) => { @@ -122,7 +151,8 @@ fn serve_display_config(receiver: mpsc::Receiver) { } }; - let snapshot = Arc::new(Mutex::new(OutputSnapshot::default())); + let initial = HashMap::from([("winit".to_string(), OutputSnapshot::default())]); + let snapshot = Arc::new(Mutex::new((1, initial))); if let Err(error) = connection .object_server() .at(OBJECT_PATH, DisplayConfigServer(snapshot.clone())) @@ -139,8 +169,23 @@ fn serve_display_config(receiver: mpsc::Receiver) { tracing::info!("serving the read-only Mutter display config query used by Wingpanel"); loop { match receiver.recv_timeout(Duration::from_millis(100)) { - Ok(output) => { - *snapshot.lock().expect("display config lock poisoned") = output; + Ok(update) => { + let mut state = snapshot.lock().expect("display config lock poisoned"); + match update { + DisplayConfigUpdate::Upsert(output) => { + if output.primary { + for current in state.1.values_mut() { + current.primary = false; + } + } + state.1.insert(output.connector.clone(), output); + } + DisplayConfigUpdate::Remove(connector) => { + state.1.remove(&connector); + } + } + state.0 = state.0.wrapping_add(1).max(1); + drop(state); if let Err(error) = connection.emit_signal( None::<&str>, OBJECT_PATH, diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index fd7277075..5895faaff 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -50,6 +50,9 @@ pub fn init_winit( height: mode.size.h, refresh_millihz: mode.refresh, scale: 1.0, + x: 0, + y: 0, + primary: true, }); let mut damage_tracker = OutputDamageTracker::from_output(&output); @@ -76,6 +79,9 @@ pub fn init_winit( height: mode.size.h, refresh_millihz: mode.refresh, scale: 1.0, + x: 0, + y: 0, + primary: true, }); } WinitEvent::Input(event) => state.process_input_event(event), From c6e20469a84e3b4cae8f2398113ae106a3969e19 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:11:02 -0400 Subject: [PATCH 057/798] Initialize native M-series render GPU --- mseries/OS9-HANDOFF.md | 12 +++ rust/pantheon-compositor/README.md | 9 ++- rust/pantheon-compositor/src/main.rs | 2 + rust/pantheon-compositor/src/native.rs | 102 +++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 rust/pantheon-compositor/src/native.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 386a3070f..9b6770c95 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5406,6 +5406,18 @@ startup continues to publish one `winit` output. Default and `native-drm` unverified. This prepares the native connector lifecycle to report hotplug state without claiming mode-setting support. +### Native renderer initialization slice — 2026-10-18 + +Added feature-gated `native.rs` with a libseat-scoped hardware renderer +initializer: it tries the session's primary GPU first, opens each candidate via +libseat, rejects software EGL, discovers its render node, and registers the GBM +device with Smithay `GpuManager`. This establishes the rendering side needed +for the measured Asahi-render/Apple-KMS split. It is not yet called by the +executable and does not initialize a KMS output, cross-device framebuffer +exporter, input backend, or native event loop. No libseat session was opened at +runtime. Default and `native-drm` cargo builds/checks pass locally; next wire +this renderer into the native backend and pair it with the Apple KMS allocator. + ### Absolute pointer mapping — 2026-10-18 Absolute pointer events previously mapped normalized coordinates against only diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 8ad0c2c06..b0cc2b9cd 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -67,9 +67,12 @@ production crash recovery also remain unimplemented. `native-drm` is an opt-in Cargo feature that stages Smithay's DRM, GBM, libinput, udev, libseat, EGL, and multi-GPU dependencies. The current executable -still starts through Winit; the native session/backend wiring is not implemented -yet. The feature-enabled `cargo check` and `cargo build` pass locally after -installing the host `libseat-dev` package; neither command initializes a native +still starts through Winit. Behind this feature, `native.rs` can initialize a +hardware EGL/GBM renderer on a libseat GPU and register it with Smithay's +multi-GPU manager. It is not yet called by the executable and does not open a +KMS output, allocate scanout buffers, or drive native input. Default and +feature-enabled `cargo check` and `cargo build` pass locally after installing +the host `libseat-dev` package; none of those commands initializes a native session. With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 43ef8c56b..dcce3a8cb 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -8,6 +8,8 @@ mod pantheon_shell; mod protocols; mod shell_compat; mod state; +#[cfg(feature = "native-drm")] +pub mod native; mod winit; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs new file mode 100644 index 000000000..f98843447 --- /dev/null +++ b/rust/pantheon-compositor/src/native.rs @@ -0,0 +1,102 @@ +//! Native M-series renderer setup. +//! +//! The renderer is selected independently of the KMS output device. On the +//! M1 Pro, the Asahi render node can render client content while the Apple DRM +//! device owns the physical connectors. Output allocation and scanout are +//! attached by the native output backend built on top of this manager. + +use std::path::Path; + +use smithay::{ + backend::{ + drm::{DrmDeviceFd, DrmNode, NodeType}, + egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, + renderer::{ + gles::GlesRenderer, + multigpu::{GpuManager, gbm::GbmGlesBackend}, + }, + session::{Session, libseat::LibSeatSession}, + udev::all_gpus, + }, + reexports::rustix::fs::OFlags, + utils::DeviceFd, +}; + +pub type GbmRendererBackend = GbmGlesBackend; +pub type NativeGpuManager = GpuManager; + +pub struct NativeRenderer { + pub render_node: DrmNode, + pub gpus: NativeGpuManager, +} + +impl NativeRenderer { + /// Open the first hardware renderer on the active libseat and register it + /// with Smithay's multi-GPU manager. A scanout-only GPU is intentionally + /// not required to expose EGL. + pub fn initialize(session: &LibSeatSession) -> Result> { + let gpu_paths = all_gpus(session.seat())?; + if gpu_paths.is_empty() { + return Err("libseat found no DRM GPU nodes".into()); + } + + let primary_path = smithay::backend::udev::primary_gpu(session.seat()) + .ok() + .flatten(); + let mut paths = gpu_paths; + if let Some(primary_path) = primary_path { + if let Some(index) = paths.iter().position(|path| path == &primary_path) { + paths.remove(index); + } + paths.insert(0, primary_path); + } + + let mut gpus = GpuManager::new(GbmRendererBackend::with_factory(|display| { + let context = EGLContext::new_with_priority(display, ContextPriority::High)?; + let capabilities = unsafe { GlesRenderer::supported_capabilities(&context)? }; + Ok(unsafe { GlesRenderer::with_capabilities(context, capabilities)? }) + }))?; + + let mut last_error = None; + for path in paths { + match Self::add_hardware_node(session, &mut gpus, &path) { + Ok(Some(render_node)) => return Ok(Self { render_node, gpus }), + Ok(None) => {} + Err(error) => last_error = Some(error), + } + } + + match last_error { + Some(error) => Err(error), + None => Err("no hardware EGL renderer was found on the active libseat".into()), + } + } + + fn add_hardware_node( + session: &LibSeatSession, + gpus: &mut NativeGpuManager, + path: &Path, + ) -> Result, Box> { + let node = DrmNode::from_path(path)?; + let mut session = session.clone(); + let fd = session.open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + )?; + let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd)); + let gbm = smithay::backend::allocator::gbm::GbmDevice::new(drm_fd.clone())?; + let display = unsafe { EGLDisplay::new(gbm.clone())? }; + let egl_device = EGLDevice::device_for_display(&display)?; + + if egl_device.is_software() { + return Ok(None); + } + + let render_node = egl_device + .try_get_render_node()? + .or_else(|| node.node_with_type(NodeType::Render).and_then(Result::ok)) + .unwrap_or(node); + gpus.as_mut().add_node(render_node, gbm)?; + Ok(Some(render_node)) + } +} From 224b06a2bb419094daf8697e2a37a1a54fd7b0ab Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:12:32 -0400 Subject: [PATCH 058/798] Prepare renderer GBM allocator for KMS import --- mseries/OS9-HANDOFF.md | 13 ++++++++----- rust/pantheon-compositor/README.md | 5 +++-- rust/pantheon-compositor/src/native.rs | 27 ++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 9b6770c95..3e116c7df 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5412,11 +5412,14 @@ Added feature-gated `native.rs` with a libseat-scoped hardware renderer initializer: it tries the session's primary GPU first, opens each candidate via libseat, rejects software EGL, discovers its render node, and registers the GBM device with Smithay `GpuManager`. This establishes the rendering side needed -for the measured Asahi-render/Apple-KMS split. It is not yet called by the -executable and does not initialize a KMS output, cross-device framebuffer -exporter, input backend, or native event loop. No libseat session was opened at -runtime. Default and `native-drm` cargo builds/checks pass locally; next wire -this renderer into the native backend and pair it with the Apple KMS allocator. +for the measured Asahi-render/Apple-KMS split. It retains the renderer's GBM +device and can create a rendering/scanout GBM allocator for the Apple KMS +framebuffer exporter to import. It is not yet called by the executable and +does not initialize a KMS output, cross-device framebuffer exporter, input +backend, or native event loop. No libseat session was opened at runtime. +Default and `native-drm` cargo checks pass, and the feature-enabled binaries +build locally; next pair this allocator with the Apple KMS device and output +manager. ### Absolute pointer mapping — 2026-10-18 diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index b0cc2b9cd..7a170d2ba 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -70,8 +70,9 @@ libinput, udev, libseat, EGL, and multi-GPU dependencies. The current executable still starts through Winit. Behind this feature, `native.rs` can initialize a hardware EGL/GBM renderer on a libseat GPU and register it with Smithay's multi-GPU manager. It is not yet called by the executable and does not open a -KMS output, allocate scanout buffers, or drive native input. Default and -feature-enabled `cargo check` and `cargo build` pass locally after installing +KMS output or framebuffer exporter, though it exposes the GBM allocator needed +to allocate render buffers for KMS import. It does not drive native input. +Default and feature-enabled `cargo check` and `cargo build` pass locally after installing the host `libseat-dev` package; none of those commands initializes a native session. diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index f98843447..f62d81815 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -8,6 +8,7 @@ use std::path::Path; use smithay::{ + backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, backend::{ drm::{DrmDeviceFd, DrmNode, NodeType}, egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, @@ -24,13 +25,25 @@ use smithay::{ pub type GbmRendererBackend = GbmGlesBackend; pub type NativeGpuManager = GpuManager; +pub type NativeAllocator = GbmAllocator; pub struct NativeRenderer { pub render_node: DrmNode, + pub render_gbm: GbmDevice, pub gpus: NativeGpuManager, } impl NativeRenderer { + /// Allocate render buffers on the hardware GPU with scanout usage enabled. + /// The Apple KMS framebuffer exporter can import these buffers for the + /// display controller, which has no EGL renderer on the measured M1 Pro. + pub fn output_allocator(&self) -> NativeAllocator { + GbmAllocator::new( + self.render_gbm.clone(), + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ) + } + /// Open the first hardware renderer on the active libseat and register it /// with Smithay's multi-GPU manager. A scanout-only GPU is intentionally /// not required to expose EGL. @@ -60,7 +73,13 @@ impl NativeRenderer { let mut last_error = None; for path in paths { match Self::add_hardware_node(session, &mut gpus, &path) { - Ok(Some(render_node)) => return Ok(Self { render_node, gpus }), + Ok(Some((render_node, render_gbm))) => { + return Ok(Self { + render_node, + render_gbm, + gpus, + }); + } Ok(None) => {} Err(error) => last_error = Some(error), } @@ -76,7 +95,7 @@ impl NativeRenderer { session: &LibSeatSession, gpus: &mut NativeGpuManager, path: &Path, - ) -> Result, Box> { + ) -> Result)>, Box> { let node = DrmNode::from_path(path)?; let mut session = session.clone(); let fd = session.open( @@ -96,7 +115,7 @@ impl NativeRenderer { .try_get_render_node()? .or_else(|| node.node_with_type(NodeType::Render).and_then(Result::ok)) .unwrap_or(node); - gpus.as_mut().add_node(render_node, gbm)?; - Ok(Some(render_node)) + gpus.as_mut().add_node(render_node, gbm.clone())?; + Ok(Some((render_node, gbm))) } } From 05c6ff28ae8b212a9e921c77b304272765eee5f0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:13:46 -0400 Subject: [PATCH 059/798] Applications menu: half-size icons (patch + installer) Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 9 +++++ .../install-applications-menu-half-icons.sh | 33 ++++++++++++++++ .../applications-menu-half-icons.patch | 39 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100755 mseries/install-applications-menu-half-icons.sh create mode 100644 mseries/patches/applications-menu-half-icons.patch diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4bdcf0571..33fa6db1e 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -8,6 +8,15 @@ files). Verified by reading the key back (24). Not visually checked on screen. T `gsettings reset io.elementary.dock icon-size`. If "applications tray" meant the Applications menu or drawers instead, that is still at its default size. +Owner then clarified they meant the Applications menu. Upstream has no icon-size setting, so +`mseries/install-applications-menu-half-icons.sh` builds elementary/applications-menu at +`06d7170` (8.0.4, matches the installed package) with `patches/applications-menu-half-icons.patch` +(grid 64->32, list rows 32->16, search results 32->16) and installs `libslingshot.so` +(package build backed up by `lib-backup.sh`; `--uninstall` restores it). Needed +`libswitchboard-3-dev` and `libzeitgeist-2.0-dev` to build. Installed and `ldd` clean; **not yet +loaded**: Wingpanel was not restarted, so it shows after the next reload/login, and the result is +not visually checked. The dock stays at 24; revert it if only the menu was wanted. + ## Official Discord via FEX in a 4 KB-page microVM — 2026-10-18 (Claude Code) Owner required official Discord (amd64 only). FEX alone aborts here (16 KB pages, `: Unsupported system diff --git a/mseries/install-applications-menu-half-icons.sh b/mseries/install-applications-menu-half-icons.sh new file mode 100755 index 000000000..2a793b40e --- /dev/null +++ b/mseries/install-applications-menu-half-icons.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Builds the elementary Applications menu (Slingshot) with icons at half size: +# grid 64->32, list rows 32->16, search results 32->16 (patches/applications-menu-half-icons.patch). +# Upstream has no setting for this. Reload Wingpanel to activate; --uninstall restores the package build. +set -euo pipefail +repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +plugin="$(pkg-config --variable=indicatorsdir wingpanel-9)/libslingshot.so" +source "$repo/mseries/lib-backup.sh" +if [[ ${1:-} == --uninstall ]]; then + mseries_restore "$plugin" io.elementary.applications-menu + echo 'Original Applications menu restored. Reload Wingpanel to activate.' + exit +fi +version=$(dpkg-query -W -f='${Version}' io.elementary.applications-menu) +[[ $version == 8.0.4* ]] || { echo "Patch targets Applications menu 8.0.4, found $version" >&2; exit 1; } +work=$(mktemp -d /tmp/mseries-applications-menu.XXXXXX) +trap 'rm -rf "$work"' EXIT +git init -q "$work/source" +git -C "$work/source" remote add origin https://github.com/elementary/applications-menu.git +git -C "$work/source" fetch -q --depth=1 origin 06d7170edab4d1f93ebd14173a0e2afea98e9339 +git -C "$work/source" checkout -q --detach FETCH_HEAD +git -C "$work/source" apply "$repo/mseries/patches/applications-menu-half-icons.patch" +meson setup "$work/build" "$work/source" --prefix=/usr --libdir=lib/aarch64-linux-gnu --buildtype=release +ninja -C "$work/build" +built=$(find "$work/build" -name libslingshot.so | head -1) +[[ -n $built ]] || { echo 'libslingshot.so was not built' >&2; exit 1; } +if ldd "$built" | grep -q 'not found'; then + echo 'Missing runtime libraries; installed Applications menu preserved.' >&2 + exit 1 +fi +mseries_backup "$plugin" io.elementary.applications-menu +sudo install -m644 "$built" "$plugin" +echo 'Installed the half-size icon Applications menu. Reload Wingpanel to activate.' diff --git a/mseries/patches/applications-menu-half-icons.patch b/mseries/patches/applications-menu-half-icons.patch new file mode 100644 index 000000000..01de3c266 --- /dev/null +++ b/mseries/patches/applications-menu-half-icons.patch @@ -0,0 +1,39 @@ +diff --git a/src/Widgets/AppButton.vala b/src/Widgets/AppButton.vala +index 154d9b2..74af87e 100644 +--- a/src/Widgets/AppButton.vala ++++ b/src/Widgets/AppButton.vala +@@ -9,7 +9,7 @@ public class Slingshot.Widgets.AppButton : Gtk.Button { + + public Backend.App app { get; construct; } + +- private const int ICON_SIZE = 64; ++ private const int ICON_SIZE = 32; + private Gtk.Label badge; + + public AppButton (Backend.App app) { +diff --git a/src/Widgets/AppListRow.vala b/src/Widgets/AppListRow.vala +index f857c21..7c37da5 100644 +--- a/src/Widgets/AppListRow.vala ++++ b/src/Widgets/AppListRow.vala +@@ -32,7 +32,7 @@ public class Slingshot.AppListRow : Gtk.ListBoxRow { + + var image = new Gtk.Image () { + gicon = icon, +- pixel_size = 32 ++ pixel_size = 16 + }; + + var name_label = new Gtk.Label (app_info.get_display_name ()) { +diff --git a/src/Widgets/SearchItem.vala b/src/Widgets/SearchItem.vala +index 033706f..680fc0f 100644 +--- a/src/Widgets/SearchItem.vala ++++ b/src/Widgets/SearchItem.vala +@@ -5,7 +5,7 @@ + */ + + public class Slingshot.Widgets.SearchItem : Gtk.ListBoxRow { +- private const int ICON_SIZE = 32; ++ private const int ICON_SIZE = 16; + + public signal bool launch_app (); + From 1f549239e3f2503c78ca4a4412bc799e0a51f518 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:14:45 -0400 Subject: [PATCH 060/798] Initialize and render native KMS output --- mseries/OS9-HANDOFF.md | 18 ++ rust/pantheon-compositor/README.md | 13 +- rust/pantheon-compositor/src/native.rs | 253 ++++++++++++++++++++++++- 3 files changed, 275 insertions(+), 9 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 3e116c7df..d96711d48 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5421,6 +5421,24 @@ Default and `native-drm` cargo checks pass, and the feature-enabled binaries build locally; next pair this allocator with the Apple KMS device and output manager. +### Native KMS output initialization — 2026-10-18 + +Added a feature-gated `NativeKmsOutput` builder. It scans libseat DRM nodes +other than the renderer device, finds a connected connector with a compatible +CRTC, prefers the built-in eDP connector, chooses its preferred mode, and +constructs Smithay's GBM output manager using Asahi render buffers and an Apple +KMS framebuffer exporter. It publishes the output and updates the multi-output +DisplayConfig state. The executable does not call this code yet; no libseat +session was opened and no output was initialized at runtime. `DrmDevice::new` +is configured to disable connectors while preparing for compositor takeover, +so this must only be called in the future native session after libseat has +paused the existing display server. The output object now renders the current +Space into its DRM swapchain, queues damaged frames, and exposes the matching +vblank submission completion. These methods are not yet connected to calloop; +frame scheduling, vblank event wiring, hotplug, pause/resume and native input +remain. No libseat session was opened at runtime. Default and `native-drm` +checks/builds pass locally. + ### Absolute pointer mapping — 2026-10-18 Absolute pointer events previously mapped normalized coordinates against only diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 7a170d2ba..a0937e3d6 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -70,11 +70,14 @@ libinput, udev, libseat, EGL, and multi-GPU dependencies. The current executable still starts through Winit. Behind this feature, `native.rs` can initialize a hardware EGL/GBM renderer on a libseat GPU and register it with Smithay's multi-GPU manager. It is not yet called by the executable and does not open a -KMS output or framebuffer exporter, though it exposes the GBM allocator needed -to allocate render buffers for KMS import. It does not drive native input. -Default and feature-enabled `cargo check` and `cargo build` pass locally after installing -the host `libseat-dev` package; none of those commands initializes a native -session. +multi-GPU manager. `NativeKmsOutput` can open a separate KMS device, choose a +connected connector/CRTC, build the GBM framebuffer exporter/output manager, +publish its Wayland output, render the current space, queue a damaged frame, +and acknowledge its KMS submission. The executable does not call these native +methods yet; the DRM vblank/session event loop, frame scheduling, hotplug, +pause/resume and native input are unfinished. Default and feature-enabled `cargo check` and +`cargo build` pass locally after installing the host `libseat-dev` package; +none of those commands initializes a native session. With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices without acquiring DRM master or changing display modes. On the current M-series diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index f62d81815..1bc0b1a7e 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -8,24 +8,49 @@ use std::path::Path; use smithay::{ - backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, backend::{ - drm::{DrmDeviceFd, DrmNode, NodeType}, + allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + drm::{ + DrmDevice, DrmDeviceFd, DrmDeviceNotifier, DrmNode, NodeType, + compositor::FrameFlags, + exporter::gbm::GbmFramebufferExporter, + output::{DrmOutput, DrmOutputManager, DrmOutputRenderElements}, + }, + }, + backend::{ egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, renderer::{ + element::surface::WaylandSurfaceRenderElement, gles::GlesRenderer, - multigpu::{GpuManager, gbm::GbmGlesBackend}, + multigpu::{GpuManager, MultiRenderer, gbm::GbmGlesBackend}, }, session::{Session, libseat::LibSeatSession}, udev::all_gpus, }, - reexports::rustix::fs::OFlags, + output::{Mode, Output, PhysicalProperties}, + reexports::{ + drm::control::{Device as DrmControlDevice, ModeTypeFlags, connector}, + rustix::fs::OFlags, + }, utils::DeviceFd, }; +use crate::{PantheonCompositor, display_config::OutputSnapshot}; + pub type GbmRendererBackend = GbmGlesBackend; pub type NativeGpuManager = GpuManager; pub type NativeAllocator = GbmAllocator; +pub type NativeExporter = GbmFramebufferExporter; +pub type NativeOutputManager = DrmOutputManager; +pub type NativeDrmOutput = DrmOutput; +type NativeMultiRenderer<'a> = MultiRenderer<'a, 'a, GbmRendererBackend, GbmRendererBackend>; + +const OUTPUT_FORMATS: &[smithay::backend::allocator::Fourcc] = &[ + smithay::backend::allocator::Fourcc::Abgr2101010, + smithay::backend::allocator::Fourcc::Argb2101010, + smithay::backend::allocator::Fourcc::Abgr8888, + smithay::backend::allocator::Fourcc::Argb8888, +]; pub struct NativeRenderer { pub render_node: DrmNode, @@ -33,6 +58,226 @@ pub struct NativeRenderer { pub gpus: NativeGpuManager, } +pub struct NativeKmsOutput { + pub node: DrmNode, + pub connector: connector::Handle, + pub output: Output, + pub manager: NativeOutputManager, + pub drm_output: NativeDrmOutput, + pub notifier: DrmDeviceNotifier, +} + +struct ConnectedOutput { + connector: connector::Info, + crtc: smithay::reexports::drm::control::crtc::Handle, + mode: smithay::reexports::drm::control::Mode, +} + +impl NativeKmsOutput { + /// Open the first usable KMS device on the active libseat and initialize + /// its preferred connected connector with buffers rendered on `renderer`. + /// This creates Smithay output state but does not queue or present a frame. + pub fn initialize( + session: &LibSeatSession, + renderer: &mut NativeRenderer, + state: &mut PantheonCompositor, + ) -> Result> { + let mut last_error = None; + for path in all_gpus(session.seat())? { + let node = match DrmNode::from_path(&path) { + Ok(node) if node.dev_id() != renderer.render_node.dev_id() => node, + _ => continue, + }; + match Self::open_output_device(session, node, &path, renderer, state) { + Ok(output) => return Ok(output), + Err(error) => last_error = Some(error), + } + } + + match last_error { + Some(error) => Err(error), + None => Err("no separate DRM KMS device was found on the active libseat".into()), + } + } + + /// Render the current Pantheon space to this output and queue damaged + /// content for scanout. The matching KMS vblank must call + /// `frame_submitted` before another frame is queued. + pub fn render( + &mut self, + renderer: &mut NativeRenderer, + state: &PantheonCompositor, + ) -> Result> { + let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; + let elements = smithay::desktop::space::space_render_elements( + &mut gpu_renderer, + [&state.space], + &self.output, + 1.0, + )?; + let frame = self.drm_output.render_frame( + &mut gpu_renderer, + &elements, + [0.1, 0.1, 0.1, 1.0], + FrameFlags::DEFAULT, + )?; + if frame.is_empty { + return Ok(false); + } + self.drm_output.queue_frame(())?; + Ok(true) + } + + pub fn frame_submitted(&mut self) -> Result<(), Box> { + self.drm_output.frame_submitted()?; + Ok(()) + } + + fn open_output_device( + session: &LibSeatSession, + node: DrmNode, + path: &Path, + renderer: &mut NativeRenderer, + state: &mut PantheonCompositor, + ) -> Result> { + let mut session = session.clone(); + let fd = session.open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + )?; + let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd)); + let (drm, notifier) = DrmDevice::new(drm_fd.clone(), true)?; + let gbm = GbmDevice::new(drm_fd)?; + let resources = drm.resource_handles()?; + + let mut connected = Vec::new(); + for handle in resources.connectors() { + let connector = drm.get_connector(*handle, false)?; + if connector.state() != connector::State::Connected || connector.modes().is_empty() { + continue; + } + let encoder = connector + .current_encoder() + .and_then(|handle| drm.get_encoder(handle).ok()) + .or_else(|| { + connector + .encoders() + .iter() + .find_map(|handle| drm.get_encoder(*handle).ok()) + }); + let Some(encoder) = encoder else { continue }; + let Some(crtc) = resources + .filter_crtcs(encoder.possible_crtcs()) + .into_iter() + .next() + else { + continue; + }; + let mode = connector + .modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .unwrap_or(connector.modes()[0]); + connected.push(ConnectedOutput { + connector, + crtc, + mode, + }); + } + + connected.sort_by_key(|output| { + output.connector.interface() != connector::Interface::EmbeddedDisplayPort + }); + let selected = connected + .into_iter() + .next() + .ok_or("KMS device has no connected connector with a compatible CRTC")?; + + let output_allocator = renderer.output_allocator(); + let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; + let render_formats = gpu_renderer + .as_mut() + .egl_context() + .dmabuf_render_formats() + .iter() + .copied() + .collect::>(); + if render_formats.is_empty() { + return Err("hardware renderer exposes no DMA-BUF render formats".into()); + } + + let exporter = GbmFramebufferExporter::new(gbm.clone(), renderer.render_node.into()); + let mut manager = DrmOutputManager::new( + drm, + output_allocator, + exporter, + Some(gbm), + OUTPUT_FORMATS.iter().copied(), + render_formats, + ); + + let mode = Mode::from(selected.mode); + let name = format!( + "{}-{}", + selected.connector.interface().as_str(), + selected.connector.interface_id() + ); + let (physical_width, physical_height) = selected.connector.size().unwrap_or((0, 0)); + let output = Output::new( + name.clone(), + PhysicalProperties { + size: (physical_width as i32, physical_height as i32).into(), + subpixel: selected.connector.subpixel().into(), + make: "Apple".into(), + model: name.clone(), + serial_number: "Unknown".into(), + }, + ); + output.set_preferred(mode); + output.change_current_state(Some(mode), None, None, Some((0, 0).into())); + + let render_elements = DrmOutputRenderElements::< + NativeMultiRenderer<'_>, + WaylandSurfaceRenderElement>, + >::default(); + let drm_output = manager.lock().initialize_output( + selected.crtc, + selected.mode, + &[selected.connector.handle()], + &output, + None, + &mut gpu_renderer, + &render_elements, + )?; + + output.create_global::(&state.display_handle); + state.space.map_output(&output, (0, 0)); + state.display_config.update(OutputSnapshot { + connector: name.clone(), + vendor: "Apple".into(), + product: name, + serial: "Unknown".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + scale: 1.0, + x: 0, + y: 0, + primary: true, + }); + + Ok(Self { + node, + connector: selected.connector.handle(), + output, + manager, + drm_output, + notifier, + }) + } +} + impl NativeRenderer { /// Allocate render buffers on the hardware GPU with scanout usage enabled. /// The Apple KMS framebuffer exporter can import these buffers for the From a36e455e7f80602c60fbf3a0d6d0711826538fa3 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:16:58 -0400 Subject: [PATCH 061/798] Machine Lab: elementary OS-style VM app (sidebar, hero, spec cards, drop-to-install, status) Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/.gitignore | 1 + mseries/machine-lab/README.md | 14 + .../data/io.github.mseries.MachineLab.desktop | 9 + .../data/machine-lab.gresource.xml | 6 + mseries/machine-lab/data/style.css | 70 +++ mseries/machine-lab/launcher/README.md | 22 + mseries/machine-lab/launcher/run-vm.sh | 64 +++ mseries/machine-lab/meson.build | 28 ++ mseries/machine-lab/src/main.vala | 432 ++++++++++++++++++ 9 files changed, 646 insertions(+) create mode 100644 mseries/machine-lab/.gitignore create mode 100644 mseries/machine-lab/README.md create mode 100644 mseries/machine-lab/data/io.github.mseries.MachineLab.desktop create mode 100644 mseries/machine-lab/data/machine-lab.gresource.xml create mode 100644 mseries/machine-lab/data/style.css create mode 100644 mseries/machine-lab/launcher/README.md create mode 100755 mseries/machine-lab/launcher/run-vm.sh create mode 100644 mseries/machine-lab/meson.build create mode 100644 mseries/machine-lab/src/main.vala diff --git a/mseries/machine-lab/.gitignore b/mseries/machine-lab/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/mseries/machine-lab/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md new file mode 100644 index 000000000..ccf7efe56 --- /dev/null +++ b/mseries/machine-lab/README.md @@ -0,0 +1,14 @@ +# Machine Lab + +Native elementary OS look (Vala + GTK 4 + Granite 7) for running another Linux ARM64 +guest under QEMU/KVM with NAT networking. Flat sidebar of machines, hero header, spec cards, +drag-and-drop installer picker, Start/Force Stop with live status, toast feedback, and +light/dark + accent following the system. + +```sh +meson setup build && meson compile -C build +./build/machine-lab # finds ../launcher/run-vm.sh, or set MACHINE_LAB_LAUNCHER +``` + +Launcher and known gaps: `launcher/README.md`. Moved here from the Codex native-vm-poc spike. +Visual check was done headless via GTK Broadway; not yet launched on the desktop or booted with a guest ISO. diff --git a/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop b/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop new file mode 100644 index 000000000..368b1502f --- /dev/null +++ b/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Machine Lab +Comment=Run another Linux on this Mac +Exec=machine-lab +Icon=computer +Terminal=false +Type=Application +Categories=System;Emulator; +Keywords=virtual;machine;vm;qemu; diff --git a/mseries/machine-lab/data/machine-lab.gresource.xml b/mseries/machine-lab/data/machine-lab.gresource.xml new file mode 100644 index 000000000..2c2cc5004 --- /dev/null +++ b/mseries/machine-lab/data/machine-lab.gresource.xml @@ -0,0 +1,6 @@ + + + + style.css + + diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css new file mode 100644 index 000000000..6d32ec3e9 --- /dev/null +++ b/mseries/machine-lab/data/style.css @@ -0,0 +1,70 @@ +/* Machine Lab: elementary OS look. Colors come from the system theme + (@bg_color, @fg_color, @accent_color, @borders, @SILVER_*, @BLUEBERRY_*) + so light/dark and the user's accent follow the desktop automatically. */ + +.ml-sidebar { + background: alpha(@fg_color, 0.04); +} + +.ml-sidebar-title { + font-size: 0.8em; + font-weight: 600; + letter-spacing: 0.04em; + opacity: 0.55; + margin: 12px 18px 4px 18px; +} + +.ml-sidebar row { + border-radius: 6px; + margin: 1px 8px; + padding: 4px 4px; +} + +.ml-machine-name { font-weight: 600; } +.ml-machine-sub { font-size: 0.85em; opacity: 0.65; } + +.ml-hero-icon { + padding: 14px; + border-radius: 18px; + background: linear-gradient(to bottom, alpha(@accent_color, 0.18), alpha(@accent_color, 0.06)); + box-shadow: inset 0 0 0 1px alpha(@accent_color, 0.25); +} + +.ml-pill { + padding: 2px 10px; + border-radius: 99px; + font-size: 0.85em; + font-weight: 600; + background: alpha(@fg_color, 0.08); +} +.ml-pill.running { + color: @success_color; + background: alpha(@success_color, 0.15); +} +.ml-pill.stopped { opacity: 0.8; } + +.ml-card { + border-radius: 12px; + background: @card_bg_color; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.1), 0 1px 3px alpha(black, 0.12); +} + +.ml-spec { + padding: 14px 16px; +} +.ml-spec-value { font-weight: 600; } +.ml-spec-label { font-size: 0.85em; opacity: 0.65; } +.ml-spec image { opacity: 0.7; } + +.ml-drop { + border-radius: 12px; + border: 2px solid alpha(@fg_color, 0.12); + background: alpha(@fg_color, 0.03); + padding: 18px; +} +.ml-drop.hover, .ml-drop.chosen { + border-color: @accent_color; + background: alpha(@accent_color, 0.08); +} + +.ml-fineprint { font-size: 0.85em; opacity: 0.65; } diff --git a/mseries/machine-lab/launcher/README.md b/mseries/machine-lab/launcher/README.md new file mode 100644 index 000000000..406a088f9 --- /dev/null +++ b/mseries/machine-lab/launcher/README.md @@ -0,0 +1,22 @@ +# ARM64 QEMU/KVM VM spike + +This is the first backend spike for the elementary-native VM app idea. It creates one persistent ARM64 VM from a local installer ISO using QEMU/KVM, AAVMF UEFI, virtio storage/network/graphics, and user-mode NAT. + +The host inspected for this spike is elementary OS 9 Early Access on an M1 Pro. QEMU/KVM, qemu-img, and AAVMF are present. No installer image has been downloaded and this launcher has not been run yet. + +## Usage + + ./run-vm.sh /path/to/arm64-installer.iso [vm-directory] + +The default VM directory is `./omarchy-arm64-vm`, relative to the current directory. It stores a sparse 64 GiB qcow2 disk and a writable UEFI variable store. Removing that directory removes the VM, so keep it somewhere intentional once it contains data. + +The guest sees a virtio Ethernet device connected through QEMU user networking. It does not receive the host's Wi-Fi adapter. TCP from host `127.0.0.1:2222` is forwarded to guest port 22 for later SSH access if the guest enables SSH. + +## Current limits + +- This is a launch harness, not the app or a VM manager. +- It accepts any ARM64 installer ISO; it does not yet validate the image architecture or automate installation. +- It assumes AAVMF firmware at `/usr/share/AAVMF/` and access to `/dev/kvm`. +- It currently uses 4 vCPUs, 6 GiB RAM, 2D virtio graphics, and a 64 GiB disk. +- It has not yet been boot-verified. The first real guest artifact should be Omarchy ARM64 from its documented edge/image-building path. +- The host currently has about 19 GiB free, below the Try Omarchy guest-builder's stated ~20 GiB build allowance, so that full build should wait until space is available or use a smaller disposable ARM64 installer image for the initial engine check. diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh new file mode 100755 index 000000000..cbde01459 --- /dev/null +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -lt 1 ]; then + echo "Usage: $0 ARM64_INSTALL_ISO [VM_DIRECTORY]" >&2 + exit 2 +fi + +ISO_PATH=$1 +if [ "$#" -ge 2 ]; then + VM_DIR=$2 +else + VM_DIR=./omarchy-arm64-vm +fi + +QEMU=$(command -v qemu-system-aarch64 || true) +QEMU_IMG=$(command -v qemu-img || true) +FW_CODE=/usr/share/AAVMF/AAVMF_CODE.fd +FW_VARS=/usr/share/AAVMF/AAVMF_VARS.fd + +if [ -z "$QEMU" ] || [ -z "$QEMU_IMG" ]; then + echo "Install qemu-system-arm and qemu-utils first." >&2 + exit 1 +fi +if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then + echo "KVM is unavailable to this user. Check /dev/kvm permissions." >&2 + exit 1 +fi +if [ ! -r "$ISO_PATH" ]; then + echo "Cannot read installer image: $ISO_PATH" >&2 + exit 1 +fi +if [ ! -r "$FW_CODE" ] || [ ! -r "$FW_VARS" ]; then + echo "AAVMF UEFI firmware was not found under /usr/share/AAVMF." >&2 + exit 1 +fi + +mkdir -p "$VM_DIR" +if [ ! -e "$VM_DIR/efi-vars.fd" ]; then + cp --reflink=auto "$FW_VARS" "$VM_DIR/efi-vars.fd" +fi +if [ ! -e "$VM_DIR/disk.qcow2" ]; then + "$QEMU_IMG" create -f qcow2 "$VM_DIR/disk.qcow2" 64G +fi + +exec "$QEMU" \ + -name Omarchy-ARM64-PoC \ + -machine virt \ + -accel kvm \ + -cpu host \ + -smp 4 \ + -m 6G \ + -drive if=pflash,format=raw,readonly=on,file="$FW_CODE" \ + -drive if=pflash,format=raw,file="$VM_DIR/efi-vars.fd" \ + -drive if=none,id=system,format=qcow2,file="$VM_DIR/disk.qcow2" \ + -device virtio-blk-pci,drive=system \ + -device virtio-scsi-pci,id=scsi \ + -drive if=none,id=installer,format=raw,media=cdrom,readonly=on,file="$ISO_PATH" \ + -device scsi-cd,drive=installer,bus=scsi.0 \ + -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 \ + -device virtio-net-pci,netdev=net0 \ + -device virtio-gpu-pci \ + -display gtk,gl=off \ + -boot menu=on diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build new file mode 100644 index 000000000..702199826 --- /dev/null +++ b/mseries/machine-lab/meson.build @@ -0,0 +1,28 @@ +project('machine-lab', 'vala', + version: '0.2.0', + meson_version: '>= 1.0.0', + default_options: ['warning_level=2'] +) + +gtk = dependency('gtk4', version: '>= 4.12') +granite = dependency('granite-7', version: '>= 7.0') + +gnome = import('gnome') +resources = gnome.compile_resources('machine-lab-resources', + 'data/machine-lab.gresource.xml', + source_dir: 'data' +) + +executable('machine-lab', + 'src/main.vala', + resources, + dependencies: [gtk, granite], + install: true +) + +install_data('data/io.github.mseries.MachineLab.desktop', + install_dir: get_option('datadir') / 'applications') +install_data('launcher/run-vm.sh', + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + rename: 'machine-lab-run-vm') diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala new file mode 100644 index 000000000..1da0c2f8d --- /dev/null +++ b/mseries/machine-lab/src/main.vala @@ -0,0 +1,432 @@ +/* + * Machine Lab: run another Linux on this Mac. + * Vala + GTK 4 + Granite 7, laid out like a first-party elementary OS app: + * flat sidebar + content pane, hero header, spec cards, suggested action. + */ +using Gtk; + +public class MachineLab : Gtk.Application { + private string? installer_path = null; + private Subprocess? vm = null; + + private Gtk.ApplicationWindow window; + private Granite.Toast toast; + private Gtk.Box drop_zone; + private Gtk.Label drop_title; + private Gtk.Label drop_detail; + private Gtk.Button start_button; + private Gtk.Button stop_button; + private Gtk.Label status_pill; + private Gtk.Label sidebar_sub; + + public MachineLab () { + Object ( + application_id: "io.github.mseries.MachineLab", + flags: ApplicationFlags.DEFAULT_FLAGS + ); + } + + protected override void startup () { + base.startup (); + Granite.init (); + + // Follow the system light/dark preference like other elementary apps. + var gtk_settings = Gtk.Settings.get_default (); + var granite_settings = Granite.Settings.get_default (); + gtk_settings.gtk_application_prefer_dark_theme = + granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + granite_settings.notify["prefers-color-scheme"].connect (() => { + gtk_settings.gtk_application_prefer_dark_theme = + granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + }); + + var provider = new Gtk.CssProvider (); + provider.load_from_resource ("/io/github/mseries/MachineLab/style.css"); + Gtk.StyleContext.add_provider_for_display ( + Gdk.Display.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + } + + protected override void activate () { + if (window != null) { + window.present (); + return; + } + + window = new Gtk.ApplicationWindow (this); + window.title = "Machine Lab"; + window.default_width = 880; + window.default_height = 600; + + var layout = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + layout.append (build_sidebar ()); + layout.append (new Gtk.Separator (Gtk.Orientation.VERTICAL)); + layout.append (build_content ()); + + // Hide the stock title bar: each pane brings its own flat header. + window.titlebar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { visible = false }; + + var overlay = new Gtk.Overlay (); + overlay.child = layout; + toast = new Granite.Toast (""); + overlay.add_overlay (toast); + window.child = overlay; + + update_state (); + window.present (); + } + + // ---- Sidebar ---------------------------------------------------------- + + private Gtk.Widget build_sidebar () { + var side = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + side.add_css_class ("ml-sidebar"); + side.width_request = 230; + + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = "close:"; + header.title_widget = new Gtk.Label (""); + var handle = new Gtk.WindowHandle (); + handle.child = header; + side.append (handle); + + var heading = new Gtk.Label ("MACHINES"); + heading.xalign = 0; + heading.add_css_class ("ml-sidebar-title"); + side.append (heading); + + var list = new Gtk.ListBox (); + list.selection_mode = Gtk.SelectionMode.SINGLE; + list.add_css_class ("navigation-sidebar"); + list.vexpand = true; + + var row_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + row_box.margin_top = 4; + row_box.margin_bottom = 4; + row_box.margin_start = 6; + row_box.margin_end = 6; + var icon = new Gtk.Image.from_icon_name ("computer"); + icon.pixel_size = 32; + row_box.append (icon); + + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + text.valign = Gtk.Align.CENTER; + var name = new Gtk.Label ("Omarchy"); + name.xalign = 0; + name.add_css_class ("ml-machine-name"); + sidebar_sub = new Gtk.Label ("ARM64 · Stopped"); + sidebar_sub.xalign = 0; + sidebar_sub.add_css_class ("ml-machine-sub"); + text.append (name); + text.append (sidebar_sub); + row_box.append (text); + + list.append (row_box); + list.select_row (list.get_row_at_index (0)); + side.append (list); + + return side; + } + + // ---- Content ---------------------------------------------------------- + + private Gtk.Widget build_content () { + var pane = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + pane.hexpand = true; + + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = ":maximize"; + header.title_widget = new Gtk.Label (""); + var handle = new Gtk.WindowHandle (); + handle.child = header; + pane.append (handle); + + var scroller = new Gtk.ScrolledWindow (); + scroller.vexpand = true; + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + pane.append (scroller); + + var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 20); + clamp.margin_start = 32; + clamp.margin_end = 32; + clamp.margin_bottom = 28; + clamp.halign = Gtk.Align.FILL; + clamp.width_request = 480; + scroller.child = clamp; + + // Hero + var hero = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18); + var hero_icon = new Gtk.Image.from_icon_name ("computer"); + hero_icon.pixel_size = 64; + hero_icon.add_css_class ("ml-hero-icon"); + hero_icon.valign = Gtk.Align.START; + hero.append (hero_icon); + + var hero_text = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); + hero_text.hexpand = true; + hero_text.valign = Gtk.Align.CENTER; + var title = new Gtk.Label ("Omarchy"); + title.add_css_class ("title-1"); + title.xalign = 0; + hero_text.append (title); + + var subtitle = new Gtk.Label ("A second Linux, running side by side with elementary OS. It uses your Mac's hardware virtualization, so it stays fast."); + subtitle.wrap = true; + subtitle.xalign = 0; + subtitle.add_css_class ("dim-label"); + hero_text.append (subtitle); + + status_pill = new Gtk.Label ("Stopped"); + status_pill.add_css_class ("ml-pill"); + status_pill.halign = Gtk.Align.START; + status_pill.margin_top = 6; + hero_text.append (status_pill); + hero.append (hero_text); + clamp.append (hero); + + // Specs + var specs = new Gtk.Grid (); + specs.column_homogeneous = true; + specs.row_homogeneous = true; + specs.column_spacing = 12; + specs.row_spacing = 12; + specs.attach (spec_card ("computer-symbolic", "4 virtual CPUs", "Processor"), 0, 0); + specs.attach (spec_card ("media-optical-symbolic", "6 GiB", "Memory"), 1, 0); + specs.attach (spec_card ("drive-harddisk-symbolic", "64 GiB", "Disk (grows as used)"), 0, 1); + specs.attach (spec_card ("network-wired-symbolic", "Shared network", "SSH on localhost:2222"), 1, 1); + clamp.append (specs); + + // Installer drop zone + drop_zone = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + drop_zone.add_css_class ("ml-drop"); + var drop_icon = new Gtk.Image.from_icon_name ("media-optical"); + drop_icon.pixel_size = 48; + drop_zone.append (drop_icon); + + var drop_text = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + drop_text.hexpand = true; + drop_text.valign = Gtk.Align.CENTER; + drop_title = new Gtk.Label ("Choose an installer"); + drop_title.xalign = 0; + drop_title.add_css_class ("title-3"); + drop_detail = new Gtk.Label ("Drop an ARM64 .iso here, or browse for one."); + drop_detail.xalign = 0; + drop_detail.ellipsize = Pango.EllipsizeMode.MIDDLE; + drop_detail.add_css_class ("dim-label"); + drop_text.append (drop_title); + drop_text.append (drop_detail); + drop_zone.append (drop_text); + + var browse = new Gtk.Button.with_label ("Browse…"); + browse.valign = Gtk.Align.CENTER; + browse.clicked.connect (() => choose_installer.begin ()); + drop_zone.append (browse); + clamp.append (drop_zone); + + var target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); + target.enter.connect (() => { drop_zone.add_css_class ("hover"); return Gdk.DragAction.COPY; }); + target.leave.connect (() => drop_zone.remove_css_class ("hover")); + target.drop.connect ((value) => { + drop_zone.remove_css_class ("hover"); + var files = (Gdk.FileList) value.get_boxed (); + foreach (var f in files.get_files ()) { + set_installer (f.get_path ()); + return true; + } + return false; + }); + drop_zone.add_controller (target); + + // Actions + var actions = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + actions.halign = Gtk.Align.END; + stop_button = new Gtk.Button.with_label ("Force Stop"); + stop_button.add_css_class (Granite.CssClass.DESTRUCTIVE); + stop_button.clicked.connect (stop_vm); + actions.append (stop_button); + + start_button = new Gtk.Button.with_label ("Start Machine"); + start_button.add_css_class (Granite.CssClass.SUGGESTED); + start_button.clicked.connect (start_vm); + actions.append (start_button); + clamp.append (actions); + + var fine = new Gtk.Label ("Prototype: this boots the installer and leaves installing the system to you. Needs QEMU, UEFI firmware and access to /dev/kvm."); + fine.wrap = true; + fine.xalign = 0; + fine.add_css_class ("ml-fineprint"); + clamp.append (fine); + + return pane; + } + + private Gtk.Widget spec_card (string icon_name, string value, string label) { + var card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + card.add_css_class ("ml-card"); + card.add_css_class ("ml-spec"); + + var icon = new Gtk.Image.from_icon_name (icon_name); + icon.pixel_size = 24; + card.append (icon); + + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + text.valign = Gtk.Align.CENTER; + var v = new Gtk.Label (value); + v.xalign = 0; + v.add_css_class ("ml-spec-value"); + var l = new Gtk.Label (label); + l.xalign = 0; + l.add_css_class ("ml-spec-label"); + text.append (v); + text.append (l); + card.append (text); + return card; + } + + // ---- State ------------------------------------------------------------ + + private bool is_running () { + return vm != null; + } + + private void update_state () { + bool running = is_running (); + start_button.visible = !running; + stop_button.visible = running; + start_button.sensitive = installer_path != null; + + status_pill.label = running ? "Running" : "Stopped"; + if (running) { + status_pill.add_css_class ("running"); + status_pill.remove_css_class ("stopped"); + } else { + status_pill.remove_css_class ("running"); + status_pill.add_css_class ("stopped"); + } + sidebar_sub.label = running ? "ARM64 · Running" : "ARM64 · Stopped"; + + if (installer_path != null) { + drop_zone.add_css_class ("chosen"); + drop_title.label = Path.get_basename (installer_path); + drop_detail.label = installer_path; + } else { + drop_zone.remove_css_class ("chosen"); + drop_title.label = "Choose an installer"; + drop_detail.label = "Drop an ARM64 .iso here, or browse for one."; + } + } + + private void set_installer (string? path) { + if (path == null) { + return; + } + installer_path = path; + update_state (); + } + + private void notify_user (string message) { + toast.title = message; + toast.send_notification (); + } + + // ---- Actions ---------------------------------------------------------- + + private async void choose_installer () { + var chooser = new Gtk.FileDialog (); + chooser.title = "Choose an ARM64 installer image"; + chooser.accept_label = "Choose"; + var filter = new Gtk.FileFilter (); + filter.name = "Installer images"; + filter.add_mime_type ("application/x-iso9660-image"); + filter.add_pattern ("*.iso"); + filter.add_pattern ("*.img"); + var filters = new GLib.ListStore (typeof (Gtk.FileFilter)); + filters.append (filter); + chooser.filters = filters; + try { + var file = yield chooser.open (window, null); + set_installer (file.get_path ()); + } catch (GLib.Error error) { + // GTK reports user cancellation through the same error path. + } + } + + private string? find_launcher () { + string? override_path = Environment.get_variable ("MACHINE_LAB_LAUNCHER"); + if (override_path != null && FileUtils.test (override_path, FileTest.IS_EXECUTABLE)) { + return override_path; + } + + string installed = "/usr/bin/machine-lab-run-vm"; + if (FileUtils.test (installed, FileTest.IS_EXECUTABLE)) { + return installed; + } + + // Source tree: build/machine-lab -> ../launcher/run-vm.sh + string exe_dir = Path.get_dirname (Environment.get_prgname () ?? ""); + string[] candidates = { + Path.build_filename (exe_dir, "run-vm.sh"), + Path.build_filename (exe_dir, "..", "launcher", "run-vm.sh") + }; + foreach (var c in candidates) { + if (FileUtils.test (c, FileTest.IS_EXECUTABLE)) { + return c; + } + } + return null; + } + + private void start_vm () { + if (installer_path == null || is_running ()) { + return; + } + string? launcher = find_launcher (); + if (launcher == null) { + show_error ("The QEMU launcher was not found. Set MACHINE_LAB_LAUNCHER to the full path of run-vm.sh."); + return; + } + + string vm_dir = Path.build_filename (Environment.get_user_data_dir (), "machine-lab", "omarchy-arm64"); + try { + string[] argv = { launcher, installer_path, vm_dir }; + vm = new Subprocess.newv (argv, SubprocessFlags.NONE); + vm.wait_async.begin (null, (obj, res) => { + try { + vm.wait_async.end (res); + int code = vm.get_if_exited () ? vm.get_exit_status () : -1; + if (code > 0) { + notify_user ("The machine stopped with an error (exit %d)".printf (code)); + } else { + notify_user ("Machine stopped"); + } + } catch (Error e) { + notify_user (e.message); + } + vm = null; + update_state (); + }); + update_state (); + notify_user ("Starting Omarchy…"); + } catch (Error error) { + show_error (error.message); + } + } + + private void stop_vm () { + if (vm != null) { + vm.force_exit (); + } + } + + private void show_error (string message) { + var dialog = new Gtk.AlertDialog ("Could not start the virtual machine"); + dialog.detail = message; + dialog.show (window); + } + + public static int main (string[] args) { + return new MachineLab ().run (args); + } +} From c1b320cb0c83d2aaf9ff2803f7ae40260319d596 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:19:40 -0400 Subject: [PATCH 062/798] Wire explicit native compositor startup --- mseries/OS9-HANDOFF.md | 33 ++++- mseries/ROADMAP.md | 7 +- rust/pantheon-compositor/Cargo.toml | 6 +- rust/pantheon-compositor/README.md | 56 ++++---- .../src/handlers/compositor.rs | 2 + rust/pantheon-compositor/src/main.rs | 86 ++++++++--- rust/pantheon-compositor/src/native.rs | 133 +++++++++++++++++- rust/pantheon-compositor/src/state.rs | 19 +++ 8 files changed, 283 insertions(+), 59 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index d96711d48..e1c10780e 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5428,16 +5428,37 @@ other than the renderer device, finds a connected connector with a compatible CRTC, prefers the built-in eDP connector, chooses its preferred mode, and constructs Smithay's GBM output manager using Asahi render buffers and an Apple KMS framebuffer exporter. It publishes the output and updates the multi-output -DisplayConfig state. The executable does not call this code yet; no libseat -session was opened and no output was initialized at runtime. `DrmDevice::new` +DisplayConfig state. At this implementation milestone no libseat session was +opened and no output was initialized at runtime. `DrmDevice::new` is configured to disable connectors while preparing for compositor takeover, so this must only be called in the future native session after libseat has paused the existing display server. The output object now renders the current Space into its DRM swapchain, queues damaged frames, and exposes the matching -vblank submission completion. These methods are not yet connected to calloop; -frame scheduling, vblank event wiring, hotplug, pause/resume and native input -remain. No libseat session was opened at runtime. Default and `native-drm` -checks/builds pass locally. +vblank submission completion. At this implementation milestone frame +scheduling, vblank event wiring, hotplug, pause/resume and native input remained +unconnected. No libseat session was opened at runtime. Default and `native-drm` +checks/builds passed locally. + +### Explicit native backend startup and event loop — 2026-10-18 + +The executable now selects `nested` by default or accepts `--backend native` +when built with `native-drm`. Native startup creates a libseat session, names +the Wayland seat from that session, initializes the hardware renderer and first +connected KMS output, assigns libinput to the active seat, and registers input, +session pause/resume, DRM vblank and redraw-channel sources with calloop. +Surface commits and input request redraws; vblank completes the matching +queued frame and sends frame callbacks. The nested path remains independent and +never creates a libseat session. Local `cargo check --bins`, +`cargo check --bins --features native-drm`, and matching `cargo build --bins` +commands pass. Formatting was applied only to the changed Rust sources; +`cargo fmt --check` still reports pre-existing formatting differences elsewhere +in the crate. Native mode was not launched: +on this host it would take over the active display, so DRM initialization, +scanout, physical input, pause/resume and the complete Pantheon session remain +runtime-unverified. This first startup slice handles one output only; hotplug +and multi-output lifecycle are still needed for the scoped M-series experience. +Next: qualify the native path in a separate session or other safe hardware +setup, then implement multi-output and hotplug lifecycle support. ### Absolute pointer mapping — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ffee49b11..305e1a75f 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -53,7 +53,12 @@ owns `eDP-1` and `DP-2`, while `HDMI-A-1` is disconnected; card2 has no EGL device. Therefore cross-device buffer import, display-controller scanout allocation and failure handling are first-session requirements for this M1 Pro, not optional general multi-GPU support. The read-only probe did not change -modes. No session startup is wired yet. +modes. The executable now keeps nested Winit as its default and provides an +explicit `--backend native` path behind `native-drm`; this wires libseat, +libinput, the first DRM output, calloop rendering and vblank completion. Both +build configurations pass locally. Native mode has not been launched because +it deliberately takes over the active display; output hotplug, multiple +outputs, session switching and actual M-series scanout remain unqualified. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index e29a88f80..0064e751a 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -3,13 +3,11 @@ name = "pantheon-compositor" version = "0.1.0" edition = "2024" license = "GPL-3.0-only" -description = "Nested Rust Wayland compositor prototype for elementary OS" +description = "Mutter-minimal Rust Wayland compositor for elementary OS" [features] default = [] -# Dependencies for the opt-in native M-series DRM/session backend. The current -# executable still starts through Winit; native session startup is being added -# separately so routine development cannot take over the active display. +# Dependencies for the explicit opt-in native M-series DRM/session backend. native-drm = [ "smithay/backend_drm", "smithay/backend_gbm", diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index a0937e3d6..0f4b19822 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -1,10 +1,10 @@ # pantheon-compositor -First development harness for the Mutter-minimal viable Rust effort: an -opt-in, nested Wayland compositor prototype using Smithay's Winit backend. It -runs in a window on an existing Wayland or X11 session and serves its own -Wayland socket. It does not open DRM/KMS, become a login session, or replace -Gala. +Mutter-minimal Rust compositor for the M-series/Pantheon experience. The +default backend is a nested Wayland compositor using Smithay's Winit backend; +an explicit, feature-gated native backend is also wired for M-series DRM/KMS, +libseat and libinput startup. This is still an early development harness, not +a drop-in Gala replacement or a qualified login session. ## Run @@ -21,6 +21,16 @@ a client. `WAYLAND_DISPLAY` is changed only inside the compositor process after the host Winit backend has initialized; the requested child uses the nested socket. The active session's services and configuration are not touched. +The native backend is selected explicitly and requires native dependencies: + +```sh +cargo run -p pantheon-compositor --features native-drm -- --backend native +``` + +Native startup acquires the active libseat session and initializes a DRM/KMS +output. It takes over the display device, so do not launch it inside the +currently active desktop. It is not yet runtime-qualified on M-series hardware. + To exercise the Pantheon protocol against the nested socket, build both binaries and start the compositor with its probe client: @@ -37,8 +47,8 @@ integration. ## Current limits -This is a nested proof of rendering and basic interactive Wayland surfaces, not -a Mutter port or a Pantheon replacement yet. The target is only the +This is an early compositor implementation, not a Mutter port or a Pantheon +replacement yet. The target is only the M-series/Pantheon behavior documented in [`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). It currently has one nested output, @@ -61,23 +71,21 @@ This is only the first shell integration slice. Autohide/overlap tracking, animated hiding, blur, overview behavior, multi-output work-area updates for already maximized windows, and session/lock integration are not complete. Widget, extended-behavior and greeter protocol requests are accepted but currently have -no behavior. Touch input, DRM/KMS, libinput device management, multi-monitor -configuration, accessibility, IBus, Xwayland, screen-sharing portals and -production crash recovery also remain unimplemented. - -`native-drm` is an opt-in Cargo feature that stages Smithay's DRM, GBM, -libinput, udev, libseat, EGL, and multi-GPU dependencies. The current executable -still starts through Winit. Behind this feature, `native.rs` can initialize a -hardware EGL/GBM renderer on a libseat GPU and register it with Smithay's -multi-GPU manager. It is not yet called by the executable and does not open a -multi-GPU manager. `NativeKmsOutput` can open a separate KMS device, choose a -connected connector/CRTC, build the GBM framebuffer exporter/output manager, -publish its Wayland output, render the current space, queue a damaged frame, -and acknowledge its KMS submission. The executable does not call these native -methods yet; the DRM vblank/session event loop, frame scheduling, hotplug, -pause/resume and native input are unfinished. Default and feature-enabled `cargo check` and -`cargo build` pass locally after installing the host `libseat-dev` package; -none of those commands initializes a native session. +no behavior. Touch input, multi-monitor configuration, accessibility, IBus, +Xwayland, screen-sharing portals and production crash recovery also remain +unimplemented. + +`native-drm` enables Smithay's DRM, GBM, libinput, udev, libseat, EGL, and +multi-GPU dependencies. The executable defaults to nested Winit; only +`--backend native` starts the native session. That path initializes a hardware +EGL/GBM renderer and a separate connected KMS output, publishes it as a +Wayland output, assigns libinput to the active seat, and registers redraw, +vblank, and session pause/resume sources with calloop. It currently selects +one output. Hotplug, multiple-output lifecycle, display mode management, +session-shell integration and M-series scanout/input behavior remain +unqualified. Default and feature-enabled `cargo check --bins` and +`cargo build --bins` pass locally; builds do not initialize or take over a +native session. With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices without acquiring DRM master or changing display modes. On the current M-series diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index 2d8a1110c..675abc888 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -45,6 +45,8 @@ impl CompositorHandler for PantheonCompositor { xdg_shell::handle_commit(self, surface); pantheon_shell::handle_surface_commit(self, surface); resize_grab::handle_commit(&mut self.space, surface); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); } } diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index dcce3a8cb..b5902ed1f 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -4,12 +4,12 @@ mod display_config; mod grabs; mod handlers; mod input; +#[cfg(feature = "native-drm")] +pub mod native; mod pantheon_shell; mod protocols; mod shell_compat; mod state; -#[cfg(feature = "native-drm")] -pub mod native; mod winit; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; @@ -18,21 +18,43 @@ pub use state::PantheonCompositor; fn main() -> Result<(), Box> { init_logging(); - if std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none() { - return Err("nested prototype requires an existing Wayland or X11 session".into()); - } - - let command = parse_command()?; + let (backend, command) = parse_arguments()?; let mut event_loop: EventLoop = EventLoop::try_new()?; let display: Display = Display::new()?; - let mut state = PantheonCompositor::new(&mut event_loop, display, "winit"); - - // This prototype deliberately uses the nested winit backend. It does not - // open DRM/KMS devices or replace the active desktop session. - crate::winit::init_winit(&mut event_loop, &mut state)?; + let mut state = match backend { + BackendChoice::Nested => { + if std::env::var_os("WAYLAND_DISPLAY").is_none() + && std::env::var_os("DISPLAY").is_none() + { + return Err("nested backend requires an existing Wayland or X11 session".into()); + } + let mut state = PantheonCompositor::new(&mut event_loop, display, "winit"); + crate::winit::init_winit(&mut event_loop, &mut state)?; + state + } + BackendChoice::Native => { + #[cfg(feature = "native-drm")] + { + use smithay::backend::session::{Session, libseat::LibSeatSession}; + let (session, notifier) = LibSeatSession::new()?; + let seat_name = session.seat(); + let mut state = PantheonCompositor::new(&mut event_loop, display, &seat_name); + crate::native::NativeBackend::start( + &mut event_loop, + &mut state, + session, + notifier, + )?; + state + } + #[cfg(not(feature = "native-drm"))] + { + return Err("native backend requires a build with --features native-drm".into()); + } + } + }; - // The host backend is already initialized from the original environment. - // Launch requested clients against the nested display only. + // Launch requested clients against the compositor socket selected above. unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; if let Some((program, args)) = command { @@ -51,14 +73,36 @@ fn init_logging() { } } -fn parse_command() -> Result)>, Box> { +#[derive(Clone, Copy)] +enum BackendChoice { + Nested, + Native, +} + +fn parse_arguments() +-> Result<(BackendChoice, Option<(String, Vec)>), Box> { let mut args = std::env::args().skip(1); - match args.next().as_deref() { - None => Ok(None), - Some("--run") => { - let program = args.next().ok_or("--run requires a program")?; - Ok(Some((program, args.collect()))) + let mut backend = BackendChoice::Nested; + let mut command = None; + while let Some(arg) = args.next() { + match arg.as_str() { + "--backend" => { + backend = match args.next().as_deref() { + Some("nested") => BackendChoice::Nested, + Some("native") => BackendChoice::Native, + _ => return Err("--backend requires either nested or native".into()), + }; + } + "--run" => { + let program = args.next().ok_or("--run requires a program")?; + command = Some((program, args.collect())); + break; + } + _ => return Err( + "usage: pantheon-compositor [--backend nested|native] [--run PROGRAM [ARGS...]]" + .into(), + ), } - Some(_) => Err("usage: pantheon-compositor [--run PROGRAM [ARGS...]]".into()), } + Ok((backend, command)) } diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 1bc0b1a7e..71297b047 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -6,6 +6,7 @@ //! attached by the native output backend built on top of this manager. use std::path::Path; +use std::time::Duration; use smithay::{ backend::{ @@ -19,17 +20,23 @@ use smithay::{ }, backend::{ egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, + libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ element::surface::WaylandSurfaceRenderElement, gles::GlesRenderer, multigpu::{GpuManager, MultiRenderer, gbm::GbmGlesBackend}, }, - session::{Session, libseat::LibSeatSession}, + session::{ + Event as SessionEvent, Session, + libseat::{LibSeatSession, LibSeatSessionNotifier}, + }, udev::all_gpus, }, output::{Mode, Output, PhysicalProperties}, reexports::{ + calloop::{EventLoop, channel, channel::Event as ChannelEvent}, drm::control::{Device as DrmControlDevice, ModeTypeFlags, connector}, + input::Libinput, rustix::fs::OFlags, }, utils::DeviceFd, @@ -61,10 +68,17 @@ pub struct NativeRenderer { pub struct NativeKmsOutput { pub node: DrmNode, pub connector: connector::Handle, + pub crtc: smithay::reexports::drm::control::crtc::Handle, pub output: Output, pub manager: NativeOutputManager, pub drm_output: NativeDrmOutput, - pub notifier: DrmDeviceNotifier, + pub notifier: Option, +} + +pub struct NativeBackend { + pub _session: LibSeatSession, + pub renderer: NativeRenderer, + pub output: NativeKmsOutput, } struct ConnectedOutput { @@ -270,14 +284,127 @@ impl NativeKmsOutput { Ok(Self { node, connector: selected.connector.handle(), + crtc: selected.crtc, output, manager, drm_output, - notifier, + notifier: Some(notifier), }) } } +impl NativeBackend { + /// Initialize native input, renderer and the first KMS output, then register + /// their event sources. This is called only for explicit `--backend native` + /// startup; the default nested backend never opens a libseat session. + pub fn start( + event_loop: &mut EventLoop, + state: &mut PantheonCompositor, + session: LibSeatSession, + session_notifier: LibSeatSessionNotifier, + ) -> Result<(), Box> { + let mut renderer = NativeRenderer::initialize(&session)?; + let mut output = NativeKmsOutput::initialize(&session, &mut renderer, state)?; + let drm_notifier = output.notifier.take().expect("DRM notifier is present"); + + let mut libinput_context = Libinput::new_with_udev::< + LibinputSessionInterface, + >(session.clone().into()); + libinput_context + .udev_assign_seat(&session.seat()) + .map_err(|_| "libinput failed to assign the active seat")?; + let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); + event_loop + .handle() + .insert_source(libinput_backend, |event, _, state| { + state.process_input_event(event); + #[cfg(feature = "native-drm")] + state.request_native_redraw(); + })?; + + event_loop.handle().insert_source( + session_notifier, + move |event, _, state| match event { + SessionEvent::PauseSession => { + libinput_context.suspend(); + if let Some(backend) = state.native_backend.as_mut() { + backend.output.manager.pause(); + } + tracing::info!("native compositor session paused"); + } + SessionEvent::ActivateSession => { + tracing::info!("native compositor session activated"); + if let Err(error) = libinput_context.resume() { + tracing::error!(?error, "failed to resume libinput"); + } + if let Some(backend) = state.native_backend.as_mut() { + if let Err(error) = backend.output.manager.lock().activate(false) { + tracing::error!(?error, "failed to reactivate DRM output"); + } + } + state.request_native_redraw(); + } + }, + )?; + + event_loop + .handle() + .insert_source(drm_notifier, |event, _, state| match event { + smithay::backend::drm::DrmEvent::VBlank(crtc) => { + let Some(backend) = state.native_backend.as_mut() else { + return; + }; + if crtc != backend.output.crtc { + return; + } + if let Err(error) = backend.output.frame_submitted() { + tracing::error!(?error, "failed to complete native DRM frame"); + return; + } + let output = backend.output.output.clone(); + state.space.elements().for_each(|window| { + window.send_frame( + &output, + state.start_time.elapsed(), + Some(Duration::ZERO), + |_, _| Some(output.clone()), + ); + }); + state.space.refresh(); + state.popups.cleanup(); + let _ = state.display_handle.flush_clients(); + } + smithay::backend::drm::DrmEvent::Error(error) => { + tracing::error!(?error, "native DRM event source failed"); + } + })?; + + let (redraw_sender, redraw_receiver) = channel::channel(); + state.native_redraw_sender = Some(redraw_sender); + state.native_backend = Some(Self { + _session: session, + renderer, + output, + }); + event_loop + .handle() + .insert_source(redraw_receiver, |event, _, state| { + if !matches!(event, ChannelEvent::Msg(())) { + return; + } + let Some(mut backend) = state.native_backend.take() else { + return; + }; + if let Err(error) = backend.output.render(&mut backend.renderer, state) { + tracing::error!(?error, "native output render failed"); + } + state.native_backend = Some(backend); + })?; + state.request_native_redraw(); + Ok(()) + } +} + impl NativeRenderer { /// Allocate render buffers on the hardware GPU with scanout usage enabled. /// The Apple KMS framebuffer exporter can import these buffers for the diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index fbe5a9d80..be1746de1 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -24,8 +24,12 @@ use smithay::{ }; use crate::display_config::DisplayConfigHandle; +#[cfg(feature = "native-drm")] +use crate::native::NativeBackend; use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::shell_compat::ShellCompatHandle; +#[cfg(feature = "native-drm")] +use smithay::reexports::calloop::channel::Sender; pub struct PantheonCompositor { pub start_time: std::time::Instant, @@ -53,6 +57,10 @@ pub struct PantheonCompositor { pub popups: PopupManager, pub seat: Seat, + #[cfg(feature = "native-drm")] + pub native_backend: Option, + #[cfg(feature = "native-drm")] + pub native_redraw_sender: Option>, } #[derive(Clone, Copy)] @@ -135,6 +143,17 @@ impl PantheonCompositor { initially_positioned: HashSet::new(), popups, seat, + #[cfg(feature = "native-drm")] + native_backend: None, + #[cfg(feature = "native-drm")] + native_redraw_sender: None, + } + } + + #[cfg(feature = "native-drm")] + pub fn request_native_redraw(&self) { + if let Some(sender) = &self.native_redraw_sender { + let _ = sender.send(()); } } From 16710a5393aa655aa3995a1ddbee3fd3eaf75a39 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:20:02 -0400 Subject: [PATCH 063/798] Reboot queue: current kernel, next boot and nothing armed after the enh1 diagnostic boot Co-Authored-By: Claude Opus 5.5 --- mseries/REBOOT-QUEUE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mseries/REBOOT-QUEUE.md b/mseries/REBOOT-QUEUE.md index 3679a5c41..660b0a40d 100644 --- a/mseries/REBOOT-QUEUE.md +++ b/mseries/REBOOT-QUEUE.md @@ -1,5 +1,19 @@ # Reboot queue — what is staged and what each reboot unlocks +## Current state — 2026-10-18 23:20 EDT + +- **Running:** `7.1.12-elementary-aurora20-enh1` from the one-shot + `elementary-aurora20-enh1-diagnostic` entry. It reached the desktop + ([receipt](aurora/hardware-tests/2026-10-18-j314s-enh1-diagnostic-boot.json)). + That is one boot; the candidate is still unqualified and the earlier hang is unexplained. +- **Next normal reboot:** `mseries-full-height` on `.20` (GRUB `next_entry` is empty). +- **Nothing is armed.** Booting enh1 again needs a new owner OK; promoting it to + default is DeepSeek's/owner's decision. +- **Relogin not needed:** the running Gala already loads the #244 libmutter fix. + No Gala crash in the first 31 minutes of this session. + +The section below predates the enh1 trials and is kept as history. + ## Separate Aurora enhancement build — do not deploy with the .20 installer The [enhancement candidate](aurora/enhancements/README.md) is source/build work From e87baef2aa4e7588d4d87bdf4b88db3d70cdb2ef Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:20:33 -0400 Subject: [PATCH 064/798] Machine Lab: multiple machines, tabs (overview/console/settings), per-VM specs, live console Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/README.md | 4 +- mseries/machine-lab/data/style.css | 26 ++ mseries/machine-lab/launcher/run-vm.sh | 28 +- mseries/machine-lab/meson.build | 2 +- mseries/machine-lab/src/machine.vala | 151 +++++++ mseries/machine-lab/src/main.vala | 538 ++++++++++++------------- mseries/machine-lab/src/page.vala | 529 ++++++++++++++++++++++++ 7 files changed, 980 insertions(+), 298 deletions(-) create mode 100644 mseries/machine-lab/src/machine.vala create mode 100644 mseries/machine-lab/src/page.vala diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md index ccf7efe56..7dfbc392b 100644 --- a/mseries/machine-lab/README.md +++ b/mseries/machine-lab/README.md @@ -2,7 +2,9 @@ Native elementary OS look (Vala + GTK 4 + Granite 7) for running another Linux ARM64 guest under QEMU/KVM with NAT networking. Flat sidebar of machines, hero header, spec cards, -drag-and-drop installer picker, Start/Force Stop with live status, toast feedback, and +multiple machines saved in ~/.config/machine-lab/machines.ini, Overview / Console / Settings tabs, +per-machine CPU, memory, disk and SSH port, drag-and-drop installer, live status and uptime, +streamed QEMU console, copyable SSH command, empty-state welcome, Ctrl+N, toasts, and light/dark + accent following the system. ```sh diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css index 6d32ec3e9..7c5027643 100644 --- a/mseries/machine-lab/data/style.css +++ b/mseries/machine-lab/data/style.css @@ -68,3 +68,29 @@ } .ml-fineprint { font-size: 0.85em; opacity: 0.65; } + +.ml-dot { + border-radius: 99px; + background: alpha(@fg_color, 0.35); + box-shadow: 0 0 0 2px @bg_color; +} +.ml-dot.running { background: #68b723; } +.ml-pill.running { color: #3a9104; background: alpha(#68b723, 0.2); } +.ml-empty-icon { opacity: 0.9; } +.ml-mono { font-family: monospace; font-weight: 600; } +.ml-note { + padding: 10px 14px; + border-radius: 10px; + background: alpha(@accent_color, 0.1); + box-shadow: inset 0 0 0 1px alpha(@accent_color, 0.25); +} +.ml-form-row { padding: 12px 16px; } +.ml-form-sep { border-bottom: 1px solid alpha(@fg_color, 0.08); } +.ml-console-frame { + border-radius: 10px; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.12); +} +textview.ml-console, textview.ml-console text { + background: #1d1f23; + color: #d8dde6; +} diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh index cbde01459..c02144723 100755 --- a/mseries/machine-lab/launcher/run-vm.sh +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -6,7 +6,12 @@ if [ "$#" -lt 1 ]; then exit 2 fi -ISO_PATH=$1 +ISO_PATH=$1 # "-" boots the installed disk without an installer +CPUS=${MACHINE_CPUS:-4} +MEM_GIB=${MACHINE_MEM_GIB:-6} +DISK_GIB=${MACHINE_DISK_GIB:-64} +SSH_PORT=${MACHINE_SSH_PORT:-2222} +VM_NAME=${MACHINE_NAME:-Omarchy-ARM64} if [ "$#" -ge 2 ]; then VM_DIR=$2 else @@ -26,7 +31,7 @@ if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then echo "KVM is unavailable to this user. Check /dev/kvm permissions." >&2 exit 1 fi -if [ ! -r "$ISO_PATH" ]; then +if [ "$ISO_PATH" != "-" ] && [ ! -r "$ISO_PATH" ]; then echo "Cannot read installer image: $ISO_PATH" >&2 exit 1 fi @@ -40,24 +45,29 @@ if [ ! -e "$VM_DIR/efi-vars.fd" ]; then cp --reflink=auto "$FW_VARS" "$VM_DIR/efi-vars.fd" fi if [ ! -e "$VM_DIR/disk.qcow2" ]; then - "$QEMU_IMG" create -f qcow2 "$VM_DIR/disk.qcow2" 64G + "$QEMU_IMG" create -f qcow2 "$VM_DIR/disk.qcow2" "${DISK_GIB}G" +fi + +CD_ARGS=() +if [ "$ISO_PATH" != "-" ]; then + CD_ARGS=(-drive if=none,id=installer,format=raw,media=cdrom,readonly=on,file="$ISO_PATH" + -device scsi-cd,drive=installer,bus=scsi.0) fi exec "$QEMU" \ - -name Omarchy-ARM64-PoC \ + -name "$VM_NAME" \ -machine virt \ -accel kvm \ -cpu host \ - -smp 4 \ - -m 6G \ + -smp "$CPUS" \ + -m "${MEM_GIB}G" \ -drive if=pflash,format=raw,readonly=on,file="$FW_CODE" \ -drive if=pflash,format=raw,file="$VM_DIR/efi-vars.fd" \ -drive if=none,id=system,format=qcow2,file="$VM_DIR/disk.qcow2" \ -device virtio-blk-pci,drive=system \ -device virtio-scsi-pci,id=scsi \ - -drive if=none,id=installer,format=raw,media=cdrom,readonly=on,file="$ISO_PATH" \ - -device scsi-cd,drive=installer,bus=scsi.0 \ - -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 \ + "${CD_ARGS[@]}" \ + -netdev user,id=net0,hostfwd=tcp:127.0.0.1:"$SSH_PORT"-:22 \ -device virtio-net-pci,netdev=net0 \ -device virtio-gpu-pci \ -display gtk,gl=off \ diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index 702199826..a491db1d4 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -14,7 +14,7 @@ resources = gnome.compile_resources('machine-lab-resources', ) executable('machine-lab', - 'src/main.vala', + 'src/main.vala', 'src/machine.vala', 'src/page.vala', resources, dependencies: [gtk, granite], install: true diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala new file mode 100644 index 000000000..daa9ed1ad --- /dev/null +++ b/mseries/machine-lab/src/machine.vala @@ -0,0 +1,151 @@ +/* Machine model, persistence and QEMU process control. */ + +public class Machine : Object { + public string id { get; set; } + public string name { get; set; default = "New Machine"; } + public int cpus { get; set; default = 4; } + public int mem_gib { get; set; default = 6; } + public int disk_gib { get; set; default = 64; } + public int ssh_port { get; set; default = 2222; } + public string? installer { get; set; default = null; } + + public Subprocess? proc { get; private set; default = null; } + public DateTime? started { get; private set; default = null; } + public Gtk.TextBuffer log { get; private set; default = new Gtk.TextBuffer (null); } + + public signal void state_changed (); + public signal void finished (int code); + + public bool running { get { return proc != null; } } + + public string dir { + owned get { + return Path.build_filename (Environment.get_user_data_dir (), "machine-lab", id); + } + } + + public bool has_disk () { + return FileUtils.test (Path.build_filename (dir, "disk.qcow2"), FileTest.EXISTS); + } + + public void start (string launcher) throws Error { + if (running) { + return; + } + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); + l.setenv ("MACHINE_CPUS", cpus.to_string (), true); + l.setenv ("MACHINE_MEM_GIB", mem_gib.to_string (), true); + l.setenv ("MACHINE_DISK_GIB", disk_gib.to_string (), true); + l.setenv ("MACHINE_SSH_PORT", ssh_port.to_string (), true); + l.setenv ("MACHINE_NAME", name, true); + string[] argv = { launcher, installer ?? "-", dir }; + proc = l.spawnv (argv); + started = new DateTime.now_local (); + log.text = ""; + append_log ("$ %s\n".printf (string.joinv (" ", argv))); + read_output.begin (proc); + watch.begin (proc); + state_changed (); + } + + public void stop () { + if (proc != null) { + proc.force_exit (); + } + } + + private void append_log (string text) { + Gtk.TextIter end; + log.get_end_iter (out end); + log.insert (ref end, text, -1); + } + + private async void read_output (Subprocess p) { + var stream = new DataInputStream (p.get_stdout_pipe ()); + try { + string? line; + while ((line = yield stream.read_line_async ()) != null) { + append_log (line + "\n"); + } + } catch (Error e) { + append_log ("[log: %s]\n".printf (e.message)); + } + } + + private async void watch (Subprocess p) { + try { + yield p.wait_async (); + } catch (Error e) { + append_log ("[wait: %s]\n".printf (e.message)); + } + int code = p.get_if_exited () ? p.get_exit_status () : -1; + append_log ("\n[machine exited with status %d]\n".printf (code)); + proc = null; + started = null; + state_changed (); + finished (code); + } + + public void delete_files () { + foreach (var f in new string[] { "disk.qcow2", "efi-vars.fd" }) { + FileUtils.remove (Path.build_filename (dir, f)); + } + DirUtils.remove (dir); + } +} + +public class MachineStore : Object { + public static string config_path () { + return Path.build_filename (Environment.get_user_config_dir (), "machine-lab", "machines.ini"); + } + + public static GenericArray load () { + var list = new GenericArray (); + var kf = new KeyFile (); + try { + kf.load_from_file (config_path (), KeyFileFlags.NONE); + foreach (var id in kf.get_groups ()) { + var m = new Machine (); + m.id = id; + m.name = kf.get_string (id, "name"); + m.cpus = kf.get_integer (id, "cpus"); + m.mem_gib = kf.get_integer (id, "mem_gib"); + m.disk_gib = kf.get_integer (id, "disk_gib"); + m.ssh_port = kf.get_integer (id, "ssh_port"); + if (kf.has_key (id, "installer")) { + m.installer = kf.get_string (id, "installer"); + } + list.add (m); + } + } catch (Error e) { + // First run, or unreadable file: start from the default profile below. + } + if (list.length == 0 && !FileUtils.test (config_path (), FileTest.EXISTS)) { + var m = new Machine (); + m.id = "omarchy-arm64"; // keeps the disk made by the earlier prototype + m.name = "Omarchy"; + list.add (m); + } + return list; + } + + public static void save (GenericArray list) { + var kf = new KeyFile (); + foreach (var m in list.data) { + kf.set_string (m.id, "name", m.name); + kf.set_integer (m.id, "cpus", m.cpus); + kf.set_integer (m.id, "mem_gib", m.mem_gib); + kf.set_integer (m.id, "disk_gib", m.disk_gib); + kf.set_integer (m.id, "ssh_port", m.ssh_port); + if (m.installer != null) { + kf.set_string (m.id, "installer", m.installer); + } + } + try { + DirUtils.create_with_parents (Path.get_dirname (config_path ()), 0755); + kf.save_to_file (config_path ()); + } catch (Error e) { + warning ("Could not save machines: %s", e.message); + } + } +} diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index 1da0c2f8d..3448e8db1 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -1,23 +1,78 @@ /* * Machine Lab: run another Linux on this Mac. * Vala + GTK 4 + Granite 7, laid out like a first-party elementary OS app: - * flat sidebar + content pane, hero header, spec cards, suggested action. + * flat sidebar of machines + content pane with tabs, suggested-action buttons, + * toasts, and an empty-state welcome. */ using Gtk; -public class MachineLab : Gtk.Application { - private string? installer_path = null; - private Subprocess? vm = null; +public class MachineRow : Gtk.ListBoxRow { + public Machine machine { get; construct; } + private Gtk.Label name_label; + private Gtk.Label sub_label; + private Gtk.Box dot; + + public MachineRow (Machine machine) { + Object (machine: machine); + } + + construct { + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + box.margin_top = 4; + box.margin_bottom = 4; + box.margin_start = 6; + box.margin_end = 6; + + var overlay = new Gtk.Overlay (); + var icon = new Gtk.Image.from_icon_name ("computer"); + icon.pixel_size = 32; + overlay.child = icon; + dot = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + dot.add_css_class ("ml-dot"); + dot.halign = Gtk.Align.END; + dot.valign = Gtk.Align.END; + dot.width_request = 10; + dot.height_request = 10; + overlay.add_overlay (dot); + box.append (overlay); + + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + text.valign = Gtk.Align.CENTER; + name_label = new Gtk.Label (""); + name_label.xalign = 0; + name_label.ellipsize = Pango.EllipsizeMode.END; + name_label.add_css_class ("ml-machine-name"); + sub_label = new Gtk.Label (""); + sub_label.xalign = 0; + sub_label.add_css_class ("ml-machine-sub"); + text.append (name_label); + text.append (sub_label); + box.append (text); + child = box; + + machine.state_changed.connect (update); + update (); + } + + public void update () { + name_label.label = machine.name; + sub_label.label = "%s · %d CPU · %d GiB".printf ( + machine.running ? "Running" : "Stopped", machine.cpus, machine.mem_gib); + if (machine.running) { + dot.add_css_class ("running"); + } else { + dot.remove_css_class ("running"); + } + } +} +public class MachineLab : Gtk.Application { + private GenericArray machines; private Gtk.ApplicationWindow window; private Granite.Toast toast; - private Gtk.Box drop_zone; - private Gtk.Label drop_title; - private Gtk.Label drop_detail; - private Gtk.Button start_button; - private Gtk.Button stop_button; - private Gtk.Label status_pill; - private Gtk.Label sidebar_sub; + private Gtk.ListBox list; + private Gtk.Stack content; + private HashTable pages = new HashTable (str_hash, str_equal); public MachineLab () { Object ( @@ -30,7 +85,6 @@ public class MachineLab : Gtk.Application { base.startup (); Granite.init (); - // Follow the system light/dark preference like other elementary apps. var gtk_settings = Gtk.Settings.get_default (); var granite_settings = Granite.Settings.get_default (); gtk_settings.gtk_application_prefer_dark_theme = @@ -45,6 +99,15 @@ public class MachineLab : Gtk.Application { Gtk.StyleContext.add_provider_for_display ( Gdk.Display.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); + + var new_action = new SimpleAction ("new-machine", null); + new_action.activate.connect (() => add_machine ()); + add_action (new_action); + set_accels_for_action ("app.new-machine", { "n" }); + set_accels_for_action ("app.quit", { "q" }); + var quit = new SimpleAction ("quit", null); + quit.activate.connect (() => window.close ()); + add_action (quit); } protected override void activate () { @@ -52,19 +115,23 @@ public class MachineLab : Gtk.Application { window.present (); return; } + machines = MachineStore.load (); window = new Gtk.ApplicationWindow (this); window.title = "Machine Lab"; - window.default_width = 880; - window.default_height = 600; + window.default_width = 940; + window.default_height = 640; + window.titlebar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { visible = false }; var layout = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); layout.append (build_sidebar ()); layout.append (new Gtk.Separator (Gtk.Orientation.VERTICAL)); - layout.append (build_content ()); - // Hide the stock title bar: each pane brings its own flat header. - window.titlebar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { visible = false }; + content = new Gtk.Stack (); + content.hexpand = true; + content.transition_type = Gtk.StackTransitionType.CROSSFADE; + content.add_named (build_empty_state (), "empty"); + layout.append (content); var overlay = new Gtk.Overlay (); overlay.child = layout; @@ -72,7 +139,17 @@ public class MachineLab : Gtk.Application { overlay.add_overlay (toast); window.child = overlay; - update_state (); + foreach (var m in machines.data) { + add_row_and_page (m); + } + select_first (); + + window.close_request.connect (() => { + foreach (var m in machines.data) { + m.stop (); + } + return false; + }); window.present (); } @@ -81,7 +158,7 @@ public class MachineLab : Gtk.Application { private Gtk.Widget build_sidebar () { var side = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); side.add_css_class ("ml-sidebar"); - side.width_request = 230; + side.width_request = 250; var header = new Gtk.HeaderBar (); header.add_css_class ("flat"); @@ -96,260 +173,187 @@ public class MachineLab : Gtk.Application { heading.add_css_class ("ml-sidebar-title"); side.append (heading); - var list = new Gtk.ListBox (); + list = new Gtk.ListBox (); list.selection_mode = Gtk.SelectionMode.SINGLE; list.add_css_class ("navigation-sidebar"); - list.vexpand = true; - - var row_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); - row_box.margin_top = 4; - row_box.margin_bottom = 4; - row_box.margin_start = 6; - row_box.margin_end = 6; - var icon = new Gtk.Image.from_icon_name ("computer"); - icon.pixel_size = 32; - row_box.append (icon); - - var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - text.valign = Gtk.Align.CENTER; - var name = new Gtk.Label ("Omarchy"); - name.xalign = 0; - name.add_css_class ("ml-machine-name"); - sidebar_sub = new Gtk.Label ("ARM64 · Stopped"); - sidebar_sub.xalign = 0; - sidebar_sub.add_css_class ("ml-machine-sub"); - text.append (name); - text.append (sidebar_sub); - row_box.append (text); - - list.append (row_box); - list.select_row (list.get_row_at_index (0)); - side.append (list); - + list.row_selected.connect ((row) => { + if (row == null) { + content.visible_child_name = "empty"; + return; + } + content.visible_child_name = ((MachineRow) row).machine.id; + }); + var scroll = new Gtk.ScrolledWindow (); + scroll.vexpand = true; + scroll.child = list; + side.append (scroll); + + side.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + var add = new Gtk.Button (); + var add_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + add_box.append (new Gtk.Image.from_icon_name ("list-add-symbolic")); + add_box.append (new Gtk.Label ("New Machine")); + add.child = add_box; + add.add_css_class ("flat"); + add.margin_top = 6; + add.margin_bottom = 6; + add.margin_start = 6; + add.margin_end = 6; + add.halign = Gtk.Align.START; + add.tooltip_markup = Granite.markup_accel_tooltip ({ "n" }, "Create a new machine"); + add.clicked.connect (() => add_machine ()); + side.append (add); return side; } - // ---- Content ---------------------------------------------------------- - - private Gtk.Widget build_content () { - var pane = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - pane.hexpand = true; - + private Gtk.Widget build_empty_state () { + var page = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); var header = new Gtk.HeaderBar (); header.add_css_class ("flat"); header.decoration_layout = ":maximize"; header.title_widget = new Gtk.Label (""); var handle = new Gtk.WindowHandle (); handle.child = header; - pane.append (handle); - - var scroller = new Gtk.ScrolledWindow (); - scroller.vexpand = true; - scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; - pane.append (scroller); - - var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 20); - clamp.margin_start = 32; - clamp.margin_end = 32; - clamp.margin_bottom = 28; - clamp.halign = Gtk.Align.FILL; - clamp.width_request = 480; - scroller.child = clamp; - - // Hero - var hero = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18); - var hero_icon = new Gtk.Image.from_icon_name ("computer"); - hero_icon.pixel_size = 64; - hero_icon.add_css_class ("ml-hero-icon"); - hero_icon.valign = Gtk.Align.START; - hero.append (hero_icon); - - var hero_text = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); - hero_text.hexpand = true; - hero_text.valign = Gtk.Align.CENTER; - var title = new Gtk.Label ("Omarchy"); - title.add_css_class ("title-1"); - title.xalign = 0; - hero_text.append (title); - - var subtitle = new Gtk.Label ("A second Linux, running side by side with elementary OS. It uses your Mac's hardware virtualization, so it stays fast."); - subtitle.wrap = true; - subtitle.xalign = 0; - subtitle.add_css_class ("dim-label"); - hero_text.append (subtitle); - - status_pill = new Gtk.Label ("Stopped"); - status_pill.add_css_class ("ml-pill"); - status_pill.halign = Gtk.Align.START; - status_pill.margin_top = 6; - hero_text.append (status_pill); - hero.append (hero_text); - clamp.append (hero); - - // Specs - var specs = new Gtk.Grid (); - specs.column_homogeneous = true; - specs.row_homogeneous = true; - specs.column_spacing = 12; - specs.row_spacing = 12; - specs.attach (spec_card ("computer-symbolic", "4 virtual CPUs", "Processor"), 0, 0); - specs.attach (spec_card ("media-optical-symbolic", "6 GiB", "Memory"), 1, 0); - specs.attach (spec_card ("drive-harddisk-symbolic", "64 GiB", "Disk (grows as used)"), 0, 1); - specs.attach (spec_card ("network-wired-symbolic", "Shared network", "SSH on localhost:2222"), 1, 1); - clamp.append (specs); - - // Installer drop zone - drop_zone = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); - drop_zone.add_css_class ("ml-drop"); - var drop_icon = new Gtk.Image.from_icon_name ("media-optical"); - drop_icon.pixel_size = 48; - drop_zone.append (drop_icon); - - var drop_text = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); - drop_text.hexpand = true; - drop_text.valign = Gtk.Align.CENTER; - drop_title = new Gtk.Label ("Choose an installer"); - drop_title.xalign = 0; - drop_title.add_css_class ("title-3"); - drop_detail = new Gtk.Label ("Drop an ARM64 .iso here, or browse for one."); - drop_detail.xalign = 0; - drop_detail.ellipsize = Pango.EllipsizeMode.MIDDLE; - drop_detail.add_css_class ("dim-label"); - drop_text.append (drop_title); - drop_text.append (drop_detail); - drop_zone.append (drop_text); - - var browse = new Gtk.Button.with_label ("Browse…"); - browse.valign = Gtk.Align.CENTER; - browse.clicked.connect (() => choose_installer.begin ()); - drop_zone.append (browse); - clamp.append (drop_zone); - - var target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); - target.enter.connect (() => { drop_zone.add_css_class ("hover"); return Gdk.DragAction.COPY; }); - target.leave.connect (() => drop_zone.remove_css_class ("hover")); - target.drop.connect ((value) => { - drop_zone.remove_css_class ("hover"); - var files = (Gdk.FileList) value.get_boxed (); - foreach (var f in files.get_files ()) { - set_installer (f.get_path ()); - return true; - } - return false; - }); - drop_zone.add_controller (target); - - // Actions - var actions = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); - actions.halign = Gtk.Align.END; - stop_button = new Gtk.Button.with_label ("Force Stop"); - stop_button.add_css_class (Granite.CssClass.DESTRUCTIVE); - stop_button.clicked.connect (stop_vm); - actions.append (stop_button); - - start_button = new Gtk.Button.with_label ("Start Machine"); - start_button.add_css_class (Granite.CssClass.SUGGESTED); - start_button.clicked.connect (start_vm); - actions.append (start_button); - clamp.append (actions); - - var fine = new Gtk.Label ("Prototype: this boots the installer and leaves installing the system to you. Needs QEMU, UEFI firmware and access to /dev/kvm."); - fine.wrap = true; - fine.xalign = 0; - fine.add_css_class ("ml-fineprint"); - clamp.append (fine); - - return pane; - } - - private Gtk.Widget spec_card (string icon_name, string value, string label) { - var card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); - card.add_css_class ("ml-card"); - card.add_css_class ("ml-spec"); - - var icon = new Gtk.Image.from_icon_name (icon_name); - icon.pixel_size = 24; - card.append (icon); + page.append (handle); - var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); - text.valign = Gtk.Align.CENTER; - var v = new Gtk.Label (value); - v.xalign = 0; - v.add_css_class ("ml-spec-value"); - var l = new Gtk.Label (label); - l.xalign = 0; - l.add_css_class ("ml-spec-label"); - text.append (v); - text.append (l); - card.append (text); - return card; + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 10); + box.vexpand = true; + box.valign = Gtk.Align.CENTER; + box.halign = Gtk.Align.CENTER; + box.width_request = 380; + var icon = new Gtk.Image.from_icon_name ("computer"); + icon.pixel_size = 128; + icon.add_css_class ("ml-empty-icon"); + box.append (icon); + var t = new Gtk.Label ("No Machines Yet"); + t.add_css_class ("title-1"); + t.margin_top = 12; + box.append (t); + var d = new Gtk.Label ("Create a machine to run another Linux next to elementary OS, with full hardware acceleration."); + d.wrap = true; + d.justify = Gtk.Justification.CENTER; + d.add_css_class ("dim-label"); + box.append (d); + var b = new Gtk.Button.with_label ("New Machine"); + b.add_css_class ("suggested-action"); + b.add_css_class ("pill"); + b.halign = Gtk.Align.CENTER; + b.margin_top = 12; + b.clicked.connect (() => add_machine ()); + box.append (b); + page.append (box); + return page; } - // ---- State ------------------------------------------------------------ + // ---- Machines --------------------------------------------------------- - private bool is_running () { - return vm != null; + private void add_row_and_page (Machine m) { + var page = new MachinePage (m); + page.toast.connect (send_toast); + page.changed.connect (() => { + MachineStore.save (machines); + row_for (m).update (); + }); + page.start_requested.connect (() => start_machine (m)); + page.remove_requested.connect (() => confirm_remove.begin (m)); + pages[m.id] = page; + content.add_named (page, m.id); + list.append (new MachineRow (m)); } - private void update_state () { - bool running = is_running (); - start_button.visible = !running; - stop_button.visible = running; - start_button.sensitive = installer_path != null; - - status_pill.label = running ? "Running" : "Stopped"; - if (running) { - status_pill.add_css_class ("running"); - status_pill.remove_css_class ("stopped"); - } else { - status_pill.remove_css_class ("running"); - status_pill.add_css_class ("stopped"); + private MachineRow row_for (Machine m) { + for (var r = list.get_row_at_index (0); r != null; r = list.get_row_at_index (r.get_index () + 1)) { + if (((MachineRow) r).machine == m) { + return (MachineRow) r; + } } - sidebar_sub.label = running ? "ARM64 · Running" : "ARM64 · Stopped"; + assert_not_reached (); + } - if (installer_path != null) { - drop_zone.add_css_class ("chosen"); - drop_title.label = Path.get_basename (installer_path); - drop_detail.label = installer_path; + private void select_first () { + var first = list.get_row_at_index (0); + if (first != null) { + list.select_row (first); } else { - drop_zone.remove_css_class ("chosen"); - drop_title.label = "Choose an installer"; - drop_detail.label = "Drop an ARM64 .iso here, or browse for one."; + content.visible_child_name = "empty"; } } - private void set_installer (string? path) { - if (path == null) { - return; + private void add_machine () { + var m = new Machine (); + m.id = "machine-%s".printf (Uuid.string_random ().substring (0, 8)); + int n = machines.length + 1; + m.name = "Machine %d".printf (n); + int port = 2222; + bool clash = true; + while (clash) { + clash = false; + foreach (var o in machines.data) { + if (o.ssh_port == port) { port++; clash = true; } + } } - installer_path = path; - update_state (); + m.ssh_port = port; + machines.add (m); + MachineStore.save (machines); + add_row_and_page (m); + list.select_row (row_for (m)); + pages[m.id].show_tab ("settings"); + send_toast ("Created %s".printf (m.name)); } - private void notify_user (string message) { - toast.title = message; - toast.send_notification (); + private void start_machine (Machine m) { + string? launcher = find_launcher (); + if (launcher == null) { + show_error ("The QEMU launcher was not found. Set MACHINE_LAB_LAUNCHER to the full path of run-vm.sh."); + return; + } + if (!m.has_disk () && m.installer == null) { + show_error ("This machine has no disk yet. Choose an ARM64 installer image first."); + return; + } + foreach (var o in machines.data) { + if (o != m && o.running && o.ssh_port == m.ssh_port) { + show_error ("%s is already using SSH port %d. Change the port in Settings.".printf (o.name, o.ssh_port)); + return; + } + } + try { + m.start (launcher); + send_toast ("Starting %s…".printf (m.name)); + } catch (Error e) { + show_error (e.message); + } } - // ---- Actions ---------------------------------------------------------- - - private async void choose_installer () { - var chooser = new Gtk.FileDialog (); - chooser.title = "Choose an ARM64 installer image"; - chooser.accept_label = "Choose"; - var filter = new Gtk.FileFilter (); - filter.name = "Installer images"; - filter.add_mime_type ("application/x-iso9660-image"); - filter.add_pattern ("*.iso"); - filter.add_pattern ("*.img"); - var filters = new GLib.ListStore (typeof (Gtk.FileFilter)); - filters.append (filter); - chooser.filters = filters; + private async void confirm_remove (Machine m) { + var dialog = new Gtk.AlertDialog ("Remove “%s”?".printf (m.name)); + dialog.detail = "The machine is removed from Machine Lab. You can also delete its virtual disk, which cannot be undone."; + dialog.buttons = { "Cancel", "Remove", "Remove and Delete Disk" }; + dialog.cancel_button = 0; + dialog.default_button = 0; try { - var file = yield chooser.open (window, null); - set_installer (file.get_path ()); - } catch (GLib.Error error) { - // GTK reports user cancellation through the same error path. + int choice = yield dialog.choose (window, null); + if (choice == 0) return; + m.stop (); + if (choice == 2) { + m.delete_files (); + } + var row = row_for (m); + int idx = row.get_index (); + list.remove (row); + content.remove (pages[m.id]); + pages.remove (m.id); + machines.remove (m); + MachineStore.save (machines); + var next = list.get_row_at_index (int.min (idx, (int) machines.length - 1)); + if (next != null) { + list.select_row (next); + } else { + content.visible_child_name = "empty"; + } + send_toast ("Removed %s".printf (m.name)); + } catch (Error e) { + // dialog dismissed } } @@ -358,13 +362,10 @@ public class MachineLab : Gtk.Application { if (override_path != null && FileUtils.test (override_path, FileTest.IS_EXECUTABLE)) { return override_path; } - string installed = "/usr/bin/machine-lab-run-vm"; if (FileUtils.test (installed, FileTest.IS_EXECUTABLE)) { return installed; } - - // Source tree: build/machine-lab -> ../launcher/run-vm.sh string exe_dir = Path.get_dirname (Environment.get_prgname () ?? ""); string[] candidates = { Path.build_filename (exe_dir, "run-vm.sh"), @@ -378,46 +379,9 @@ public class MachineLab : Gtk.Application { return null; } - private void start_vm () { - if (installer_path == null || is_running ()) { - return; - } - string? launcher = find_launcher (); - if (launcher == null) { - show_error ("The QEMU launcher was not found. Set MACHINE_LAB_LAUNCHER to the full path of run-vm.sh."); - return; - } - - string vm_dir = Path.build_filename (Environment.get_user_data_dir (), "machine-lab", "omarchy-arm64"); - try { - string[] argv = { launcher, installer_path, vm_dir }; - vm = new Subprocess.newv (argv, SubprocessFlags.NONE); - vm.wait_async.begin (null, (obj, res) => { - try { - vm.wait_async.end (res); - int code = vm.get_if_exited () ? vm.get_exit_status () : -1; - if (code > 0) { - notify_user ("The machine stopped with an error (exit %d)".printf (code)); - } else { - notify_user ("Machine stopped"); - } - } catch (Error e) { - notify_user (e.message); - } - vm = null; - update_state (); - }); - update_state (); - notify_user ("Starting Omarchy…"); - } catch (Error error) { - show_error (error.message); - } - } - - private void stop_vm () { - if (vm != null) { - vm.force_exit (); - } + private void send_toast (string message) { + toast.title = message; + toast.send_notification (); } private void show_error (string message) { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala new file mode 100644 index 000000000..fbd7ec6b7 --- /dev/null +++ b/mseries/machine-lab/src/page.vala @@ -0,0 +1,529 @@ +/* One machine's content pane: header with tabs, Overview / Console / Settings. */ +using Gtk; + +public class MachinePage : Gtk.Box { + public Machine machine { get; construct; } + + public signal void toast (string message); + public signal void changed (); // a persisted setting changed + public signal void remove_requested (); + public signal void start_requested (); + + private Gtk.Label title; + private Gtk.Label status_pill; + private Gtk.Label uptime; + private Gtk.Label cpu_v; + private Gtk.Label mem_v; + private Gtk.Label disk_v; + private Gtk.Label net_v; + private Gtk.Label ssh_cmd; + private Gtk.Box drop_zone; + private Gtk.Label drop_title; + private Gtk.Label drop_detail; + private Gtk.Button start_button; + private Gtk.Button stop_button; + private Gtk.Stack stack; + private Gtk.Box settings_form; + private Gtk.Label settings_note; + private Gtk.Entry name_entry; + private Gtk.Scale cpu_scale; + private Gtk.Scale mem_scale; + private Gtk.Scale disk_scale; + private Gtk.SpinButton port_spin; + private Gtk.Label cpu_scale_v; + private Gtk.Label mem_scale_v; + private Gtk.Label disk_scale_v; + private bool loading = false; + private uint tick = 0; + + public MachinePage (Machine machine) { + Object (machine: machine, orientation: Gtk.Orientation.VERTICAL, spacing: 0); + } + + construct { + hexpand = true; + stack = new Gtk.Stack (); + stack.transition_type = Gtk.StackTransitionType.CROSSFADE; + stack.vexpand = true; + stack.add_titled (build_overview (), "overview", "Overview"); + stack.add_titled (build_console (), "console", "Console"); + stack.add_titled (build_settings (), "settings", "Settings"); + + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = ":maximize"; + var switcher = new Gtk.StackSwitcher (); + switcher.stack = stack; + header.title_widget = switcher; + var more = new Gtk.MenuButton (); + more.icon_name = "view-more-symbolic"; + more.tooltip_text = "More actions"; + var pop = new Gtk.Popover (); + var remove = new Gtk.Button.with_label ("Remove Machine…"); + remove.add_css_class ("flat"); + remove.clicked.connect (() => { pop.popdown (); remove_requested (); }); + pop.child = remove; + more.popover = pop; + header.pack_end (more); + var handle = new Gtk.WindowHandle (); + handle.child = header; + append (handle); + append (stack); + + machine.state_changed.connect (refresh); + machine.finished.connect ((code) => { + toast (code > 0 ? "%s stopped with an error (exit %d)".printf (machine.name, code) + : "%s stopped".printf (machine.name)); + }); + tick = Timeout.add_seconds (1, () => { update_uptime (); return Source.CONTINUE; }); + refresh (); + } + + ~MachinePage () { + if (tick != 0) { + Source.remove (tick); + } + } + + public void show_tab (string name) { + stack.visible_child_name = name; + } + + // ---- Overview --------------------------------------------------------- + + private Gtk.Widget build_overview () { + var scroller = new Gtk.ScrolledWindow (); + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 20); + clamp.margin_start = 32; + clamp.margin_end = 32; + clamp.margin_top = 8; + clamp.margin_bottom = 28; + clamp.width_request = 460; + scroller.child = clamp; + + var hero = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18); + var icon = new Gtk.Image.from_icon_name ("computer"); + icon.pixel_size = 64; + icon.add_css_class ("ml-hero-icon"); + icon.valign = Gtk.Align.START; + hero.append (icon); + + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); + text.hexpand = true; + text.valign = Gtk.Align.CENTER; + title = new Gtk.Label (""); + title.add_css_class ("title-1"); + title.xalign = 0; + title.ellipsize = Pango.EllipsizeMode.END; + text.append (title); + var sub = new Gtk.Label ("Linux ARM64 · hardware accelerated"); + sub.xalign = 0; + sub.add_css_class ("dim-label"); + text.append (sub); + + var pills = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + pills.margin_top = 6; + status_pill = new Gtk.Label (""); + status_pill.add_css_class ("ml-pill"); + pills.append (status_pill); + uptime = new Gtk.Label (""); + uptime.add_css_class ("ml-fineprint"); + pills.append (uptime); + text.append (pills); + hero.append (text); + clamp.append (hero); + + var specs = new Gtk.Grid (); + specs.column_homogeneous = true; + specs.row_homogeneous = true; + specs.column_spacing = 12; + specs.row_spacing = 12; + specs.attach (spec_card ("computer-symbolic", out cpu_v, "Processor"), 0, 0); + specs.attach (spec_card ("media-optical-symbolic", out mem_v, "Memory"), 1, 0); + specs.attach (spec_card ("drive-harddisk-symbolic", out disk_v, "Disk, grows as used"), 0, 1); + specs.attach (spec_card ("network-wired-symbolic", out net_v, "Network"), 1, 1); + clamp.append (specs); + + // Connect row + var connect = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + connect.add_css_class ("ml-card"); + connect.add_css_class ("ml-spec"); + var term = new Gtk.Image.from_icon_name ("utilities-terminal-symbolic"); + term.pixel_size = 24; + connect.append (term); + var ct = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + ct.hexpand = true; + ct.valign = Gtk.Align.CENTER; + ssh_cmd = new Gtk.Label (""); + ssh_cmd.xalign = 0; + ssh_cmd.selectable = true; + ssh_cmd.add_css_class ("ml-mono"); + var cl = new Gtk.Label ("Connect once the guest is running"); + cl.xalign = 0; + cl.add_css_class ("ml-spec-label"); + ct.append (ssh_cmd); + ct.append (cl); + connect.append (ct); + var copy = new Gtk.Button.from_icon_name ("edit-copy-symbolic"); + copy.valign = Gtk.Align.CENTER; + copy.add_css_class ("flat"); + copy.tooltip_text = "Copy command"; + copy.clicked.connect (() => { + Gdk.Display.get_default ().get_clipboard ().set_text (ssh_cmd.label); + toast ("Copied SSH command"); + }); + connect.append (copy); + clamp.append (connect); + + // Installer + drop_zone = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + drop_zone.add_css_class ("ml-drop"); + var di = new Gtk.Image.from_icon_name ("media-optical"); + di.pixel_size = 48; + drop_zone.append (di); + var dt = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + dt.hexpand = true; + dt.valign = Gtk.Align.CENTER; + drop_title = new Gtk.Label (""); + drop_title.xalign = 0; + drop_title.add_css_class ("title-4"); + drop_detail = new Gtk.Label (""); + drop_detail.xalign = 0; + drop_detail.ellipsize = Pango.EllipsizeMode.MIDDLE; + drop_detail.add_css_class ("dim-label"); + dt.append (drop_title); + dt.append (drop_detail); + drop_zone.append (dt); + var browse = new Gtk.Button.with_label ("Browse…"); + browse.valign = Gtk.Align.CENTER; + browse.clicked.connect (() => choose_installer.begin ()); + drop_zone.append (browse); + var eject = new Gtk.Button.from_icon_name ("media-eject-symbolic"); + eject.valign = Gtk.Align.CENTER; + eject.add_css_class ("flat"); + eject.tooltip_text = "Boot from disk only"; + eject.clicked.connect (() => { machine.installer = null; changed (); refresh (); }); + drop_zone.append (eject); + clamp.append (drop_zone); + + var target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); + target.enter.connect (() => { drop_zone.add_css_class ("hover"); return Gdk.DragAction.COPY; }); + target.leave.connect (() => drop_zone.remove_css_class ("hover")); + target.drop.connect ((value) => { + drop_zone.remove_css_class ("hover"); + foreach (var f in ((Gdk.FileList) value.get_boxed ()).get_files ()) { + set_installer (f.get_path ()); + return true; + } + return false; + }); + drop_zone.add_controller (target); + + var actions = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + actions.halign = Gtk.Align.END; + stop_button = new Gtk.Button.with_label ("Force Stop"); + stop_button.add_css_class ("destructive-action"); + stop_button.clicked.connect (() => machine.stop ()); + actions.append (stop_button); + start_button = new Gtk.Button.with_label ("Start Machine"); + start_button.add_css_class ("suggested-action"); + start_button.clicked.connect (() => start_requested ()); + actions.append (start_button); + clamp.append (actions); + + var fine = new Gtk.Label ("Needs QEMU, UEFI firmware and access to /dev/kvm. With an installer attached the machine boots it; remove it with the eject button once the system is installed."); + fine.wrap = true; + fine.xalign = 0; + fine.add_css_class ("ml-fineprint"); + clamp.append (fine); + return scroller; + } + + private Gtk.Widget spec_card (string icon_name, out Gtk.Label value, string label) { + var card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + card.add_css_class ("ml-card"); + card.add_css_class ("ml-spec"); + var icon = new Gtk.Image.from_icon_name (icon_name); + icon.pixel_size = 24; + card.append (icon); + var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + t.valign = Gtk.Align.CENTER; + value = new Gtk.Label (""); + value.xalign = 0; + value.add_css_class ("ml-spec-value"); + var l = new Gtk.Label (label); + l.xalign = 0; + l.add_css_class ("ml-spec-label"); + t.append (value); + t.append (l); + card.append (t); + return card; + } + + // ---- Console ---------------------------------------------------------- + + private Gtk.Widget build_console () { + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 10); + box.margin_start = 24; + box.margin_end = 24; + box.margin_bottom = 20; + box.margin_top = 4; + + var view = new Gtk.TextView.with_buffer (machine.log); + view.editable = false; + view.cursor_visible = false; + view.monospace = true; + view.wrap_mode = Gtk.WrapMode.WORD_CHAR; + view.top_margin = 12; + view.bottom_margin = 12; + view.left_margin = 14; + view.right_margin = 14; + view.add_css_class ("ml-console"); + + var scroller = new Gtk.ScrolledWindow (); + scroller.vexpand = true; + scroller.child = view; + scroller.add_css_class ("ml-console-frame"); + machine.log.changed.connect (() => { + Gtk.TextIter end; + machine.log.get_end_iter (out end); + var mark = machine.log.create_mark (null, end, false); + view.scroll_to_mark (mark, 0.0, false, 0, 0); + machine.log.delete_mark (mark); + }); + box.append (scroller); + + var bar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + var hint = new Gtk.Label ("Launcher and QEMU output"); + hint.hexpand = true; + hint.xalign = 0; + hint.add_css_class ("ml-fineprint"); + bar.append (hint); + var copy = new Gtk.Button.with_label ("Copy"); + copy.clicked.connect (() => { + Gtk.TextIter s, e; + machine.log.get_bounds (out s, out e); + Gdk.Display.get_default ().get_clipboard ().set_text (machine.log.get_text (s, e, false)); + toast ("Console copied"); + }); + bar.append (copy); + var clear = new Gtk.Button.with_label ("Clear"); + clear.clicked.connect (() => machine.log.text = ""); + bar.append (clear); + box.append (bar); + return box; + } + + // ---- Settings --------------------------------------------------------- + + private Gtk.Widget build_settings () { + var scroller = new Gtk.ScrolledWindow (); + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 16); + clamp.margin_start = 32; + clamp.margin_end = 32; + clamp.margin_top = 8; + clamp.margin_bottom = 28; + clamp.width_request = 460; + scroller.child = clamp; + + settings_note = new Gtk.Label (""); + settings_note.wrap = true; + settings_note.xalign = 0; + settings_note.add_css_class ("ml-note"); + clamp.append (settings_note); + + settings_form = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + settings_form.add_css_class ("ml-card"); + clamp.append (settings_form); + + int cores = (int) get_num_processors (); + int host_gib = host_memory_gib (); + + name_entry = new Gtk.Entry (); + name_entry.hexpand = true; + name_entry.max_length = 40; + name_entry.changed.connect (() => { + if (loading) return; + var n = name_entry.text.strip (); + machine.name = n == "" ? "Untitled" : n; + title.label = machine.name; + changed (); + }); + settings_form.append (form_row ("Name", null, name_entry)); + + cpu_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 1, cores, 1); + cpu_scale_v = new Gtk.Label (""); + settings_form.append (form_row ("Processors", cpu_scale_v, cpu_scale)); + cpu_scale.value_changed.connect (() => { + if (loading) return; + machine.cpus = (int) cpu_scale.get_value (); + changed (); refresh (); + }); + + mem_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 1, int.max (2, host_gib - 2), 1); + mem_scale_v = new Gtk.Label (""); + settings_form.append (form_row ("Memory", mem_scale_v, mem_scale)); + mem_scale.value_changed.connect (() => { + if (loading) return; + machine.mem_gib = (int) mem_scale.get_value (); + changed (); refresh (); + }); + + disk_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 8, 512, 8); + disk_scale_v = new Gtk.Label (""); + settings_form.append (form_row ("Disk size", disk_scale_v, disk_scale)); + disk_scale.value_changed.connect (() => { + if (loading) return; + machine.disk_gib = (int) disk_scale.get_value (); + changed (); refresh (); + }); + + port_spin = new Gtk.SpinButton.with_range (1024, 65535, 1); + port_spin.value_changed.connect (() => { + if (loading) return; + machine.ssh_port = (int) port_spin.get_value (); + changed (); refresh (); + }); + settings_form.append (form_row ("SSH port on this Mac", null, port_spin, true)); + + foreach (var s in new Gtk.Scale[] { cpu_scale, mem_scale, disk_scale }) { + s.draw_value = false; + } + return scroller; + } + + private Gtk.Widget form_row (string label, Gtk.Label? value, Gtk.Widget control, bool last = false) { + var row = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + row.add_css_class ("ml-form-row"); + if (!last) { + row.add_css_class ("ml-form-sep"); + } + var top = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + var l = new Gtk.Label (label); + l.xalign = 0; + l.hexpand = true; + top.append (l); + if (value != null) { + value.add_css_class ("ml-spec-value"); + top.append (value); + row.append (top); + row.append (control); + } else { + control.halign = Gtk.Align.END; + control.hexpand = false; + if (control is Gtk.Entry) { + control.hexpand = true; + control.halign = Gtk.Align.FILL; + ((Gtk.Entry) control).width_chars = 24; + } + top.append (control); + row.append (top); + } + return row; + } + + private static int host_memory_gib () { + try { + string contents; + FileUtils.get_contents ("/proc/meminfo", out contents); + foreach (var line in contents.split ("\n")) { + if (line.has_prefix ("MemTotal:")) { + var kb = int64.parse (line.substring (9).strip ().split (" ")[0]); + return (int) (kb / 1024 / 1024); + } + } + } catch (Error e) {} + return 16; + } + + // ---- State ------------------------------------------------------------ + + public void refresh () { + bool running = machine.running; + title.label = machine.name; + cpu_v.label = machine.cpus == 1 ? "1 virtual CPU" : "%d virtual CPUs".printf (machine.cpus); + mem_v.label = "%d GiB".printf (machine.mem_gib); + disk_v.label = "%d GiB".printf (machine.disk_gib); + net_v.label = "Shared (NAT)"; + ssh_cmd.label = "ssh -p %d localhost".printf (machine.ssh_port); + + start_button.visible = !running; + stop_button.visible = running; + start_button.sensitive = true; + start_button.label = machine.installer != null ? "Start Installer" : "Start Machine"; + + status_pill.label = running ? "Running" : "Stopped"; + if (running) { + status_pill.add_css_class ("running"); + } else { + status_pill.remove_css_class ("running"); + } + + if (machine.installer != null) { + drop_zone.add_css_class ("chosen"); + drop_title.label = Path.get_basename (machine.installer); + drop_detail.label = machine.installer; + } else { + drop_zone.remove_css_class ("chosen"); + drop_title.label = machine.has_disk () ? "Boots from its disk" : "Choose an installer"; + drop_detail.label = "Drop an ARM64 .iso here, or browse for one."; + } + + loading = true; + if (name_entry.text != machine.name) name_entry.text = machine.name; + cpu_scale.set_value (machine.cpus); + mem_scale.set_value (machine.mem_gib); + disk_scale.set_value (machine.disk_gib); + port_spin.set_value (machine.ssh_port); + cpu_scale_v.label = "%d".printf (machine.cpus); + mem_scale_v.label = "%d GiB".printf (machine.mem_gib); + disk_scale_v.label = "%d GiB".printf (machine.disk_gib); + loading = false; + + settings_form.sensitive = !running; + disk_scale.sensitive = !running && !machine.has_disk (); + settings_note.visible = running || machine.has_disk (); + settings_note.label = running + ? "Stop the machine to change its settings." + : "The disk already exists, so its size is fixed. Other settings apply the next time you start."; + update_uptime (); + } + + private void update_uptime () { + if (machine.started == null) { + uptime.label = ""; + return; + } + int64 secs = new DateTime.now_local ().difference (machine.started) / 1000000; + uptime.label = "Up %02d:%02d:%02d".printf ((int) (secs / 3600), (int) (secs / 60 % 60), (int) (secs % 60)); + } + + private void set_installer (string? path) { + if (path == null) return; + machine.installer = path; + changed (); + refresh (); + } + + private async void choose_installer () { + var chooser = new Gtk.FileDialog (); + chooser.title = "Choose an ARM64 installer image"; + chooser.accept_label = "Choose"; + var filter = new Gtk.FileFilter (); + filter.name = "Installer images"; + filter.add_mime_type ("application/x-iso9660-image"); + filter.add_pattern ("*.iso"); + filter.add_pattern ("*.img"); + var filters = new GLib.ListStore (typeof (Gtk.FileFilter)); + filters.append (filter); + chooser.filters = filters; + try { + var file = yield chooser.open ((Gtk.Window) get_root (), null); + set_installer (file.get_path ()); + } catch (GLib.Error error) { + // Cancelling the dialog lands here too. + } + } +} From c07d142cc7075a62f0d99eddf0721794cc577e48 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:21:15 -0400 Subject: [PATCH 065/798] Applications menu: 7x4 grid page to fit more apps with half-size icons Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 4 ++ .../install-applications-menu-half-icons.sh | 3 +- .../applications-menu-half-icons.patch | 37 ++++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 33fa6db1e..9dd5518a2 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -17,6 +17,10 @@ Owner then clarified they meant the Applications menu. Upstream has no icon-size loaded**: Wingpanel was not restarted, so it shows after the next reload/login, and the result is not visually checked. The dock stays at 24; revert it if only the menu was wanted. +Follow-up: the same patch now also makes the grid page 7x4 (28 apps, was 5x3 = 15), label width +12 chars, row spacing 12, to roughly keep the popover footprint. Rebuilt, installed and Wingpanel +reloaded; layout not visually checked. + ## Official Discord via FEX in a 4 KB-page microVM — 2026-10-18 (Claude Code) Owner required official Discord (amd64 only). FEX alone aborts here (16 KB pages, `: Unsupported system diff --git a/mseries/install-applications-menu-half-icons.sh b/mseries/install-applications-menu-half-icons.sh index 2a793b40e..14cbcdeef 100755 --- a/mseries/install-applications-menu-half-icons.sh +++ b/mseries/install-applications-menu-half-icons.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Builds the elementary Applications menu (Slingshot) with icons at half size: -# grid 64->32, list rows 32->16, search results 32->16 (patches/applications-menu-half-icons.patch). +# grid 64->32 and a denser 7x4 page (was 5x3), list rows 32->16, search results 32->16 +# (patches/applications-menu-half-icons.patch). # Upstream has no setting for this. Reload Wingpanel to activate; --uninstall restores the package build. set -euo pipefail repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) diff --git a/mseries/patches/applications-menu-half-icons.patch b/mseries/patches/applications-menu-half-icons.patch index 01de3c266..9c3b0e448 100644 --- a/mseries/patches/applications-menu-half-icons.patch +++ b/mseries/patches/applications-menu-half-icons.patch @@ -1,5 +1,29 @@ +diff --git a/src/Views/GridView.vala b/src/Views/GridView.vala +index 3c43a57..52d299f 100644 +--- a/src/Views/GridView.vala ++++ b/src/Views/GridView.vala +@@ -5,8 +5,8 @@ + */ + + public class Slingshot.Widgets.Grid : Gtk.Box { +- private const int PAGE_ROWS = 3; +- private const int PAGE_COLUMNS = 5; ++ private const int PAGE_ROWS = 4; ++ private const int PAGE_COLUMNS = 7; + private const int MAX_APPS_IN_PAGE = (PAGE_ROWS * PAGE_COLUMNS); + + private Adw.Carousel paginator; +@@ -132,7 +132,7 @@ public class Slingshot.Widgets.Grid : Gtk.Box { + column_homogeneous = true, + margin_start = 12, + margin_end = 12, +- row_spacing = 24, ++ row_spacing = 12, + column_spacing = 0 + }; + diff --git a/src/Widgets/AppButton.vala b/src/Widgets/AppButton.vala -index 154d9b2..74af87e 100644 +index 154d9b2..af77640 100644 --- a/src/Widgets/AppButton.vala +++ b/src/Widgets/AppButton.vala @@ -9,7 +9,7 @@ public class Slingshot.Widgets.AppButton : Gtk.Button { @@ -11,6 +35,17 @@ index 154d9b2..74af87e 100644 private Gtk.Label badge; public AppButton (Backend.App app) { +@@ -25,8 +25,8 @@ public class Slingshot.Widgets.AppButton : Gtk.Button { + ellipsize = END, + justify = CENTER, + lines = 2, +- max_width_chars = 16, +- width_chars = 16, ++ max_width_chars = 12, ++ width_chars = 12, + wrap_mode = WORD_CHAR + }; + diff --git a/src/Widgets/AppListRow.vala b/src/Widgets/AppListRow.vala index f857c21..7c37da5 100644 --- a/src/Widgets/AppListRow.vala From 90999433f74f001c4336e42f17efd36f81cf7379 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:22:04 -0400 Subject: [PATCH 066/798] Machine Lab: colors, distro presets, readiness check, disk usage, pulsing status Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/README.md | 2 + mseries/machine-lab/data/style.css | 42 +++++++++++++ mseries/machine-lab/meson.build | 2 + mseries/machine-lab/src/machine.vala | 35 +++++++++++ mseries/machine-lab/src/main.vala | 73 +++++++++++++++++++--- mseries/machine-lab/src/page.vala | 91 ++++++++++++++++++++++++++-- 6 files changed, 233 insertions(+), 12 deletions(-) diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md index 7dfbc392b..244a003fa 100644 --- a/mseries/machine-lab/README.md +++ b/mseries/machine-lab/README.md @@ -14,3 +14,5 @@ meson setup build && meson compile -C build Launcher and known gaps: `launcher/README.md`. Moved here from the Codex native-vm-poc spike. Visual check was done headless via GTK Broadway; not yet launched on the desktop or booted with a guest ISO. + +Also: per-machine color (elementary palette), distro presets from New Machine, host readiness check (QEMU, UEFI, /dev/kvm), real disk usage, pulsing running indicator. diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css index 7c5027643..a06bd0021 100644 --- a/mseries/machine-lab/data/style.css +++ b/mseries/machine-lab/data/style.css @@ -94,3 +94,45 @@ textview.ml-console, textview.ml-console text { background: #1d1f23; color: #d8dde6; } + +/* Per-machine colors (elementary palette) */ +.ml-c-0.ml-hero-icon { background: linear-gradient(to bottom, alpha(#3689e6, 0.38), alpha(#3689e6, 0.12)); box-shadow: inset 0 0 0 1px alpha(#3689e6, 0.5); } +.ml-c-0.ml-row-icon { background: linear-gradient(to bottom, alpha(#3689e6, 0.35), alpha(#3689e6, 0.1)); box-shadow: inset 0 0 0 1px alpha(#3689e6, 0.45); } +.ml-s-0 { background: #3689e6; } +.ml-c-1.ml-hero-icon { background: linear-gradient(to bottom, alpha(#28bca3, 0.38), alpha(#28bca3, 0.12)); box-shadow: inset 0 0 0 1px alpha(#28bca3, 0.5); } +.ml-c-1.ml-row-icon { background: linear-gradient(to bottom, alpha(#28bca3, 0.35), alpha(#28bca3, 0.1)); box-shadow: inset 0 0 0 1px alpha(#28bca3, 0.45); } +.ml-s-1 { background: #28bca3; } +.ml-c-2.ml-hero-icon { background: linear-gradient(to bottom, alpha(#68b723, 0.38), alpha(#68b723, 0.12)); box-shadow: inset 0 0 0 1px alpha(#68b723, 0.5); } +.ml-c-2.ml-row-icon { background: linear-gradient(to bottom, alpha(#68b723, 0.35), alpha(#68b723, 0.1)); box-shadow: inset 0 0 0 1px alpha(#68b723, 0.45); } +.ml-s-2 { background: #68b723; } +.ml-c-3.ml-hero-icon { background: linear-gradient(to bottom, alpha(#f9c440, 0.38), alpha(#f9c440, 0.12)); box-shadow: inset 0 0 0 1px alpha(#f9c440, 0.5); } +.ml-c-3.ml-row-icon { background: linear-gradient(to bottom, alpha(#f9c440, 0.35), alpha(#f9c440, 0.1)); box-shadow: inset 0 0 0 1px alpha(#f9c440, 0.45); } +.ml-s-3 { background: #f9c440; } +.ml-c-4.ml-hero-icon { background: linear-gradient(to bottom, alpha(#ffa154, 0.38), alpha(#ffa154, 0.12)); box-shadow: inset 0 0 0 1px alpha(#ffa154, 0.5); } +.ml-c-4.ml-row-icon { background: linear-gradient(to bottom, alpha(#ffa154, 0.35), alpha(#ffa154, 0.1)); box-shadow: inset 0 0 0 1px alpha(#ffa154, 0.45); } +.ml-s-4 { background: #ffa154; } +.ml-c-5.ml-hero-icon { background: linear-gradient(to bottom, alpha(#ed5353, 0.38), alpha(#ed5353, 0.12)); box-shadow: inset 0 0 0 1px alpha(#ed5353, 0.5); } +.ml-c-5.ml-row-icon { background: linear-gradient(to bottom, alpha(#ed5353, 0.35), alpha(#ed5353, 0.1)); box-shadow: inset 0 0 0 1px alpha(#ed5353, 0.45); } +.ml-s-5 { background: #ed5353; } +.ml-c-6.ml-hero-icon { background: linear-gradient(to bottom, alpha(#de3e80, 0.38), alpha(#de3e80, 0.12)); box-shadow: inset 0 0 0 1px alpha(#de3e80, 0.5); } +.ml-c-6.ml-row-icon { background: linear-gradient(to bottom, alpha(#de3e80, 0.35), alpha(#de3e80, 0.1)); box-shadow: inset 0 0 0 1px alpha(#de3e80, 0.45); } +.ml-s-6 { background: #de3e80; } +.ml-c-7.ml-hero-icon { background: linear-gradient(to bottom, alpha(#a56de2, 0.38), alpha(#a56de2, 0.12)); box-shadow: inset 0 0 0 1px alpha(#a56de2, 0.5); } +.ml-c-7.ml-row-icon { background: linear-gradient(to bottom, alpha(#a56de2, 0.35), alpha(#a56de2, 0.1)); box-shadow: inset 0 0 0 1px alpha(#a56de2, 0.45); } +.ml-s-7 { background: #a56de2; } + +.ml-row-icon { padding: 3px; border-radius: 8px; } +.ml-swatch { + min-width: 22px; min-height: 22px; padding: 0; + border-radius: 99px; border: none; + box-shadow: inset 0 0 0 1px alpha(black, 0.2); +} +.ml-swatch:checked { box-shadow: 0 0 0 2px @bg_color, 0 0 0 4px alpha(@fg_color, 0.55); } +.ml-ok { color: #3a9104; } +.ml-card.bad { box-shadow: 0 0 0 1px alpha(#ed5353, 0.6), 0 1px 3px alpha(black, 0.12); } + +@keyframes ml-pulse { + 0% { box-shadow: 0 0 0 2px @bg_color, 0 0 0 2px alpha(#68b723, 0.7); } + 100% { box-shadow: 0 0 0 2px @bg_color, 0 0 0 7px alpha(#68b723, 0); } +} +.ml-dot.running { animation: ml-pulse 1.6s ease-out infinite; } diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index a491db1d4..9c86e5bfa 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -13,6 +13,8 @@ resources = gnome.compile_resources('machine-lab-resources', source_dir: 'data' ) +add_project_arguments(['--pkg', 'posix'], language: 'vala') + executable('machine-lab', 'src/main.vala', 'src/machine.vala', 'src/page.vala', resources, diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index daa9ed1ad..c60dd66ff 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -7,6 +7,7 @@ public class Machine : Object { public int mem_gib { get; set; default = 6; } public int disk_gib { get; set; default = 64; } public int ssh_port { get; set; default = 2222; } + public int color { get; set; default = 0; } public string? installer { get; set; default = null; } public Subprocess? proc { get; private set; default = null; } @@ -28,6 +29,16 @@ public class Machine : Object { return FileUtils.test (Path.build_filename (dir, "disk.qcow2"), FileTest.EXISTS); } + public int64 disk_used_bytes () { + try { + var info = File.new_for_path (Path.build_filename (dir, "disk.qcow2")) + .query_info ("standard::allocated-size", FileQueryInfoFlags.NONE); + return (int64) info.get_attribute_uint64 ("standard::allocated-size"); + } catch (Error e) { + return 0; + } + } + public void start (string launcher) throws Error { if (running) { return; @@ -112,6 +123,9 @@ public class MachineStore : Object { m.mem_gib = kf.get_integer (id, "mem_gib"); m.disk_gib = kf.get_integer (id, "disk_gib"); m.ssh_port = kf.get_integer (id, "ssh_port"); + if (kf.has_key (id, "color")) { + m.color = kf.get_integer (id, "color"); + } if (kf.has_key (id, "installer")) { m.installer = kf.get_string (id, "installer"); } @@ -137,6 +151,7 @@ public class MachineStore : Object { kf.set_integer (m.id, "mem_gib", m.mem_gib); kf.set_integer (m.id, "disk_gib", m.disk_gib); kf.set_integer (m.id, "ssh_port", m.ssh_port); + kf.set_integer (m.id, "color", m.color); if (m.installer != null) { kf.set_string (m.id, "installer", m.installer); } @@ -149,3 +164,23 @@ public class MachineStore : Object { } } } + +/* Checks the host has what the launcher needs. */ +public class Readiness : Object { + public static string[] problems () { + string[] out = {}; + if (Environment.find_program_in_path ("qemu-system-aarch64") == null) { + out += "QEMU is missing. Install qemu-system-arm."; + } + if (Environment.find_program_in_path ("qemu-img") == null) { + out += "qemu-img is missing. Install qemu-utils."; + } + if (!FileUtils.test ("/usr/share/AAVMF/AAVMF_CODE.fd", FileTest.EXISTS)) { + out += "UEFI firmware is missing. Install qemu-efi-aarch64."; + } + if (Posix.access ("/dev/kvm", Posix.R_OK | Posix.W_OK) != 0) { + out += "No access to /dev/kvm. Add your user to the kvm group."; + } + return out; + } +} diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index 3448e8db1..c26b7829a 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -6,11 +6,29 @@ */ using Gtk; +struct Preset { + public string name; + public string blurb; + public int cpus; + public int mem; + public int disk; + public int color; +} + +const Preset[] presets = { + { "Omarchy", "Arch-based tiling desktop", 4, 6, 64, 0 }, + { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 48, 4 }, + { "Debian", "Rock-solid and lightweight", 2, 2, 32, 5 }, + { "Fedora", "Fresh packages, GNOME", 4, 4, 48, 0 }, + { "Blank Machine", "Start from scratch", 2, 4, 32, 7 } +}; + public class MachineRow : Gtk.ListBoxRow { public Machine machine { get; construct; } private Gtk.Label name_label; private Gtk.Label sub_label; private Gtk.Box dot; + private Gtk.Image icon; public MachineRow (Machine machine) { Object (machine: machine); @@ -24,8 +42,9 @@ public class MachineRow : Gtk.ListBoxRow { box.margin_end = 6; var overlay = new Gtk.Overlay (); - var icon = new Gtk.Image.from_icon_name ("computer"); + icon = new Gtk.Image.from_icon_name ("computer"); icon.pixel_size = 32; + icon.add_css_class ("ml-row-icon"); overlay.child = icon; dot = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); dot.add_css_class ("ml-dot"); @@ -58,6 +77,10 @@ public class MachineRow : Gtk.ListBoxRow { name_label.label = machine.name; sub_label.label = "%s · %d CPU · %d GiB".printf ( machine.running ? "Running" : "Stopped", machine.cpus, machine.mem_gib); + for (int i = 0; i < 8; i++) { + icon.remove_css_class ("ml-c-%d".printf (i)); + } + icon.add_css_class ("ml-c-%d".printf (machine.color)); if (machine.running) { dot.add_css_class ("running"); } else { @@ -101,7 +124,7 @@ public class MachineLab : Gtk.Application { ); var new_action = new SimpleAction ("new-machine", null); - new_action.activate.connect (() => add_machine ()); + new_action.activate.connect (() => add_machine (presets[0])); add_action (new_action); set_accels_for_action ("app.new-machine", { "n" }); set_accels_for_action ("app.quit", { "q" }); @@ -189,7 +212,7 @@ public class MachineLab : Gtk.Application { side.append (scroll); side.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); - var add = new Gtk.Button (); + var add = new Gtk.MenuButton (); var add_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); add_box.append (new Gtk.Image.from_icon_name ("list-add-symbolic")); add_box.append (new Gtk.Label ("New Machine")); @@ -200,8 +223,40 @@ public class MachineLab : Gtk.Application { add.margin_start = 6; add.margin_end = 6; add.halign = Gtk.Align.START; + add.direction = Gtk.ArrowType.UP; add.tooltip_markup = Granite.markup_accel_tooltip ({ "n" }, "Create a new machine"); - add.clicked.connect (() => add_machine ()); + var pop = new Gtk.Popover (); + var pbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + pbox.margin_top = 6; + pbox.margin_bottom = 6; + pbox.margin_start = 6; + pbox.margin_end = 6; + foreach (var p in presets) { + var item = new Gtk.Button (); + item.add_css_class ("flat"); + var ib = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + var ic = new Gtk.Image.from_icon_name ("computer"); + ic.pixel_size = 28; + ic.add_css_class ("ml-row-icon"); + ic.add_css_class ("ml-c-%d".printf (p.color)); + ib.append (ic); + var tb = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + var n = new Gtk.Label (p.name); + n.xalign = 0; + n.add_css_class ("ml-machine-name"); + var d = new Gtk.Label ("%s · %d CPU · %d GiB".printf (p.blurb, p.cpus, p.mem)); + d.xalign = 0; + d.add_css_class ("ml-machine-sub"); + tb.append (n); + tb.append (d); + ib.append (tb); + item.child = ib; + var preset = p; + item.clicked.connect (() => { pop.popdown (); add_machine (preset); }); + pbox.append (item); + } + pop.child = pbox; + add.popover = pop; side.append (add); return side; } @@ -239,7 +294,7 @@ public class MachineLab : Gtk.Application { b.add_css_class ("pill"); b.halign = Gtk.Align.CENTER; b.margin_top = 12; - b.clicked.connect (() => add_machine ()); + b.clicked.connect (() => add_machine (presets[0])); box.append (b); page.append (box); return page; @@ -279,11 +334,15 @@ public class MachineLab : Gtk.Application { } } - private void add_machine () { + private void add_machine (Preset preset) { var m = new Machine (); m.id = "machine-%s".printf (Uuid.string_random ().substring (0, 8)); int n = machines.length + 1; - m.name = "Machine %d".printf (n); + m.name = machines.length == 0 ? preset.name : "%s %d".printf (preset.name, n); + m.cpus = preset.cpus; + m.mem_gib = preset.mem; + m.disk_gib = preset.disk; + m.color = preset.color; int port = 2222; bool clash = true; while (clash) { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index fbd7ec6b7..337d5fec4 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -1,6 +1,8 @@ /* One machine's content pane: header with tabs, Overview / Console / Settings. */ using Gtk; +const string[] swatch_names = { "Blueberry", "Mint", "Lime", "Banana", "Orange", "Strawberry", "Bubblegum", "Grape" }; + public class MachinePage : Gtk.Box { public Machine machine { get; construct; } @@ -17,6 +19,12 @@ public class MachinePage : Gtk.Box { private Gtk.Label disk_v; private Gtk.Label net_v; private Gtk.Label ssh_cmd; + private Gtk.Image hero_icon; + private Gtk.Box ready_card; + private Gtk.Image ready_icon; + private Gtk.Label ready_title; + private Gtk.Label ready_detail; + private Gtk.ToggleButton[] swatches = {}; private Gtk.Box drop_zone; private Gtk.Label drop_title; private Gtk.Label drop_detail; @@ -103,11 +111,11 @@ public class MachinePage : Gtk.Box { scroller.child = clamp; var hero = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18); - var icon = new Gtk.Image.from_icon_name ("computer"); - icon.pixel_size = 64; - icon.add_css_class ("ml-hero-icon"); - icon.valign = Gtk.Align.START; - hero.append (icon); + hero_icon = new Gtk.Image.from_icon_name ("computer"); + hero_icon.pixel_size = 64; + hero_icon.add_css_class ("ml-hero-icon"); + hero_icon.valign = Gtk.Align.START; + hero.append (hero_icon); var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); text.hexpand = true; @@ -145,6 +153,28 @@ public class MachinePage : Gtk.Box { specs.attach (spec_card ("network-wired-symbolic", out net_v, "Network"), 1, 1); clamp.append (specs); + // Readiness + ready_card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + ready_card.add_css_class ("ml-card"); + ready_card.add_css_class ("ml-spec"); + ready_icon = new Gtk.Image (); + ready_icon.pixel_size = 24; + ready_icon.valign = Gtk.Align.START; + ready_card.append (ready_icon); + var rt = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + rt.hexpand = true; + ready_title = new Gtk.Label (""); + ready_title.xalign = 0; + ready_title.add_css_class ("ml-spec-value"); + ready_detail = new Gtk.Label (""); + ready_detail.xalign = 0; + ready_detail.wrap = true; + ready_detail.add_css_class ("ml-spec-label"); + rt.append (ready_title); + rt.append (ready_detail); + ready_card.append (rt); + clamp.append (ready_card); + // Connect row var connect = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); connect.add_css_class ("ml-card"); @@ -353,6 +383,26 @@ public class MachinePage : Gtk.Box { }); settings_form.append (form_row ("Name", null, name_entry)); + var sw_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + for (int i = 0; i < 8; i++) { + var b = new Gtk.ToggleButton (); + b.add_css_class ("ml-swatch"); + b.add_css_class ("ml-s-%d".printf (i)); + b.tooltip_text = swatch_names[i]; + int idx = i; + b.toggled.connect (() => { + if (loading || !b.active) return; + machine.color = idx; + changed (); refresh (); + }); + if (swatches.length > 0) { + b.group = swatches[0]; + } + swatches += b; + sw_box.append (b); + } + settings_form.append (form_row ("Color", null, sw_box)); + cpu_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 1, cores, 1); cpu_scale_v = new Gtk.Label (""); settings_form.append (form_row ("Processors", cpu_scale_v, cpu_scale)); @@ -471,7 +521,32 @@ public class MachinePage : Gtk.Box { drop_detail.label = "Drop an ARM64 .iso here, or browse for one."; } + for (int i = 0; i < 8; i++) { + hero_icon.remove_css_class ("ml-c-%d".printf (i)); + } + hero_icon.add_css_class ("ml-c-%d".printf (machine.color)); + int64 used = machine.disk_used_bytes (); + if (used > 0) { + disk_v.label = "%.1f of %d GiB".printf (used / 1073741824.0, machine.disk_gib); + } + + var problems = Readiness.problems (); + ready_card.remove_css_class ("bad"); + if (problems.length == 0) { + ready_icon.icon_name = "emblem-default-symbolic"; + ready_icon.add_css_class ("ml-ok"); + ready_title.label = "Ready to run"; + ready_detail.label = "QEMU, UEFI firmware and hardware acceleration are available."; + } else { + ready_icon.icon_name = "dialog-warning-symbolic"; + ready_icon.remove_css_class ("ml-ok"); + ready_card.add_css_class ("bad"); + ready_title.label = problems.length == 1 ? "One thing to fix" : "%d things to fix".printf (problems.length); + ready_detail.label = string.joinv ("\n", problems); + } + loading = true; + if (swatches.length == 8) swatches[machine.color.clamp (0, 7)].active = true; if (name_entry.text != machine.name) name_entry.text = machine.name; cpu_scale.set_value (machine.cpus); mem_scale.set_value (machine.mem_gib); @@ -492,6 +567,12 @@ public class MachinePage : Gtk.Box { } private void update_uptime () { + if (machine.running) { + int64 used = machine.disk_used_bytes (); + if (used > 0) { + disk_v.label = "%.1f of %d GiB".printf (used / 1073741824.0, machine.disk_gib); + } + } if (machine.started == null) { uptime.label = ""; return; From 5c41dd6495ba19697aa139469da95b938c43bbb7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:23:25 -0400 Subject: [PATCH 067/798] Initialize connected native display outputs --- mseries/OS9-HANDOFF.md | 24 +- mseries/ROADMAP.md | 13 +- rust/pantheon-compositor/README.md | 9 +- .../pantheon-compositor/src/display_config.rs | 22 +- rust/pantheon-compositor/src/native.rs | 248 +++++++++++------- 5 files changed, 182 insertions(+), 134 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e1c10780e..4dfa4a6b7 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5443,22 +5443,30 @@ checks/builds passed locally. The executable now selects `nested` by default or accepts `--backend native` when built with `native-drm`. Native startup creates a libseat session, names -the Wayland seat from that session, initializes the hardware renderer and first -connected KMS output, assigns libinput to the active seat, and registers input, -session pause/resume, DRM vblank and redraw-channel sources with calloop. +the Wayland seat from that session, initializes the hardware renderer and all +connected outputs with compatible CRTCs on the first usable KMS device, assigns +libinput to the active seat, and registers input, session pause/resume, DRM +vblank and redraw-channel sources with calloop. It prioritizes eDP, places +additional outputs to its right, and shares one DRM manager/notifier across +their scanout surfaces. DisplayConfig starts empty and only advertises outputs +published by the selected backend, avoiding a phantom Winit monitor in native +mode. Surface commits and input request redraws; vblank completes the matching queued frame and sends frame callbacks. The nested path remains independent and never creates a libseat session. Local `cargo check --bins`, `cargo check --bins --features native-drm`, and matching `cargo build --bins` -commands pass. Formatting was applied only to the changed Rust sources; +commands pass for the multi-output update, including both local feature-enabled +and default builds. Formatting was applied only to the changed Rust sources; `cargo fmt --check` still reports pre-existing formatting differences elsewhere in the crate. Native mode was not launched: on this host it would take over the active display, so DRM initialization, scanout, physical input, pause/resume and the complete Pantheon session remain -runtime-unverified. This first startup slice handles one output only; hotplug -and multi-output lifecycle are still needed for the scoped M-series experience. -Next: qualify the native path in a separate session or other safe hardware -setup, then implement multi-output and hotplug lifecycle support. +runtime-unverified. Connector enumeration and CRTC/output wiring only have +compile evidence; the two-output M1 Pro topology has not been modeset. Hotplug, +user-configured arrangement, monitor mode application, rollback, and connector +removal lifecycle are still needed for the scoped M-series experience. Next: +qualify native output initialization in a separate session or other safe +hardware setup, then add dynamic connector lifecycle handling. ### Absolute pointer mapping — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 305e1a75f..cd549f004 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -55,10 +55,15 @@ allocation and failure handling are first-session requirements for this M1 Pro, not optional general multi-GPU support. The read-only probe did not change modes. The executable now keeps nested Winit as its default and provides an explicit `--backend native` path behind `native-drm`; this wires libseat, -libinput, the first DRM output, calloop rendering and vblank completion. Both -build configurations pass locally. Native mode has not been launched because -it deliberately takes over the active display; output hotplug, multiple -outputs, session switching and actual M-series scanout remain unqualified. +libinput, connected outputs on the first usable KMS device, calloop rendering +and vblank completion. It prioritizes eDP and places additional outputs to its +right. DisplayConfig no longer advertises a fabricated Winit output in native +mode. Both build configurations pass locally. Native mode has not been +launched because it deliberately takes over the active display; output hotplug, +user-configured layouts, session switching and actual M-series scanout remain +unqualified. Local `cargo check --bins` and `cargo build --bins` pass both with +and without `native-drm`; they establish compilation only, not successful +connector modesetting. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 0f4b19822..952be5998 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -80,10 +80,11 @@ multi-GPU dependencies. The executable defaults to nested Winit; only `--backend native` starts the native session. That path initializes a hardware EGL/GBM renderer and a separate connected KMS output, publishes it as a Wayland output, assigns libinput to the active seat, and registers redraw, -vblank, and session pause/resume sources with calloop. It currently selects -one output. Hotplug, multiple-output lifecycle, display mode management, -session-shell integration and M-series scanout/input behavior remain -unqualified. Default and feature-enabled `cargo check --bins` and +vblank, and session pause/resume sources with calloop. It initializes all +connected outputs with compatible CRTCs on the selected KMS device, puts eDP +first, and places other outputs to its right. Hotplug, changing display +arrangement and modes, session-shell integration, and M-series scanout/input +behavior remain unqualified. Default and feature-enabled `cargo check --bins` and `cargo build --bins` pass locally; builds do not initialize or take over a native session. diff --git a/rust/pantheon-compositor/src/display_config.rs b/rust/pantheon-compositor/src/display_config.rs index 4c11e2af0..a6987e428 100644 --- a/rust/pantheon-compositor/src/display_config.rs +++ b/rust/pantheon-compositor/src/display_config.rs @@ -26,24 +26,6 @@ pub struct OutputSnapshot { pub primary: bool, } -impl Default for OutputSnapshot { - fn default() -> Self { - Self { - connector: "winit".into(), - vendor: "Smithay".into(), - product: "Winit".into(), - serial: "Nested".into(), - width: 1280, - height: 800, - refresh_millihz: 60_000, - scale: 1.0, - x: 0, - y: 0, - primary: true, - } - } -} - #[derive(Clone)] pub struct DisplayConfigHandle { updates: mpsc::Sender, @@ -151,7 +133,9 @@ fn serve_display_config(receiver: mpsc::Receiver) { } }; - let initial = HashMap::from([("winit".to_string(), OutputSnapshot::default())]); + // Backends publish their real outputs during startup. Avoid advertising a + // fabricated Winit monitor when native DRM is selected. + let initial = HashMap::new(); let snapshot = Arc::new(Mutex::new((1, initial))); if let Err(error) = connection .object_server() diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 71297b047..e1ddebd01 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -5,6 +5,7 @@ //! device owns the physical connectors. Output allocation and scanout are //! attached by the native output backend built on top of this manager. +use std::collections::HashSet; use std::path::Path; use std::time::Duration; @@ -66,19 +67,24 @@ pub struct NativeRenderer { } pub struct NativeKmsOutput { - pub node: DrmNode, pub connector: connector::Handle, pub crtc: smithay::reexports::drm::control::crtc::Handle, pub output: Output, - pub manager: NativeOutputManager, pub drm_output: NativeDrmOutput, + pub frame_pending: bool, +} + +pub struct NativeKmsDevice { + pub node: DrmNode, + pub manager: NativeOutputManager, + pub outputs: Vec, pub notifier: Option, } pub struct NativeBackend { pub _session: LibSeatSession, pub renderer: NativeRenderer, - pub output: NativeKmsOutput, + pub kms: NativeKmsDevice, } struct ConnectedOutput { @@ -87,10 +93,11 @@ struct ConnectedOutput { mode: smithay::reexports::drm::control::Mode, } -impl NativeKmsOutput { +impl NativeKmsDevice { /// Open the first usable KMS device on the active libseat and initialize - /// its preferred connected connector with buffers rendered on `renderer`. - /// This creates Smithay output state but does not queue or present a frame. + /// every connected connector with a compatible, unclaimed CRTC. Each + /// output receives its own scanout surface while sharing the device, + /// allocator, exporter and event source. pub fn initialize( session: &LibSeatSession, renderer: &mut NativeRenderer, @@ -103,7 +110,7 @@ impl NativeKmsOutput { _ => continue, }; match Self::open_output_device(session, node, &path, renderer, state) { - Ok(output) => return Ok(output), + Ok(kms) => return Ok(kms), Err(error) => last_error = Some(error), } } @@ -121,30 +128,45 @@ impl NativeKmsOutput { &mut self, renderer: &mut NativeRenderer, state: &PantheonCompositor, - ) -> Result> { - let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; - let elements = smithay::desktop::space::space_render_elements( - &mut gpu_renderer, - [&state.space], - &self.output, - 1.0, - )?; - let frame = self.drm_output.render_frame( - &mut gpu_renderer, - &elements, - [0.1, 0.1, 0.1, 1.0], - FrameFlags::DEFAULT, - )?; - if frame.is_empty { - return Ok(false); + ) -> Result> { + let mut rendered = 0; + for output in &mut self.outputs { + if output.frame_pending { + continue; + } + let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; + let elements = smithay::desktop::space::space_render_elements( + &mut gpu_renderer, + [&state.space], + &output.output, + 1.0, + )?; + let frame = output.drm_output.render_frame( + &mut gpu_renderer, + &elements, + [0.1, 0.1, 0.1, 1.0], + FrameFlags::DEFAULT, + )?; + if frame.is_empty { + continue; + } + output.drm_output.queue_frame(())?; + output.frame_pending = true; + rendered += 1; } - self.drm_output.queue_frame(())?; - Ok(true) + Ok(rendered) } - pub fn frame_submitted(&mut self) -> Result<(), Box> { - self.drm_output.frame_submitted()?; - Ok(()) + pub fn frame_submitted( + &mut self, + crtc: smithay::reexports::drm::control::crtc::Handle, + ) -> Result, Box> { + let Some(output) = self.outputs.iter_mut().find(|output| output.crtc == crtc) else { + return Ok(None); + }; + output.drm_output.frame_submitted()?; + output.frame_pending = false; + Ok(Some(output.output.clone())) } fn open_output_device( @@ -164,12 +186,24 @@ impl NativeKmsOutput { let gbm = GbmDevice::new(drm_fd)?; let resources = drm.resource_handles()?; - let mut connected = Vec::new(); + let mut discovered = Vec::new(); for handle in resources.connectors() { let connector = drm.get_connector(*handle, false)?; if connector.state() != connector::State::Connected || connector.modes().is_empty() { continue; } + discovered.push(connector); + } + discovered.sort_by_key(|connector| { + ( + connector.interface() != connector::Interface::EmbeddedDisplayPort, + connector.interface_id(), + ) + }); + + let mut connected = Vec::new(); + let mut claimed_crtcs = HashSet::new(); + for connector in discovered { let encoder = connector .current_encoder() .and_then(|handle| drm.get_encoder(handle).ok()) @@ -183,10 +217,11 @@ impl NativeKmsOutput { let Some(crtc) = resources .filter_crtcs(encoder.possible_crtcs()) .into_iter() - .next() + .find(|crtc| !claimed_crtcs.contains(crtc)) else { continue; }; + claimed_crtcs.insert(crtc); let mode = connector .modes() .iter() @@ -200,13 +235,9 @@ impl NativeKmsOutput { }); } - connected.sort_by_key(|output| { - output.connector.interface() != connector::Interface::EmbeddedDisplayPort - }); - let selected = connected - .into_iter() - .next() - .ok_or("KMS device has no connected connector with a compatible CRTC")?; + if connected.is_empty() { + return Err("KMS device has no connected connector with a compatible CRTC".into()); + } let output_allocator = renderer.output_allocator(); let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; @@ -231,63 +262,81 @@ impl NativeKmsOutput { render_formats, ); - let mode = Mode::from(selected.mode); - let name = format!( - "{}-{}", - selected.connector.interface().as_str(), - selected.connector.interface_id() - ); - let (physical_width, physical_height) = selected.connector.size().unwrap_or((0, 0)); - let output = Output::new( - name.clone(), - PhysicalProperties { - size: (physical_width as i32, physical_height as i32).into(), - subpixel: selected.connector.subpixel().into(), - make: "Apple".into(), - model: name.clone(), - serial_number: "Unknown".into(), - }, - ); - output.set_preferred(mode); - output.change_current_state(Some(mode), None, None, Some((0, 0).into())); - let render_elements = DrmOutputRenderElements::< NativeMultiRenderer<'_>, WaylandSurfaceRenderElement>, >::default(); - let drm_output = manager.lock().initialize_output( - selected.crtc, - selected.mode, - &[selected.connector.handle()], - &output, - None, - &mut gpu_renderer, - &render_elements, - )?; + let mut outputs = Vec::with_capacity(connected.len()); + let mut x = 0; + for selected in connected { + let mode = Mode::from(selected.mode); + let name = format!( + "{}-{}", + selected.connector.interface().as_str(), + selected.connector.interface_id() + ); + let (physical_width, physical_height) = selected.connector.size().unwrap_or((0, 0)); + let output = Output::new( + name.clone(), + PhysicalProperties { + size: (physical_width as i32, physical_height as i32).into(), + subpixel: selected.connector.subpixel().into(), + make: "Apple".into(), + model: name.clone(), + serial_number: "Unknown".into(), + }, + ); + output.set_preferred(mode); + output.change_current_state(Some(mode), None, None, Some((x, 0).into())); + + let drm_output = match manager.lock().initialize_output( + selected.crtc, + selected.mode, + &[selected.connector.handle()], + &output, + None, + &mut gpu_renderer, + &render_elements, + ) { + Ok(output) => output, + Err(error) => { + tracing::warn!(%name, ?error, "could not initialize connected KMS output"); + continue; + } + }; - output.create_global::(&state.display_handle); - state.space.map_output(&output, (0, 0)); - state.display_config.update(OutputSnapshot { - connector: name.clone(), - vendor: "Apple".into(), - product: name, - serial: "Unknown".into(), - width: mode.size.w, - height: mode.size.h, - refresh_millihz: mode.refresh, - scale: 1.0, - x: 0, - y: 0, - primary: true, - }); + output.create_global::(&state.display_handle); + state.space.map_output(&output, (x, 0)); + state.display_config.update(OutputSnapshot { + connector: name.clone(), + vendor: "Apple".into(), + product: name, + serial: "Unknown".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + scale: 1.0, + x, + y: 0, + primary: outputs.is_empty(), + }); + x += mode.size.w; + outputs.push(NativeKmsOutput { + connector: selected.connector.handle(), + crtc: selected.crtc, + output, + drm_output, + frame_pending: false, + }); + } + if outputs.is_empty() { + return Err("could not initialize any connected KMS output".into()); + } Ok(Self { node, - connector: selected.connector.handle(), - crtc: selected.crtc, - output, manager, - drm_output, + outputs, notifier: Some(notifier), }) } @@ -304,8 +353,8 @@ impl NativeBackend { session_notifier: LibSeatSessionNotifier, ) -> Result<(), Box> { let mut renderer = NativeRenderer::initialize(&session)?; - let mut output = NativeKmsOutput::initialize(&session, &mut renderer, state)?; - let drm_notifier = output.notifier.take().expect("DRM notifier is present"); + let mut kms = NativeKmsDevice::initialize(&session, &mut renderer, state)?; + let drm_notifier = kms.notifier.take().expect("DRM notifier is present"); let mut libinput_context = Libinput::new_with_udev::< LibinputSessionInterface, @@ -328,7 +377,7 @@ impl NativeBackend { SessionEvent::PauseSession => { libinput_context.suspend(); if let Some(backend) = state.native_backend.as_mut() { - backend.output.manager.pause(); + backend.kms.manager.pause(); } tracing::info!("native compositor session paused"); } @@ -338,7 +387,7 @@ impl NativeBackend { tracing::error!(?error, "failed to resume libinput"); } if let Some(backend) = state.native_backend.as_mut() { - if let Err(error) = backend.output.manager.lock().activate(false) { + if let Err(error) = backend.kms.manager.lock().activate(false) { tracing::error!(?error, "failed to reactivate DRM output"); } } @@ -354,14 +403,14 @@ impl NativeBackend { let Some(backend) = state.native_backend.as_mut() else { return; }; - if crtc != backend.output.crtc { - return; - } - if let Err(error) = backend.output.frame_submitted() { - tracing::error!(?error, "failed to complete native DRM frame"); - return; - } - let output = backend.output.output.clone(); + let output = match backend.kms.frame_submitted(crtc) { + Ok(Some(output)) => output, + Ok(None) => return, + Err(error) => { + tracing::error!(?error, "failed to complete native DRM frame"); + return; + } + }; state.space.elements().for_each(|window| { window.send_frame( &output, @@ -373,6 +422,7 @@ impl NativeBackend { state.space.refresh(); state.popups.cleanup(); let _ = state.display_handle.flush_clients(); + state.request_native_redraw(); } smithay::backend::drm::DrmEvent::Error(error) => { tracing::error!(?error, "native DRM event source failed"); @@ -384,7 +434,7 @@ impl NativeBackend { state.native_backend = Some(Self { _session: session, renderer, - output, + kms, }); event_loop .handle() @@ -395,7 +445,7 @@ impl NativeBackend { let Some(mut backend) = state.native_backend.take() else { return; }; - if let Err(error) = backend.output.render(&mut backend.renderer, state) { + if let Err(error) = backend.kms.render(&mut backend.renderer, state) { tracing::error!(?error, "native output render failed"); } state.native_backend = Some(backend); From b583ab20f0937dc0fdb207e47d6beabecaac58ab Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:28:23 -0400 Subject: [PATCH 068/798] Machine Lab: adopt existing mseries-vm guests, new-vs-installed panels, guided setup, free-space warning, dock installer Co-Authored-By: Claude Sonnet 5.5 --- mseries/install-machine-lab.sh | 23 ++ .../data/io.github.mseries.MachineLab.desktop | 3 +- .../data/io.github.mseries.MachineLab.svg | 17 ++ mseries/machine-lab/data/style.css | 18 ++ mseries/machine-lab/meson.build | 2 + mseries/machine-lab/src/machine.vala | 274 ++++++++++++++++-- mseries/machine-lab/src/main.vala | 151 +++++++--- mseries/machine-lab/src/page.vala | 159 ++++++++-- 8 files changed, 567 insertions(+), 80 deletions(-) create mode 100755 mseries/install-machine-lab.sh create mode 100644 mseries/machine-lab/data/io.github.mseries.MachineLab.svg diff --git a/mseries/install-machine-lab.sh b/mseries/install-machine-lab.sh new file mode 100755 index 000000000..ae45544cf --- /dev/null +++ b/mseries/install-machine-lab.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Build Machine Lab and install it for the current user: binary, launcher, icon, +# desktop entry, and a pin in the elementary dock. +set -euo pipefail +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/machine-lab +build=$(mktemp -d) +trap 'rm -rf "$build"' EXIT + +meson setup "$build" "$here" --prefix="$HOME/.local" >/dev/null +meson compile -C "$build" +meson install -C "$build" >/dev/null + +# The launcher is installed as bin/machine-lab-run-vm by meson; refresh caches. +update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true +gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true + +# Pin to the dock (idempotent). +desktop=io.github.mseries.MachineLab.desktop +current=$(dconf read /io/elementary/dock/launchers 2>/dev/null || true) +if [ -n "$current" ] && [[ "$current" != *"$desktop"* ]]; then + dconf write /io/elementary/dock/launchers "${current%]}, '$desktop']" +fi +echo "Machine Lab installed. Launch it from the dock or: $HOME/.local/bin/machine-lab" diff --git a/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop b/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop index 368b1502f..8e73dbfc0 100644 --- a/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop +++ b/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop @@ -2,8 +2,9 @@ Name=Machine Lab Comment=Run another Linux on this Mac Exec=machine-lab -Icon=computer +Icon=io.github.mseries.MachineLab Terminal=false Type=Application Categories=System;Emulator; Keywords=virtual;machine;vm;qemu; +StartupWMClass=io.github.mseries.MachineLab diff --git a/mseries/machine-lab/data/io.github.mseries.MachineLab.svg b/mseries/machine-lab/data/io.github.mseries.MachineLab.svg new file mode 100644 index 000000000..af45a46c2 --- /dev/null +++ b/mseries/machine-lab/data/io.github.mseries.MachineLab.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css index a06bd0021..21155d500 100644 --- a/mseries/machine-lab/data/style.css +++ b/mseries/machine-lab/data/style.css @@ -136,3 +136,21 @@ textview.ml-console, textview.ml-console text { 100% { box-shadow: 0 0 0 2px @bg_color, 0 0 0 7px alpha(#68b723, 0); } } .ml-dot.running { animation: ml-pulse 1.6s ease-out infinite; } + +.ml-step-badge { + min-width: 26px; min-height: 26px; + border-radius: 99px; + font-weight: 700; font-size: 0.9em; + background: alpha(@accent_color, 0.15); + color: @accent_color; +} +.ml-step-badge.done { background: #68b723; color: white; } +.ml-pill.new { color: @accent_color; background: alpha(@accent_color, 0.15); } + +.ml-gallery-card { + padding: 16px; + border-radius: 14px; + background: @card_bg_color; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.1), 0 1px 3px alpha(black, 0.12); +} +.ml-gallery-card:hover { box-shadow: 0 0 0 2px @accent_color, 0 2px 8px alpha(black, 0.18); } diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index 9c86e5bfa..a95071c88 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -22,6 +22,8 @@ executable('machine-lab', install: true ) +install_data('data/io.github.mseries.MachineLab.svg', + install_dir: get_option('datadir') / 'icons/hicolor/scalable/apps') install_data('data/io.github.mseries.MachineLab.desktop', install_dir: get_option('datadir') / 'applications') install_data('launcher/run-vm.sh', diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index c60dd66ff..f8bdfab32 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -1,30 +1,52 @@ -/* Machine model, persistence and QEMU process control. */ +/* Machine model, persistence, discovery of mseries-vm guests, and process control. */ public class Machine : Object { public string id { get; set; } public string name { get; set; default = "New Machine"; } + public string kind { get; set; default = "native"; } // "native" = ours, "managed" = mseries-vm guest + public string vm_name { get; set; default = ""; } // managed: directory name under ~/vms public int cpus { get; set; default = 4; } public int mem_gib { get; set; default = 6; } public int disk_gib { get; set; default = 64; } public int ssh_port { get; set; default = 2222; } public int color { get; set; default = 0; } + public string? url { get; set; default = null; } // where to get an installer public string? installer { get; set; default = null; } public Subprocess? proc { get; private set; default = null; } public DateTime? started { get; private set; default = null; } public Gtk.TextBuffer log { get; private set; default = new Gtk.TextBuffer (null); } + private bool managed_running = false; public signal void state_changed (); public signal void finished (int code); + public signal void installer_detached (); - public bool running { get { return proc != null; } } + public bool managed { get { return kind == "managed"; } } + public bool running { get { return managed ? managed_running : proc != null; } } public string dir { owned get { + if (managed) { + return Path.build_filename (vms_dir (), vm_name); + } return Path.build_filename (Environment.get_user_data_dir (), "machine-lab", id); } } + public static string vms_dir () { + return Environment.get_variable ("MSERIES_VM_DIR") ?? Path.build_filename (Environment.get_home_dir (), "vms"); + } + + /* An empty disk file is just a placeholder: installed means the guest wrote real data. */ + public bool installed () { + return disk_used_bytes () >= 1073741824; + } + + public bool is_fresh () { + return !running && !installed (); + } + public bool has_disk () { return FileUtils.test (Path.build_filename (dir, "disk.qcow2"), FileTest.EXISTS); } @@ -39,28 +61,42 @@ public class Machine : Object { } } + // ---- Native ---- + public void start (string launcher) throws Error { if (running) { return; } var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); - l.setenv ("MACHINE_CPUS", cpus.to_string (), true); - l.setenv ("MACHINE_MEM_GIB", mem_gib.to_string (), true); - l.setenv ("MACHINE_DISK_GIB", disk_gib.to_string (), true); - l.setenv ("MACHINE_SSH_PORT", ssh_port.to_string (), true); - l.setenv ("MACHINE_NAME", name, true); - string[] argv = { launcher, installer ?? "-", dir }; - proc = l.spawnv (argv); - started = new DateTime.now_local (); + string[] argv; + if (managed) { + argv = { launcher, "start", vm_name, "--daemon" }; + } else { + l.setenv ("MACHINE_CPUS", cpus.to_string (), true); + l.setenv ("MACHINE_MEM_GIB", mem_gib.to_string (), true); + l.setenv ("MACHINE_DISK_GIB", disk_gib.to_string (), true); + l.setenv ("MACHINE_SSH_PORT", ssh_port.to_string (), true); + l.setenv ("MACHINE_NAME", name, true); + argv = { launcher, installer ?? "-", dir }; + } + var p = l.spawnv (argv); log.text = ""; append_log ("$ %s\n".printf (string.joinv (" ", argv))); - read_output.begin (proc); - watch.begin (proc); + read_output.begin (p); + if (managed) { + watch_managed_start.begin (p); + } else { + proc = p; + started = new DateTime.now_local (); + watch.begin (p); + } state_changed (); } public void stop () { - if (proc != null) { + if (managed) { + stop_managed.begin (); + } else if (proc != null) { proc.force_exit (); } } @@ -93,6 +129,11 @@ public class Machine : Object { append_log ("\n[machine exited with status %d]\n".printf (code)); proc = null; started = null; + // The installer did its job once a disk exists: next start boots the disk. + if (installer != null && installed ()) { + installer = null; + installer_detached (); + } state_changed (); finished (code); } @@ -103,6 +144,114 @@ public class Machine : Object { } DirUtils.remove (dir); } + + // ---- Managed (mseries-vm) ---- + + private async void watch_managed_start (Subprocess p) { + try { + yield p.wait_async (); + } catch (Error e) { + append_log ("[wait: %s]\n".printf (e.message)); + } + int code = p.get_if_exited () ? p.get_exit_status () : -1; + poll (); + if (code != 0) { + finished (code); + } + } + + private async void stop_managed () { + string? tool = Machine.find_mseries_vm (); + if (tool == null) { + return; + } + try { + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); + var p = l.spawnv ({ tool, "stop", vm_name }); + append_log ("\n$ mseries-vm stop %s (asks the guest to power off)\n".printf (vm_name)); + read_output.begin (p); + yield p.wait_async (); + } catch (Error e) { + append_log ("[stop: %s]\n".printf (e.message)); + } + poll (); + finished (0); + } + + public static string? find_mseries_vm () { + var env = Environment.get_variable ("MSERIES_VM_TOOL"); + if (env != null && FileUtils.test (env, FileTest.IS_EXECUTABLE)) { + return env; + } + string exe_dir = ""; + try { + exe_dir = Path.get_dirname (FileUtils.read_link ("/proc/self/exe")); + } catch (Error e) {} + string[] candidates = { + Path.build_filename (exe_dir, "..", "..", "vms", "mseries-vm"), + Path.build_filename (Environment.get_home_dir (), "ElementaryOS_MSeries", "mseries", "vms", "mseries-vm") + }; + foreach (var c in candidates) { + if (FileUtils.test (c, FileTest.IS_EXECUTABLE)) { + return c; + } + } + return Environment.find_program_in_path ("mseries-vm"); + } + + /* Refresh run state from the guest's qemu.pid, and tail its serial log. */ + public void poll () { + if (!managed) { + return; + } + bool now = false; + DateTime? since = null; + string pidfile = Path.build_filename (dir, "qemu.pid"); + try { + string pid; + FileUtils.get_contents (pidfile, out pid); + pid = pid.strip (); + string cmdline; + FileUtils.get_contents ("/proc/%s/cmdline".printf (pid), out cmdline); + now = cmdline.contains ("qemu-system"); + if (now) { + var info = File.new_for_path (pidfile).query_info ("time::modified", FileQueryInfoFlags.NONE); + since = info.get_modification_date_time ().to_local (); + } + } catch (Error e) { + now = false; + } + bool changed = now != managed_running; + managed_running = now; + started = now ? since : null; + if (now) { + load_serial_tail (); + } + if (changed) { + state_changed (); + } + } + + private void load_serial_tail () { + try { + var f = File.new_for_path (Path.build_filename (dir, "serial.log")); + var stream = f.read (); + var info = stream.query_info ("standard::size"); + int64 size = info.get_size (); + int64 max = 12000; + if (size > max) { + stream.seek (size - max, SeekType.SET); + } + var bytes = stream.read_bytes ((size_t) int64.min (size, max)); + string text = ((string) bytes.get_data ()).make_valid ((ssize_t) bytes.length); + try { + text = new Regex ("\\x1b\\[[0-9;?]*[ -/]*[@-~]|\\x1b\\][^\\x07\\x1b]*(\\x07|\\x1b\\\\)|\\x1b[()][A-Z0-9]|[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]").replace (text, -1, 0, ""); + } catch (RegexError e) {} + log.text = text; + } catch (Error e) { + // no serial log yet + } + } } public class MachineStore : Object { @@ -126,32 +275,94 @@ public class MachineStore : Object { if (kf.has_key (id, "color")) { m.color = kf.get_integer (id, "color"); } + if (kf.has_key (id, "url")) { + m.url = kf.get_string (id, "url"); + } if (kf.has_key (id, "installer")) { m.installer = kf.get_string (id, "installer"); } list.add (m); } } catch (Error e) { - // First run, or unreadable file: start from the default profile below. - } - if (list.length == 0 && !FileUtils.test (config_path (), FileTest.EXISTS)) { - var m = new Machine (); - m.id = "omarchy-arm64"; // keeps the disk made by the earlier prototype - m.name = "Omarchy"; - list.add (m); + // First run: no machines until the user creates one. } return list; } + // Guests already set up with mseries-vm: one vm.conf per directory under ~/vms. + public static void discover (GenericArray list) { + try { + var d = Dir.open (Machine.vms_dir ()); + string? entry; + string[] names = {}; + while ((entry = d.read_name ()) != null) { + names += entry; + } + qsort_with_data (names, sizeof (string), (a, b) => strcmp (a, b)); + foreach (var n in names) { + string conf; + string path = Path.build_filename (Machine.vms_dir (), n, "vm.conf"); + var kf = new KeyFile (); + try { + FileUtils.get_contents (path, out conf); + kf.load_from_data ("[c]\n" + conf, -1, KeyFileFlags.NONE); + } catch (Error e) { + continue; // not a VM directory (downloads, trials, ...) + } + var m = new Machine (); + m.kind = "managed"; + m.vm_name = n; + m.id = "vm-" + n; + string guest = kf.has_key ("c", "GUEST") ? kf.get_string ("c", "GUEST") : n; + switch (guest) { + case "omarchy": m.name = "Omarchy"; m.color = 7; break; + case "elementary": m.name = "elementary OS"; m.color = 0; break; + case "windows": m.name = "Windows 11"; m.color = 3; break; + default: m.name = n; m.color = 1; break; + } + if (kf.has_key ("c", "CPUS")) { + m.cpus = kf.get_integer ("c", "CPUS"); + } + if (kf.has_key ("c", "MEMORY_MB")) { + m.mem_gib = int.max (1, (kf.get_integer ("c", "MEMORY_MB") + 512) / 1024); + } + m.disk_gib = virtual_size_gib (Path.build_filename (Machine.vms_dir (), n, "disk.qcow2")); + list.add (m); + } + } catch (Error e) { + // No ~/vms directory: nothing to import. + } + } + + private static int virtual_size_gib (string disk) { + try { + string output; + Process.spawn_sync (null, { "qemu-img", "info", "--output=json", disk }, null, + SpawnFlags.SEARCH_PATH | SpawnFlags.STDERR_TO_DEV_NULL, null, out output); + Regex r = new Regex ("\\n \"virtual-size\":\\s*(\\d+)"); + MatchInfo mi; + if (r.match (output, 0, out mi)) { + return (int) (int64.parse (mi.fetch (1)) / 1073741824); + } + } catch (Error e) {} + return 0; + } + public static void save (GenericArray list) { var kf = new KeyFile (); foreach (var m in list.data) { + if (m.managed) { + continue; + } kf.set_string (m.id, "name", m.name); kf.set_integer (m.id, "cpus", m.cpus); kf.set_integer (m.id, "mem_gib", m.mem_gib); kf.set_integer (m.id, "disk_gib", m.disk_gib); kf.set_integer (m.id, "ssh_port", m.ssh_port); kf.set_integer (m.id, "color", m.color); + if (m.url != null) { + kf.set_string (m.id, "url", m.url); + } if (m.installer != null) { kf.set_string (m.id, "installer", m.installer); } @@ -168,19 +379,30 @@ public class MachineStore : Object { /* Checks the host has what the launcher needs. */ public class Readiness : Object { public static string[] problems () { - string[] out = {}; + string[] found = {}; if (Environment.find_program_in_path ("qemu-system-aarch64") == null) { - out += "QEMU is missing. Install qemu-system-arm."; + found += "QEMU is missing. Install qemu-system-arm."; } if (Environment.find_program_in_path ("qemu-img") == null) { - out += "qemu-img is missing. Install qemu-utils."; + found += "qemu-img is missing. Install qemu-utils."; } if (!FileUtils.test ("/usr/share/AAVMF/AAVMF_CODE.fd", FileTest.EXISTS)) { - out += "UEFI firmware is missing. Install qemu-efi-aarch64."; + found += "UEFI firmware is missing. Install qemu-efi-aarch64."; } if (Posix.access ("/dev/kvm", Posix.R_OK | Posix.W_OK) != 0) { - out += "No access to /dev/kvm. Add your user to the kvm group."; + found += "No access to /dev/kvm. Add your user to the kvm group."; + } + return found; + } + + /* Free space, in GiB, on the volume that holds machine disks. */ + public static int64 free_gib () { + try { + var info = File.new_for_path (Environment.get_user_data_dir ()) + .query_filesystem_info ("filesystem:free"); + return (int64) (info.get_attribute_uint64 ("filesystem:free") / 1073741824); + } catch (Error e) { + return -1; } - return out; } } diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index c26b7829a..aa85f1999 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -13,14 +13,15 @@ struct Preset { public int mem; public int disk; public int color; + public string url; } const Preset[] presets = { - { "Omarchy", "Arch-based tiling desktop", 4, 6, 64, 0 }, - { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 48, 4 }, - { "Debian", "Rock-solid and lightweight", 2, 2, 32, 5 }, - { "Fedora", "Fresh packages, GNOME", 4, 4, 48, 0 }, - { "Blank Machine", "Start from scratch", 2, 4, 32, 7 } + { "Omarchy", "Arch-based tiling desktop", 4, 6, 32, 7, "https://omarchy.org" }, + { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 24, 4, "https://ubuntu.com/download/server/arm" }, + { "Debian", "Rock-solid and lightweight", 2, 2, 16, 5, "https://www.debian.org/CD/netinst/" }, + { "Fedora", "Fresh packages, GNOME", 4, 4, 24, 0, "https://fedoraproject.org/server/download" }, + { "Blank Machine", "Bring any ARM64 .iso", 2, 4, 16, 1, "" } }; public class MachineRow : Gtk.ListBoxRow { @@ -30,6 +31,9 @@ public class MachineRow : Gtk.ListBoxRow { private Gtk.Box dot; private Gtk.Image icon; + public signal void group_changed (); + public int group { get { return machine.is_fresh () ? 1 : 0; } } + public MachineRow (Machine machine) { Object (machine: machine); } @@ -75,8 +79,8 @@ public class MachineRow : Gtk.ListBoxRow { public void update () { name_label.label = machine.name; - sub_label.label = "%s · %d CPU · %d GiB".printf ( - machine.running ? "Running" : "Stopped", machine.cpus, machine.mem_gib); + string state = machine.running ? "Running" : machine.is_fresh () ? "Not installed" : "Stopped"; + sub_label.label = "%s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); for (int i = 0; i < 8; i++) { icon.remove_css_class ("ml-c-%d".printf (i)); } @@ -139,6 +143,7 @@ public class MachineLab : Gtk.Application { return; } machines = MachineStore.load (); + MachineStore.discover (machines); window = new Gtk.ApplicationWindow (this); window.title = "Machine Lab"; @@ -173,6 +178,15 @@ public class MachineLab : Gtk.Application { } return false; }); + Timeout.add_seconds (2, () => { + foreach (var m in machines.data) { + m.poll (); + } + return Source.CONTINUE; + }); + foreach (var m in machines.data) { + m.poll (); + } window.present (); } @@ -191,14 +205,27 @@ public class MachineLab : Gtk.Application { handle.child = header; side.append (handle); - var heading = new Gtk.Label ("MACHINES"); - heading.xalign = 0; - heading.add_css_class ("ml-sidebar-title"); - side.append (heading); list = new Gtk.ListBox (); list.selection_mode = Gtk.SelectionMode.SINGLE; list.add_css_class ("navigation-sidebar"); + list.set_sort_func ((a, b) => { + var ra = (MachineRow) a; + var rb = (MachineRow) b; + if (ra.group != rb.group) return ra.group - rb.group; + return strcmp (ra.machine.name.down (), rb.machine.name.down ()); + }); + list.set_header_func ((row, before) => { + var r = (MachineRow) row; + if (before != null && ((MachineRow) before).group == r.group) { + row.set_header (null); + return; + } + var h = new Gtk.Label (r.group == 0 ? "YOUR MACHINES" : "NEW — NOT INSTALLED"); + h.xalign = 0; + h.add_css_class ("ml-sidebar-title"); + row.set_header (h); + }); list.row_selected.connect ((row) => { if (row == null) { content.visible_child_name = "empty"; @@ -271,31 +298,60 @@ public class MachineLab : Gtk.Application { handle.child = header; page.append (handle); - var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 10); + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); box.vexpand = true; box.valign = Gtk.Align.CENTER; box.halign = Gtk.Align.CENTER; - box.width_request = 380; - var icon = new Gtk.Image.from_icon_name ("computer"); - icon.pixel_size = 128; - icon.add_css_class ("ml-empty-icon"); - box.append (icon); - var t = new Gtk.Label ("No Machines Yet"); + box.margin_start = 32; + box.margin_end = 32; + box.width_request = 520; + var t = new Gtk.Label ("Create a Machine"); t.add_css_class ("title-1"); - t.margin_top = 12; box.append (t); - var d = new Gtk.Label ("Create a machine to run another Linux next to elementary OS, with full hardware acceleration."); + var d = new Gtk.Label ("Pick a starting point. Machine Lab sets up sensible hardware, then walks you through the installer."); d.wrap = true; d.justify = Gtk.Justification.CENTER; + d.margin_bottom = 14; d.add_css_class ("dim-label"); box.append (d); - var b = new Gtk.Button.with_label ("New Machine"); - b.add_css_class ("suggested-action"); - b.add_css_class ("pill"); - b.halign = Gtk.Align.CENTER; - b.margin_top = 12; - b.clicked.connect (() => add_machine (presets[0])); - box.append (b); + + var grid = new Gtk.Grid (); + grid.column_homogeneous = true; + grid.column_spacing = 12; + grid.row_spacing = 12; + int i = 0; + foreach (var p in presets) { + var card = new Gtk.Button (); + card.add_css_class ("flat"); + var cb = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + cb.add_css_class ("ml-gallery-card"); + var ic = new Gtk.Image.from_icon_name ("computer"); + ic.pixel_size = 40; + ic.add_css_class ("ml-row-icon"); + ic.add_css_class ("ml-c-%d".printf (p.color)); + cb.append (ic); + var tb = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + tb.valign = Gtk.Align.CENTER; + var n = new Gtk.Label (p.name); + n.xalign = 0; + n.add_css_class ("ml-spec-value"); + var bl = new Gtk.Label (p.blurb); + bl.xalign = 0; + bl.add_css_class ("ml-spec-label"); + var sp = new Gtk.Label ("%d CPU · %d GiB RAM · %d GiB disk".printf (p.cpus, p.mem, p.disk)); + sp.xalign = 0; + sp.add_css_class ("ml-fineprint"); + tb.append (n); + tb.append (bl); + tb.append (sp); + cb.append (tb); + card.child = cb; + var preset = p; + card.clicked.connect (() => add_machine (preset)); + grid.attach (card, i % 2, i / 2); + i++; + } + box.append (grid); page.append (box); return page; } @@ -309,11 +365,18 @@ public class MachineLab : Gtk.Application { MachineStore.save (machines); row_for (m).update (); }); + m.installer_detached.connect (() => { + MachineStore.save (machines); + send_toast ("%s is installed. It will boot from its disk now.".printf (m.name)); + }); page.start_requested.connect (() => start_machine (m)); page.remove_requested.connect (() => confirm_remove.begin (m)); pages[m.id] = page; content.add_named (page, m.id); - list.append (new MachineRow (m)); + var row = new MachineRow (m); + m.state_changed.connect (() => list.invalidate_sort ()); + m.state_changed.connect (() => list.invalidate_headers ()); + list.append (row); } private MachineRow row_for (Machine m) { @@ -343,6 +406,7 @@ public class MachineLab : Gtk.Application { m.mem_gib = preset.mem; m.disk_gib = preset.disk; m.color = preset.color; + m.url = preset.url == "" ? null : preset.url; int port = 2222; bool clash = true; while (clash) { @@ -361,9 +425,23 @@ public class MachineLab : Gtk.Application { } private void start_machine (Machine m) { + if (m.managed) { + string? tool = Machine.find_mseries_vm (); + if (tool == null) { + show_error ("mseries-vm was not found. Set MSERIES_VM_TOOL to its full path."); + return; + } + try { + m.start (tool); + send_toast ("Starting %s…".printf (m.name)); + } catch (Error e) { + show_error (e.message); + } + return; + } string? launcher = find_launcher (); if (launcher == null) { - show_error ("The QEMU launcher was not found. Set MACHINE_LAB_LAUNCHER to the full path of run-vm.sh."); + show_error ("The QEMU launcher (run-vm.sh) was not found next to the app. Set MACHINE_LAB_LAUNCHER to its full path."); return; } if (!m.has_disk () && m.installer == null) { @@ -421,11 +499,18 @@ public class MachineLab : Gtk.Application { if (override_path != null && FileUtils.test (override_path, FileTest.IS_EXECUTABLE)) { return override_path; } - string installed = "/usr/bin/machine-lab-run-vm"; - if (FileUtils.test (installed, FileTest.IS_EXECUTABLE)) { - return installed; + foreach (var installed in new string[] { + Path.build_filename (Environment.get_home_dir (), ".local", "bin", "machine-lab-run-vm"), + "/usr/bin/machine-lab-run-vm" + }) { + if (FileUtils.test (installed, FileTest.IS_EXECUTABLE)) { + return installed; + } } - string exe_dir = Path.get_dirname (Environment.get_prgname () ?? ""); + string exe_dir = ""; + try { + exe_dir = Path.get_dirname (FileUtils.read_link ("/proc/self/exe")); + } catch (Error e) {} string[] candidates = { Path.build_filename (exe_dir, "run-vm.sh"), Path.build_filename (exe_dir, "..", "launcher", "run-vm.sh") diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 337d5fec4..71484a24b 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -20,6 +20,15 @@ public class MachinePage : Gtk.Box { private Gtk.Label net_v; private Gtk.Label ssh_cmd; private Gtk.Image hero_icon; + private Gtk.Label hero_sub; + private Gtk.Box setup_card; + private Gtk.Box connect_card; + private Gtk.MenuButton more_button; + private Gtk.Label[] step_badges = {}; + private Gtk.Label step2_detail; + private Gtk.Label step1_detail; + private Gtk.Label step3_detail; + private Gtk.Button get_button; private Gtk.Box ready_card; private Gtk.Image ready_icon; private Gtk.Label ready_title; @@ -64,6 +73,7 @@ public class MachinePage : Gtk.Box { switcher.stack = stack; header.title_widget = switcher; var more = new Gtk.MenuButton (); + more_button = more; more.icon_name = "view-more-symbolic"; more.tooltip_text = "More actions"; var pop = new Gtk.Popover (); @@ -80,6 +90,9 @@ public class MachinePage : Gtk.Box { machine.state_changed.connect (refresh); machine.finished.connect ((code) => { + if (code > 0) { + show_tab ("console"); + } toast (code > 0 ? "%s stopped with an error (exit %d)".printf (machine.name, code) : "%s stopped".printf (machine.name)); }); @@ -125,10 +138,11 @@ public class MachinePage : Gtk.Box { title.xalign = 0; title.ellipsize = Pango.EllipsizeMode.END; text.append (title); - var sub = new Gtk.Label ("Linux ARM64 · hardware accelerated"); - sub.xalign = 0; - sub.add_css_class ("dim-label"); - text.append (sub); + hero_sub = new Gtk.Label (""); + hero_sub.xalign = 0; + hero_sub.wrap = true; + hero_sub.add_css_class ("dim-label"); + text.append (hero_sub); var pills = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); pills.margin_top = 6; @@ -142,6 +156,35 @@ public class MachinePage : Gtk.Box { hero.append (text); clamp.append (hero); + // Guided first run: shown only for a machine that has never been installed. + setup_card = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + setup_card.add_css_class ("ml-card"); + var setup_head = new Gtk.Label ("Set up in three steps"); + setup_head.xalign = 0; + setup_head.add_css_class ("title-4"); + setup_head.margin_top = 14; + setup_head.margin_start = 16; + setup_head.margin_bottom = 4; + setup_card.append (setup_head); + + get_button = new Gtk.Button.with_label ("Get ISO"); + get_button.valign = Gtk.Align.CENTER; + get_button.clicked.connect (() => { + if (machine.url != null) { + new Gtk.UriLauncher (machine.url).launch.begin ((Gtk.Window) get_root (), null); + } + }); + setup_card.append (step_row (1, "Get an ARM64 installer", + "Download a Linux ARM64 .iso, or use one you already have.", get_button, out step1_detail)); + var pick = new Gtk.Button.with_label ("Choose…"); + pick.valign = Gtk.Align.CENTER; + pick.clicked.connect (() => choose_installer.begin ()); + setup_card.append (step_row (2, "Choose the installer file", + "Pick it here, or drop it on the box below.", pick, out step2_detail)); + setup_card.append (step_row (3, "Start the installer", + "Install inside the window. Shut the guest down when it finishes and Machine Lab boots the new disk from then on.", null, out step3_detail, true)); + clamp.append (setup_card); + var specs = new Gtk.Grid (); specs.column_homogeneous = true; specs.row_homogeneous = true; @@ -177,6 +220,7 @@ public class MachinePage : Gtk.Box { // Connect row var connect = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + connect_card = connect; connect.add_css_class ("ml-card"); connect.add_css_class ("ml-spec"); var term = new Gtk.Image.from_icon_name ("utilities-terminal-symbolic"); @@ -270,6 +314,38 @@ public class MachinePage : Gtk.Box { return scroller; } + private Gtk.Widget step_row (int n, string title, string detail, Gtk.Widget? action, + out Gtk.Label? detail_label, bool last = false) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + row.add_css_class ("ml-form-row"); + if (!last) { + row.add_css_class ("ml-form-sep"); + } + var badge = new Gtk.Label ("%d".printf (n)); + badge.add_css_class ("ml-step-badge"); + badge.valign = Gtk.Align.START; + step_badges += badge; + row.append (badge); + var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + t.hexpand = true; + var tl = new Gtk.Label (title); + tl.xalign = 0; + tl.add_css_class ("ml-spec-value"); + var dl = new Gtk.Label (detail); + dl.xalign = 0; + dl.wrap = true; + dl.ellipsize = Pango.EllipsizeMode.NONE; + dl.add_css_class ("ml-spec-label"); + t.append (tl); + t.append (dl); + row.append (t); + detail_label = dl; + if (action != null) { + row.append (action); + } + return row; + } + private Gtk.Widget spec_card (string icon_name, out Gtk.Label value, string label) { var card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); card.add_css_class ("ml-card"); @@ -492,23 +568,53 @@ public class MachinePage : Gtk.Box { public void refresh () { bool running = machine.running; + bool managed = machine.managed; + bool fresh = machine.is_fresh (); title.label = machine.name; cpu_v.label = machine.cpus == 1 ? "1 virtual CPU" : "%d virtual CPUs".printf (machine.cpus); mem_v.label = "%d GiB".printf (machine.mem_gib); - disk_v.label = "%d GiB".printf (machine.disk_gib); + disk_v.label = machine.disk_gib > 0 ? "%d GiB".printf (machine.disk_gib) : "Disk"; net_v.label = "Shared (NAT)"; ssh_cmd.label = "ssh -p %d localhost".printf (machine.ssh_port); + setup_card.visible = fresh && !managed; + connect_card.visible = !managed && !fresh; + drop_zone.visible = !managed; + more_button.visible = !managed; + get_button.visible = machine.url != null; + hero_sub.label = managed ? (fresh ? "Imported from mseries-vm · installer not finished" : "Installed · imported from mseries-vm") + : fresh ? "New machine · not installed yet" + : "Linux ARM64 · installed"; + + var problems = Readiness.problems (); + bool installing = running && machine.installer != null; start_button.visible = !running; stop_button.visible = running; - start_button.sensitive = true; - start_button.label = machine.installer != null ? "Start Installer" : "Start Machine"; - - status_pill.label = running ? "Running" : "Stopped"; + stop_button.label = managed ? "Shut Down" : "Force Stop"; + start_button.label = fresh ? "Start Installer" : "Start"; + start_button.sensitive = problems.length == 0 && (!fresh || managed || machine.installer != null); + start_button.tooltip_text = problems.length > 0 ? "Fix the items in the setup check first" + : (fresh && !managed && machine.installer == null ? "Choose an installer first" : null); + + status_pill.label = running ? (installing ? "Installing" : "Running") + : fresh ? "Not installed" : "Stopped"; + status_pill.remove_css_class ("running"); + status_pill.remove_css_class ("new"); if (running) { status_pill.add_css_class ("running"); - } else { - status_pill.remove_css_class ("running"); + } else if (fresh) { + status_pill.add_css_class ("new"); + } + + // Step badges: a check once done. + bool have_iso = machine.installer != null; + for (int i = 0; i < step_badges.length; i++) { + bool done = i == 1 && have_iso; + step_badges[i].label = done ? "✓" : "%d".printf (i + 1); + if (done) step_badges[i].add_css_class ("done"); else step_badges[i].remove_css_class ("done"); + } + if (step2_detail != null) { + step2_detail.label = have_iso ? Path.get_basename (machine.installer) : "Pick it here, or drop it on the box below."; } if (machine.installer != null) { @@ -517,8 +623,10 @@ public class MachinePage : Gtk.Box { drop_detail.label = machine.installer; } else { drop_zone.remove_css_class ("chosen"); - drop_title.label = machine.has_disk () ? "Boots from its disk" : "Choose an installer"; - drop_detail.label = "Drop an ARM64 .iso here, or browse for one."; + drop_title.label = !fresh ? "Boots from its disk" : "Drop an installer here"; + drop_detail.label = !fresh + ? "Attach an ARM64 .iso only if you want to reinstall." + : "Drop an ARM64 .iso here, or browse for one."; } for (int i = 0; i < 8; i++) { @@ -530,13 +638,22 @@ public class MachinePage : Gtk.Box { disk_v.label = "%.1f of %d GiB".printf (used / 1073741824.0, machine.disk_gib); } - var problems = Readiness.problems (); ready_card.remove_css_class ("bad"); - if (problems.length == 0) { + int64 free = Readiness.free_gib (); + bool low_space = fresh && free >= 0 && free < machine.disk_gib / 2 + 10; + if (problems.length == 0 && !low_space) { ready_icon.icon_name = "emblem-default-symbolic"; ready_icon.add_css_class ("ml-ok"); ready_title.label = "Ready to run"; - ready_detail.label = "QEMU, UEFI firmware and hardware acceleration are available."; + ready_detail.label = free >= 0 + ? "QEMU, UEFI firmware and acceleration are available. %d GiB free on this Mac.".printf ((int) free) + : "QEMU, UEFI firmware and acceleration are available."; + } else if (problems.length == 0) { + ready_icon.icon_name = "dialog-warning-symbolic"; + ready_icon.remove_css_class ("ml-ok"); + ready_card.add_css_class ("bad"); + ready_title.label = "Low disk space"; + ready_detail.label = "Only %d GiB free on this Mac. The %d GiB disk grows as the guest writes, so free some space or shrink the disk in Settings before installing.".printf ((int) free, machine.disk_gib); } else { ready_icon.icon_name = "dialog-warning-symbolic"; ready_icon.remove_css_class ("ml-ok"); @@ -557,10 +674,12 @@ public class MachinePage : Gtk.Box { disk_scale_v.label = "%d GiB".printf (machine.disk_gib); loading = false; - settings_form.sensitive = !running; - disk_scale.sensitive = !running && !machine.has_disk (); - settings_note.visible = running || machine.has_disk (); - settings_note.label = running + settings_form.sensitive = !running && !managed; + disk_scale.sensitive = !running && !managed && !machine.has_disk (); + settings_note.visible = running || machine.has_disk () || managed; + settings_note.label = managed + ? "This machine is managed by mseries-vm. Change it with: mseries-vm set %s KEY VALUE".printf (machine.vm_name) + : running ? "Stop the machine to change its settings." : "The disk already exists, so its size is fixed. Other settings apply the next time you start."; update_uptime (); From 6c8a709572cba845b4bd9aef19dc5b1e9cbd8402 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:28:45 -0400 Subject: [PATCH 069/798] Render compositor pointer on outputs --- mseries/OS9-HANDOFF.md | 13 ++ rust/pantheon-compositor/README.md | 5 +- rust/pantheon-compositor/src/cursor.rs | 123 +++++++++++++++ rust/pantheon-compositor/src/handlers/mod.rs | 10 +- rust/pantheon-compositor/src/main.rs | 1 + rust/pantheon-compositor/src/native.rs | 27 +++- rust/pantheon-compositor/src/state.rs | 8 +- rust/pantheon-compositor/src/winit.rs | 155 ++++++++++--------- 8 files changed, 259 insertions(+), 83 deletions(-) create mode 100644 rust/pantheon-compositor/src/cursor.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4dfa4a6b7..eb7e1a150 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5468,6 +5468,19 @@ removal lifecycle are still needed for the scoped M-series experience. Next: qualify native output initialization in a separate session or other safe hardware setup, then add dynamic connector lifecycle handling. +### Software pointer rendering — 2026-10-18 + +The compositor now retains Smithay's pointer image status and requests native +redraws when clients change the cursor. It renders client cursor surfaces above +the window space in both Winit and native DRM output paths. Named cursor +requests use a small built-in arrow bitmap; cursor theme loading, named shape +mapping, animation and hardware cursor planes are not implemented. Winit's +host pointer is hidden to avoid drawing a second cursor. Default and +`native-drm` configurations compile; this feature has not been visually +qualified with a Wayland client or on DRM hardware. Next: build both modes and +qualify cursor position/hotspot behavior in nested mode, then native mode when +an isolated session is available. + ### Absolute pointer mapping — 2026-10-18 Absolute pointer events previously mapped normalized coordinates against only diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 952be5998..08f8c7d17 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -53,7 +53,10 @@ M-series/Pantheon behavior documented in [`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize -grabs, clipboard and drag-and-drop. On the first non-empty toplevel commit, +grabs, clipboard, drag-and-drop and a compositor-rendered pointer. Client-set +cursor surfaces are rendered over windows on nested and native outputs; named +cursor requests currently use a built-in arrow fallback, not the installed +elementary cursor theme. On the first non-empty toplevel commit, windows are centered and cascaded on the first output, activated, and given keyboard focus; when the focused toplevel is destroyed, focus moves to the topmost remaining app window. Toplevel destruction removes placement tracking. diff --git a/rust/pantheon-compositor/src/cursor.rs b/rust/pantheon-compositor/src/cursor.rs new file mode 100644 index 000000000..1af20dd35 --- /dev/null +++ b/rust/pantheon-compositor/src/cursor.rs @@ -0,0 +1,123 @@ +use smithay::{ + backend::{ + allocator::Fourcc, + renderer::{ + ImportAll, ImportMem, Renderer, Texture, + element::{ + Kind, Wrap, + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, + }, + }, + }, + input::pointer::{CursorImageAttributes, CursorImageStatus}, + render_elements, + utils::{IsAlive, Scale, Transform}, + wayland::compositor::with_states, +}; + +use crate::PantheonCompositor; + +render_elements! { + pub CursorRenderElement where R: ImportAll + ImportMem; + Surface=WaylandSurfaceRenderElement, + Memory=MemoryRenderBufferRenderElement, + WrappedSurface=Wrap>, +} + +pub fn default_cursor_buffer() -> MemoryRenderBuffer { + const WIDTH: usize = 16; + const HEIGHT: usize = 20; + let mut pixels = vec![0; WIDTH * HEIGHT * 4]; + + for y in 0..HEIGHT { + let (first_x, last_x) = match y { + 0..=12 => (0, y), + 13..=17 => (6, 9), + _ => (6, 8), + }; + for x in first_x..=last_x { + // Build a black outline around the white pointer body and stem. + let outline = x == first_x || x == last_x || y == 12 || y == 18; + let color = if outline { + [0, 0, 0, 255] + } else { + [255, 255, 255, 255] + }; + let offset = (y * WIDTH + x) * 4; + pixels[offset..offset + 4].copy_from_slice(&color); + } + } + + MemoryRenderBuffer::from_slice( + &pixels, + Fourcc::Argb8888, + (WIDTH as i32, HEIGHT as i32), + 1, + Transform::Normal, + None, + ) +} + +pub fn render_cursor_elements( + renderer: &mut R, + state: &PantheonCompositor, + output: &smithay::output::Output, +) -> Vec> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Texture + Clone + Send + 'static, +{ + let Some(output_geometry) = state.space.output_geometry(output) else { + return Vec::new(); + }; + let pointer_location = state.seat.get_pointer().unwrap().current_location(); + if !output_geometry.to_f64().contains(pointer_location) { + return Vec::new(); + } + + let scale = Scale::from(output.current_scale().fractional_scale()); + match &state.cursor_status { + CursorImageStatus::Hidden => Vec::new(), + CursorImageStatus::Surface(surface) if surface.alive() => { + let hotspot = with_states(surface, |states| { + states + .data_map + .get::>() + .and_then(|attributes| { + attributes.lock().ok().map(|attributes| attributes.hotspot) + }) + .unwrap_or_default() + }); + let location = (pointer_location - output_geometry.loc.to_f64() - hotspot.to_f64()) + .to_physical(scale) + .to_i32_round(); + render_elements_from_surface_tree::>( + renderer, + surface, + location, + scale, + 1.0, + Kind::Cursor, + ) + } + CursorImageStatus::Surface(_) | CursorImageStatus::Named(_) => { + let location = (pointer_location - output_geometry.loc.to_f64()) + .to_physical(scale) + .to_i32_round(); + MemoryRenderBufferRenderElement::from_buffer( + renderer, + location, + &state.default_cursor, + None, + None, + None, + Kind::Cursor, + ) + .map(CursorRenderElement::from) + .ok() + .into_iter() + .collect() + } + } +} diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs index 9373ac100..77405cac0 100644 --- a/rust/pantheon-compositor/src/handlers/mod.rs +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -29,7 +29,15 @@ impl SeatHandler for PantheonCompositor { &mut self.seat_state } - fn cursor_image(&mut self, _seat: &Seat, _image: smithay::input::pointer::CursorImageStatus) {} + fn cursor_image( + &mut self, + _seat: &Seat, + image: smithay::input::pointer::CursorImageStatus, + ) { + self.cursor_status = image; + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { let dh = &self.display_handle; diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index b5902ed1f..e76652125 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -1,5 +1,6 @@ #![allow(irrefutable_let_patterns)] +mod cursor; mod display_config; mod grabs; mod handlers; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index e1ddebd01..0087c37ba 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -23,7 +23,6 @@ use smithay::{ egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ - element::surface::WaylandSurfaceRenderElement, gles::GlesRenderer, multigpu::{GpuManager, MultiRenderer, gbm::GbmGlesBackend}, }, @@ -43,7 +42,11 @@ use smithay::{ utils::DeviceFd, }; -use crate::{PantheonCompositor, display_config::OutputSnapshot}; +use crate::{ + PantheonCompositor, + cursor::{CursorRenderElement, render_cursor_elements}, + display_config::OutputSnapshot, +}; pub type GbmRendererBackend = GbmGlesBackend; pub type NativeGpuManager = GpuManager; @@ -135,12 +138,28 @@ impl NativeKmsDevice { continue; } let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; - let elements = smithay::desktop::space::space_render_elements( + let space_elements = smithay::desktop::space::space_render_elements( &mut gpu_renderer, [&state.space], &output.output, 1.0, )?; + let cursor_elements = render_cursor_elements(&mut gpu_renderer, state, &output.output); + let mut elements = space_elements + .into_iter() + .map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + }) + .collect::>(); + elements.extend(cursor_elements); let frame = output.drm_output.render_frame( &mut gpu_renderer, &elements, @@ -264,7 +283,7 @@ impl NativeKmsDevice { let render_elements = DrmOutputRenderElements::< NativeMultiRenderer<'_>, - WaylandSurfaceRenderElement>, + CursorRenderElement>, >::default(); let mut outputs = Vec::with_capacity(connected.len()); let mut x = 0; diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index be1746de1..71cc48fed 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -2,8 +2,9 @@ use std::collections::{HashMap, HashSet}; use std::{ffi::OsString, sync::Arc}; use smithay::{ + backend::renderer::element::memory::MemoryRenderBuffer, desktop::{PopupManager, Space, Window, WindowSurfaceType}, - input::{Seat, SeatState}, + input::{Seat, SeatState, pointer::CursorImageStatus}, reexports::{ calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, wayland_server::{ @@ -23,6 +24,7 @@ use smithay::{ }, }; +use crate::cursor::default_cursor_buffer; use crate::display_config::DisplayConfigHandle; #[cfg(feature = "native-drm")] use crate::native::NativeBackend; @@ -55,6 +57,8 @@ pub struct PantheonCompositor { HashMap, pub initially_positioned: HashSet, pub popups: PopupManager, + pub cursor_status: CursorImageStatus, + pub default_cursor: MemoryRenderBuffer, pub seat: Seat, #[cfg(feature = "native-drm")] @@ -142,6 +146,8 @@ impl PantheonCompositor { managed_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, + cursor_status: CursorImageStatus::default_named(), + default_cursor: default_cursor_buffer(), seat, #[cfg(feature = "native-drm")] native_backend: None, diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 5895faaff..251573082 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -2,9 +2,7 @@ use std::time::Duration; use smithay::{ backend::{ - renderer::{ - damage::OutputDamageTracker, element::surface::WaylandSurfaceRenderElement, gles::GlesRenderer, - }, + renderer::{damage::OutputDamageTracker, gles::GlesRenderer}, winit::{self, WinitEvent}, }, output::{Mode, Output, PhysicalProperties, Subpixel}, @@ -13,6 +11,7 @@ use smithay::{ }; use crate::PantheonCompositor; +use crate::cursor::{CursorRenderElement, render_cursor_elements}; use crate::display_config::OutputSnapshot; pub fn init_winit( @@ -37,7 +36,12 @@ pub fn init_winit( }, ); let _global = output.create_global::(&state.display_handle); - output.change_current_state(Some(mode), Some(Transform::Flipped180), None, Some((0, 0).into())); + output.change_current_state( + Some(mode), + Some(Transform::Flipped180), + None, + Some((0, 0).into()), + ); output.set_preferred(mode); state.space.map_output(&output, (0, 0)); @@ -57,82 +61,81 @@ pub fn init_winit( let mut damage_tracker = OutputDamageTracker::from_output(&output); - event_loop.handle().insert_source(winit, move |event, _, state| { - match event { - WinitEvent::Resized { size, .. } => { - let mode = Mode { - size, - refresh: 60_000, - }; - output.change_current_state( - Some(mode), - None, - None, - None, - ); - state.display_config.update(OutputSnapshot { - connector: "winit".into(), - vendor: "Smithay".into(), - product: "Winit".into(), - serial: "Nested".into(), - width: mode.size.w, - height: mode.size.h, - refresh_millihz: mode.refresh, - scale: 1.0, - x: 0, - y: 0, - primary: true, - }); - } - WinitEvent::Input(event) => state.process_input_event(event), - WinitEvent::Redraw => { - let size = backend.window_size(); - let damage = Rectangle::from_size(size); - - { - let (renderer, mut framebuffer) = backend.bind().unwrap(); - smithay::desktop::space::render_output::< - _, - WaylandSurfaceRenderElement, - _, - _, - >( - &output, - renderer, - &mut framebuffer, - 1.0, - 0, - [&state.space], - &[], - &mut damage_tracker, - [0.1, 0.1, 0.1, 1.0], - ) - .unwrap(); + event_loop + .handle() + .insert_source(winit, move |event, _, state| { + match event { + WinitEvent::Resized { size, .. } => { + let mode = Mode { + size, + refresh: 60_000, + }; + output.change_current_state(Some(mode), None, None, None); + state.display_config.update(OutputSnapshot { + connector: "winit".into(), + vendor: "Smithay".into(), + product: "Winit".into(), + serial: "Nested".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + scale: 1.0, + x: 0, + y: 0, + primary: true, + }); } - backend.submit(Some(&[damage])).unwrap(); + WinitEvent::Input(event) => state.process_input_event(event), + WinitEvent::Redraw => { + let size = backend.window_size(); + let damage = Rectangle::from_size(size); - state.space.elements().for_each(|window| { - window.send_frame( - &output, - state.start_time.elapsed(), - Some(Duration::ZERO), - |_, _| Some(output.clone()), - ) - }); + { + backend.window().set_cursor_visible(false); + let (renderer, mut framebuffer) = backend.bind().unwrap(); + let cursor_elements = render_cursor_elements(renderer, state, &output); + smithay::desktop::space::render_output::< + _, + CursorRenderElement, + _, + _, + >( + &output, + renderer, + &mut framebuffer, + 1.0, + 0, + [&state.space], + &cursor_elements, + &mut damage_tracker, + [0.1, 0.1, 0.1, 1.0], + ) + .unwrap(); + } + backend.submit(Some(&[damage])).unwrap(); - state.space.refresh(); - state.popups.cleanup(); - let _ = state.display_handle.flush_clients(); + state.space.elements().for_each(|window| { + window.send_frame( + &output, + state.start_time.elapsed(), + Some(Duration::ZERO), + |_, _| Some(output.clone()), + ) + }); - // Ask for redraw to schedule new frame. - backend.window().request_redraw(); - } - WinitEvent::CloseRequested => { - state.loop_signal.stop(); - } - _ => (), - }; - })?; + state.space.refresh(); + state.popups.cleanup(); + let _ = state.display_handle.flush_clients(); + + // Ask for redraw to schedule new frame. + backend.window().request_redraw(); + } + WinitEvent::CloseRequested => { + state.loop_signal.stop(); + } + _ => (), + }; + })?; Ok(()) } From baf935021eea744e846cc8e41ecae07e61268058 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:32:03 -0400 Subject: [PATCH 070/798] Add systemd readiness notification support --- mseries/OS9-HANDOFF.md | 20 ++++++++++++-- mseries/ROADMAP.md | 7 +++++ rust/pantheon-compositor/README.md | 6 +++++ rust/pantheon-compositor/src/main.rs | 39 ++++++++++++++++++++++++---- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index eb7e1a150..3b7f4cfb8 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5502,5 +5502,21 @@ destroyed toplevel had keyboard focus, focus moves to the topmost remaining non-panel window (or clears if none remains); destroying a background window does not steal focus. Default and `native-drm` feature `cargo check --bins` both pass locally. Runtime window launch/focus/close behavior remains -unverified; this does not add native DRM startup, and native session work -remains the next major milestone. +unverified. The opt-in native DRM startup path is now implemented but has not +been launched on this active desktop; isolated session qualification and +dynamic connector lifecycle remain open. + +### systemd readiness notification — 2026-10-18 + +Added opt-in `--notify-systemd` support. After the Wayland listener and chosen +backend are initialized, it sends `READY=1` and a status string to the +`NOTIFY_SOCKET`, supporting both filesystem and Linux abstract Unix sockets. +For native startup, this point follows renderer and connected-output +initialization. Missing notification socket or send errors fail startup when +the flag is supplied. Updated the compositor README and roadmap. The active +session, installed service units and login chooser were not changed. Default +and `native-drm` `cargo build -p pantheon-compositor --bins` both pass locally; +no runtime notification or native display check was performed. Next, design a +distinct opt-in user service/session entry after validating the installed +Pantheon session-manager startup contract. No native runtime qualification is +claimed. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index cd549f004..712f14a65 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -65,6 +65,13 @@ unqualified. Local `cargo check --bins` and `cargo build --bins` pass both with and without `native-drm`; they establish compilation only, not successful connector modesetting. +The compositor can now optionally send systemd `READY=1` after its Wayland +listener and selected backend initialize (`--notify-systemd`). On native +startup, readiness follows renderer and connected-output initialization. This +provides a truthful readiness boundary for a future opt-in service; it does +not add or install a session unit, and native runtime/session behavior remains +unqualified. + ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 08f8c7d17..f538b5660 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -31,6 +31,12 @@ Native startup acquires the active libseat session and initializes a DRM/KMS output. It takes over the display device, so do not launch it inside the currently active desktop. It is not yet runtime-qualified on M-series hardware. +For a future systemd user service, `--notify-systemd` sends `READY=1` to +`NOTIFY_SOCKET` after the Wayland listener and selected backend initialize. On +the native path, that includes renderer and connected-output initialization. +The flag fails startup if systemd did not provide the notification socket. No +service unit or login-session entry is installed by the compositor build. + To exercise the Pantheon protocol against the nested socket, build both binaries and start the compositor with its probe client: diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index e76652125..39a486075 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -15,11 +15,16 @@ mod winit; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; pub use state::PantheonCompositor; +use std::{ + io, + os::{linux::net::SocketAddrExt, unix::net::UnixDatagram}, + path::Path, +}; fn main() -> Result<(), Box> { init_logging(); - let (backend, command) = parse_arguments()?; + let (backend, notify_systemd, command) = parse_arguments()?; let mut event_loop: EventLoop = EventLoop::try_new()?; let display: Display = Display::new()?; let mut state = match backend { @@ -58,6 +63,10 @@ fn main() -> Result<(), Box> { // Launch requested clients against the compositor socket selected above. unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; + if notify_systemd { + notify_systemd_ready()?; + } + if let Some((program, args)) = command { std::process::Command::new(program).args(args).spawn()?; } @@ -74,6 +83,25 @@ fn init_logging() { } } +/// Tell a systemd user service that the compositor has initialized its +/// Wayland socket and selected backend. Native startup only reaches this point +/// after the renderer and connected KMS outputs have been initialized. +fn notify_systemd_ready() -> io::Result<()> { + let socket = std::env::var_os("NOTIFY_SOCKET") + .ok_or_else(|| io::Error::other("--notify-systemd requires NOTIFY_SOCKET"))?; + let encoded_socket = socket.as_encoded_bytes(); + let address = if let Some(abstract_name) = encoded_socket.strip_prefix(b"@") { + std::os::unix::net::SocketAddr::from_abstract_name(abstract_name)? + } else { + std::os::unix::net::SocketAddr::from_pathname(Path::new(&socket))? + }; + + let notification = UnixDatagram::unbound()?; + notification.connect_addr(&address)?; + notification.send(b"READY=1\nSTATUS=Wayland socket and compositor backend initialized")?; + Ok(()) +} + #[derive(Clone, Copy)] enum BackendChoice { Nested, @@ -81,9 +109,10 @@ enum BackendChoice { } fn parse_arguments() --> Result<(BackendChoice, Option<(String, Vec)>), Box> { +-> Result<(BackendChoice, bool, Option<(String, Vec)>), Box> { let mut args = std::env::args().skip(1); let mut backend = BackendChoice::Nested; + let mut notify_systemd = false; let mut command = None; while let Some(arg) = args.next() { match arg.as_str() { @@ -99,11 +128,11 @@ fn parse_arguments() command = Some((program, args.collect())); break; } + "--notify-systemd" => notify_systemd = true, _ => return Err( - "usage: pantheon-compositor [--backend nested|native] [--run PROGRAM [ARGS...]]" - .into(), + "usage: pantheon-compositor [--backend nested|native] [--notify-systemd] [--run PROGRAM [ARGS...]]".into(), ), } } - Ok((backend, command)) + Ok((backend, notify_systemd, command)) } From e9f1f20fa1ae7f2b4ad86907e1d538e384b237b4 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:33:44 -0400 Subject: [PATCH 071/798] Machine Lab: merge Codex Omarchy auto-install launcher, add pause/resume (QMP), docs and handoff Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 10 + mseries/machine-lab/README.md | 33 +- mseries/machine-lab/data/style.css | 2 + .../launcher/prepare-latest-omarchy.py | 433 ++++++++++++++++++ mseries/machine-lab/launcher/run-vm.sh | 98 ++-- mseries/machine-lab/meson.build | 4 + mseries/machine-lab/src/machine.vala | 180 +++++++- mseries/machine-lab/src/main.vala | 14 +- mseries/machine-lab/src/page.vala | 94 +++- 9 files changed, 813 insertions(+), 55 deletions(-) create mode 100755 mseries/machine-lab/launcher/prepare-latest-omarchy.py diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 9dd5518a2..5ac3de5cf 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -4520,3 +4520,13 @@ References: OpenCode `214fb7fdbab9ad13773629839f1178382400cf97`, auth/switch.ts, provider/opencode.ts and integration.connection.resolve (native activation event and refresh behavior); https://dev.opencode.ai/v2/docs/cli/commands/ and /providers/. No upstream code was copied. CodexBar remains pinned to 0.77.0. Publish, merge to main and deploy as explicitly requested. + +## 2026-10-18: Machine Lab (native VM app) + +`mseries/machine-lab/` is the elementary-styled VM app (Vala/GTK4/Granite 7); `bash mseries/install-machine-lab.sh` +installs it to `~/.local` and pins it in the dock. Merged in the Codex spike's Omarchy nightly updater and +unattended-install launcher; added pause/resume (QMP), import of `mseries-vm` guests, new-vs-installed panels, +free-space warning. Verified: builds, Broadway renders, pause/resume against a headless QEMU, `--check` of the +Omarchy nightly. Not verified: a complete guest install/boot (~6 GiB download; host had ~19 GiB free) and +suspend/resume of the app itself. Next: run the Omarchy automatic install once space is freed, and lift +`~/Documents/Codex/2026-10-18/f-2/work/youtube-iso-test` (42 GB, owner decides) if space is needed. diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md index 244a003fa..ca557f35b 100644 --- a/mseries/machine-lab/README.md +++ b/mseries/machine-lab/README.md @@ -1,18 +1,31 @@ # Machine Lab -Native elementary OS look (Vala + GTK 4 + Granite 7) for running another Linux ARM64 -guest under QEMU/KVM with NAT networking. Flat sidebar of machines, hero header, spec cards, -multiple machines saved in ~/.config/machine-lab/machines.ini, Overview / Console / Settings tabs, -per-machine CPU, memory, disk and SSH port, drag-and-drop installer, live status and uptime, -streamed QEMU console, copyable SSH command, empty-state welcome, Ctrl+N, toasts, and -light/dark + accent following the system. +Native elementary OS look (Vala + GTK 4 + Granite 7) for running other Linux ARM64 guests under +QEMU/KVM with NAT networking. Install for the current user and pin it to the dock with +`bash mseries/install-machine-lab.sh`. + +**Design:** flat sidebar of machines ("YOUR MACHINES" vs "NEW — NOT INSTALLED"), preset gallery, +Overview / Console / Settings tabs, per-machine color, status pill with uptime, readiness and +free-space check, toasts, light/dark and accent following the system. + +**Function:** +- Imports guests already managed by `mseries/vms/mseries-vm` (`~/vms/*/vm.conf`) and starts/stops them through it. +- Native machines saved in `~/.config/machine-lab/machines.ini`, with CPU, memory, disk and SSH port settings. +- **Omarchy, automatically** (merged from the Codex spike): `launcher/prepare-latest-omarchy.py` checks Omarchy's + latest ARM64 nightly, downloads and SHA-256-verifies the ISO, builds unattended-install media, and boots + QEMU. Sign-in details are shown in the app and saved with owner-only permissions. +- Own ISO path: drop or choose any ARM64 `.iso`; once the disk holds real data the installer detaches itself. +- **Pause / Resume** over QEMU's QMP socket (`qmp.sock` in the machine folder), plus Shut Down / Force Stop. +- Live console from the launcher output, or the guest serial log for imported machines. ```sh meson setup build && meson compile -C build -./build/machine-lab # finds ../launcher/run-vm.sh, or set MACHINE_LAB_LAUNCHER +./build/machine-lab # finds ../launcher/run-vm.sh; MACHINE_LAB_LAUNCHER overrides ``` -Launcher and known gaps: `launcher/README.md`. Moved here from the Codex native-vm-poc spike. -Visual check was done headless via GTK Broadway; not yet launched on the desktop or booted with a guest ISO. +Layout: `src/machine.vala` (model, QMP, process control, discovery), `src/page.vala` (machine pane), +`src/main.vala` (window, sidebar, presets), `launcher/` (QEMU launcher and Omarchy updater). -Also: per-machine color (elementary palette), distro presets from New Machine, host readiness check (QEMU, UEFI, /dev/kvm), real disk usage, pulsing running indicator. +**Not verified:** a full guest install and boot end to end (the Omarchy nightly is ~6 GiB and the host had +~19 GiB free). Pause/resume was verified against a headless QEMU through the app. Visual checks used GTK Broadway. +Provenance: started as the Codex `native-vm-poc` spike; its launcher and Omarchy updater are merged here. diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css index 21155d500..e8f363d18 100644 --- a/mseries/machine-lab/data/style.css +++ b/mseries/machine-lab/data/style.css @@ -154,3 +154,5 @@ textview.ml-console, textview.ml-console text { box-shadow: 0 0 0 1px alpha(@fg_color, 0.1), 0 1px 3px alpha(black, 0.12); } .ml-gallery-card:hover { box-shadow: 0 0 0 2px @accent_color, 0 2px 8px alpha(black, 0.18); } + +.ml-pill.paused { color: #946800; background: alpha(#f9c440, 0.35); } diff --git a/mseries/machine-lab/launcher/prepare-latest-omarchy.py b/mseries/machine-lab/launcher/prepare-latest-omarchy.py new file mode 100755 index 000000000..9e0d08ba0 --- /dev/null +++ b/mseries/machine-lab/launcher/prepare-latest-omarchy.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +"""Fetch and verify Omarchy's current published ARM64 nightly, then boot it.""" + +import hashlib +import json +import os +import re +import secrets +import shutil +import subprocess +import sys +import urllib.error +import urllib.request +from pathlib import Path +from urllib.parse import urlparse + +METADATA_URL = "https://nightly.omarchy.org/edge-aarch64/latest.json" +ALLOWED_HOST = "nightly.omarchy.org" +CHUNK_SIZE = 1024 * 1024 +MIN_FREE_BYTES = 10 * 1024 * 1024 * 1024 +EXISTING_VM_FREE_BYTES = 2 * 1024 * 1024 * 1024 + + +def status(message: str) -> None: + print(f"STATUS\t{message}", flush=True) + + +def fail(message: str, code: int = 1) -> None: + status(f"ERROR: {message}") + raise SystemExit(code) + + +def fetch_metadata() -> dict: + request = urllib.request.Request( + METADATA_URL, + headers={"User-Agent": "MachineLab/0.1 (Omarchy ARM64 VM setup)"}, + ) + with urllib.request.urlopen(request, timeout=30) as response: + metadata = json.load(response) + if metadata.get("channel") != "edge-aarch64" or metadata.get("arch") != "aarch64": + fail("Omarchy's latest metadata is not for the ARM64 edge channel.") + version = metadata.get("version") + digest = metadata.get("sha256") + size = metadata.get("size") + url = metadata.get("url") + if not isinstance(version, str) or not re.fullmatch(r"[0-9A-Za-z._-]+", version): + fail("Omarchy returned an invalid image version.") + if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest): + fail("Omarchy returned an invalid SHA-256 digest.") + if not isinstance(size, int) or size <= 0: + fail("Omarchy returned an invalid image size.") + expected_name = f"omarchy-edge-{version}-aarch64.iso" + parsed = urlparse(url if isinstance(url, str) else "") + if parsed.scheme != "https" or parsed.hostname != ALLOWED_HOST or parsed.path != f"/edge-aarch64/{version}/{expected_name}": + fail("Omarchy returned an image URL outside the expected nightly path.") + metadata["expected_name"] = expected_name + return metadata + + +def digest_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + while chunk := source.read(CHUNK_SIZE): + digest.update(chunk) + return digest.hexdigest() + + +def disk_installed(disk: Path) -> bool: + """An empty qcow2 header is not an installation: require real data on disk.""" + return disk.is_file() and disk.stat().st_blocks * 512 >= 1024 ** 3 + + +def existing_path(path: Path) -> Path: + """Return the nearest existing directory so disk_usage works before setup.""" + candidate = path + while not candidate.exists(): + candidate = candidate.parent + return candidate + + +def check_space(metadata: dict, vm_dir: Path, cache: Path) -> dict: + disk = vm_dir / "disk.qcow2" + existing_vm = disk_installed(disk) + image = cache / metadata["expected_name"] + part = cache / (metadata["expected_name"] + ".part") + if existing_vm: + download_remaining = 0 + vm_required = EXISTING_VM_FREE_BYTES + cache_required = 0 + else: + partial_size = part.stat().st_size if part.exists() else 0 + download_remaining = metadata["size"] if partial_size >= metadata["size"] else metadata["size"] - partial_size + if image.is_file() and image.stat().st_size == metadata["size"]: + # The launch helper still verifies the checksum, but no more download space is needed. + download_remaining = 0 + vm_required = MIN_FREE_BYTES + cache_required = download_remaining + + cache_location = existing_path(cache) + vm_location = existing_path(vm_dir) + cache_free = shutil.disk_usage(cache_location).free + vm_free = shutil.disk_usage(vm_location).free + same_filesystem = cache_location.stat().st_dev == vm_location.stat().st_dev + if existing_vm: + enough = vm_free >= vm_required + elif same_filesystem: + # The install reserve and image download consume the same free-space pool. + vm_required += cache_required + cache_required = vm_required + enough = min(cache_free, vm_free) >= vm_required + else: + enough = cache_free >= cache_required and vm_free >= vm_required + + return { + "existing_vm": existing_vm, + "download_remaining": download_remaining, + "cache_free": cache_free, + "vm_free": vm_free, + "cache_required": cache_required, + "vm_required": vm_required, + "same_filesystem": same_filesystem, + "enough": enough, + } + + +def runtime_prerequisites(existing_vm: bool) -> list[str]: + problems = [] + missing = [tool for tool in ("qemu-system-aarch64", "qemu-img") if shutil.which(tool) is None] + if missing: + problems.append("Install " + " and ".join(missing)) + if not os.access("/dev/kvm", os.R_OK | os.W_OK): + problems.append("KVM access is unavailable to this user (/dev/kvm)") + if not Path("/usr/share/AAVMF/AAVMF_CODE.fd").is_file() or not Path("/usr/share/AAVMF/AAVMF_VARS.fd").is_file(): + problems.append("AAVMF UEFI firmware is missing") + if not existing_vm: + missing = [tool for tool in ("openssl", "genisoimage") if shutil.which(tool) is None] + if missing: + problems.append("Install " + " and ".join(missing)) + return problems + + +def print_check(vm_dir: Path, cache: Path) -> None: + metadata = fetch_metadata() + space = check_space(metadata, vm_dir, cache) + problems = runtime_prerequisites(space["existing_vm"]) + fields = [ + metadata["version"], metadata.get("built_at", "unknown"), + metadata.get("omarchy_version", "unknown"), str(metadata["size"]), + "1" if space["existing_vm"] else "0", + str(space["cache_free"]), str(space["vm_free"]), + str(space["cache_required"]), str(space["vm_required"]), + str(space["download_remaining"]), + "1" if space["enough"] else "0", + "1" if space["same_filesystem"] else "0", + (" ".join(problems).replace("\t", " ").replace("\n", " ") or "-"), + ] + print("CHECK\t" + "\t".join(fields), flush=True) + + +def get_image(metadata: dict, cache: Path, vm_dir: Path) -> Path: + cache.mkdir(parents=True, exist_ok=True) + image = cache / metadata["expected_name"] + part = cache / (metadata["expected_name"] + ".part") + expected_size = metadata["size"] + expected_hash = metadata["sha256"] + + if image.exists() and image.stat().st_size == expected_size: + status("Checking cached ISO checksum…") + if digest_file(image) == expected_hash: + return image + image.unlink() + + offset = part.stat().st_size if part.exists() else 0 + if offset >= expected_size: + part.unlink() + offset = 0 + space = check_space(metadata, vm_dir, cache) + if not space["enough"]: + fail( + "Not enough free space to download and install Omarchy safely. " + f"Download storage needs {(space['cache_required'] / 1024**3):.1f} GiB " + f"and has {(space['cache_free'] / 1024**3):.1f} GiB; " + f"VM storage needs {(space['vm_required'] / 1024**3):.1f} GiB " + f"and has {(space['vm_free'] / 1024**3):.1f} GiB." + ) + + headers = {"User-Agent": "MachineLab/0.1 (Omarchy ARM64 VM setup)"} + if offset: + headers["Range"] = f"bytes={offset}-" + status(f"Resuming Omarchy {metadata['omarchy_version']} ARM64 nightly at {(offset / 1024**3):.1f} GiB…") + else: + status(f"Downloading Omarchy {metadata['omarchy_version']} ARM64 nightly ({expected_size / 1024**3:.1f} GiB)…") + + try: + response = urllib.request.urlopen( + urllib.request.Request(metadata["url"], headers=headers), timeout=60 + ) + except (urllib.error.URLError, TimeoutError) as error: + fail(f"Could not download the Omarchy image: {error}") + + if offset and response.status != 206: + response.close() + offset = 0 + response = urllib.request.urlopen( + urllib.request.Request(metadata["url"], headers={"User-Agent": headers["User-Agent"]}), + timeout=60, + ) + mode = "ab" if offset else "wb" + received = offset + last_report = -1 + try: + with response, part.open(mode) as target: + while chunk := response.read(CHUNK_SIZE): + target.write(chunk) + received += len(chunk) + percent = min(100, int(received * 100 / expected_size)) + if percent != last_report: + status(f"Downloading Omarchy {metadata['omarchy_version']} ARM64 nightly… {percent}%") + last_report = percent + except (OSError, urllib.error.URLError, TimeoutError) as error: + fail(f"Download stopped; the partial file is kept so the next attempt can resume: {error}") + + if part.stat().st_size != expected_size: + fail(f"Downloaded image size does not match metadata ({part.stat().st_size} of {expected_size} bytes). The partial file is kept for retry.") + status("Verifying Omarchy image SHA-256…") + if digest_file(part) != expected_hash: + part.unlink(missing_ok=True) + fail("The downloaded image failed its SHA-256 check and was deleted.") + part.replace(image) + return image + + +def create_autoinstall_seed(metadata: dict, vm_dir: Path) -> tuple[Path, str]: + required = ("openssl", "genisoimage") + missing = [tool for tool in required if shutil.which(tool) is None] + if missing: + fail("Install the required tools first: " + ", ".join(missing)) + + vm_dir.mkdir(parents=True, exist_ok=True, mode=0o700) + os.chmod(vm_dir, 0o700) + password_file = vm_dir / "credentials.txt" + if password_file.exists(): + saved = password_file.read_text().splitlines() + password = next((line.split(": ", 1)[1] for line in saved if line.startswith("Sudo password: ")), "") + if not password: + fail(f"The saved VM credentials file is invalid: {password_file}") + else: + password = secrets.token_hex(24) + fd = os.open(password_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "w") as output: + output.write(f"Username: omarchy\nSudo password: {password}\n") + + hash_result = subprocess.run( + ["openssl", "passwd", "-6", "-stdin"], + check=True, + capture_output=True, + text=True, + input=password + "\n", + ) + password_hash = hash_result.stdout.strip() + gib = 1024**3 + mib = 1024**2 + disk_bytes = 64 * gib + boot_start = mib + boot_size = 2 * gib + main_start = boot_start + boot_size + main_size = disk_bytes - main_start - mib + seed_dir = vm_dir / f"autoinstall-{metadata['version']}" + seed_dir.mkdir(mode=0o700, parents=True, exist_ok=True) + + configuration = { + "app_config": None, + "archinstall-language": "English", + "auth_config": {}, + "audio_config": {"audio": "pipewire"}, + "bootloader_config": {"bootloader": "Limine", "uki": False, "removable": False}, + "custom_commands": [], + "omarchy_install": { + "mode": "full_disk", + "defer_provisioning": False, + "target_mount": "/mnt", + "boot": { + "esp_mount": "/boot", + "esp_path": "/EFI/limine", + "efi_binary": "limine_aa64.efi", + "enable_fallback": True, + }, + "storage": {"kernel": "linux-omarchy"}, + }, + "disk_config": { + "config_type": "default_layout", + "device_modifications": [ + { + "device": "/dev/vda", + "partitions": [ + { + "btrfs": [], + "dev_path": None, + "flags": ["boot", "esp"], + "fs_type": "fat32", + "mount_options": [], + "mountpoint": "/boot", + "obj_id": "ea21d3f2-82bb-49cc-ab5d-6f81ae94e18d", + "size": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": boot_size}, + "start": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": boot_start}, + "status": "create", + "type": "primary", + }, + { + "btrfs": [ + {"mountpoint": "/", "name": "@"}, + {"mountpoint": "/home", "name": "@home"}, + {"mountpoint": "/var/log", "name": "@log"}, + {"mountpoint": "/var/cache/pacman/pkg", "name": "@pkg"}, + ], + "dev_path": None, + "flags": [], + "fs_type": "btrfs", + "mount_options": ["compress=zstd"], + "mountpoint": None, + "obj_id": "8c2c2b92-1070-455d-b76a-56263bab24aa", + "size": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": main_size}, + "start": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": main_start}, + "status": "create", + "type": "primary", + }, + ], + "wipe": True, + } + ], + }, + "hostname": "omarchy-vm", + "kernels": ["linux-omarchy"], + "network_config": {"type": "iso"}, + "ntp": True, + "parallel_downloads": 8, + "script": None, + "services": [], + "swap": True, + "timezone": "UTC", + "locale_config": {"kb_layout": "us", "sys_enc": "UTF-8", "sys_lang": "en_US.UTF-8"}, + "mirror_config": { + "custom_repositories": [], + "custom_servers": [{"url": "https://arm-mirror.omarchy.org/$arch/$repo"}], + "mirror_regions": {}, + "optional_repositories": [], + }, + "packages": ["base-devel", "git", "omarchy-keyring", "omarchy-settings-dev", "omarchy-dev"], + "profile_config": {"gfx_driver": None, "greeter": None, "profile": {}}, + "version": "3.0.9", + } + credentials = { + "root_enc_password": password_hash, + "users": [{"enc_password": password_hash, "groups": [], "sudo": True, "username": "omarchy"}], + } + (seed_dir / "user_configuration.json").write_text(json.dumps(configuration, indent=2) + "\n") + (seed_dir / "user_credentials.json").write_text(json.dumps(credentials, indent=2) + "\n") + (seed_dir / "user_encrypt_installation.txt").write_text("false\n") + + cidata = vm_dir / f"cidata-{metadata['version']}.iso" + temporary = cidata.with_suffix(".iso.part") + try: + subprocess.run( + ["genisoimage", "-quiet", "-output", str(temporary), "-volid", "cidata", "-joliet", "-rock", str(seed_dir)], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as error: + fail(f"Could not create Omarchy's unattended install seed: {error.stderr.strip()}") + temporary.replace(cidata) + return cidata, password + + +def emit_saved_credentials(vm_dir: Path) -> None: + password_file = vm_dir / "credentials.txt" + if password_file.is_file(): + credentials = password_file.read_text().replace("\n", " · ").strip(" ·") + print(f"CREDENTIAL\t{credentials} · saved in {password_file}", flush=True) + + +def main() -> None: + if len(sys.argv) == 4 and sys.argv[1] == "--check": + try: + print_check(Path(sys.argv[2]), Path(sys.argv[3])) + except urllib.error.URLError as error: + fail(f"Could not check Omarchy's latest version: {error}") + return + if len(sys.argv) != 4: + fail("Internal updater arguments are invalid.") + launcher, vm_dir, cache_dir = map(Path, sys.argv[1:]) + if not launcher.is_file() or not os.access(launcher, os.X_OK): + fail("The QEMU launcher could not be found.") + + missing = [tool for tool in ("qemu-system-aarch64", "qemu-img") if shutil.which(tool) is None] + if missing: + fail("Install the required tools first: " + ", ".join(missing)) + if not os.access("/dev/kvm", os.R_OK | os.W_OK): + fail("KVM is unavailable to this user. Check /dev/kvm permissions.") + if not Path("/usr/share/AAVMF/AAVMF_CODE.fd").is_file() or not Path("/usr/share/AAVMF/AAVMF_VARS.fd").is_file(): + fail("AAVMF UEFI firmware was not found under /usr/share/AAVMF.") + + # Starting an existing installation must not silently replace/update it + # from a newly downloaded installer. Omarchy updates its installed guest + # through its own updater, run inside the VM. + disk = vm_dir / "disk.qcow2" + if disk_installed(disk): + cached_images = sorted(cache_dir.glob("omarchy-edge-*-aarch64.iso")) + if cached_images: + status("Starting the existing VM. To update installed Omarchy, run `omarchy update` inside the guest.") + emit_saved_credentials(vm_dir) + os.execv(str(launcher), [str(launcher), str(cached_images[-1]), str(vm_dir)]) + status("Starting the existing VM without installer media. To update installed Omarchy, run `omarchy update` inside the guest.") + emit_saved_credentials(vm_dir) + os.execv(str(launcher), [str(launcher), "--disk-only", str(vm_dir)]) + + missing = [tool for tool in ("openssl", "genisoimage") if shutil.which(tool) is None] + if missing: + fail("Install the required tools first: " + ", ".join(missing)) + try: + status("Checking Omarchy's current ARM64 nightly…") + metadata = fetch_metadata() + image = get_image(metadata, cache_dir, vm_dir) + cidata, password = create_autoinstall_seed(metadata, vm_dir) + except urllib.error.URLError as error: + fail(f"Could not check Omarchy's latest version: {error}") + status(f"Installing Omarchy {metadata['omarchy_version']} (nightly {metadata['version']}) automatically. The VM will reboot into the installed guest when setup finishes.") + print(f"CREDENTIAL\tUsername: omarchy · sudo password: {password} · saved in {vm_dir / 'credentials.txt'}", flush=True) + os.execv(str(launcher), [str(launcher), str(image), str(vm_dir), str(cidata)]) + + +if __name__ == "__main__": + main() diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh index c02144723..ade2a912b 100755 --- a/mseries/machine-lab/launcher/run-vm.sh +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -1,21 +1,35 @@ #!/usr/bin/env bash +# Boot a Linux ARM64 guest under QEMU/KVM with user-mode NAT. +# run-vm.sh ISO [VM_DIR] [CIDATA_ISO] boot an installer (ISO "-" = none) +# run-vm.sh --disk-only VM_DIR boot the installed disk +# Hardware comes from the environment: MACHINE_CPUS, MACHINE_MEM_GIB, +# MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME. A QMP socket (qmp.sock in +# VM_DIR) lets Machine Lab pause and resume the guest. set -euo pipefail -if [ "$#" -lt 1 ]; then - echo "Usage: $0 ARM64_INSTALL_ISO [VM_DIRECTORY]" >&2 - exit 2 -fi - -ISO_PATH=$1 # "-" boots the installed disk without an installer CPUS=${MACHINE_CPUS:-4} MEM_GIB=${MACHINE_MEM_GIB:-6} DISK_GIB=${MACHINE_DISK_GIB:-64} SSH_PORT=${MACHINE_SSH_PORT:-2222} VM_NAME=${MACHINE_NAME:-Omarchy-ARM64} -if [ "$#" -ge 2 ]; then + +if [ "${1:-}" = "--disk-only" ]; then + if [ "$#" -ne 2 ]; then + echo "Usage: $0 --disk-only VM_DIRECTORY" >&2 + exit 2 + fi + ISO_PATH="" + CIDATA_PATH="" VM_DIR=$2 else - VM_DIR=./omarchy-arm64-vm + if [ "$#" -lt 1 ]; then + echo "Usage: $0 ARM64_INSTALL_ISO [VM_DIRECTORY] [CIDATA_ISO]" >&2 + exit 2 + fi + ISO_PATH=$1 + [ "$ISO_PATH" = "-" ] && ISO_PATH="" + VM_DIR=${2:-./omarchy-arm64-vm} + CIDATA_PATH=${3:-} fi QEMU=$(command -v qemu-system-aarch64 || true) @@ -31,10 +45,14 @@ if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then echo "KVM is unavailable to this user. Check /dev/kvm permissions." >&2 exit 1 fi -if [ "$ISO_PATH" != "-" ] && [ ! -r "$ISO_PATH" ]; then +if [ -n "$ISO_PATH" ] && [ ! -r "$ISO_PATH" ]; then echo "Cannot read installer image: $ISO_PATH" >&2 exit 1 fi +if [ -n "$CIDATA_PATH" ] && [ ! -r "$CIDATA_PATH" ]; then + echo "Cannot read unattended-install seed: $CIDATA_PATH" >&2 + exit 1 +fi if [ ! -r "$FW_CODE" ] || [ ! -r "$FW_VARS" ]; then echo "AAVMF UEFI firmware was not found under /usr/share/AAVMF." >&2 exit 1 @@ -47,28 +65,48 @@ fi if [ ! -e "$VM_DIR/disk.qcow2" ]; then "$QEMU_IMG" create -f qcow2 "$VM_DIR/disk.qcow2" "${DISK_GIB}G" fi +rm -f "$VM_DIR/qmp.sock" -CD_ARGS=() -if [ "$ISO_PATH" != "-" ]; then - CD_ARGS=(-drive if=none,id=installer,format=raw,media=cdrom,readonly=on,file="$ISO_PATH" - -device scsi-cd,drive=installer,bus=scsi.0) +QEMU_ARGS=( + -name "$VM_NAME" + -machine virt,gic-version=3,msi=gicv2m,acpi=off + -accel kvm + -cpu host + -smp "$CPUS" + -m "${MEM_GIB}G" + -drive if=pflash,format=raw,readonly=on,file="$FW_CODE" + -drive if=pflash,format=raw,file="$VM_DIR/efi-vars.fd" + -drive if=none,id=system,format=qcow2,file="$VM_DIR/disk.qcow2" + -device virtio-blk-pci,drive=system,bootindex=1 +) + +if [ -n "$ISO_PATH" ] || [ -n "$CIDATA_PATH" ]; then + QEMU_ARGS+=(-device virtio-scsi-pci,id=scsi) +fi +if [ -n "$ISO_PATH" ]; then + QEMU_ARGS+=( + -drive if=none,id=installer,format=raw,media=cdrom,readonly=on,file="$ISO_PATH" + -device scsi-cd,drive=installer,bus=scsi.0,bootindex=2 + ) +fi +if [ -n "$CIDATA_PATH" ]; then + QEMU_ARGS+=( + -drive if=none,id=cidata,format=raw,media=cdrom,readonly=on,file="$CIDATA_PATH" + -device scsi-cd,drive=cidata,bus=scsi.0,bootindex=3 + ) fi -exec "$QEMU" \ - -name "$VM_NAME" \ - -machine virt \ - -accel kvm \ - -cpu host \ - -smp "$CPUS" \ - -m "${MEM_GIB}G" \ - -drive if=pflash,format=raw,readonly=on,file="$FW_CODE" \ - -drive if=pflash,format=raw,file="$VM_DIR/efi-vars.fd" \ - -drive if=none,id=system,format=qcow2,file="$VM_DIR/disk.qcow2" \ - -device virtio-blk-pci,drive=system \ - -device virtio-scsi-pci,id=scsi \ - "${CD_ARGS[@]}" \ - -netdev user,id=net0,hostfwd=tcp:127.0.0.1:"$SSH_PORT"-:22 \ - -device virtio-net-pci,netdev=net0 \ - -device virtio-gpu-pci \ - -display gtk,gl=off \ +QEMU_ARGS+=( + -netdev user,id=net0,hostfwd=tcp:127.0.0.1:"$SSH_PORT"-:22 + -device virtio-net-pci,netdev=net0 + -device qemu-xhci + -device usb-kbd + -device usb-tablet + -device virtio-gpu-pci + -device virtio-rng-pci + -qmp unix:"$VM_DIR/qmp.sock",server=on,wait=off + -display gtk,gl=off -boot menu=on +) + +exec "$QEMU" "${QEMU_ARGS[@]}" diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index a95071c88..6012b34c0 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -30,3 +30,7 @@ install_data('launcher/run-vm.sh', install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x', rename: 'machine-lab-run-vm') +install_data('launcher/prepare-latest-omarchy.py', + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + rename: 'machine-lab-prepare-omarchy') diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index f8bdfab32..02025b67b 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -12,6 +12,14 @@ public class Machine : Object { public int color { get; set; default = 0; } public string? url { get; set; default = null; } // where to get an installer public string? installer { get; set; default = null; } + public string? recipe { get; set; default = null; } // "omarchy": download and install unattended + + // Live information, not persisted. + public bool paused { get; private set; default = false; } + public string status_text { get; private set; default = ""; } + public string? check_summary { get; private set; default = null; } + public bool check_ok { get; private set; default = false; } + public bool checking { get; private set; default = false; } public Subprocess? proc { get; private set; default = null; } public DateTime? started { get; private set; default = null; } @@ -61,6 +69,142 @@ public class Machine : Object { } } + /* Sign-in details the unattended installer saved for the guest. */ + public string? credentials () { + try { + string text; + FileUtils.get_contents (Path.build_filename (dir, "credentials.txt"), out text); + return text.strip ().replace ("\n", " · "); + } catch (Error e) { + return null; + } + } + + public static string cache_dir () { + return Path.build_filename (Environment.get_user_data_dir (), "machine-lab", "cache", "omarchy-arm64"); + } + + /* Find a helper script: installed beside the app, or in the source tree. */ + public static string? find_runtime (string installed_name, string source_name) { + string exe_dir = ""; + try { + exe_dir = Path.get_dirname (FileUtils.read_link ("/proc/self/exe")); + } catch (Error e) {} + string[] candidates = { + Path.build_filename (Environment.get_home_dir (), ".local", "bin", installed_name), + "/usr/bin/" + installed_name, + Path.build_filename (exe_dir, installed_name), + Path.build_filename (exe_dir, "..", "launcher", source_name) + }; + foreach (var c in candidates) { + if (FileUtils.test (c, FileTest.IS_EXECUTABLE)) { + return c; + } + } + return null; + } + + // ---- QMP: pause and resume ---- + + private string? qmp (string command) { + var sock = Path.build_filename (dir, "qmp.sock"); + if (!FileUtils.test (sock, FileTest.EXISTS)) { + return null; + } + try { + var client = new SocketClient (); + client.timeout = 2; + var conn = client.connect (new UnixSocketAddress (sock)); + var input = new DataInputStream (conn.input_stream); + var output = conn.output_stream; + input.read_line (); // greeting + output.write ("{\"execute\":\"qmp_capabilities\"}\n".data); + input.read_line (); + output.write ("{\"execute\":\"%s\"}\n".printf (command).data); + string? reply = input.read_line (); + // Events can arrive before the return value. + while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { + reply = input.read_line (); + } + conn.close (); + return reply; + } catch (Error e) { + return null; + } + } + + public void pause () { + if (running && qmp ("stop") != null) { + sync_paused (); + } + } + + public void resume () { + if (running && qmp ("cont") != null) { + sync_paused (); + } + } + + private void sync_paused () { + var reply = qmp ("query-status"); + bool now = reply != null && reply.contains ("\"status\": \"paused\""); + if (reply != null && !now) { + now = reply.contains ("\"status\":\"paused\""); + } + if (now != paused) { + paused = now; + state_changed (); + } + } + + // ---- Unattended Omarchy: latest-version check ---- + + public async void check_latest () { + if (recipe != "omarchy" || checking) { + return; + } + string? updater = find_runtime ("machine-lab-prepare-omarchy", "prepare-latest-omarchy.py"); + if (updater == null) { + return; + } + checking = true; + check_summary = "Checking Omarchy's latest ARM64 nightly…"; + state_changed (); + try { + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_SILENCE); + var p = l.spawnv ({ "python3", updater, "--check", dir, cache_dir () }); + var stream = new DataInputStream (p.get_stdout_pipe ()); + string? line; + check_ok = false; + check_summary = "Couldn't reach nightly.omarchy.org. Choose your own ISO instead."; + while ((line = yield stream.read_line_async ()) != null) { + var f = line.split ("\t"); + if (f[0] == "CHECK" && f.length >= 14) { + double size_gib = double.parse (f[4]) / 1073741824.0; + bool enough = f[11] == "1"; + string problems = f[13]; + check_ok = enough && problems == "-"; + if (problems != "-") { + check_summary = "Fix first: " + problems; + } else if (!enough) { + check_summary = "Latest nightly %s · not enough free space (needs %.0f GiB free).".printf ( + f[1], (double.parse (f[9]) / 1073741824.0)); + } else { + check_summary = "Latest nightly %s · Omarchy %s · %.1f GiB download · space OK".printf ( + f[1], f[3], size_gib); + } + } else if (f[0] == "STATUS" && f.length > 1) { + check_summary = f[1]; + } + } + yield p.wait_async (); + } catch (Error e) { + check_summary = "Couldn't check the latest version: " + e.message; + } + checking = false; + state_changed (); + } + // ---- Native ---- public void start (string launcher) throws Error { @@ -77,10 +221,20 @@ public class Machine : Object { l.setenv ("MACHINE_DISK_GIB", disk_gib.to_string (), true); l.setenv ("MACHINE_SSH_PORT", ssh_port.to_string (), true); l.setenv ("MACHINE_NAME", name, true); - argv = { launcher, installer ?? "-", dir }; + if (recipe == "omarchy" && installer == null) { + string? updater = find_runtime ("machine-lab-prepare-omarchy", "prepare-latest-omarchy.py"); + if (updater == null) { + throw new IOError.NOT_FOUND ("The Omarchy installer helper (machine-lab-prepare-omarchy) was not found."); + } + argv = { "python3", updater, launcher, dir, cache_dir () }; + } else { + argv = { launcher, installer ?? "-", dir }; + } } var p = l.spawnv (argv); log.text = ""; + status_text = ""; + paused = false; append_log ("$ %s\n".printf (string.joinv (" ", argv))); read_output.begin (p); if (managed) { @@ -112,7 +266,16 @@ public class Machine : Object { try { string? line; while ((line = yield stream.read_line_async ()) != null) { - append_log (line + "\n"); + if (line.has_prefix ("STATUS\t")) { + status_text = line.substring (7); + append_log (status_text + "\n"); + state_changed (); + } else if (line.has_prefix ("CREDENTIAL\t")) { + append_log ("Sign-in: " + line.substring (11) + "\n"); + state_changed (); + } else { + append_log (line + "\n"); + } } } catch (Error e) { append_log ("[log: %s]\n".printf (e.message)); @@ -129,6 +292,7 @@ public class Machine : Object { append_log ("\n[machine exited with status %d]\n".printf (code)); proc = null; started = null; + paused = false; // The installer did its job once a disk exists: next start boots the disk. if (installer != null && installed ()) { installer = null; @@ -202,6 +366,9 @@ public class Machine : Object { /* Refresh run state from the guest's qemu.pid, and tail its serial log. */ public void poll () { if (!managed) { + if (running) { + sync_paused (); + } return; } bool now = false; @@ -226,6 +393,9 @@ public class Machine : Object { started = now ? since : null; if (now) { load_serial_tail (); + sync_paused (); + } else if (paused) { + paused = false; } if (changed) { state_changed (); @@ -278,6 +448,9 @@ public class MachineStore : Object { if (kf.has_key (id, "url")) { m.url = kf.get_string (id, "url"); } + if (kf.has_key (id, "recipe")) { + m.recipe = kf.get_string (id, "recipe"); + } if (kf.has_key (id, "installer")) { m.installer = kf.get_string (id, "installer"); } @@ -363,6 +536,9 @@ public class MachineStore : Object { if (m.url != null) { kf.set_string (m.id, "url", m.url); } + if (m.recipe != null) { + kf.set_string (m.id, "recipe", m.recipe); + } if (m.installer != null) { kf.set_string (m.id, "installer", m.installer); } diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index aa85f1999..81b394406 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -14,14 +14,15 @@ struct Preset { public int disk; public int color; public string url; + public string recipe; } const Preset[] presets = { - { "Omarchy", "Arch-based tiling desktop", 4, 6, 32, 7, "https://omarchy.org" }, - { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 24, 4, "https://ubuntu.com/download/server/arm" }, - { "Debian", "Rock-solid and lightweight", 2, 2, 16, 5, "https://www.debian.org/CD/netinst/" }, - { "Fedora", "Fresh packages, GNOME", 4, 4, 24, 0, "https://fedoraproject.org/server/download" }, - { "Blank Machine", "Bring any ARM64 .iso", 2, 4, 16, 1, "" } + { "Omarchy", "Arch-based tiling desktop", 4, 6, 32, 7, "https://omarchy.org", "omarchy" }, + { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 24, 4, "https://ubuntu.com/download/server/arm", "" }, + { "Debian", "Rock-solid and lightweight", 2, 2, 16, 5, "https://www.debian.org/CD/netinst/", "" }, + { "Fedora", "Fresh packages, GNOME", 4, 4, 24, 0, "https://fedoraproject.org/server/download", "" }, + { "Blank Machine", "Bring any ARM64 .iso", 2, 4, 16, 1, "", "" } }; public class MachineRow : Gtk.ListBoxRow { @@ -79,7 +80,7 @@ public class MachineRow : Gtk.ListBoxRow { public void update () { name_label.label = machine.name; - string state = machine.running ? "Running" : machine.is_fresh () ? "Not installed" : "Stopped"; + string state = machine.running ? (machine.paused ? "Paused" : "Running") : machine.is_fresh () ? "Not installed" : "Stopped"; sub_label.label = "%s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); for (int i = 0; i < 8; i++) { icon.remove_css_class ("ml-c-%d".printf (i)); @@ -407,6 +408,7 @@ public class MachineLab : Gtk.Application { m.disk_gib = preset.disk; m.color = preset.color; m.url = preset.url == "" ? null : preset.url; + m.recipe = preset.recipe == "" ? null : preset.recipe; int port = 2222; bool clash = true; while (clash) { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 71484a24b..011a5bbed 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -29,6 +29,12 @@ public class MachinePage : Gtk.Box { private Gtk.Label step1_detail; private Gtk.Label step3_detail; private Gtk.Button get_button; + private Gtk.Button pause_button; + private Gtk.Label step1_title; + private Gtk.Label step2_title; + private Gtk.Label strip; + private Gtk.Box creds_card; + private Gtk.Label creds_label; private Gtk.Box ready_card; private Gtk.Image ready_icon; private Gtk.Label ready_title; @@ -96,6 +102,9 @@ public class MachinePage : Gtk.Box { toast (code > 0 ? "%s stopped with an error (exit %d)".printf (machine.name, code) : "%s stopped".printf (machine.name)); }); + if (machine.recipe != null && machine.is_fresh ()) { + machine.check_latest.begin (); + } tick = Timeout.add_seconds (1, () => { update_uptime (); return Source.CONTINUE; }); refresh (); } @@ -170,7 +179,9 @@ public class MachinePage : Gtk.Box { get_button = new Gtk.Button.with_label ("Get ISO"); get_button.valign = Gtk.Align.CENTER; get_button.clicked.connect (() => { - if (machine.url != null) { + if (machine.recipe != null) { + machine.check_latest.begin (); + } else if (machine.url != null) { new Gtk.UriLauncher (machine.url).launch.begin ((Gtk.Window) get_root (), null); } }); @@ -185,6 +196,43 @@ public class MachinePage : Gtk.Box { "Install inside the window. Shut the guest down when it finishes and Machine Lab boots the new disk from then on.", null, out step3_detail, true)); clamp.append (setup_card); + strip = new Gtk.Label (""); + strip.wrap = true; + strip.xalign = 0; + strip.add_css_class ("ml-note"); + clamp.append (strip); + + creds_card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + creds_card.add_css_class ("ml-card"); + creds_card.add_css_class ("ml-spec"); + var key = new Gtk.Image.from_icon_name ("dialog-password-symbolic"); + key.pixel_size = 24; + creds_card.append (key); + var ctext = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + ctext.hexpand = true; + ctext.valign = Gtk.Align.CENTER; + creds_label = new Gtk.Label (""); + creds_label.xalign = 0; + creds_label.wrap = true; + creds_label.selectable = true; + creds_label.add_css_class ("ml-mono"); + var cl2 = new Gtk.Label ("Sign-in for this machine, saved with its files"); + cl2.xalign = 0; + cl2.add_css_class ("ml-spec-label"); + ctext.append (creds_label); + ctext.append (cl2); + creds_card.append (ctext); + var ccopy = new Gtk.Button.from_icon_name ("edit-copy-symbolic"); + ccopy.add_css_class ("flat"); + ccopy.valign = Gtk.Align.CENTER; + ccopy.tooltip_text = "Copy sign-in"; + ccopy.clicked.connect (() => { + Gdk.Display.get_default ().get_clipboard ().set_text (creds_label.label); + toast ("Copied sign-in"); + }); + creds_card.append (ccopy); + clamp.append (creds_card); + var specs = new Gtk.Grid (); specs.column_homogeneous = true; specs.row_homogeneous = true; @@ -296,6 +344,12 @@ public class MachinePage : Gtk.Box { var actions = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); actions.halign = Gtk.Align.END; + pause_button = new Gtk.Button.with_label ("Pause"); + pause_button.clicked.connect (() => { + if (machine.paused) machine.resume (); else machine.pause (); + refresh (); + }); + actions.append (pause_button); stop_button = new Gtk.Button.with_label ("Force Stop"); stop_button.add_css_class ("destructive-action"); stop_button.clicked.connect (() => machine.stop ()); @@ -329,6 +383,8 @@ public class MachinePage : Gtk.Box { var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); t.hexpand = true; var tl = new Gtk.Label (title); + if (n == 1) step1_title = tl; + if (n == 2) step2_title = tl; tl.xalign = 0; tl.add_css_class ("ml-spec-value"); var dl = new Gtk.Label (detail); @@ -581,7 +637,24 @@ public class MachinePage : Gtk.Box { connect_card.visible = !managed && !fresh; drop_zone.visible = !managed; more_button.visible = !managed; - get_button.visible = machine.url != null; + get_button.visible = machine.url != null || machine.recipe != null; + if (machine.recipe != null) { + step1_title.label = "Download Omarchy automatically"; + step1_detail.label = machine.check_summary ?? "Fetches the latest verified ARM64 nightly and installs it unattended."; + get_button.label = machine.checking ? "Checking…" : "Check again"; + get_button.sensitive = !machine.checking; + step2_title.label = "Or use your own installer"; + } else { + step1_title.label = "Get an ARM64 installer"; + step1_detail.label = "Download a Linux ARM64 .iso, or use one you already have."; + get_button.label = "Get ISO"; + step2_title.label = "Choose the installer file"; + } + strip.visible = machine.status_text != ""; + strip.label = machine.status_text; + var creds = machine.credentials (); + creds_card.visible = creds != null; + if (creds != null) creds_label.label = creds; hero_sub.label = managed ? (fresh ? "Imported from mseries-vm · installer not finished" : "Installed · imported from mseries-vm") : fresh ? "New machine · not installed yet" : "Linux ARM64 · installed"; @@ -591,16 +664,23 @@ public class MachinePage : Gtk.Box { start_button.visible = !running; stop_button.visible = running; stop_button.label = managed ? "Shut Down" : "Force Stop"; - start_button.label = fresh ? "Start Installer" : "Start"; - start_button.sensitive = problems.length == 0 && (!fresh || managed || machine.installer != null); + start_button.label = fresh ? (machine.recipe != null && machine.installer == null ? "Download and Install" : "Start Installer") : "Start"; + pause_button.visible = running; + pause_button.label = machine.paused ? "Resume" : "Pause"; + if (machine.paused) pause_button.add_css_class ("suggested-action"); else pause_button.remove_css_class ("suggested-action"); + bool auto_ok = machine.recipe != null && machine.installer == null && machine.check_ok; + start_button.sensitive = problems.length == 0 && (!fresh || managed || machine.installer != null || auto_ok); start_button.tooltip_text = problems.length > 0 ? "Fix the items in the setup check first" - : (fresh && !managed && machine.installer == null ? "Choose an installer first" : null); + : (fresh && !managed && machine.installer == null && !auto_ok ? (machine.recipe != null ? "Wait for the version check, or choose an installer" : "Choose an installer first") : null); - status_pill.label = running ? (installing ? "Installing" : "Running") + status_pill.label = running ? (machine.paused ? "Paused" : (installing || (machine.recipe != null && machine.status_text.contains ("Installing")) ? "Installing" : "Running")) : fresh ? "Not installed" : "Stopped"; status_pill.remove_css_class ("running"); status_pill.remove_css_class ("new"); - if (running) { + status_pill.remove_css_class ("paused"); + if (running && machine.paused) { + status_pill.add_css_class ("paused"); + } else if (running) { status_pill.add_css_class ("running"); } else if (fresh) { status_pill.add_css_class ("new"); From 5e342bac47fb97682b7d3dc62b6d8fc190b797e0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:36:03 -0400 Subject: [PATCH 072/798] Machine Lab: refresh button (F5/Ctrl+R) for disk space and requirement checks, periodic recheck Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/src/machine.vala | 8 ++++++-- mseries/machine-lab/src/main.vala | 9 +++++++++ mseries/machine-lab/src/page.vala | 26 +++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index 02025b67b..c1a3ef005 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -574,8 +574,12 @@ public class Readiness : Object { /* Free space, in GiB, on the volume that holds machine disks. */ public static int64 free_gib () { try { - var info = File.new_for_path (Environment.get_user_data_dir ()) - .query_filesystem_info ("filesystem:free"); + // The data folder may not exist yet: ask about the nearest folder that does. + var dir = File.new_for_path (Environment.get_user_data_dir ()); + while (!dir.query_exists () && dir.get_parent () != null) { + dir = dir.get_parent (); + } + var info = dir.query_filesystem_info ("filesystem:free"); return (int64) (info.get_attribute_uint64 ("filesystem:free") / 1073741824); } catch (Error e) { return -1; diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index 81b394406..40efe80a9 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -132,6 +132,15 @@ public class MachineLab : Gtk.Application { new_action.activate.connect (() => add_machine (presets[0])); add_action (new_action); set_accels_for_action ("app.new-machine", { "n" }); + var refresh_action = new SimpleAction ("refresh", null); + refresh_action.activate.connect (() => { + var row = list.get_selected_row (); + if (row != null) { + pages[((MachineRow) row).machine.id].recheck (); + } + }); + add_action (refresh_action); + set_accels_for_action ("app.refresh", { "F5", "r" }); set_accels_for_action ("app.quit", { "q" }); var quit = new SimpleAction ("quit", null); quit.activate.connect (() => window.close ()); diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 011a5bbed..885b431aa 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -105,7 +105,15 @@ public class MachinePage : Gtk.Box { if (machine.recipe != null && machine.is_fresh ()) { machine.check_latest.begin (); } - tick = Timeout.add_seconds (1, () => { update_uptime (); return Source.CONTINUE; }); + uint ticks = 0; + tick = Timeout.add_seconds (1, () => { + update_uptime (); + // Keep free-space and requirement checks current without a click. + if (++ticks % 15 == 0 && !machine.running) { + refresh (); + } + return Source.CONTINUE; + }); refresh (); } @@ -115,6 +123,16 @@ public class MachinePage : Gtk.Box { } } + /* Re-read free space and host requirements (and Omarchy's latest nightly). */ + public void recheck () { + if (machine.recipe != null && machine.is_fresh ()) { + machine.check_latest.begin (); + } + refresh (); + int64 free = Readiness.free_gib (); + toast (free >= 0 ? "Checked again: %d GiB free".printf ((int) free) : "Checked again"); + } + public void show_tab (string name) { stack.visible_child_name = name; } @@ -264,6 +282,12 @@ public class MachinePage : Gtk.Box { rt.append (ready_title); rt.append (ready_detail); ready_card.append (rt); + var recheck_btn = new Gtk.Button.from_icon_name ("view-refresh-symbolic"); + recheck_btn.valign = Gtk.Align.CENTER; + recheck_btn.add_css_class ("flat"); + recheck_btn.tooltip_markup = Granite.markup_accel_tooltip ({ "F5", "r" }, "Check disk space and requirements again"); + recheck_btn.clicked.connect (() => recheck ()); + ready_card.append (recheck_btn); clamp.append (ready_card); // Connect row From 2e1dd93d953c7d1a64c7c129cce47ae062ad0f92 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:36:11 -0400 Subject: [PATCH 073/798] Machine Lab: elementary-styled native VM app with Omarchy auto-install, pause/resume, refresh Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 10 + mseries/install-machine-lab.sh | 23 + mseries/machine-lab/.gitignore | 1 + mseries/machine-lab/README.md | 31 + .../data/io.github.mseries.MachineLab.desktop | 10 + .../data/io.github.mseries.MachineLab.svg | 17 + .../data/machine-lab.gresource.xml | 6 + mseries/machine-lab/data/style.css | 158 ++++ mseries/machine-lab/launcher/README.md | 22 + .../launcher/prepare-latest-omarchy.py | 433 +++++++++ mseries/machine-lab/launcher/run-vm.sh | 112 +++ mseries/machine-lab/meson.build | 36 + mseries/machine-lab/src/machine.vala | 588 +++++++++++++ mseries/machine-lab/src/main.vala | 551 ++++++++++++ mseries/machine-lab/src/page.vala | 833 ++++++++++++++++++ 15 files changed, 2831 insertions(+) create mode 100755 mseries/install-machine-lab.sh create mode 100644 mseries/machine-lab/.gitignore create mode 100644 mseries/machine-lab/README.md create mode 100644 mseries/machine-lab/data/io.github.mseries.MachineLab.desktop create mode 100644 mseries/machine-lab/data/io.github.mseries.MachineLab.svg create mode 100644 mseries/machine-lab/data/machine-lab.gresource.xml create mode 100644 mseries/machine-lab/data/style.css create mode 100644 mseries/machine-lab/launcher/README.md create mode 100755 mseries/machine-lab/launcher/prepare-latest-omarchy.py create mode 100755 mseries/machine-lab/launcher/run-vm.sh create mode 100644 mseries/machine-lab/meson.build create mode 100644 mseries/machine-lab/src/machine.vala create mode 100644 mseries/machine-lab/src/main.vala create mode 100644 mseries/machine-lab/src/page.vala diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index bb3424581..8e76ea88b 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6160,3 +6160,13 @@ about two minutes. Changed: greeter default session set back to `pantheon-waylan `~/.dmrc`); new `mseries/install-portal-relogin-fix.sh` installed for the owner (portal needs `WAYLAND_DISPLAY`; unverified until the next crash). Mutter frames not yet symbolized (debuginfod timed out); stock-Gala A/B is the owner's call. Details: `mseries/PANTHEON-CRASH-2026-10-18.md`. + +## 2026-10-18: Machine Lab (native VM app) + +`mseries/machine-lab/` is the elementary-styled VM app (Vala/GTK4/Granite 7); `bash mseries/install-machine-lab.sh` +installs it to `~/.local` and pins it in the dock. Merged in the Codex spike's Omarchy nightly updater and +unattended-install launcher; added pause/resume (QMP), import of `mseries-vm` guests, new-vs-installed panels, +free-space warning. Verified: builds, Broadway renders, pause/resume against a headless QEMU, `--check` of the +Omarchy nightly. Not verified: a complete guest install/boot (~6 GiB download; host had ~19 GiB free) and +suspend/resume of the app itself. Next: run the Omarchy automatic install once space is freed, and lift +`~/Documents/Codex/2026-10-18/f-2/work/youtube-iso-test` (42 GB, owner decides) if space is needed. diff --git a/mseries/install-machine-lab.sh b/mseries/install-machine-lab.sh new file mode 100755 index 000000000..ae45544cf --- /dev/null +++ b/mseries/install-machine-lab.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Build Machine Lab and install it for the current user: binary, launcher, icon, +# desktop entry, and a pin in the elementary dock. +set -euo pipefail +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/machine-lab +build=$(mktemp -d) +trap 'rm -rf "$build"' EXIT + +meson setup "$build" "$here" --prefix="$HOME/.local" >/dev/null +meson compile -C "$build" +meson install -C "$build" >/dev/null + +# The launcher is installed as bin/machine-lab-run-vm by meson; refresh caches. +update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true +gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true + +# Pin to the dock (idempotent). +desktop=io.github.mseries.MachineLab.desktop +current=$(dconf read /io/elementary/dock/launchers 2>/dev/null || true) +if [ -n "$current" ] && [[ "$current" != *"$desktop"* ]]; then + dconf write /io/elementary/dock/launchers "${current%]}, '$desktop']" +fi +echo "Machine Lab installed. Launch it from the dock or: $HOME/.local/bin/machine-lab" diff --git a/mseries/machine-lab/.gitignore b/mseries/machine-lab/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/mseries/machine-lab/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md new file mode 100644 index 000000000..ca557f35b --- /dev/null +++ b/mseries/machine-lab/README.md @@ -0,0 +1,31 @@ +# Machine Lab + +Native elementary OS look (Vala + GTK 4 + Granite 7) for running other Linux ARM64 guests under +QEMU/KVM with NAT networking. Install for the current user and pin it to the dock with +`bash mseries/install-machine-lab.sh`. + +**Design:** flat sidebar of machines ("YOUR MACHINES" vs "NEW — NOT INSTALLED"), preset gallery, +Overview / Console / Settings tabs, per-machine color, status pill with uptime, readiness and +free-space check, toasts, light/dark and accent following the system. + +**Function:** +- Imports guests already managed by `mseries/vms/mseries-vm` (`~/vms/*/vm.conf`) and starts/stops them through it. +- Native machines saved in `~/.config/machine-lab/machines.ini`, with CPU, memory, disk and SSH port settings. +- **Omarchy, automatically** (merged from the Codex spike): `launcher/prepare-latest-omarchy.py` checks Omarchy's + latest ARM64 nightly, downloads and SHA-256-verifies the ISO, builds unattended-install media, and boots + QEMU. Sign-in details are shown in the app and saved with owner-only permissions. +- Own ISO path: drop or choose any ARM64 `.iso`; once the disk holds real data the installer detaches itself. +- **Pause / Resume** over QEMU's QMP socket (`qmp.sock` in the machine folder), plus Shut Down / Force Stop. +- Live console from the launcher output, or the guest serial log for imported machines. + +```sh +meson setup build && meson compile -C build +./build/machine-lab # finds ../launcher/run-vm.sh; MACHINE_LAB_LAUNCHER overrides +``` + +Layout: `src/machine.vala` (model, QMP, process control, discovery), `src/page.vala` (machine pane), +`src/main.vala` (window, sidebar, presets), `launcher/` (QEMU launcher and Omarchy updater). + +**Not verified:** a full guest install and boot end to end (the Omarchy nightly is ~6 GiB and the host had +~19 GiB free). Pause/resume was verified against a headless QEMU through the app. Visual checks used GTK Broadway. +Provenance: started as the Codex `native-vm-poc` spike; its launcher and Omarchy updater are merged here. diff --git a/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop b/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop new file mode 100644 index 000000000..8e73dbfc0 --- /dev/null +++ b/mseries/machine-lab/data/io.github.mseries.MachineLab.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Machine Lab +Comment=Run another Linux on this Mac +Exec=machine-lab +Icon=io.github.mseries.MachineLab +Terminal=false +Type=Application +Categories=System;Emulator; +Keywords=virtual;machine;vm;qemu; +StartupWMClass=io.github.mseries.MachineLab diff --git a/mseries/machine-lab/data/io.github.mseries.MachineLab.svg b/mseries/machine-lab/data/io.github.mseries.MachineLab.svg new file mode 100644 index 000000000..af45a46c2 --- /dev/null +++ b/mseries/machine-lab/data/io.github.mseries.MachineLab.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/mseries/machine-lab/data/machine-lab.gresource.xml b/mseries/machine-lab/data/machine-lab.gresource.xml new file mode 100644 index 000000000..2c2cc5004 --- /dev/null +++ b/mseries/machine-lab/data/machine-lab.gresource.xml @@ -0,0 +1,6 @@ + + + + style.css + + diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css new file mode 100644 index 000000000..e8f363d18 --- /dev/null +++ b/mseries/machine-lab/data/style.css @@ -0,0 +1,158 @@ +/* Machine Lab: elementary OS look. Colors come from the system theme + (@bg_color, @fg_color, @accent_color, @borders, @SILVER_*, @BLUEBERRY_*) + so light/dark and the user's accent follow the desktop automatically. */ + +.ml-sidebar { + background: alpha(@fg_color, 0.04); +} + +.ml-sidebar-title { + font-size: 0.8em; + font-weight: 600; + letter-spacing: 0.04em; + opacity: 0.55; + margin: 12px 18px 4px 18px; +} + +.ml-sidebar row { + border-radius: 6px; + margin: 1px 8px; + padding: 4px 4px; +} + +.ml-machine-name { font-weight: 600; } +.ml-machine-sub { font-size: 0.85em; opacity: 0.65; } + +.ml-hero-icon { + padding: 14px; + border-radius: 18px; + background: linear-gradient(to bottom, alpha(@accent_color, 0.18), alpha(@accent_color, 0.06)); + box-shadow: inset 0 0 0 1px alpha(@accent_color, 0.25); +} + +.ml-pill { + padding: 2px 10px; + border-radius: 99px; + font-size: 0.85em; + font-weight: 600; + background: alpha(@fg_color, 0.08); +} +.ml-pill.running { + color: @success_color; + background: alpha(@success_color, 0.15); +} +.ml-pill.stopped { opacity: 0.8; } + +.ml-card { + border-radius: 12px; + background: @card_bg_color; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.1), 0 1px 3px alpha(black, 0.12); +} + +.ml-spec { + padding: 14px 16px; +} +.ml-spec-value { font-weight: 600; } +.ml-spec-label { font-size: 0.85em; opacity: 0.65; } +.ml-spec image { opacity: 0.7; } + +.ml-drop { + border-radius: 12px; + border: 2px solid alpha(@fg_color, 0.12); + background: alpha(@fg_color, 0.03); + padding: 18px; +} +.ml-drop.hover, .ml-drop.chosen { + border-color: @accent_color; + background: alpha(@accent_color, 0.08); +} + +.ml-fineprint { font-size: 0.85em; opacity: 0.65; } + +.ml-dot { + border-radius: 99px; + background: alpha(@fg_color, 0.35); + box-shadow: 0 0 0 2px @bg_color; +} +.ml-dot.running { background: #68b723; } +.ml-pill.running { color: #3a9104; background: alpha(#68b723, 0.2); } +.ml-empty-icon { opacity: 0.9; } +.ml-mono { font-family: monospace; font-weight: 600; } +.ml-note { + padding: 10px 14px; + border-radius: 10px; + background: alpha(@accent_color, 0.1); + box-shadow: inset 0 0 0 1px alpha(@accent_color, 0.25); +} +.ml-form-row { padding: 12px 16px; } +.ml-form-sep { border-bottom: 1px solid alpha(@fg_color, 0.08); } +.ml-console-frame { + border-radius: 10px; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.12); +} +textview.ml-console, textview.ml-console text { + background: #1d1f23; + color: #d8dde6; +} + +/* Per-machine colors (elementary palette) */ +.ml-c-0.ml-hero-icon { background: linear-gradient(to bottom, alpha(#3689e6, 0.38), alpha(#3689e6, 0.12)); box-shadow: inset 0 0 0 1px alpha(#3689e6, 0.5); } +.ml-c-0.ml-row-icon { background: linear-gradient(to bottom, alpha(#3689e6, 0.35), alpha(#3689e6, 0.1)); box-shadow: inset 0 0 0 1px alpha(#3689e6, 0.45); } +.ml-s-0 { background: #3689e6; } +.ml-c-1.ml-hero-icon { background: linear-gradient(to bottom, alpha(#28bca3, 0.38), alpha(#28bca3, 0.12)); box-shadow: inset 0 0 0 1px alpha(#28bca3, 0.5); } +.ml-c-1.ml-row-icon { background: linear-gradient(to bottom, alpha(#28bca3, 0.35), alpha(#28bca3, 0.1)); box-shadow: inset 0 0 0 1px alpha(#28bca3, 0.45); } +.ml-s-1 { background: #28bca3; } +.ml-c-2.ml-hero-icon { background: linear-gradient(to bottom, alpha(#68b723, 0.38), alpha(#68b723, 0.12)); box-shadow: inset 0 0 0 1px alpha(#68b723, 0.5); } +.ml-c-2.ml-row-icon { background: linear-gradient(to bottom, alpha(#68b723, 0.35), alpha(#68b723, 0.1)); box-shadow: inset 0 0 0 1px alpha(#68b723, 0.45); } +.ml-s-2 { background: #68b723; } +.ml-c-3.ml-hero-icon { background: linear-gradient(to bottom, alpha(#f9c440, 0.38), alpha(#f9c440, 0.12)); box-shadow: inset 0 0 0 1px alpha(#f9c440, 0.5); } +.ml-c-3.ml-row-icon { background: linear-gradient(to bottom, alpha(#f9c440, 0.35), alpha(#f9c440, 0.1)); box-shadow: inset 0 0 0 1px alpha(#f9c440, 0.45); } +.ml-s-3 { background: #f9c440; } +.ml-c-4.ml-hero-icon { background: linear-gradient(to bottom, alpha(#ffa154, 0.38), alpha(#ffa154, 0.12)); box-shadow: inset 0 0 0 1px alpha(#ffa154, 0.5); } +.ml-c-4.ml-row-icon { background: linear-gradient(to bottom, alpha(#ffa154, 0.35), alpha(#ffa154, 0.1)); box-shadow: inset 0 0 0 1px alpha(#ffa154, 0.45); } +.ml-s-4 { background: #ffa154; } +.ml-c-5.ml-hero-icon { background: linear-gradient(to bottom, alpha(#ed5353, 0.38), alpha(#ed5353, 0.12)); box-shadow: inset 0 0 0 1px alpha(#ed5353, 0.5); } +.ml-c-5.ml-row-icon { background: linear-gradient(to bottom, alpha(#ed5353, 0.35), alpha(#ed5353, 0.1)); box-shadow: inset 0 0 0 1px alpha(#ed5353, 0.45); } +.ml-s-5 { background: #ed5353; } +.ml-c-6.ml-hero-icon { background: linear-gradient(to bottom, alpha(#de3e80, 0.38), alpha(#de3e80, 0.12)); box-shadow: inset 0 0 0 1px alpha(#de3e80, 0.5); } +.ml-c-6.ml-row-icon { background: linear-gradient(to bottom, alpha(#de3e80, 0.35), alpha(#de3e80, 0.1)); box-shadow: inset 0 0 0 1px alpha(#de3e80, 0.45); } +.ml-s-6 { background: #de3e80; } +.ml-c-7.ml-hero-icon { background: linear-gradient(to bottom, alpha(#a56de2, 0.38), alpha(#a56de2, 0.12)); box-shadow: inset 0 0 0 1px alpha(#a56de2, 0.5); } +.ml-c-7.ml-row-icon { background: linear-gradient(to bottom, alpha(#a56de2, 0.35), alpha(#a56de2, 0.1)); box-shadow: inset 0 0 0 1px alpha(#a56de2, 0.45); } +.ml-s-7 { background: #a56de2; } + +.ml-row-icon { padding: 3px; border-radius: 8px; } +.ml-swatch { + min-width: 22px; min-height: 22px; padding: 0; + border-radius: 99px; border: none; + box-shadow: inset 0 0 0 1px alpha(black, 0.2); +} +.ml-swatch:checked { box-shadow: 0 0 0 2px @bg_color, 0 0 0 4px alpha(@fg_color, 0.55); } +.ml-ok { color: #3a9104; } +.ml-card.bad { box-shadow: 0 0 0 1px alpha(#ed5353, 0.6), 0 1px 3px alpha(black, 0.12); } + +@keyframes ml-pulse { + 0% { box-shadow: 0 0 0 2px @bg_color, 0 0 0 2px alpha(#68b723, 0.7); } + 100% { box-shadow: 0 0 0 2px @bg_color, 0 0 0 7px alpha(#68b723, 0); } +} +.ml-dot.running { animation: ml-pulse 1.6s ease-out infinite; } + +.ml-step-badge { + min-width: 26px; min-height: 26px; + border-radius: 99px; + font-weight: 700; font-size: 0.9em; + background: alpha(@accent_color, 0.15); + color: @accent_color; +} +.ml-step-badge.done { background: #68b723; color: white; } +.ml-pill.new { color: @accent_color; background: alpha(@accent_color, 0.15); } + +.ml-gallery-card { + padding: 16px; + border-radius: 14px; + background: @card_bg_color; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.1), 0 1px 3px alpha(black, 0.12); +} +.ml-gallery-card:hover { box-shadow: 0 0 0 2px @accent_color, 0 2px 8px alpha(black, 0.18); } + +.ml-pill.paused { color: #946800; background: alpha(#f9c440, 0.35); } diff --git a/mseries/machine-lab/launcher/README.md b/mseries/machine-lab/launcher/README.md new file mode 100644 index 000000000..406a088f9 --- /dev/null +++ b/mseries/machine-lab/launcher/README.md @@ -0,0 +1,22 @@ +# ARM64 QEMU/KVM VM spike + +This is the first backend spike for the elementary-native VM app idea. It creates one persistent ARM64 VM from a local installer ISO using QEMU/KVM, AAVMF UEFI, virtio storage/network/graphics, and user-mode NAT. + +The host inspected for this spike is elementary OS 9 Early Access on an M1 Pro. QEMU/KVM, qemu-img, and AAVMF are present. No installer image has been downloaded and this launcher has not been run yet. + +## Usage + + ./run-vm.sh /path/to/arm64-installer.iso [vm-directory] + +The default VM directory is `./omarchy-arm64-vm`, relative to the current directory. It stores a sparse 64 GiB qcow2 disk and a writable UEFI variable store. Removing that directory removes the VM, so keep it somewhere intentional once it contains data. + +The guest sees a virtio Ethernet device connected through QEMU user networking. It does not receive the host's Wi-Fi adapter. TCP from host `127.0.0.1:2222` is forwarded to guest port 22 for later SSH access if the guest enables SSH. + +## Current limits + +- This is a launch harness, not the app or a VM manager. +- It accepts any ARM64 installer ISO; it does not yet validate the image architecture or automate installation. +- It assumes AAVMF firmware at `/usr/share/AAVMF/` and access to `/dev/kvm`. +- It currently uses 4 vCPUs, 6 GiB RAM, 2D virtio graphics, and a 64 GiB disk. +- It has not yet been boot-verified. The first real guest artifact should be Omarchy ARM64 from its documented edge/image-building path. +- The host currently has about 19 GiB free, below the Try Omarchy guest-builder's stated ~20 GiB build allowance, so that full build should wait until space is available or use a smaller disposable ARM64 installer image for the initial engine check. diff --git a/mseries/machine-lab/launcher/prepare-latest-omarchy.py b/mseries/machine-lab/launcher/prepare-latest-omarchy.py new file mode 100755 index 000000000..9e0d08ba0 --- /dev/null +++ b/mseries/machine-lab/launcher/prepare-latest-omarchy.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +"""Fetch and verify Omarchy's current published ARM64 nightly, then boot it.""" + +import hashlib +import json +import os +import re +import secrets +import shutil +import subprocess +import sys +import urllib.error +import urllib.request +from pathlib import Path +from urllib.parse import urlparse + +METADATA_URL = "https://nightly.omarchy.org/edge-aarch64/latest.json" +ALLOWED_HOST = "nightly.omarchy.org" +CHUNK_SIZE = 1024 * 1024 +MIN_FREE_BYTES = 10 * 1024 * 1024 * 1024 +EXISTING_VM_FREE_BYTES = 2 * 1024 * 1024 * 1024 + + +def status(message: str) -> None: + print(f"STATUS\t{message}", flush=True) + + +def fail(message: str, code: int = 1) -> None: + status(f"ERROR: {message}") + raise SystemExit(code) + + +def fetch_metadata() -> dict: + request = urllib.request.Request( + METADATA_URL, + headers={"User-Agent": "MachineLab/0.1 (Omarchy ARM64 VM setup)"}, + ) + with urllib.request.urlopen(request, timeout=30) as response: + metadata = json.load(response) + if metadata.get("channel") != "edge-aarch64" or metadata.get("arch") != "aarch64": + fail("Omarchy's latest metadata is not for the ARM64 edge channel.") + version = metadata.get("version") + digest = metadata.get("sha256") + size = metadata.get("size") + url = metadata.get("url") + if not isinstance(version, str) or not re.fullmatch(r"[0-9A-Za-z._-]+", version): + fail("Omarchy returned an invalid image version.") + if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest): + fail("Omarchy returned an invalid SHA-256 digest.") + if not isinstance(size, int) or size <= 0: + fail("Omarchy returned an invalid image size.") + expected_name = f"omarchy-edge-{version}-aarch64.iso" + parsed = urlparse(url if isinstance(url, str) else "") + if parsed.scheme != "https" or parsed.hostname != ALLOWED_HOST or parsed.path != f"/edge-aarch64/{version}/{expected_name}": + fail("Omarchy returned an image URL outside the expected nightly path.") + metadata["expected_name"] = expected_name + return metadata + + +def digest_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + while chunk := source.read(CHUNK_SIZE): + digest.update(chunk) + return digest.hexdigest() + + +def disk_installed(disk: Path) -> bool: + """An empty qcow2 header is not an installation: require real data on disk.""" + return disk.is_file() and disk.stat().st_blocks * 512 >= 1024 ** 3 + + +def existing_path(path: Path) -> Path: + """Return the nearest existing directory so disk_usage works before setup.""" + candidate = path + while not candidate.exists(): + candidate = candidate.parent + return candidate + + +def check_space(metadata: dict, vm_dir: Path, cache: Path) -> dict: + disk = vm_dir / "disk.qcow2" + existing_vm = disk_installed(disk) + image = cache / metadata["expected_name"] + part = cache / (metadata["expected_name"] + ".part") + if existing_vm: + download_remaining = 0 + vm_required = EXISTING_VM_FREE_BYTES + cache_required = 0 + else: + partial_size = part.stat().st_size if part.exists() else 0 + download_remaining = metadata["size"] if partial_size >= metadata["size"] else metadata["size"] - partial_size + if image.is_file() and image.stat().st_size == metadata["size"]: + # The launch helper still verifies the checksum, but no more download space is needed. + download_remaining = 0 + vm_required = MIN_FREE_BYTES + cache_required = download_remaining + + cache_location = existing_path(cache) + vm_location = existing_path(vm_dir) + cache_free = shutil.disk_usage(cache_location).free + vm_free = shutil.disk_usage(vm_location).free + same_filesystem = cache_location.stat().st_dev == vm_location.stat().st_dev + if existing_vm: + enough = vm_free >= vm_required + elif same_filesystem: + # The install reserve and image download consume the same free-space pool. + vm_required += cache_required + cache_required = vm_required + enough = min(cache_free, vm_free) >= vm_required + else: + enough = cache_free >= cache_required and vm_free >= vm_required + + return { + "existing_vm": existing_vm, + "download_remaining": download_remaining, + "cache_free": cache_free, + "vm_free": vm_free, + "cache_required": cache_required, + "vm_required": vm_required, + "same_filesystem": same_filesystem, + "enough": enough, + } + + +def runtime_prerequisites(existing_vm: bool) -> list[str]: + problems = [] + missing = [tool for tool in ("qemu-system-aarch64", "qemu-img") if shutil.which(tool) is None] + if missing: + problems.append("Install " + " and ".join(missing)) + if not os.access("/dev/kvm", os.R_OK | os.W_OK): + problems.append("KVM access is unavailable to this user (/dev/kvm)") + if not Path("/usr/share/AAVMF/AAVMF_CODE.fd").is_file() or not Path("/usr/share/AAVMF/AAVMF_VARS.fd").is_file(): + problems.append("AAVMF UEFI firmware is missing") + if not existing_vm: + missing = [tool for tool in ("openssl", "genisoimage") if shutil.which(tool) is None] + if missing: + problems.append("Install " + " and ".join(missing)) + return problems + + +def print_check(vm_dir: Path, cache: Path) -> None: + metadata = fetch_metadata() + space = check_space(metadata, vm_dir, cache) + problems = runtime_prerequisites(space["existing_vm"]) + fields = [ + metadata["version"], metadata.get("built_at", "unknown"), + metadata.get("omarchy_version", "unknown"), str(metadata["size"]), + "1" if space["existing_vm"] else "0", + str(space["cache_free"]), str(space["vm_free"]), + str(space["cache_required"]), str(space["vm_required"]), + str(space["download_remaining"]), + "1" if space["enough"] else "0", + "1" if space["same_filesystem"] else "0", + (" ".join(problems).replace("\t", " ").replace("\n", " ") or "-"), + ] + print("CHECK\t" + "\t".join(fields), flush=True) + + +def get_image(metadata: dict, cache: Path, vm_dir: Path) -> Path: + cache.mkdir(parents=True, exist_ok=True) + image = cache / metadata["expected_name"] + part = cache / (metadata["expected_name"] + ".part") + expected_size = metadata["size"] + expected_hash = metadata["sha256"] + + if image.exists() and image.stat().st_size == expected_size: + status("Checking cached ISO checksum…") + if digest_file(image) == expected_hash: + return image + image.unlink() + + offset = part.stat().st_size if part.exists() else 0 + if offset >= expected_size: + part.unlink() + offset = 0 + space = check_space(metadata, vm_dir, cache) + if not space["enough"]: + fail( + "Not enough free space to download and install Omarchy safely. " + f"Download storage needs {(space['cache_required'] / 1024**3):.1f} GiB " + f"and has {(space['cache_free'] / 1024**3):.1f} GiB; " + f"VM storage needs {(space['vm_required'] / 1024**3):.1f} GiB " + f"and has {(space['vm_free'] / 1024**3):.1f} GiB." + ) + + headers = {"User-Agent": "MachineLab/0.1 (Omarchy ARM64 VM setup)"} + if offset: + headers["Range"] = f"bytes={offset}-" + status(f"Resuming Omarchy {metadata['omarchy_version']} ARM64 nightly at {(offset / 1024**3):.1f} GiB…") + else: + status(f"Downloading Omarchy {metadata['omarchy_version']} ARM64 nightly ({expected_size / 1024**3:.1f} GiB)…") + + try: + response = urllib.request.urlopen( + urllib.request.Request(metadata["url"], headers=headers), timeout=60 + ) + except (urllib.error.URLError, TimeoutError) as error: + fail(f"Could not download the Omarchy image: {error}") + + if offset and response.status != 206: + response.close() + offset = 0 + response = urllib.request.urlopen( + urllib.request.Request(metadata["url"], headers={"User-Agent": headers["User-Agent"]}), + timeout=60, + ) + mode = "ab" if offset else "wb" + received = offset + last_report = -1 + try: + with response, part.open(mode) as target: + while chunk := response.read(CHUNK_SIZE): + target.write(chunk) + received += len(chunk) + percent = min(100, int(received * 100 / expected_size)) + if percent != last_report: + status(f"Downloading Omarchy {metadata['omarchy_version']} ARM64 nightly… {percent}%") + last_report = percent + except (OSError, urllib.error.URLError, TimeoutError) as error: + fail(f"Download stopped; the partial file is kept so the next attempt can resume: {error}") + + if part.stat().st_size != expected_size: + fail(f"Downloaded image size does not match metadata ({part.stat().st_size} of {expected_size} bytes). The partial file is kept for retry.") + status("Verifying Omarchy image SHA-256…") + if digest_file(part) != expected_hash: + part.unlink(missing_ok=True) + fail("The downloaded image failed its SHA-256 check and was deleted.") + part.replace(image) + return image + + +def create_autoinstall_seed(metadata: dict, vm_dir: Path) -> tuple[Path, str]: + required = ("openssl", "genisoimage") + missing = [tool for tool in required if shutil.which(tool) is None] + if missing: + fail("Install the required tools first: " + ", ".join(missing)) + + vm_dir.mkdir(parents=True, exist_ok=True, mode=0o700) + os.chmod(vm_dir, 0o700) + password_file = vm_dir / "credentials.txt" + if password_file.exists(): + saved = password_file.read_text().splitlines() + password = next((line.split(": ", 1)[1] for line in saved if line.startswith("Sudo password: ")), "") + if not password: + fail(f"The saved VM credentials file is invalid: {password_file}") + else: + password = secrets.token_hex(24) + fd = os.open(password_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "w") as output: + output.write(f"Username: omarchy\nSudo password: {password}\n") + + hash_result = subprocess.run( + ["openssl", "passwd", "-6", "-stdin"], + check=True, + capture_output=True, + text=True, + input=password + "\n", + ) + password_hash = hash_result.stdout.strip() + gib = 1024**3 + mib = 1024**2 + disk_bytes = 64 * gib + boot_start = mib + boot_size = 2 * gib + main_start = boot_start + boot_size + main_size = disk_bytes - main_start - mib + seed_dir = vm_dir / f"autoinstall-{metadata['version']}" + seed_dir.mkdir(mode=0o700, parents=True, exist_ok=True) + + configuration = { + "app_config": None, + "archinstall-language": "English", + "auth_config": {}, + "audio_config": {"audio": "pipewire"}, + "bootloader_config": {"bootloader": "Limine", "uki": False, "removable": False}, + "custom_commands": [], + "omarchy_install": { + "mode": "full_disk", + "defer_provisioning": False, + "target_mount": "/mnt", + "boot": { + "esp_mount": "/boot", + "esp_path": "/EFI/limine", + "efi_binary": "limine_aa64.efi", + "enable_fallback": True, + }, + "storage": {"kernel": "linux-omarchy"}, + }, + "disk_config": { + "config_type": "default_layout", + "device_modifications": [ + { + "device": "/dev/vda", + "partitions": [ + { + "btrfs": [], + "dev_path": None, + "flags": ["boot", "esp"], + "fs_type": "fat32", + "mount_options": [], + "mountpoint": "/boot", + "obj_id": "ea21d3f2-82bb-49cc-ab5d-6f81ae94e18d", + "size": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": boot_size}, + "start": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": boot_start}, + "status": "create", + "type": "primary", + }, + { + "btrfs": [ + {"mountpoint": "/", "name": "@"}, + {"mountpoint": "/home", "name": "@home"}, + {"mountpoint": "/var/log", "name": "@log"}, + {"mountpoint": "/var/cache/pacman/pkg", "name": "@pkg"}, + ], + "dev_path": None, + "flags": [], + "fs_type": "btrfs", + "mount_options": ["compress=zstd"], + "mountpoint": None, + "obj_id": "8c2c2b92-1070-455d-b76a-56263bab24aa", + "size": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": main_size}, + "start": {"sector_size": {"unit": "B", "value": 512}, "unit": "B", "value": main_start}, + "status": "create", + "type": "primary", + }, + ], + "wipe": True, + } + ], + }, + "hostname": "omarchy-vm", + "kernels": ["linux-omarchy"], + "network_config": {"type": "iso"}, + "ntp": True, + "parallel_downloads": 8, + "script": None, + "services": [], + "swap": True, + "timezone": "UTC", + "locale_config": {"kb_layout": "us", "sys_enc": "UTF-8", "sys_lang": "en_US.UTF-8"}, + "mirror_config": { + "custom_repositories": [], + "custom_servers": [{"url": "https://arm-mirror.omarchy.org/$arch/$repo"}], + "mirror_regions": {}, + "optional_repositories": [], + }, + "packages": ["base-devel", "git", "omarchy-keyring", "omarchy-settings-dev", "omarchy-dev"], + "profile_config": {"gfx_driver": None, "greeter": None, "profile": {}}, + "version": "3.0.9", + } + credentials = { + "root_enc_password": password_hash, + "users": [{"enc_password": password_hash, "groups": [], "sudo": True, "username": "omarchy"}], + } + (seed_dir / "user_configuration.json").write_text(json.dumps(configuration, indent=2) + "\n") + (seed_dir / "user_credentials.json").write_text(json.dumps(credentials, indent=2) + "\n") + (seed_dir / "user_encrypt_installation.txt").write_text("false\n") + + cidata = vm_dir / f"cidata-{metadata['version']}.iso" + temporary = cidata.with_suffix(".iso.part") + try: + subprocess.run( + ["genisoimage", "-quiet", "-output", str(temporary), "-volid", "cidata", "-joliet", "-rock", str(seed_dir)], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as error: + fail(f"Could not create Omarchy's unattended install seed: {error.stderr.strip()}") + temporary.replace(cidata) + return cidata, password + + +def emit_saved_credentials(vm_dir: Path) -> None: + password_file = vm_dir / "credentials.txt" + if password_file.is_file(): + credentials = password_file.read_text().replace("\n", " · ").strip(" ·") + print(f"CREDENTIAL\t{credentials} · saved in {password_file}", flush=True) + + +def main() -> None: + if len(sys.argv) == 4 and sys.argv[1] == "--check": + try: + print_check(Path(sys.argv[2]), Path(sys.argv[3])) + except urllib.error.URLError as error: + fail(f"Could not check Omarchy's latest version: {error}") + return + if len(sys.argv) != 4: + fail("Internal updater arguments are invalid.") + launcher, vm_dir, cache_dir = map(Path, sys.argv[1:]) + if not launcher.is_file() or not os.access(launcher, os.X_OK): + fail("The QEMU launcher could not be found.") + + missing = [tool for tool in ("qemu-system-aarch64", "qemu-img") if shutil.which(tool) is None] + if missing: + fail("Install the required tools first: " + ", ".join(missing)) + if not os.access("/dev/kvm", os.R_OK | os.W_OK): + fail("KVM is unavailable to this user. Check /dev/kvm permissions.") + if not Path("/usr/share/AAVMF/AAVMF_CODE.fd").is_file() or not Path("/usr/share/AAVMF/AAVMF_VARS.fd").is_file(): + fail("AAVMF UEFI firmware was not found under /usr/share/AAVMF.") + + # Starting an existing installation must not silently replace/update it + # from a newly downloaded installer. Omarchy updates its installed guest + # through its own updater, run inside the VM. + disk = vm_dir / "disk.qcow2" + if disk_installed(disk): + cached_images = sorted(cache_dir.glob("omarchy-edge-*-aarch64.iso")) + if cached_images: + status("Starting the existing VM. To update installed Omarchy, run `omarchy update` inside the guest.") + emit_saved_credentials(vm_dir) + os.execv(str(launcher), [str(launcher), str(cached_images[-1]), str(vm_dir)]) + status("Starting the existing VM without installer media. To update installed Omarchy, run `omarchy update` inside the guest.") + emit_saved_credentials(vm_dir) + os.execv(str(launcher), [str(launcher), "--disk-only", str(vm_dir)]) + + missing = [tool for tool in ("openssl", "genisoimage") if shutil.which(tool) is None] + if missing: + fail("Install the required tools first: " + ", ".join(missing)) + try: + status("Checking Omarchy's current ARM64 nightly…") + metadata = fetch_metadata() + image = get_image(metadata, cache_dir, vm_dir) + cidata, password = create_autoinstall_seed(metadata, vm_dir) + except urllib.error.URLError as error: + fail(f"Could not check Omarchy's latest version: {error}") + status(f"Installing Omarchy {metadata['omarchy_version']} (nightly {metadata['version']}) automatically. The VM will reboot into the installed guest when setup finishes.") + print(f"CREDENTIAL\tUsername: omarchy · sudo password: {password} · saved in {vm_dir / 'credentials.txt'}", flush=True) + os.execv(str(launcher), [str(launcher), str(image), str(vm_dir), str(cidata)]) + + +if __name__ == "__main__": + main() diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh new file mode 100755 index 000000000..ade2a912b --- /dev/null +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Boot a Linux ARM64 guest under QEMU/KVM with user-mode NAT. +# run-vm.sh ISO [VM_DIR] [CIDATA_ISO] boot an installer (ISO "-" = none) +# run-vm.sh --disk-only VM_DIR boot the installed disk +# Hardware comes from the environment: MACHINE_CPUS, MACHINE_MEM_GIB, +# MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME. A QMP socket (qmp.sock in +# VM_DIR) lets Machine Lab pause and resume the guest. +set -euo pipefail + +CPUS=${MACHINE_CPUS:-4} +MEM_GIB=${MACHINE_MEM_GIB:-6} +DISK_GIB=${MACHINE_DISK_GIB:-64} +SSH_PORT=${MACHINE_SSH_PORT:-2222} +VM_NAME=${MACHINE_NAME:-Omarchy-ARM64} + +if [ "${1:-}" = "--disk-only" ]; then + if [ "$#" -ne 2 ]; then + echo "Usage: $0 --disk-only VM_DIRECTORY" >&2 + exit 2 + fi + ISO_PATH="" + CIDATA_PATH="" + VM_DIR=$2 +else + if [ "$#" -lt 1 ]; then + echo "Usage: $0 ARM64_INSTALL_ISO [VM_DIRECTORY] [CIDATA_ISO]" >&2 + exit 2 + fi + ISO_PATH=$1 + [ "$ISO_PATH" = "-" ] && ISO_PATH="" + VM_DIR=${2:-./omarchy-arm64-vm} + CIDATA_PATH=${3:-} +fi + +QEMU=$(command -v qemu-system-aarch64 || true) +QEMU_IMG=$(command -v qemu-img || true) +FW_CODE=/usr/share/AAVMF/AAVMF_CODE.fd +FW_VARS=/usr/share/AAVMF/AAVMF_VARS.fd + +if [ -z "$QEMU" ] || [ -z "$QEMU_IMG" ]; then + echo "Install qemu-system-arm and qemu-utils first." >&2 + exit 1 +fi +if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then + echo "KVM is unavailable to this user. Check /dev/kvm permissions." >&2 + exit 1 +fi +if [ -n "$ISO_PATH" ] && [ ! -r "$ISO_PATH" ]; then + echo "Cannot read installer image: $ISO_PATH" >&2 + exit 1 +fi +if [ -n "$CIDATA_PATH" ] && [ ! -r "$CIDATA_PATH" ]; then + echo "Cannot read unattended-install seed: $CIDATA_PATH" >&2 + exit 1 +fi +if [ ! -r "$FW_CODE" ] || [ ! -r "$FW_VARS" ]; then + echo "AAVMF UEFI firmware was not found under /usr/share/AAVMF." >&2 + exit 1 +fi + +mkdir -p "$VM_DIR" +if [ ! -e "$VM_DIR/efi-vars.fd" ]; then + cp --reflink=auto "$FW_VARS" "$VM_DIR/efi-vars.fd" +fi +if [ ! -e "$VM_DIR/disk.qcow2" ]; then + "$QEMU_IMG" create -f qcow2 "$VM_DIR/disk.qcow2" "${DISK_GIB}G" +fi +rm -f "$VM_DIR/qmp.sock" + +QEMU_ARGS=( + -name "$VM_NAME" + -machine virt,gic-version=3,msi=gicv2m,acpi=off + -accel kvm + -cpu host + -smp "$CPUS" + -m "${MEM_GIB}G" + -drive if=pflash,format=raw,readonly=on,file="$FW_CODE" + -drive if=pflash,format=raw,file="$VM_DIR/efi-vars.fd" + -drive if=none,id=system,format=qcow2,file="$VM_DIR/disk.qcow2" + -device virtio-blk-pci,drive=system,bootindex=1 +) + +if [ -n "$ISO_PATH" ] || [ -n "$CIDATA_PATH" ]; then + QEMU_ARGS+=(-device virtio-scsi-pci,id=scsi) +fi +if [ -n "$ISO_PATH" ]; then + QEMU_ARGS+=( + -drive if=none,id=installer,format=raw,media=cdrom,readonly=on,file="$ISO_PATH" + -device scsi-cd,drive=installer,bus=scsi.0,bootindex=2 + ) +fi +if [ -n "$CIDATA_PATH" ]; then + QEMU_ARGS+=( + -drive if=none,id=cidata,format=raw,media=cdrom,readonly=on,file="$CIDATA_PATH" + -device scsi-cd,drive=cidata,bus=scsi.0,bootindex=3 + ) +fi + +QEMU_ARGS+=( + -netdev user,id=net0,hostfwd=tcp:127.0.0.1:"$SSH_PORT"-:22 + -device virtio-net-pci,netdev=net0 + -device qemu-xhci + -device usb-kbd + -device usb-tablet + -device virtio-gpu-pci + -device virtio-rng-pci + -qmp unix:"$VM_DIR/qmp.sock",server=on,wait=off + -display gtk,gl=off + -boot menu=on +) + +exec "$QEMU" "${QEMU_ARGS[@]}" diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build new file mode 100644 index 000000000..6012b34c0 --- /dev/null +++ b/mseries/machine-lab/meson.build @@ -0,0 +1,36 @@ +project('machine-lab', 'vala', + version: '0.2.0', + meson_version: '>= 1.0.0', + default_options: ['warning_level=2'] +) + +gtk = dependency('gtk4', version: '>= 4.12') +granite = dependency('granite-7', version: '>= 7.0') + +gnome = import('gnome') +resources = gnome.compile_resources('machine-lab-resources', + 'data/machine-lab.gresource.xml', + source_dir: 'data' +) + +add_project_arguments(['--pkg', 'posix'], language: 'vala') + +executable('machine-lab', + 'src/main.vala', 'src/machine.vala', 'src/page.vala', + resources, + dependencies: [gtk, granite], + install: true +) + +install_data('data/io.github.mseries.MachineLab.svg', + install_dir: get_option('datadir') / 'icons/hicolor/scalable/apps') +install_data('data/io.github.mseries.MachineLab.desktop', + install_dir: get_option('datadir') / 'applications') +install_data('launcher/run-vm.sh', + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + rename: 'machine-lab-run-vm') +install_data('launcher/prepare-latest-omarchy.py', + install_dir: get_option('bindir'), + install_mode: 'rwxr-xr-x', + rename: 'machine-lab-prepare-omarchy') diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala new file mode 100644 index 000000000..c1a3ef005 --- /dev/null +++ b/mseries/machine-lab/src/machine.vala @@ -0,0 +1,588 @@ +/* Machine model, persistence, discovery of mseries-vm guests, and process control. */ + +public class Machine : Object { + public string id { get; set; } + public string name { get; set; default = "New Machine"; } + public string kind { get; set; default = "native"; } // "native" = ours, "managed" = mseries-vm guest + public string vm_name { get; set; default = ""; } // managed: directory name under ~/vms + public int cpus { get; set; default = 4; } + public int mem_gib { get; set; default = 6; } + public int disk_gib { get; set; default = 64; } + public int ssh_port { get; set; default = 2222; } + public int color { get; set; default = 0; } + public string? url { get; set; default = null; } // where to get an installer + public string? installer { get; set; default = null; } + public string? recipe { get; set; default = null; } // "omarchy": download and install unattended + + // Live information, not persisted. + public bool paused { get; private set; default = false; } + public string status_text { get; private set; default = ""; } + public string? check_summary { get; private set; default = null; } + public bool check_ok { get; private set; default = false; } + public bool checking { get; private set; default = false; } + + public Subprocess? proc { get; private set; default = null; } + public DateTime? started { get; private set; default = null; } + public Gtk.TextBuffer log { get; private set; default = new Gtk.TextBuffer (null); } + private bool managed_running = false; + + public signal void state_changed (); + public signal void finished (int code); + public signal void installer_detached (); + + public bool managed { get { return kind == "managed"; } } + public bool running { get { return managed ? managed_running : proc != null; } } + + public string dir { + owned get { + if (managed) { + return Path.build_filename (vms_dir (), vm_name); + } + return Path.build_filename (Environment.get_user_data_dir (), "machine-lab", id); + } + } + + public static string vms_dir () { + return Environment.get_variable ("MSERIES_VM_DIR") ?? Path.build_filename (Environment.get_home_dir (), "vms"); + } + + /* An empty disk file is just a placeholder: installed means the guest wrote real data. */ + public bool installed () { + return disk_used_bytes () >= 1073741824; + } + + public bool is_fresh () { + return !running && !installed (); + } + + public bool has_disk () { + return FileUtils.test (Path.build_filename (dir, "disk.qcow2"), FileTest.EXISTS); + } + + public int64 disk_used_bytes () { + try { + var info = File.new_for_path (Path.build_filename (dir, "disk.qcow2")) + .query_info ("standard::allocated-size", FileQueryInfoFlags.NONE); + return (int64) info.get_attribute_uint64 ("standard::allocated-size"); + } catch (Error e) { + return 0; + } + } + + /* Sign-in details the unattended installer saved for the guest. */ + public string? credentials () { + try { + string text; + FileUtils.get_contents (Path.build_filename (dir, "credentials.txt"), out text); + return text.strip ().replace ("\n", " · "); + } catch (Error e) { + return null; + } + } + + public static string cache_dir () { + return Path.build_filename (Environment.get_user_data_dir (), "machine-lab", "cache", "omarchy-arm64"); + } + + /* Find a helper script: installed beside the app, or in the source tree. */ + public static string? find_runtime (string installed_name, string source_name) { + string exe_dir = ""; + try { + exe_dir = Path.get_dirname (FileUtils.read_link ("/proc/self/exe")); + } catch (Error e) {} + string[] candidates = { + Path.build_filename (Environment.get_home_dir (), ".local", "bin", installed_name), + "/usr/bin/" + installed_name, + Path.build_filename (exe_dir, installed_name), + Path.build_filename (exe_dir, "..", "launcher", source_name) + }; + foreach (var c in candidates) { + if (FileUtils.test (c, FileTest.IS_EXECUTABLE)) { + return c; + } + } + return null; + } + + // ---- QMP: pause and resume ---- + + private string? qmp (string command) { + var sock = Path.build_filename (dir, "qmp.sock"); + if (!FileUtils.test (sock, FileTest.EXISTS)) { + return null; + } + try { + var client = new SocketClient (); + client.timeout = 2; + var conn = client.connect (new UnixSocketAddress (sock)); + var input = new DataInputStream (conn.input_stream); + var output = conn.output_stream; + input.read_line (); // greeting + output.write ("{\"execute\":\"qmp_capabilities\"}\n".data); + input.read_line (); + output.write ("{\"execute\":\"%s\"}\n".printf (command).data); + string? reply = input.read_line (); + // Events can arrive before the return value. + while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { + reply = input.read_line (); + } + conn.close (); + return reply; + } catch (Error e) { + return null; + } + } + + public void pause () { + if (running && qmp ("stop") != null) { + sync_paused (); + } + } + + public void resume () { + if (running && qmp ("cont") != null) { + sync_paused (); + } + } + + private void sync_paused () { + var reply = qmp ("query-status"); + bool now = reply != null && reply.contains ("\"status\": \"paused\""); + if (reply != null && !now) { + now = reply.contains ("\"status\":\"paused\""); + } + if (now != paused) { + paused = now; + state_changed (); + } + } + + // ---- Unattended Omarchy: latest-version check ---- + + public async void check_latest () { + if (recipe != "omarchy" || checking) { + return; + } + string? updater = find_runtime ("machine-lab-prepare-omarchy", "prepare-latest-omarchy.py"); + if (updater == null) { + return; + } + checking = true; + check_summary = "Checking Omarchy's latest ARM64 nightly…"; + state_changed (); + try { + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_SILENCE); + var p = l.spawnv ({ "python3", updater, "--check", dir, cache_dir () }); + var stream = new DataInputStream (p.get_stdout_pipe ()); + string? line; + check_ok = false; + check_summary = "Couldn't reach nightly.omarchy.org. Choose your own ISO instead."; + while ((line = yield stream.read_line_async ()) != null) { + var f = line.split ("\t"); + if (f[0] == "CHECK" && f.length >= 14) { + double size_gib = double.parse (f[4]) / 1073741824.0; + bool enough = f[11] == "1"; + string problems = f[13]; + check_ok = enough && problems == "-"; + if (problems != "-") { + check_summary = "Fix first: " + problems; + } else if (!enough) { + check_summary = "Latest nightly %s · not enough free space (needs %.0f GiB free).".printf ( + f[1], (double.parse (f[9]) / 1073741824.0)); + } else { + check_summary = "Latest nightly %s · Omarchy %s · %.1f GiB download · space OK".printf ( + f[1], f[3], size_gib); + } + } else if (f[0] == "STATUS" && f.length > 1) { + check_summary = f[1]; + } + } + yield p.wait_async (); + } catch (Error e) { + check_summary = "Couldn't check the latest version: " + e.message; + } + checking = false; + state_changed (); + } + + // ---- Native ---- + + public void start (string launcher) throws Error { + if (running) { + return; + } + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); + string[] argv; + if (managed) { + argv = { launcher, "start", vm_name, "--daemon" }; + } else { + l.setenv ("MACHINE_CPUS", cpus.to_string (), true); + l.setenv ("MACHINE_MEM_GIB", mem_gib.to_string (), true); + l.setenv ("MACHINE_DISK_GIB", disk_gib.to_string (), true); + l.setenv ("MACHINE_SSH_PORT", ssh_port.to_string (), true); + l.setenv ("MACHINE_NAME", name, true); + if (recipe == "omarchy" && installer == null) { + string? updater = find_runtime ("machine-lab-prepare-omarchy", "prepare-latest-omarchy.py"); + if (updater == null) { + throw new IOError.NOT_FOUND ("The Omarchy installer helper (machine-lab-prepare-omarchy) was not found."); + } + argv = { "python3", updater, launcher, dir, cache_dir () }; + } else { + argv = { launcher, installer ?? "-", dir }; + } + } + var p = l.spawnv (argv); + log.text = ""; + status_text = ""; + paused = false; + append_log ("$ %s\n".printf (string.joinv (" ", argv))); + read_output.begin (p); + if (managed) { + watch_managed_start.begin (p); + } else { + proc = p; + started = new DateTime.now_local (); + watch.begin (p); + } + state_changed (); + } + + public void stop () { + if (managed) { + stop_managed.begin (); + } else if (proc != null) { + proc.force_exit (); + } + } + + private void append_log (string text) { + Gtk.TextIter end; + log.get_end_iter (out end); + log.insert (ref end, text, -1); + } + + private async void read_output (Subprocess p) { + var stream = new DataInputStream (p.get_stdout_pipe ()); + try { + string? line; + while ((line = yield stream.read_line_async ()) != null) { + if (line.has_prefix ("STATUS\t")) { + status_text = line.substring (7); + append_log (status_text + "\n"); + state_changed (); + } else if (line.has_prefix ("CREDENTIAL\t")) { + append_log ("Sign-in: " + line.substring (11) + "\n"); + state_changed (); + } else { + append_log (line + "\n"); + } + } + } catch (Error e) { + append_log ("[log: %s]\n".printf (e.message)); + } + } + + private async void watch (Subprocess p) { + try { + yield p.wait_async (); + } catch (Error e) { + append_log ("[wait: %s]\n".printf (e.message)); + } + int code = p.get_if_exited () ? p.get_exit_status () : -1; + append_log ("\n[machine exited with status %d]\n".printf (code)); + proc = null; + started = null; + paused = false; + // The installer did its job once a disk exists: next start boots the disk. + if (installer != null && installed ()) { + installer = null; + installer_detached (); + } + state_changed (); + finished (code); + } + + public void delete_files () { + foreach (var f in new string[] { "disk.qcow2", "efi-vars.fd" }) { + FileUtils.remove (Path.build_filename (dir, f)); + } + DirUtils.remove (dir); + } + + // ---- Managed (mseries-vm) ---- + + private async void watch_managed_start (Subprocess p) { + try { + yield p.wait_async (); + } catch (Error e) { + append_log ("[wait: %s]\n".printf (e.message)); + } + int code = p.get_if_exited () ? p.get_exit_status () : -1; + poll (); + if (code != 0) { + finished (code); + } + } + + private async void stop_managed () { + string? tool = Machine.find_mseries_vm (); + if (tool == null) { + return; + } + try { + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); + var p = l.spawnv ({ tool, "stop", vm_name }); + append_log ("\n$ mseries-vm stop %s (asks the guest to power off)\n".printf (vm_name)); + read_output.begin (p); + yield p.wait_async (); + } catch (Error e) { + append_log ("[stop: %s]\n".printf (e.message)); + } + poll (); + finished (0); + } + + public static string? find_mseries_vm () { + var env = Environment.get_variable ("MSERIES_VM_TOOL"); + if (env != null && FileUtils.test (env, FileTest.IS_EXECUTABLE)) { + return env; + } + string exe_dir = ""; + try { + exe_dir = Path.get_dirname (FileUtils.read_link ("/proc/self/exe")); + } catch (Error e) {} + string[] candidates = { + Path.build_filename (exe_dir, "..", "..", "vms", "mseries-vm"), + Path.build_filename (Environment.get_home_dir (), "ElementaryOS_MSeries", "mseries", "vms", "mseries-vm") + }; + foreach (var c in candidates) { + if (FileUtils.test (c, FileTest.IS_EXECUTABLE)) { + return c; + } + } + return Environment.find_program_in_path ("mseries-vm"); + } + + /* Refresh run state from the guest's qemu.pid, and tail its serial log. */ + public void poll () { + if (!managed) { + if (running) { + sync_paused (); + } + return; + } + bool now = false; + DateTime? since = null; + string pidfile = Path.build_filename (dir, "qemu.pid"); + try { + string pid; + FileUtils.get_contents (pidfile, out pid); + pid = pid.strip (); + string cmdline; + FileUtils.get_contents ("/proc/%s/cmdline".printf (pid), out cmdline); + now = cmdline.contains ("qemu-system"); + if (now) { + var info = File.new_for_path (pidfile).query_info ("time::modified", FileQueryInfoFlags.NONE); + since = info.get_modification_date_time ().to_local (); + } + } catch (Error e) { + now = false; + } + bool changed = now != managed_running; + managed_running = now; + started = now ? since : null; + if (now) { + load_serial_tail (); + sync_paused (); + } else if (paused) { + paused = false; + } + if (changed) { + state_changed (); + } + } + + private void load_serial_tail () { + try { + var f = File.new_for_path (Path.build_filename (dir, "serial.log")); + var stream = f.read (); + var info = stream.query_info ("standard::size"); + int64 size = info.get_size (); + int64 max = 12000; + if (size > max) { + stream.seek (size - max, SeekType.SET); + } + var bytes = stream.read_bytes ((size_t) int64.min (size, max)); + string text = ((string) bytes.get_data ()).make_valid ((ssize_t) bytes.length); + try { + text = new Regex ("\\x1b\\[[0-9;?]*[ -/]*[@-~]|\\x1b\\][^\\x07\\x1b]*(\\x07|\\x1b\\\\)|\\x1b[()][A-Z0-9]|[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]").replace (text, -1, 0, ""); + } catch (RegexError e) {} + log.text = text; + } catch (Error e) { + // no serial log yet + } + } +} + +public class MachineStore : Object { + public static string config_path () { + return Path.build_filename (Environment.get_user_config_dir (), "machine-lab", "machines.ini"); + } + + public static GenericArray load () { + var list = new GenericArray (); + var kf = new KeyFile (); + try { + kf.load_from_file (config_path (), KeyFileFlags.NONE); + foreach (var id in kf.get_groups ()) { + var m = new Machine (); + m.id = id; + m.name = kf.get_string (id, "name"); + m.cpus = kf.get_integer (id, "cpus"); + m.mem_gib = kf.get_integer (id, "mem_gib"); + m.disk_gib = kf.get_integer (id, "disk_gib"); + m.ssh_port = kf.get_integer (id, "ssh_port"); + if (kf.has_key (id, "color")) { + m.color = kf.get_integer (id, "color"); + } + if (kf.has_key (id, "url")) { + m.url = kf.get_string (id, "url"); + } + if (kf.has_key (id, "recipe")) { + m.recipe = kf.get_string (id, "recipe"); + } + if (kf.has_key (id, "installer")) { + m.installer = kf.get_string (id, "installer"); + } + list.add (m); + } + } catch (Error e) { + // First run: no machines until the user creates one. + } + return list; + } + + // Guests already set up with mseries-vm: one vm.conf per directory under ~/vms. + public static void discover (GenericArray list) { + try { + var d = Dir.open (Machine.vms_dir ()); + string? entry; + string[] names = {}; + while ((entry = d.read_name ()) != null) { + names += entry; + } + qsort_with_data (names, sizeof (string), (a, b) => strcmp (a, b)); + foreach (var n in names) { + string conf; + string path = Path.build_filename (Machine.vms_dir (), n, "vm.conf"); + var kf = new KeyFile (); + try { + FileUtils.get_contents (path, out conf); + kf.load_from_data ("[c]\n" + conf, -1, KeyFileFlags.NONE); + } catch (Error e) { + continue; // not a VM directory (downloads, trials, ...) + } + var m = new Machine (); + m.kind = "managed"; + m.vm_name = n; + m.id = "vm-" + n; + string guest = kf.has_key ("c", "GUEST") ? kf.get_string ("c", "GUEST") : n; + switch (guest) { + case "omarchy": m.name = "Omarchy"; m.color = 7; break; + case "elementary": m.name = "elementary OS"; m.color = 0; break; + case "windows": m.name = "Windows 11"; m.color = 3; break; + default: m.name = n; m.color = 1; break; + } + if (kf.has_key ("c", "CPUS")) { + m.cpus = kf.get_integer ("c", "CPUS"); + } + if (kf.has_key ("c", "MEMORY_MB")) { + m.mem_gib = int.max (1, (kf.get_integer ("c", "MEMORY_MB") + 512) / 1024); + } + m.disk_gib = virtual_size_gib (Path.build_filename (Machine.vms_dir (), n, "disk.qcow2")); + list.add (m); + } + } catch (Error e) { + // No ~/vms directory: nothing to import. + } + } + + private static int virtual_size_gib (string disk) { + try { + string output; + Process.spawn_sync (null, { "qemu-img", "info", "--output=json", disk }, null, + SpawnFlags.SEARCH_PATH | SpawnFlags.STDERR_TO_DEV_NULL, null, out output); + Regex r = new Regex ("\\n \"virtual-size\":\\s*(\\d+)"); + MatchInfo mi; + if (r.match (output, 0, out mi)) { + return (int) (int64.parse (mi.fetch (1)) / 1073741824); + } + } catch (Error e) {} + return 0; + } + + public static void save (GenericArray list) { + var kf = new KeyFile (); + foreach (var m in list.data) { + if (m.managed) { + continue; + } + kf.set_string (m.id, "name", m.name); + kf.set_integer (m.id, "cpus", m.cpus); + kf.set_integer (m.id, "mem_gib", m.mem_gib); + kf.set_integer (m.id, "disk_gib", m.disk_gib); + kf.set_integer (m.id, "ssh_port", m.ssh_port); + kf.set_integer (m.id, "color", m.color); + if (m.url != null) { + kf.set_string (m.id, "url", m.url); + } + if (m.recipe != null) { + kf.set_string (m.id, "recipe", m.recipe); + } + if (m.installer != null) { + kf.set_string (m.id, "installer", m.installer); + } + } + try { + DirUtils.create_with_parents (Path.get_dirname (config_path ()), 0755); + kf.save_to_file (config_path ()); + } catch (Error e) { + warning ("Could not save machines: %s", e.message); + } + } +} + +/* Checks the host has what the launcher needs. */ +public class Readiness : Object { + public static string[] problems () { + string[] found = {}; + if (Environment.find_program_in_path ("qemu-system-aarch64") == null) { + found += "QEMU is missing. Install qemu-system-arm."; + } + if (Environment.find_program_in_path ("qemu-img") == null) { + found += "qemu-img is missing. Install qemu-utils."; + } + if (!FileUtils.test ("/usr/share/AAVMF/AAVMF_CODE.fd", FileTest.EXISTS)) { + found += "UEFI firmware is missing. Install qemu-efi-aarch64."; + } + if (Posix.access ("/dev/kvm", Posix.R_OK | Posix.W_OK) != 0) { + found += "No access to /dev/kvm. Add your user to the kvm group."; + } + return found; + } + + /* Free space, in GiB, on the volume that holds machine disks. */ + public static int64 free_gib () { + try { + // The data folder may not exist yet: ask about the nearest folder that does. + var dir = File.new_for_path (Environment.get_user_data_dir ()); + while (!dir.query_exists () && dir.get_parent () != null) { + dir = dir.get_parent (); + } + var info = dir.query_filesystem_info ("filesystem:free"); + return (int64) (info.get_attribute_uint64 ("filesystem:free") / 1073741824); + } catch (Error e) { + return -1; + } + } +} diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala new file mode 100644 index 000000000..40efe80a9 --- /dev/null +++ b/mseries/machine-lab/src/main.vala @@ -0,0 +1,551 @@ +/* + * Machine Lab: run another Linux on this Mac. + * Vala + GTK 4 + Granite 7, laid out like a first-party elementary OS app: + * flat sidebar of machines + content pane with tabs, suggested-action buttons, + * toasts, and an empty-state welcome. + */ +using Gtk; + +struct Preset { + public string name; + public string blurb; + public int cpus; + public int mem; + public int disk; + public int color; + public string url; + public string recipe; +} + +const Preset[] presets = { + { "Omarchy", "Arch-based tiling desktop", 4, 6, 32, 7, "https://omarchy.org", "omarchy" }, + { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 24, 4, "https://ubuntu.com/download/server/arm", "" }, + { "Debian", "Rock-solid and lightweight", 2, 2, 16, 5, "https://www.debian.org/CD/netinst/", "" }, + { "Fedora", "Fresh packages, GNOME", 4, 4, 24, 0, "https://fedoraproject.org/server/download", "" }, + { "Blank Machine", "Bring any ARM64 .iso", 2, 4, 16, 1, "", "" } +}; + +public class MachineRow : Gtk.ListBoxRow { + public Machine machine { get; construct; } + private Gtk.Label name_label; + private Gtk.Label sub_label; + private Gtk.Box dot; + private Gtk.Image icon; + + public signal void group_changed (); + public int group { get { return machine.is_fresh () ? 1 : 0; } } + + public MachineRow (Machine machine) { + Object (machine: machine); + } + + construct { + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + box.margin_top = 4; + box.margin_bottom = 4; + box.margin_start = 6; + box.margin_end = 6; + + var overlay = new Gtk.Overlay (); + icon = new Gtk.Image.from_icon_name ("computer"); + icon.pixel_size = 32; + icon.add_css_class ("ml-row-icon"); + overlay.child = icon; + dot = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + dot.add_css_class ("ml-dot"); + dot.halign = Gtk.Align.END; + dot.valign = Gtk.Align.END; + dot.width_request = 10; + dot.height_request = 10; + overlay.add_overlay (dot); + box.append (overlay); + + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + text.valign = Gtk.Align.CENTER; + name_label = new Gtk.Label (""); + name_label.xalign = 0; + name_label.ellipsize = Pango.EllipsizeMode.END; + name_label.add_css_class ("ml-machine-name"); + sub_label = new Gtk.Label (""); + sub_label.xalign = 0; + sub_label.add_css_class ("ml-machine-sub"); + text.append (name_label); + text.append (sub_label); + box.append (text); + child = box; + + machine.state_changed.connect (update); + update (); + } + + public void update () { + name_label.label = machine.name; + string state = machine.running ? (machine.paused ? "Paused" : "Running") : machine.is_fresh () ? "Not installed" : "Stopped"; + sub_label.label = "%s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); + for (int i = 0; i < 8; i++) { + icon.remove_css_class ("ml-c-%d".printf (i)); + } + icon.add_css_class ("ml-c-%d".printf (machine.color)); + if (machine.running) { + dot.add_css_class ("running"); + } else { + dot.remove_css_class ("running"); + } + } +} + +public class MachineLab : Gtk.Application { + private GenericArray machines; + private Gtk.ApplicationWindow window; + private Granite.Toast toast; + private Gtk.ListBox list; + private Gtk.Stack content; + private HashTable pages = new HashTable (str_hash, str_equal); + + public MachineLab () { + Object ( + application_id: "io.github.mseries.MachineLab", + flags: ApplicationFlags.DEFAULT_FLAGS + ); + } + + protected override void startup () { + base.startup (); + Granite.init (); + + var gtk_settings = Gtk.Settings.get_default (); + var granite_settings = Granite.Settings.get_default (); + gtk_settings.gtk_application_prefer_dark_theme = + granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + granite_settings.notify["prefers-color-scheme"].connect (() => { + gtk_settings.gtk_application_prefer_dark_theme = + granite_settings.prefers_color_scheme == Granite.Settings.ColorScheme.DARK; + }); + + var provider = new Gtk.CssProvider (); + provider.load_from_resource ("/io/github/mseries/MachineLab/style.css"); + Gtk.StyleContext.add_provider_for_display ( + Gdk.Display.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + var new_action = new SimpleAction ("new-machine", null); + new_action.activate.connect (() => add_machine (presets[0])); + add_action (new_action); + set_accels_for_action ("app.new-machine", { "n" }); + var refresh_action = new SimpleAction ("refresh", null); + refresh_action.activate.connect (() => { + var row = list.get_selected_row (); + if (row != null) { + pages[((MachineRow) row).machine.id].recheck (); + } + }); + add_action (refresh_action); + set_accels_for_action ("app.refresh", { "F5", "r" }); + set_accels_for_action ("app.quit", { "q" }); + var quit = new SimpleAction ("quit", null); + quit.activate.connect (() => window.close ()); + add_action (quit); + } + + protected override void activate () { + if (window != null) { + window.present (); + return; + } + machines = MachineStore.load (); + MachineStore.discover (machines); + + window = new Gtk.ApplicationWindow (this); + window.title = "Machine Lab"; + window.default_width = 940; + window.default_height = 640; + window.titlebar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { visible = false }; + + var layout = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + layout.append (build_sidebar ()); + layout.append (new Gtk.Separator (Gtk.Orientation.VERTICAL)); + + content = new Gtk.Stack (); + content.hexpand = true; + content.transition_type = Gtk.StackTransitionType.CROSSFADE; + content.add_named (build_empty_state (), "empty"); + layout.append (content); + + var overlay = new Gtk.Overlay (); + overlay.child = layout; + toast = new Granite.Toast (""); + overlay.add_overlay (toast); + window.child = overlay; + + foreach (var m in machines.data) { + add_row_and_page (m); + } + select_first (); + + window.close_request.connect (() => { + foreach (var m in machines.data) { + m.stop (); + } + return false; + }); + Timeout.add_seconds (2, () => { + foreach (var m in machines.data) { + m.poll (); + } + return Source.CONTINUE; + }); + foreach (var m in machines.data) { + m.poll (); + } + window.present (); + } + + // ---- Sidebar ---------------------------------------------------------- + + private Gtk.Widget build_sidebar () { + var side = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + side.add_css_class ("ml-sidebar"); + side.width_request = 250; + + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = "close:"; + header.title_widget = new Gtk.Label (""); + var handle = new Gtk.WindowHandle (); + handle.child = header; + side.append (handle); + + + list = new Gtk.ListBox (); + list.selection_mode = Gtk.SelectionMode.SINGLE; + list.add_css_class ("navigation-sidebar"); + list.set_sort_func ((a, b) => { + var ra = (MachineRow) a; + var rb = (MachineRow) b; + if (ra.group != rb.group) return ra.group - rb.group; + return strcmp (ra.machine.name.down (), rb.machine.name.down ()); + }); + list.set_header_func ((row, before) => { + var r = (MachineRow) row; + if (before != null && ((MachineRow) before).group == r.group) { + row.set_header (null); + return; + } + var h = new Gtk.Label (r.group == 0 ? "YOUR MACHINES" : "NEW — NOT INSTALLED"); + h.xalign = 0; + h.add_css_class ("ml-sidebar-title"); + row.set_header (h); + }); + list.row_selected.connect ((row) => { + if (row == null) { + content.visible_child_name = "empty"; + return; + } + content.visible_child_name = ((MachineRow) row).machine.id; + }); + var scroll = new Gtk.ScrolledWindow (); + scroll.vexpand = true; + scroll.child = list; + side.append (scroll); + + side.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + var add = new Gtk.MenuButton (); + var add_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + add_box.append (new Gtk.Image.from_icon_name ("list-add-symbolic")); + add_box.append (new Gtk.Label ("New Machine")); + add.child = add_box; + add.add_css_class ("flat"); + add.margin_top = 6; + add.margin_bottom = 6; + add.margin_start = 6; + add.margin_end = 6; + add.halign = Gtk.Align.START; + add.direction = Gtk.ArrowType.UP; + add.tooltip_markup = Granite.markup_accel_tooltip ({ "n" }, "Create a new machine"); + var pop = new Gtk.Popover (); + var pbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + pbox.margin_top = 6; + pbox.margin_bottom = 6; + pbox.margin_start = 6; + pbox.margin_end = 6; + foreach (var p in presets) { + var item = new Gtk.Button (); + item.add_css_class ("flat"); + var ib = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + var ic = new Gtk.Image.from_icon_name ("computer"); + ic.pixel_size = 28; + ic.add_css_class ("ml-row-icon"); + ic.add_css_class ("ml-c-%d".printf (p.color)); + ib.append (ic); + var tb = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + var n = new Gtk.Label (p.name); + n.xalign = 0; + n.add_css_class ("ml-machine-name"); + var d = new Gtk.Label ("%s · %d CPU · %d GiB".printf (p.blurb, p.cpus, p.mem)); + d.xalign = 0; + d.add_css_class ("ml-machine-sub"); + tb.append (n); + tb.append (d); + ib.append (tb); + item.child = ib; + var preset = p; + item.clicked.connect (() => { pop.popdown (); add_machine (preset); }); + pbox.append (item); + } + pop.child = pbox; + add.popover = pop; + side.append (add); + return side; + } + + private Gtk.Widget build_empty_state () { + var page = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = ":maximize"; + header.title_widget = new Gtk.Label (""); + var handle = new Gtk.WindowHandle (); + handle.child = header; + page.append (handle); + + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); + box.vexpand = true; + box.valign = Gtk.Align.CENTER; + box.halign = Gtk.Align.CENTER; + box.margin_start = 32; + box.margin_end = 32; + box.width_request = 520; + var t = new Gtk.Label ("Create a Machine"); + t.add_css_class ("title-1"); + box.append (t); + var d = new Gtk.Label ("Pick a starting point. Machine Lab sets up sensible hardware, then walks you through the installer."); + d.wrap = true; + d.justify = Gtk.Justification.CENTER; + d.margin_bottom = 14; + d.add_css_class ("dim-label"); + box.append (d); + + var grid = new Gtk.Grid (); + grid.column_homogeneous = true; + grid.column_spacing = 12; + grid.row_spacing = 12; + int i = 0; + foreach (var p in presets) { + var card = new Gtk.Button (); + card.add_css_class ("flat"); + var cb = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + cb.add_css_class ("ml-gallery-card"); + var ic = new Gtk.Image.from_icon_name ("computer"); + ic.pixel_size = 40; + ic.add_css_class ("ml-row-icon"); + ic.add_css_class ("ml-c-%d".printf (p.color)); + cb.append (ic); + var tb = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + tb.valign = Gtk.Align.CENTER; + var n = new Gtk.Label (p.name); + n.xalign = 0; + n.add_css_class ("ml-spec-value"); + var bl = new Gtk.Label (p.blurb); + bl.xalign = 0; + bl.add_css_class ("ml-spec-label"); + var sp = new Gtk.Label ("%d CPU · %d GiB RAM · %d GiB disk".printf (p.cpus, p.mem, p.disk)); + sp.xalign = 0; + sp.add_css_class ("ml-fineprint"); + tb.append (n); + tb.append (bl); + tb.append (sp); + cb.append (tb); + card.child = cb; + var preset = p; + card.clicked.connect (() => add_machine (preset)); + grid.attach (card, i % 2, i / 2); + i++; + } + box.append (grid); + page.append (box); + return page; + } + + // ---- Machines --------------------------------------------------------- + + private void add_row_and_page (Machine m) { + var page = new MachinePage (m); + page.toast.connect (send_toast); + page.changed.connect (() => { + MachineStore.save (machines); + row_for (m).update (); + }); + m.installer_detached.connect (() => { + MachineStore.save (machines); + send_toast ("%s is installed. It will boot from its disk now.".printf (m.name)); + }); + page.start_requested.connect (() => start_machine (m)); + page.remove_requested.connect (() => confirm_remove.begin (m)); + pages[m.id] = page; + content.add_named (page, m.id); + var row = new MachineRow (m); + m.state_changed.connect (() => list.invalidate_sort ()); + m.state_changed.connect (() => list.invalidate_headers ()); + list.append (row); + } + + private MachineRow row_for (Machine m) { + for (var r = list.get_row_at_index (0); r != null; r = list.get_row_at_index (r.get_index () + 1)) { + if (((MachineRow) r).machine == m) { + return (MachineRow) r; + } + } + assert_not_reached (); + } + + private void select_first () { + var first = list.get_row_at_index (0); + if (first != null) { + list.select_row (first); + } else { + content.visible_child_name = "empty"; + } + } + + private void add_machine (Preset preset) { + var m = new Machine (); + m.id = "machine-%s".printf (Uuid.string_random ().substring (0, 8)); + int n = machines.length + 1; + m.name = machines.length == 0 ? preset.name : "%s %d".printf (preset.name, n); + m.cpus = preset.cpus; + m.mem_gib = preset.mem; + m.disk_gib = preset.disk; + m.color = preset.color; + m.url = preset.url == "" ? null : preset.url; + m.recipe = preset.recipe == "" ? null : preset.recipe; + int port = 2222; + bool clash = true; + while (clash) { + clash = false; + foreach (var o in machines.data) { + if (o.ssh_port == port) { port++; clash = true; } + } + } + m.ssh_port = port; + machines.add (m); + MachineStore.save (machines); + add_row_and_page (m); + list.select_row (row_for (m)); + pages[m.id].show_tab ("settings"); + send_toast ("Created %s".printf (m.name)); + } + + private void start_machine (Machine m) { + if (m.managed) { + string? tool = Machine.find_mseries_vm (); + if (tool == null) { + show_error ("mseries-vm was not found. Set MSERIES_VM_TOOL to its full path."); + return; + } + try { + m.start (tool); + send_toast ("Starting %s…".printf (m.name)); + } catch (Error e) { + show_error (e.message); + } + return; + } + string? launcher = find_launcher (); + if (launcher == null) { + show_error ("The QEMU launcher (run-vm.sh) was not found next to the app. Set MACHINE_LAB_LAUNCHER to its full path."); + return; + } + if (!m.has_disk () && m.installer == null) { + show_error ("This machine has no disk yet. Choose an ARM64 installer image first."); + return; + } + foreach (var o in machines.data) { + if (o != m && o.running && o.ssh_port == m.ssh_port) { + show_error ("%s is already using SSH port %d. Change the port in Settings.".printf (o.name, o.ssh_port)); + return; + } + } + try { + m.start (launcher); + send_toast ("Starting %s…".printf (m.name)); + } catch (Error e) { + show_error (e.message); + } + } + + private async void confirm_remove (Machine m) { + var dialog = new Gtk.AlertDialog ("Remove “%s”?".printf (m.name)); + dialog.detail = "The machine is removed from Machine Lab. You can also delete its virtual disk, which cannot be undone."; + dialog.buttons = { "Cancel", "Remove", "Remove and Delete Disk" }; + dialog.cancel_button = 0; + dialog.default_button = 0; + try { + int choice = yield dialog.choose (window, null); + if (choice == 0) return; + m.stop (); + if (choice == 2) { + m.delete_files (); + } + var row = row_for (m); + int idx = row.get_index (); + list.remove (row); + content.remove (pages[m.id]); + pages.remove (m.id); + machines.remove (m); + MachineStore.save (machines); + var next = list.get_row_at_index (int.min (idx, (int) machines.length - 1)); + if (next != null) { + list.select_row (next); + } else { + content.visible_child_name = "empty"; + } + send_toast ("Removed %s".printf (m.name)); + } catch (Error e) { + // dialog dismissed + } + } + + private string? find_launcher () { + string? override_path = Environment.get_variable ("MACHINE_LAB_LAUNCHER"); + if (override_path != null && FileUtils.test (override_path, FileTest.IS_EXECUTABLE)) { + return override_path; + } + foreach (var installed in new string[] { + Path.build_filename (Environment.get_home_dir (), ".local", "bin", "machine-lab-run-vm"), + "/usr/bin/machine-lab-run-vm" + }) { + if (FileUtils.test (installed, FileTest.IS_EXECUTABLE)) { + return installed; + } + } + string exe_dir = ""; + try { + exe_dir = Path.get_dirname (FileUtils.read_link ("/proc/self/exe")); + } catch (Error e) {} + string[] candidates = { + Path.build_filename (exe_dir, "run-vm.sh"), + Path.build_filename (exe_dir, "..", "launcher", "run-vm.sh") + }; + foreach (var c in candidates) { + if (FileUtils.test (c, FileTest.IS_EXECUTABLE)) { + return c; + } + } + return null; + } + + private void send_toast (string message) { + toast.title = message; + toast.send_notification (); + } + + private void show_error (string message) { + var dialog = new Gtk.AlertDialog ("Could not start the virtual machine"); + dialog.detail = message; + dialog.show (window); + } + + public static int main (string[] args) { + return new MachineLab ().run (args); + } +} diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala new file mode 100644 index 000000000..885b431aa --- /dev/null +++ b/mseries/machine-lab/src/page.vala @@ -0,0 +1,833 @@ +/* One machine's content pane: header with tabs, Overview / Console / Settings. */ +using Gtk; + +const string[] swatch_names = { "Blueberry", "Mint", "Lime", "Banana", "Orange", "Strawberry", "Bubblegum", "Grape" }; + +public class MachinePage : Gtk.Box { + public Machine machine { get; construct; } + + public signal void toast (string message); + public signal void changed (); // a persisted setting changed + public signal void remove_requested (); + public signal void start_requested (); + + private Gtk.Label title; + private Gtk.Label status_pill; + private Gtk.Label uptime; + private Gtk.Label cpu_v; + private Gtk.Label mem_v; + private Gtk.Label disk_v; + private Gtk.Label net_v; + private Gtk.Label ssh_cmd; + private Gtk.Image hero_icon; + private Gtk.Label hero_sub; + private Gtk.Box setup_card; + private Gtk.Box connect_card; + private Gtk.MenuButton more_button; + private Gtk.Label[] step_badges = {}; + private Gtk.Label step2_detail; + private Gtk.Label step1_detail; + private Gtk.Label step3_detail; + private Gtk.Button get_button; + private Gtk.Button pause_button; + private Gtk.Label step1_title; + private Gtk.Label step2_title; + private Gtk.Label strip; + private Gtk.Box creds_card; + private Gtk.Label creds_label; + private Gtk.Box ready_card; + private Gtk.Image ready_icon; + private Gtk.Label ready_title; + private Gtk.Label ready_detail; + private Gtk.ToggleButton[] swatches = {}; + private Gtk.Box drop_zone; + private Gtk.Label drop_title; + private Gtk.Label drop_detail; + private Gtk.Button start_button; + private Gtk.Button stop_button; + private Gtk.Stack stack; + private Gtk.Box settings_form; + private Gtk.Label settings_note; + private Gtk.Entry name_entry; + private Gtk.Scale cpu_scale; + private Gtk.Scale mem_scale; + private Gtk.Scale disk_scale; + private Gtk.SpinButton port_spin; + private Gtk.Label cpu_scale_v; + private Gtk.Label mem_scale_v; + private Gtk.Label disk_scale_v; + private bool loading = false; + private uint tick = 0; + + public MachinePage (Machine machine) { + Object (machine: machine, orientation: Gtk.Orientation.VERTICAL, spacing: 0); + } + + construct { + hexpand = true; + stack = new Gtk.Stack (); + stack.transition_type = Gtk.StackTransitionType.CROSSFADE; + stack.vexpand = true; + stack.add_titled (build_overview (), "overview", "Overview"); + stack.add_titled (build_console (), "console", "Console"); + stack.add_titled (build_settings (), "settings", "Settings"); + + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = ":maximize"; + var switcher = new Gtk.StackSwitcher (); + switcher.stack = stack; + header.title_widget = switcher; + var more = new Gtk.MenuButton (); + more_button = more; + more.icon_name = "view-more-symbolic"; + more.tooltip_text = "More actions"; + var pop = new Gtk.Popover (); + var remove = new Gtk.Button.with_label ("Remove Machine…"); + remove.add_css_class ("flat"); + remove.clicked.connect (() => { pop.popdown (); remove_requested (); }); + pop.child = remove; + more.popover = pop; + header.pack_end (more); + var handle = new Gtk.WindowHandle (); + handle.child = header; + append (handle); + append (stack); + + machine.state_changed.connect (refresh); + machine.finished.connect ((code) => { + if (code > 0) { + show_tab ("console"); + } + toast (code > 0 ? "%s stopped with an error (exit %d)".printf (machine.name, code) + : "%s stopped".printf (machine.name)); + }); + if (machine.recipe != null && machine.is_fresh ()) { + machine.check_latest.begin (); + } + uint ticks = 0; + tick = Timeout.add_seconds (1, () => { + update_uptime (); + // Keep free-space and requirement checks current without a click. + if (++ticks % 15 == 0 && !machine.running) { + refresh (); + } + return Source.CONTINUE; + }); + refresh (); + } + + ~MachinePage () { + if (tick != 0) { + Source.remove (tick); + } + } + + /* Re-read free space and host requirements (and Omarchy's latest nightly). */ + public void recheck () { + if (machine.recipe != null && machine.is_fresh ()) { + machine.check_latest.begin (); + } + refresh (); + int64 free = Readiness.free_gib (); + toast (free >= 0 ? "Checked again: %d GiB free".printf ((int) free) : "Checked again"); + } + + public void show_tab (string name) { + stack.visible_child_name = name; + } + + // ---- Overview --------------------------------------------------------- + + private Gtk.Widget build_overview () { + var scroller = new Gtk.ScrolledWindow (); + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 20); + clamp.margin_start = 32; + clamp.margin_end = 32; + clamp.margin_top = 8; + clamp.margin_bottom = 28; + clamp.width_request = 460; + scroller.child = clamp; + + var hero = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18); + hero_icon = new Gtk.Image.from_icon_name ("computer"); + hero_icon.pixel_size = 64; + hero_icon.add_css_class ("ml-hero-icon"); + hero_icon.valign = Gtk.Align.START; + hero.append (hero_icon); + + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); + text.hexpand = true; + text.valign = Gtk.Align.CENTER; + title = new Gtk.Label (""); + title.add_css_class ("title-1"); + title.xalign = 0; + title.ellipsize = Pango.EllipsizeMode.END; + text.append (title); + hero_sub = new Gtk.Label (""); + hero_sub.xalign = 0; + hero_sub.wrap = true; + hero_sub.add_css_class ("dim-label"); + text.append (hero_sub); + + var pills = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + pills.margin_top = 6; + status_pill = new Gtk.Label (""); + status_pill.add_css_class ("ml-pill"); + pills.append (status_pill); + uptime = new Gtk.Label (""); + uptime.add_css_class ("ml-fineprint"); + pills.append (uptime); + text.append (pills); + hero.append (text); + clamp.append (hero); + + // Guided first run: shown only for a machine that has never been installed. + setup_card = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + setup_card.add_css_class ("ml-card"); + var setup_head = new Gtk.Label ("Set up in three steps"); + setup_head.xalign = 0; + setup_head.add_css_class ("title-4"); + setup_head.margin_top = 14; + setup_head.margin_start = 16; + setup_head.margin_bottom = 4; + setup_card.append (setup_head); + + get_button = new Gtk.Button.with_label ("Get ISO"); + get_button.valign = Gtk.Align.CENTER; + get_button.clicked.connect (() => { + if (machine.recipe != null) { + machine.check_latest.begin (); + } else if (machine.url != null) { + new Gtk.UriLauncher (machine.url).launch.begin ((Gtk.Window) get_root (), null); + } + }); + setup_card.append (step_row (1, "Get an ARM64 installer", + "Download a Linux ARM64 .iso, or use one you already have.", get_button, out step1_detail)); + var pick = new Gtk.Button.with_label ("Choose…"); + pick.valign = Gtk.Align.CENTER; + pick.clicked.connect (() => choose_installer.begin ()); + setup_card.append (step_row (2, "Choose the installer file", + "Pick it here, or drop it on the box below.", pick, out step2_detail)); + setup_card.append (step_row (3, "Start the installer", + "Install inside the window. Shut the guest down when it finishes and Machine Lab boots the new disk from then on.", null, out step3_detail, true)); + clamp.append (setup_card); + + strip = new Gtk.Label (""); + strip.wrap = true; + strip.xalign = 0; + strip.add_css_class ("ml-note"); + clamp.append (strip); + + creds_card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + creds_card.add_css_class ("ml-card"); + creds_card.add_css_class ("ml-spec"); + var key = new Gtk.Image.from_icon_name ("dialog-password-symbolic"); + key.pixel_size = 24; + creds_card.append (key); + var ctext = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + ctext.hexpand = true; + ctext.valign = Gtk.Align.CENTER; + creds_label = new Gtk.Label (""); + creds_label.xalign = 0; + creds_label.wrap = true; + creds_label.selectable = true; + creds_label.add_css_class ("ml-mono"); + var cl2 = new Gtk.Label ("Sign-in for this machine, saved with its files"); + cl2.xalign = 0; + cl2.add_css_class ("ml-spec-label"); + ctext.append (creds_label); + ctext.append (cl2); + creds_card.append (ctext); + var ccopy = new Gtk.Button.from_icon_name ("edit-copy-symbolic"); + ccopy.add_css_class ("flat"); + ccopy.valign = Gtk.Align.CENTER; + ccopy.tooltip_text = "Copy sign-in"; + ccopy.clicked.connect (() => { + Gdk.Display.get_default ().get_clipboard ().set_text (creds_label.label); + toast ("Copied sign-in"); + }); + creds_card.append (ccopy); + clamp.append (creds_card); + + var specs = new Gtk.Grid (); + specs.column_homogeneous = true; + specs.row_homogeneous = true; + specs.column_spacing = 12; + specs.row_spacing = 12; + specs.attach (spec_card ("computer-symbolic", out cpu_v, "Processor"), 0, 0); + specs.attach (spec_card ("media-optical-symbolic", out mem_v, "Memory"), 1, 0); + specs.attach (spec_card ("drive-harddisk-symbolic", out disk_v, "Disk, grows as used"), 0, 1); + specs.attach (spec_card ("network-wired-symbolic", out net_v, "Network"), 1, 1); + clamp.append (specs); + + // Readiness + ready_card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + ready_card.add_css_class ("ml-card"); + ready_card.add_css_class ("ml-spec"); + ready_icon = new Gtk.Image (); + ready_icon.pixel_size = 24; + ready_icon.valign = Gtk.Align.START; + ready_card.append (ready_icon); + var rt = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + rt.hexpand = true; + ready_title = new Gtk.Label (""); + ready_title.xalign = 0; + ready_title.add_css_class ("ml-spec-value"); + ready_detail = new Gtk.Label (""); + ready_detail.xalign = 0; + ready_detail.wrap = true; + ready_detail.add_css_class ("ml-spec-label"); + rt.append (ready_title); + rt.append (ready_detail); + ready_card.append (rt); + var recheck_btn = new Gtk.Button.from_icon_name ("view-refresh-symbolic"); + recheck_btn.valign = Gtk.Align.CENTER; + recheck_btn.add_css_class ("flat"); + recheck_btn.tooltip_markup = Granite.markup_accel_tooltip ({ "F5", "r" }, "Check disk space and requirements again"); + recheck_btn.clicked.connect (() => recheck ()); + ready_card.append (recheck_btn); + clamp.append (ready_card); + + // Connect row + var connect = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + connect_card = connect; + connect.add_css_class ("ml-card"); + connect.add_css_class ("ml-spec"); + var term = new Gtk.Image.from_icon_name ("utilities-terminal-symbolic"); + term.pixel_size = 24; + connect.append (term); + var ct = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + ct.hexpand = true; + ct.valign = Gtk.Align.CENTER; + ssh_cmd = new Gtk.Label (""); + ssh_cmd.xalign = 0; + ssh_cmd.selectable = true; + ssh_cmd.add_css_class ("ml-mono"); + var cl = new Gtk.Label ("Connect once the guest is running"); + cl.xalign = 0; + cl.add_css_class ("ml-spec-label"); + ct.append (ssh_cmd); + ct.append (cl); + connect.append (ct); + var copy = new Gtk.Button.from_icon_name ("edit-copy-symbolic"); + copy.valign = Gtk.Align.CENTER; + copy.add_css_class ("flat"); + copy.tooltip_text = "Copy command"; + copy.clicked.connect (() => { + Gdk.Display.get_default ().get_clipboard ().set_text (ssh_cmd.label); + toast ("Copied SSH command"); + }); + connect.append (copy); + clamp.append (connect); + + // Installer + drop_zone = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + drop_zone.add_css_class ("ml-drop"); + var di = new Gtk.Image.from_icon_name ("media-optical"); + di.pixel_size = 48; + drop_zone.append (di); + var dt = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + dt.hexpand = true; + dt.valign = Gtk.Align.CENTER; + drop_title = new Gtk.Label (""); + drop_title.xalign = 0; + drop_title.add_css_class ("title-4"); + drop_detail = new Gtk.Label (""); + drop_detail.xalign = 0; + drop_detail.ellipsize = Pango.EllipsizeMode.MIDDLE; + drop_detail.add_css_class ("dim-label"); + dt.append (drop_title); + dt.append (drop_detail); + drop_zone.append (dt); + var browse = new Gtk.Button.with_label ("Browse…"); + browse.valign = Gtk.Align.CENTER; + browse.clicked.connect (() => choose_installer.begin ()); + drop_zone.append (browse); + var eject = new Gtk.Button.from_icon_name ("media-eject-symbolic"); + eject.valign = Gtk.Align.CENTER; + eject.add_css_class ("flat"); + eject.tooltip_text = "Boot from disk only"; + eject.clicked.connect (() => { machine.installer = null; changed (); refresh (); }); + drop_zone.append (eject); + clamp.append (drop_zone); + + var target = new Gtk.DropTarget (typeof (Gdk.FileList), Gdk.DragAction.COPY); + target.enter.connect (() => { drop_zone.add_css_class ("hover"); return Gdk.DragAction.COPY; }); + target.leave.connect (() => drop_zone.remove_css_class ("hover")); + target.drop.connect ((value) => { + drop_zone.remove_css_class ("hover"); + foreach (var f in ((Gdk.FileList) value.get_boxed ()).get_files ()) { + set_installer (f.get_path ()); + return true; + } + return false; + }); + drop_zone.add_controller (target); + + var actions = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + actions.halign = Gtk.Align.END; + pause_button = new Gtk.Button.with_label ("Pause"); + pause_button.clicked.connect (() => { + if (machine.paused) machine.resume (); else machine.pause (); + refresh (); + }); + actions.append (pause_button); + stop_button = new Gtk.Button.with_label ("Force Stop"); + stop_button.add_css_class ("destructive-action"); + stop_button.clicked.connect (() => machine.stop ()); + actions.append (stop_button); + start_button = new Gtk.Button.with_label ("Start Machine"); + start_button.add_css_class ("suggested-action"); + start_button.clicked.connect (() => start_requested ()); + actions.append (start_button); + clamp.append (actions); + + var fine = new Gtk.Label ("Needs QEMU, UEFI firmware and access to /dev/kvm. With an installer attached the machine boots it; remove it with the eject button once the system is installed."); + fine.wrap = true; + fine.xalign = 0; + fine.add_css_class ("ml-fineprint"); + clamp.append (fine); + return scroller; + } + + private Gtk.Widget step_row (int n, string title, string detail, Gtk.Widget? action, + out Gtk.Label? detail_label, bool last = false) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + row.add_css_class ("ml-form-row"); + if (!last) { + row.add_css_class ("ml-form-sep"); + } + var badge = new Gtk.Label ("%d".printf (n)); + badge.add_css_class ("ml-step-badge"); + badge.valign = Gtk.Align.START; + step_badges += badge; + row.append (badge); + var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + t.hexpand = true; + var tl = new Gtk.Label (title); + if (n == 1) step1_title = tl; + if (n == 2) step2_title = tl; + tl.xalign = 0; + tl.add_css_class ("ml-spec-value"); + var dl = new Gtk.Label (detail); + dl.xalign = 0; + dl.wrap = true; + dl.ellipsize = Pango.EllipsizeMode.NONE; + dl.add_css_class ("ml-spec-label"); + t.append (tl); + t.append (dl); + row.append (t); + detail_label = dl; + if (action != null) { + row.append (action); + } + return row; + } + + private Gtk.Widget spec_card (string icon_name, out Gtk.Label value, string label) { + var card = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + card.add_css_class ("ml-card"); + card.add_css_class ("ml-spec"); + var icon = new Gtk.Image.from_icon_name (icon_name); + icon.pixel_size = 24; + card.append (icon); + var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + t.valign = Gtk.Align.CENTER; + value = new Gtk.Label (""); + value.xalign = 0; + value.add_css_class ("ml-spec-value"); + var l = new Gtk.Label (label); + l.xalign = 0; + l.add_css_class ("ml-spec-label"); + t.append (value); + t.append (l); + card.append (t); + return card; + } + + // ---- Console ---------------------------------------------------------- + + private Gtk.Widget build_console () { + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 10); + box.margin_start = 24; + box.margin_end = 24; + box.margin_bottom = 20; + box.margin_top = 4; + + var view = new Gtk.TextView.with_buffer (machine.log); + view.editable = false; + view.cursor_visible = false; + view.monospace = true; + view.wrap_mode = Gtk.WrapMode.WORD_CHAR; + view.top_margin = 12; + view.bottom_margin = 12; + view.left_margin = 14; + view.right_margin = 14; + view.add_css_class ("ml-console"); + + var scroller = new Gtk.ScrolledWindow (); + scroller.vexpand = true; + scroller.child = view; + scroller.add_css_class ("ml-console-frame"); + machine.log.changed.connect (() => { + Gtk.TextIter end; + machine.log.get_end_iter (out end); + var mark = machine.log.create_mark (null, end, false); + view.scroll_to_mark (mark, 0.0, false, 0, 0); + machine.log.delete_mark (mark); + }); + box.append (scroller); + + var bar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + var hint = new Gtk.Label ("Launcher and QEMU output"); + hint.hexpand = true; + hint.xalign = 0; + hint.add_css_class ("ml-fineprint"); + bar.append (hint); + var copy = new Gtk.Button.with_label ("Copy"); + copy.clicked.connect (() => { + Gtk.TextIter s, e; + machine.log.get_bounds (out s, out e); + Gdk.Display.get_default ().get_clipboard ().set_text (machine.log.get_text (s, e, false)); + toast ("Console copied"); + }); + bar.append (copy); + var clear = new Gtk.Button.with_label ("Clear"); + clear.clicked.connect (() => machine.log.text = ""); + bar.append (clear); + box.append (bar); + return box; + } + + // ---- Settings --------------------------------------------------------- + + private Gtk.Widget build_settings () { + var scroller = new Gtk.ScrolledWindow (); + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 16); + clamp.margin_start = 32; + clamp.margin_end = 32; + clamp.margin_top = 8; + clamp.margin_bottom = 28; + clamp.width_request = 460; + scroller.child = clamp; + + settings_note = new Gtk.Label (""); + settings_note.wrap = true; + settings_note.xalign = 0; + settings_note.add_css_class ("ml-note"); + clamp.append (settings_note); + + settings_form = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + settings_form.add_css_class ("ml-card"); + clamp.append (settings_form); + + int cores = (int) get_num_processors (); + int host_gib = host_memory_gib (); + + name_entry = new Gtk.Entry (); + name_entry.hexpand = true; + name_entry.max_length = 40; + name_entry.changed.connect (() => { + if (loading) return; + var n = name_entry.text.strip (); + machine.name = n == "" ? "Untitled" : n; + title.label = machine.name; + changed (); + }); + settings_form.append (form_row ("Name", null, name_entry)); + + var sw_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + for (int i = 0; i < 8; i++) { + var b = new Gtk.ToggleButton (); + b.add_css_class ("ml-swatch"); + b.add_css_class ("ml-s-%d".printf (i)); + b.tooltip_text = swatch_names[i]; + int idx = i; + b.toggled.connect (() => { + if (loading || !b.active) return; + machine.color = idx; + changed (); refresh (); + }); + if (swatches.length > 0) { + b.group = swatches[0]; + } + swatches += b; + sw_box.append (b); + } + settings_form.append (form_row ("Color", null, sw_box)); + + cpu_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 1, cores, 1); + cpu_scale_v = new Gtk.Label (""); + settings_form.append (form_row ("Processors", cpu_scale_v, cpu_scale)); + cpu_scale.value_changed.connect (() => { + if (loading) return; + machine.cpus = (int) cpu_scale.get_value (); + changed (); refresh (); + }); + + mem_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 1, int.max (2, host_gib - 2), 1); + mem_scale_v = new Gtk.Label (""); + settings_form.append (form_row ("Memory", mem_scale_v, mem_scale)); + mem_scale.value_changed.connect (() => { + if (loading) return; + machine.mem_gib = (int) mem_scale.get_value (); + changed (); refresh (); + }); + + disk_scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 8, 512, 8); + disk_scale_v = new Gtk.Label (""); + settings_form.append (form_row ("Disk size", disk_scale_v, disk_scale)); + disk_scale.value_changed.connect (() => { + if (loading) return; + machine.disk_gib = (int) disk_scale.get_value (); + changed (); refresh (); + }); + + port_spin = new Gtk.SpinButton.with_range (1024, 65535, 1); + port_spin.value_changed.connect (() => { + if (loading) return; + machine.ssh_port = (int) port_spin.get_value (); + changed (); refresh (); + }); + settings_form.append (form_row ("SSH port on this Mac", null, port_spin, true)); + + foreach (var s in new Gtk.Scale[] { cpu_scale, mem_scale, disk_scale }) { + s.draw_value = false; + } + return scroller; + } + + private Gtk.Widget form_row (string label, Gtk.Label? value, Gtk.Widget control, bool last = false) { + var row = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + row.add_css_class ("ml-form-row"); + if (!last) { + row.add_css_class ("ml-form-sep"); + } + var top = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + var l = new Gtk.Label (label); + l.xalign = 0; + l.hexpand = true; + top.append (l); + if (value != null) { + value.add_css_class ("ml-spec-value"); + top.append (value); + row.append (top); + row.append (control); + } else { + control.halign = Gtk.Align.END; + control.hexpand = false; + if (control is Gtk.Entry) { + control.hexpand = true; + control.halign = Gtk.Align.FILL; + ((Gtk.Entry) control).width_chars = 24; + } + top.append (control); + row.append (top); + } + return row; + } + + private static int host_memory_gib () { + try { + string contents; + FileUtils.get_contents ("/proc/meminfo", out contents); + foreach (var line in contents.split ("\n")) { + if (line.has_prefix ("MemTotal:")) { + var kb = int64.parse (line.substring (9).strip ().split (" ")[0]); + return (int) (kb / 1024 / 1024); + } + } + } catch (Error e) {} + return 16; + } + + // ---- State ------------------------------------------------------------ + + public void refresh () { + bool running = machine.running; + bool managed = machine.managed; + bool fresh = machine.is_fresh (); + title.label = machine.name; + cpu_v.label = machine.cpus == 1 ? "1 virtual CPU" : "%d virtual CPUs".printf (machine.cpus); + mem_v.label = "%d GiB".printf (machine.mem_gib); + disk_v.label = machine.disk_gib > 0 ? "%d GiB".printf (machine.disk_gib) : "Disk"; + net_v.label = "Shared (NAT)"; + ssh_cmd.label = "ssh -p %d localhost".printf (machine.ssh_port); + + setup_card.visible = fresh && !managed; + connect_card.visible = !managed && !fresh; + drop_zone.visible = !managed; + more_button.visible = !managed; + get_button.visible = machine.url != null || machine.recipe != null; + if (machine.recipe != null) { + step1_title.label = "Download Omarchy automatically"; + step1_detail.label = machine.check_summary ?? "Fetches the latest verified ARM64 nightly and installs it unattended."; + get_button.label = machine.checking ? "Checking…" : "Check again"; + get_button.sensitive = !machine.checking; + step2_title.label = "Or use your own installer"; + } else { + step1_title.label = "Get an ARM64 installer"; + step1_detail.label = "Download a Linux ARM64 .iso, or use one you already have."; + get_button.label = "Get ISO"; + step2_title.label = "Choose the installer file"; + } + strip.visible = machine.status_text != ""; + strip.label = machine.status_text; + var creds = machine.credentials (); + creds_card.visible = creds != null; + if (creds != null) creds_label.label = creds; + hero_sub.label = managed ? (fresh ? "Imported from mseries-vm · installer not finished" : "Installed · imported from mseries-vm") + : fresh ? "New machine · not installed yet" + : "Linux ARM64 · installed"; + + var problems = Readiness.problems (); + bool installing = running && machine.installer != null; + start_button.visible = !running; + stop_button.visible = running; + stop_button.label = managed ? "Shut Down" : "Force Stop"; + start_button.label = fresh ? (machine.recipe != null && machine.installer == null ? "Download and Install" : "Start Installer") : "Start"; + pause_button.visible = running; + pause_button.label = machine.paused ? "Resume" : "Pause"; + if (machine.paused) pause_button.add_css_class ("suggested-action"); else pause_button.remove_css_class ("suggested-action"); + bool auto_ok = machine.recipe != null && machine.installer == null && machine.check_ok; + start_button.sensitive = problems.length == 0 && (!fresh || managed || machine.installer != null || auto_ok); + start_button.tooltip_text = problems.length > 0 ? "Fix the items in the setup check first" + : (fresh && !managed && machine.installer == null && !auto_ok ? (machine.recipe != null ? "Wait for the version check, or choose an installer" : "Choose an installer first") : null); + + status_pill.label = running ? (machine.paused ? "Paused" : (installing || (machine.recipe != null && machine.status_text.contains ("Installing")) ? "Installing" : "Running")) + : fresh ? "Not installed" : "Stopped"; + status_pill.remove_css_class ("running"); + status_pill.remove_css_class ("new"); + status_pill.remove_css_class ("paused"); + if (running && machine.paused) { + status_pill.add_css_class ("paused"); + } else if (running) { + status_pill.add_css_class ("running"); + } else if (fresh) { + status_pill.add_css_class ("new"); + } + + // Step badges: a check once done. + bool have_iso = machine.installer != null; + for (int i = 0; i < step_badges.length; i++) { + bool done = i == 1 && have_iso; + step_badges[i].label = done ? "✓" : "%d".printf (i + 1); + if (done) step_badges[i].add_css_class ("done"); else step_badges[i].remove_css_class ("done"); + } + if (step2_detail != null) { + step2_detail.label = have_iso ? Path.get_basename (machine.installer) : "Pick it here, or drop it on the box below."; + } + + if (machine.installer != null) { + drop_zone.add_css_class ("chosen"); + drop_title.label = Path.get_basename (machine.installer); + drop_detail.label = machine.installer; + } else { + drop_zone.remove_css_class ("chosen"); + drop_title.label = !fresh ? "Boots from its disk" : "Drop an installer here"; + drop_detail.label = !fresh + ? "Attach an ARM64 .iso only if you want to reinstall." + : "Drop an ARM64 .iso here, or browse for one."; + } + + for (int i = 0; i < 8; i++) { + hero_icon.remove_css_class ("ml-c-%d".printf (i)); + } + hero_icon.add_css_class ("ml-c-%d".printf (machine.color)); + int64 used = machine.disk_used_bytes (); + if (used > 0) { + disk_v.label = "%.1f of %d GiB".printf (used / 1073741824.0, machine.disk_gib); + } + + ready_card.remove_css_class ("bad"); + int64 free = Readiness.free_gib (); + bool low_space = fresh && free >= 0 && free < machine.disk_gib / 2 + 10; + if (problems.length == 0 && !low_space) { + ready_icon.icon_name = "emblem-default-symbolic"; + ready_icon.add_css_class ("ml-ok"); + ready_title.label = "Ready to run"; + ready_detail.label = free >= 0 + ? "QEMU, UEFI firmware and acceleration are available. %d GiB free on this Mac.".printf ((int) free) + : "QEMU, UEFI firmware and acceleration are available."; + } else if (problems.length == 0) { + ready_icon.icon_name = "dialog-warning-symbolic"; + ready_icon.remove_css_class ("ml-ok"); + ready_card.add_css_class ("bad"); + ready_title.label = "Low disk space"; + ready_detail.label = "Only %d GiB free on this Mac. The %d GiB disk grows as the guest writes, so free some space or shrink the disk in Settings before installing.".printf ((int) free, machine.disk_gib); + } else { + ready_icon.icon_name = "dialog-warning-symbolic"; + ready_icon.remove_css_class ("ml-ok"); + ready_card.add_css_class ("bad"); + ready_title.label = problems.length == 1 ? "One thing to fix" : "%d things to fix".printf (problems.length); + ready_detail.label = string.joinv ("\n", problems); + } + + loading = true; + if (swatches.length == 8) swatches[machine.color.clamp (0, 7)].active = true; + if (name_entry.text != machine.name) name_entry.text = machine.name; + cpu_scale.set_value (machine.cpus); + mem_scale.set_value (machine.mem_gib); + disk_scale.set_value (machine.disk_gib); + port_spin.set_value (machine.ssh_port); + cpu_scale_v.label = "%d".printf (machine.cpus); + mem_scale_v.label = "%d GiB".printf (machine.mem_gib); + disk_scale_v.label = "%d GiB".printf (machine.disk_gib); + loading = false; + + settings_form.sensitive = !running && !managed; + disk_scale.sensitive = !running && !managed && !machine.has_disk (); + settings_note.visible = running || machine.has_disk () || managed; + settings_note.label = managed + ? "This machine is managed by mseries-vm. Change it with: mseries-vm set %s KEY VALUE".printf (machine.vm_name) + : running + ? "Stop the machine to change its settings." + : "The disk already exists, so its size is fixed. Other settings apply the next time you start."; + update_uptime (); + } + + private void update_uptime () { + if (machine.running) { + int64 used = machine.disk_used_bytes (); + if (used > 0) { + disk_v.label = "%.1f of %d GiB".printf (used / 1073741824.0, machine.disk_gib); + } + } + if (machine.started == null) { + uptime.label = ""; + return; + } + int64 secs = new DateTime.now_local ().difference (machine.started) / 1000000; + uptime.label = "Up %02d:%02d:%02d".printf ((int) (secs / 3600), (int) (secs / 60 % 60), (int) (secs % 60)); + } + + private void set_installer (string? path) { + if (path == null) return; + machine.installer = path; + changed (); + refresh (); + } + + private async void choose_installer () { + var chooser = new Gtk.FileDialog (); + chooser.title = "Choose an ARM64 installer image"; + chooser.accept_label = "Choose"; + var filter = new Gtk.FileFilter (); + filter.name = "Installer images"; + filter.add_mime_type ("application/x-iso9660-image"); + filter.add_pattern ("*.iso"); + filter.add_pattern ("*.img"); + var filters = new GLib.ListStore (typeof (Gtk.FileFilter)); + filters.append (filter); + chooser.filters = filters; + try { + var file = yield chooser.open ((Gtk.Window) get_root (), null); + set_installer (file.get_path ()); + } catch (GLib.Error error) { + // Cancelling the dialog lands here too. + } + } +} From 485943dcdc35ad9fd85c2d17badfb4ff760eaab5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:37:31 -0400 Subject: [PATCH 074/798] Add opt-in Pantheon Rust session payload --- mseries/OS9-HANDOFF.md | 25 +++++++--- mseries/ROADMAP.md | 20 +++++--- .../session/README.md | 33 +++++++++++++ .../session.conf | 23 +++++++++ .../user/pantheon-rust-compositor.target | 9 ++++ .../user/pantheon-rust-compositor@.service | 25 ++++++++++ .../pantheon-rust-compositor.desktop | 13 +++++ .../sessions/pantheon-rust-wayland.session | 5 ++ .../pantheon-rust-wayland.desktop | 7 +++ rust/pantheon-compositor/README.md | 7 ++- rust/pantheon-compositor/src/main.rs | 4 +- rust/pantheon-compositor/src/state.rs | 47 +++++++++++-------- 12 files changed, 184 insertions(+), 34 deletions(-) create mode 100644 mseries/pantheon-rust-compositor/session/README.md create mode 100644 mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf create mode 100644 mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor.target create mode 100644 mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service create mode 100644 mseries/pantheon-rust-compositor/session/usr/share/applications/pantheon-rust-compositor.desktop create mode 100644 mseries/pantheon-rust-compositor/session/usr/share/gnome-session/sessions/pantheon-rust-wayland.session create mode 100644 mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 3b7f4cfb8..0ae06a307 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -41,8 +41,8 @@ connected `eDP-1` (3024×1964@120 Hz) and `DP-2` (up to 3840×2160@30 Hz); its `HDMI-A-1` connector is disconnected. Card2 has no EGL device, so native startup must bridge the renderer and display controller. The probe uses read-only file descriptors, does not acquire DRM master, and does not force-probe connectors or -change modes. No native session startup is wired or tested yet. Next: implement -an opt-in DRM path with this split-device topology. The WD22TB4 HDMI G3 +change modes. The opt-in native path now wires this split-device topology, but +it has not been launched in a real session. The WD22TB4 HDMI G3 DCP/kernel issue remains separate. Scope and limits: `mseries/pantheon-rust-compositor/`. @@ -5490,7 +5490,8 @@ coordinates to reach secondary outputs. `rustfmt --edition 2024` on the changed file, `cargo check --bins`, and `cargo check --features native-drm --bins` pass on the M1 Pro build host. This is compile evidence only; no absolute-input device or multi-output runtime check was run. The pinned Smithay input and -multi-GPU paths remain the next focus; native session startup is still absent. +multi-GPU paths remain unqualified; opt-in native startup is now implemented, +but has not been launched on the active desktop. ### Initial toplevel placement and focus — 2026-10-18 @@ -5516,7 +5517,19 @@ initialization. Missing notification socket or send errors fail startup when the flag is supplied. Updated the compositor README and roadmap. The active session, installed service units and login chooser were not changed. Default and `native-drm` `cargo build -p pantheon-compositor --bins` both pass locally; -no runtime notification or native display check was performed. Next, design a -distinct opt-in user service/session entry after validating the installed -Pantheon session-manager startup contract. No native runtime qualification is +no runtime notification or native display check was performed. Read-only +inspection confirmed the installed `gnome-session@.target` drop-in +and systemd-hidden component pattern. Added a distinct +`pantheon-rust-wayland` login entry, `.session`, compositor desktop component, +notify service, target and session-target drop-in under +`mseries/pantheon-rust-compositor/session/`. The service binds `wayland-0`, +imports it into the user manager after compositor readiness, and clears it on +stop. `PANTHEON_WAYLAND_DISPLAY` now lets the compositor bind a requested +socket name; nested startup still allocates automatically. Staged +`systemd-analyze --user verify` passes with one unrelated warning from the +host's `spice-vdagent.service`. The desktop-file validator flags `DesktopNames` +on the new entry, as it does on the installed Pantheon entry; that key is +retained to match the local session convention. The payload is not installed; +Gala and the active login chooser remain untouched. Next: qualify the native +path only in a separate login environment. No native runtime qualification is claimed. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 712f14a65..891197f63 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -44,9 +44,9 @@ first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. Read-only enumeration on the current M-series host found the Asahi GPU at `/dev/dri/card1`, Apple display controller at `/dev/dri/card2`, and connected `eDP-1`/`DP-2` outputs; this is device presence, -not evidence of compositor modesetting. Next implementation work is an opt-in -native DRM path while keeping the nested backend available. The opt-in -`native-drm` Smithay dependency feature now compiles locally after installing +not evidence of compositor modesetting. The opt-in native DRM path is +implemented while keeping nested Winit as the default. The `native-drm` +Smithay dependency feature now compiles locally after installing `libseat-dev`. The new non-invasive `pantheon-drm-probe` reports the Asahi renderer on card1/renderD128, with KMS resource queries unsupported there. Card2 owns `eDP-1` and `DP-2`, while `HDMI-A-1` is disconnected; card2 has no EGL @@ -68,9 +68,17 @@ connector modesetting. The compositor can now optionally send systemd `READY=1` after its Wayland listener and selected backend initialize (`--notify-systemd`). On native startup, readiness follows renderer and connected-output initialization. This -provides a truthful readiness boundary for a future opt-in service; it does -not add or install a session unit, and native runtime/session behavior remains -unqualified. +provides a truthful readiness boundary used by the repository-only opt-in +service; it does not install or switch the host login session. Native +runtime/session behavior remains unqualified. + +A separate `pantheon-rust-wayland` session payload now mirrors the installed +GNOME session/systemd contract: a unique login entry, a compositor component +hidden under systemd, a notify service with an explicit Wayland socket, and a +session-target drop-in preserving the installed Pantheon support services. +It is repository-only and has not been installed or runtime-qualified. The +next gate is native-session qualification in an isolated login, including +socket/environment handoff, ordinary apps, lock/logout and failure recovery. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md new file mode 100644 index 000000000..d0ecea961 --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -0,0 +1,33 @@ +# Experimental Pantheon Rust Wayland session + +This tree contains an opt-in GNOME session definition for the Rust compositor. +It uses the installed `gnome-session@SESSION.target` contract: GNOME hides the +required compositor component from its own launcher, and a session target +drop-in requires the dedicated compositor target. That target starts a +`Type=notify` service and therefore waits for the compositor's `READY=1` +notification before session initialization continues. This follows the +[GNOME session systemd documentation](https://gnome.pages.gitlab.gnome.org/gnome-session/re01.html) +and the matching Pantheon units installed on this machine. + +The session is named **Pantheon (Rust compositor, experimental)** and uses the +separate `pantheon-rust-wayland` session ID. The existing +`pantheon-wayland` entry and Gala units are not modified. The service binds +`wayland-0`, publishes that value to the systemd user manager after readiness, +and clears the manager value when it stops. `PANTHEON_WAYLAND_DISPLAY` selects +the explicit socket; without that variable the nested development backend +continues to allocate an automatic socket, and the native backend can also use +automatic allocation for manual development launches. + +The directory layout mirrors install paths below `usr/`. A distro package can +stage the payload under its package root and install it with the compositor +binary at `/usr/bin/pantheon-compositor`. Do not add these files to +the active host session until the native backend, login, app startup, lock, +logout and recovery have been qualified on a separate session. Native DRM +startup takes over the display device and has not been runtime-qualified on +this machine. + +The session carries forward the installed Pantheon settings-daemon, keyring, +Orca and elementary settings-daemon targets. It intentionally does not promise +that those services or the compositor are fully compatible; this is an +experimental selection for staged qualification, with Gala still available +as rollback. diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf new file mode 100644 index 000000000..8cf14faef --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf @@ -0,0 +1,23 @@ +[Unit] +Wants=org.gnome.SettingsDaemon.A11ySettings.target +Wants=org.gnome.SettingsDaemon.Color.target +Wants=org.gnome.SettingsDaemon.Datetime.target +Wants=org.gnome.SettingsDaemon.Housekeeping.target +Wants=org.gnome.SettingsDaemon.Keyboard.target +Wants=org.gnome.SettingsDaemon.MediaKeys.target +Wants=org.gnome.SettingsDaemon.Power.target +Wants=org.gnome.SettingsDaemon.PrintNotifications.target +Wants=org.gnome.SettingsDaemon.Rfkill.target +Wants=org.gnome.SettingsDaemon.ScreensaverProxy.target +Wants=org.gnome.SettingsDaemon.Sharing.target +Wants=org.gnome.SettingsDaemon.Smartcard.target +Wants=org.gnome.SettingsDaemon.Sound.target +Wants=org.gnome.SettingsDaemon.UsbProtection.target +Wants=org.gnome.SettingsDaemon.Wacom.target +Wants=org.gnome.SettingsDaemon.XSettings.target +Wants=gnome-keyring-daemon.socket +Wants=gcr-ssh-agent.socket +Wants=orca.service +Wants=io.elementary.settings-daemon.service + +Requires=pantheon-rust-compositor.target diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor.target b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor.target new file mode 100644 index 000000000..083e8232d --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor.target @@ -0,0 +1,9 @@ +[Unit] +Description=Pantheon Rust compositor +DefaultDependencies=no + +Requisite=gnome-session-initialized.target +PartOf=gnome-session-initialized.target +Before=gnome-session-initialized.target + +Wants=pantheon-rust-compositor@wayland.service diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service new file mode 100644 index 000000000..3860d12bf --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service @@ -0,0 +1,25 @@ +[Unit] +Description=Pantheon Rust compositor (%I) +OnFailure=gnome-session-shutdown.target +OnFailureJobMode=replace-irreversibly +CollectMode=inactive-or-failed +RefuseManualStart=on +RefuseManualStop=on + +After=gnome-session-manager.target +Requisite=gnome-session-initialized.target +PartOf=gnome-session-initialized.target +Before=gnome-session-initialized.target +ConditionEnvironment=XDG_SESSION_TYPE=%I + +[Service] +Slice=session.slice +Type=notify +Environment=WAYLAND_DISPLAY=wayland-0 +Environment=PANTHEON_WAYLAND_DISPLAY=wayland-0 +ExecStart=/usr/bin/pantheon-compositor --backend native --notify-systemd +ExecStartPost=/usr/bin/systemctl --user import-environment WAYLAND_DISPLAY +ExecStopPost=-/usr/bin/systemctl --user unset-environment WAYLAND_DISPLAY +Restart=no +TimeoutStopSec=5 +OOMScoreAdjust=-1000 diff --git a/mseries/pantheon-rust-compositor/session/usr/share/applications/pantheon-rust-compositor.desktop b/mseries/pantheon-rust-compositor/session/usr/share/applications/pantheon-rust-compositor.desktop new file mode 100644 index 000000000..6510b7287 --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/share/applications/pantheon-rust-compositor.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=Pantheon Rust Compositor +Comment=Experimental native Wayland compositor for Pantheon +Exec=/usr/bin/pantheon-compositor --backend native --notify-systemd +TryExec=/usr/bin/pantheon-compositor +NoDisplay=true +X-GNOME-WMName=Pantheon Rust Compositor +X-GNOME-Autostart-Phase=DisplayServer +X-GNOME-Provides=windowmanager +X-GNOME-Autostart-Notify=true +X-GNOME-AutoRestart=false +X-GNOME-HiddenUnderSystemd=true diff --git a/mseries/pantheon-rust-compositor/session/usr/share/gnome-session/sessions/pantheon-rust-wayland.session b/mseries/pantheon-rust-compositor/session/usr/share/gnome-session/sessions/pantheon-rust-wayland.session new file mode 100644 index 000000000..7f7194cee --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/share/gnome-session/sessions/pantheon-rust-wayland.session @@ -0,0 +1,5 @@ +[GNOME Session] +Name=Pantheon (Rust compositor, experimental) +RequiredComponents=pantheon-rust-compositor;org.gnome.SettingsDaemon.A11ySettings;org.gnome.SettingsDaemon.Color;org.gnome.SettingsDaemon.Datetime;org.gnome.SettingsDaemon.Housekeeping;org.gnome.SettingsDaemon.Keyboard;org.gnome.SettingsDaemon.MediaKeys;org.gnome.SettingsDaemon.Power;org.gnome.SettingsDaemon.PrintNotifications;org.gnome.SettingsDaemon.Rfkill;org.gnome.SettingsDaemon.ScreensaverProxy;org.gnome.SettingsDaemon.Sharing;org.gnome.SettingsDaemon.Smartcard;org.gnome.SettingsDaemon.Sound;org.gnome.SettingsDaemon.UsbProtection;org.gnome.SettingsDaemon.Wacom;org.gnome.SettingsDaemon.XSettings +FallbackSession=pantheon +DesktopName=Pantheon diff --git a/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop b/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop new file mode 100644 index 000000000..f633cc49d --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Pantheon (Rust compositor, experimental) +Comment=Experimental Pantheon session using the Rust compositor +Exec=gnome-session --session=pantheon-rust-wayland +TryExec=pantheon-compositor +DesktopNames=Pantheon +Type=Application diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index f538b5660..59c1a4ffd 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -31,11 +31,16 @@ Native startup acquires the active libseat session and initializes a DRM/KMS output. It takes over the display device, so do not launch it inside the currently active desktop. It is not yet runtime-qualified on M-series hardware. -For a future systemd user service, `--notify-systemd` sends `READY=1` to +The experimental systemd user service uses `--notify-systemd` to send `READY=1` to `NOTIFY_SOCKET` after the Wayland listener and selected backend initialize. On the native path, that includes renderer and connected-output initialization. The flag fails startup if systemd did not provide the notification socket. No service unit or login-session entry is installed by the compositor build. +The separate Pantheon login-session payload is maintained in +[`../../mseries/pantheon-rust-compositor/session/`](../../mseries/pantheon-rust-compositor/session/README.md). +Its `PANTHEON_WAYLAND_DISPLAY` environment override selects a fixed socket for +the session service; without it the compositor chooses an available socket. +The payload has not been installed or runtime-qualified. To exercise the Pantheon protocol against the nested socket, build both binaries and start the compositor with its probe client: diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 39a486075..4906b5da4 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -34,7 +34,7 @@ fn main() -> Result<(), Box> { { return Err("nested backend requires an existing Wayland or X11 session".into()); } - let mut state = PantheonCompositor::new(&mut event_loop, display, "winit"); + let mut state = PantheonCompositor::new(&mut event_loop, display, "winit")?; crate::winit::init_winit(&mut event_loop, &mut state)?; state } @@ -44,7 +44,7 @@ fn main() -> Result<(), Box> { use smithay::backend::session::{Session, libseat::LibSeatSession}; let (session, notifier) = LibSeatSession::new()?; let seat_name = session.seat(); - let mut state = PantheonCompositor::new(&mut event_loop, display, &seat_name); + let mut state = PantheonCompositor::new(&mut event_loop, display, &seat_name)?; crate::native::NativeBackend::start( &mut event_loop, &mut state, diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 71cc48fed..af330c9a0 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -76,7 +76,11 @@ pub struct ManagedWindow { } impl PantheonCompositor { - pub fn new(event_loop: &mut EventLoop, display: Display, seat_name: &str) -> Self { + pub fn new( + event_loop: &mut EventLoop, + display: Display, + seat_name: &str, + ) -> Result> { let start_time = std::time::Instant::now(); let dh = display.handle(); @@ -119,12 +123,12 @@ impl PantheonCompositor { let space = Space::default(); // Setup a wayland socket that will be used to accept clients - let socket_name = Self::init_wayland_listener(display, event_loop); + let socket_name = Self::init_wayland_listener(display, event_loop)?; // Get the loop signal, used to stop the event loop let loop_signal = event_loop.get_signal(); - Self { + Ok(Self { start_time, display_handle: dh, @@ -153,7 +157,7 @@ impl PantheonCompositor { native_backend: None, #[cfg(feature = "native-drm")] native_redraw_sender: None, - } + }) } #[cfg(feature = "native-drm")] @@ -166,9 +170,16 @@ impl PantheonCompositor { fn init_wayland_listener( display: Display, event_loop: &mut EventLoop, - ) -> OsString { - // Creates a new listening socket, automatically choosing the next available `wayland` socket name. - let listening_socket = ListeningSocketSource::new_auto().unwrap(); + ) -> Result> { + // A session service may request a stable name; nested development keeps automatic allocation. + let listening_socket = if let Some(name) = std::env::var_os("PANTHEON_WAYLAND_DISPLAY") { + let name = name + .to_str() + .ok_or("PANTHEON_WAYLAND_DISPLAY must be valid UTF-8")?; + ListeningSocketSource::with_name(name)? + } else { + ListeningSocketSource::new_auto()? + }; // Get the name of the listening socket. // Clients will connect to this socket. @@ -176,17 +187,15 @@ impl PantheonCompositor { let loop_handle = event_loop.handle(); - loop_handle - .insert_source(listening_socket, move |client_stream, _, state| { - // Inside the callback, you should insert the client into the display. - // - // You may also associate some data with the client when inserting the client. - state - .display_handle - .insert_client(client_stream, Arc::new(ClientState::default())) - .unwrap(); - }) - .expect("Failed to init the wayland event source."); + loop_handle.insert_source(listening_socket, move |client_stream, _, state| { + // Inside the callback, you should insert the client into the display. + // + // You may also associate some data with the client when inserting the client. + state + .display_handle + .insert_client(client_stream, Arc::new(ClientState::default())) + .unwrap(); + })?; // You also need to add the display itself to the event loop, so that client events will be processed by wayland-server. loop_handle @@ -202,7 +211,7 @@ impl PantheonCompositor { ) .unwrap(); - socket_name + Ok(socket_name) } pub fn surface_under( From 0fd6663661eca8486e2898ef273c32b086d81921 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:40:08 -0400 Subject: [PATCH 075/798] Machine Lab: fix free-space read (filesystem::free attribute) Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/src/machine.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index c1a3ef005..4958963e6 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -579,8 +579,8 @@ public class Readiness : Object { while (!dir.query_exists () && dir.get_parent () != null) { dir = dir.get_parent (); } - var info = dir.query_filesystem_info ("filesystem:free"); - return (int64) (info.get_attribute_uint64 ("filesystem:free") / 1073741824); + var info = dir.query_filesystem_info ("filesystem::free"); + return (int64) (info.get_attribute_uint64 ("filesystem::free") / 1073741824); } catch (Error e) { return -1; } From c28ef4822dcd6441a3df96153b3091e8ed1567fd Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:40:10 -0400 Subject: [PATCH 076/798] Machine Lab: fix free-space read (filesystem::free attribute) Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/src/machine.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index c1a3ef005..4958963e6 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -579,8 +579,8 @@ public class Readiness : Object { while (!dir.query_exists () && dir.get_parent () != null) { dir = dir.get_parent (); } - var info = dir.query_filesystem_info ("filesystem:free"); - return (int64) (info.get_attribute_uint64 ("filesystem:free") / 1073741824); + var info = dir.query_filesystem_info ("filesystem::free"); + return (int64) (info.get_attribute_uint64 ("filesystem::free") / 1073741824); } catch (Error e) { return -1; } From 5597f1431d8b904787dceb3c7f8e704525674619 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:41:44 -0400 Subject: [PATCH 077/798] Record Torrential UI polish and close fix Co-Authored-By: Claude Sonnet 5.5 --- mseries/TORRENTIAL-PORT.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/mseries/TORRENTIAL-PORT.md b/mseries/TORRENTIAL-PORT.md index 05a1686e1..546f67417 100644 --- a/mseries/TORRENTIAL-PORT.md +++ b/mseries/TORRENTIAL-PORT.md @@ -69,3 +69,33 @@ UI add/pause/resume/remove qualification, close/reopen state restoration, file a folder policy handling, and measured idle CPU/RSS/wakeup comparisons before further optimization. Upstream's rewrite also documents incomplete features in its README. + +## UI polish and close fix — 2026-10-18 + +Fork `techmore/torrential`, `mseries-native`, merged PRs +[#2](https://github.com/techmore/torrential/pull/2) (UI polish) and +[#3](https://github.com/techmore/torrential/pull/3) (quit on window close); +head `6ab0de5`. Package `elementary-torrential_0.1.0-1~mseries3_arm64.deb` +built with `build-deb.sh` from that head and installed; `/usr/bin/torrential-relm4` +is byte-identical to the merged build. + +- UI: scrollable list, segmented All/Downloading/Seeding/Paused filter, Ctrl+F + search, richer rows (size, rates, ETA, ratio, checking, error, paused), + status bar with totals, drag and drop of `.torrent`/magnet, Granite dark-style + sync, completion toast or notification, Ctrl+O/L/F/comma/Q and Delete, + symbolic magnet icon, validating magnet dialog, real download folder in + Preferences. The Quit menu item previously did nothing (action unregistered). +- Bug found on the desktop: closing the window stopped the daemon but left the + app spinning (CPU 60-90%), because hidden hide-on-close dialogs kept + GtkApplication alive. Reproduced headless with the real daemon in a private + netns, fixed by routing close-request through `quit()`, re-verified the same way. +- Verification method: private Broadway display, private netns/D-Bus/GSettings, + mock Transmission RPC for populated states, real daemon for close/quit. +- Operational note: the packaged `transmission-daemon` system service must be + disabled (`systemctl disable --now transmission-daemon.service`); with it + running the app attaches to it, gets HTTP 401 and shows nothing. It was + disabled by the owner on 2026-10-18. +- Still unverified: drag and drop, completion notification, Ctrl+comma + preferences, system dark-mode switch, and a real-torrent transfer with the new + UI. Magnet/.torrent file association (single-instance open handling) is not + implemented. From e51fc4769f5bab14ac21556c4dd18ea26ec61192 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:42:20 -0400 Subject: [PATCH 078/798] Machine Lab: elementary OS preset using the local ARM64 ISO, auto-detach installer for imported guests, macOS explainer Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/src/machine.vala | 24 ++++++++++ mseries/machine-lab/src/main.vala | 72 ++++++++++++++++++++++++++++ mseries/machine-lab/src/page.vala | 2 +- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index 4958963e6..f24253dc9 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -400,6 +400,27 @@ public class Machine : Object { if (changed) { state_changed (); } + if (changed && !now && installer != null && installed ()) { + detach_managed_installer.begin (); + } + } + + /* The guest has real data on its disk: stop booting the installer ISO. */ + private async void detach_managed_installer () { + string? tool = find_mseries_vm (); + if (tool == null) { + return; + } + try { + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE); + yield l.spawnv ({ tool, "set", vm_name, "ISO", "" }).wait_async (); + yield l.spawnv ({ tool, "set", vm_name, "BOOT", "disk" }).wait_async (); + installer = null; + installer_detached (); + state_changed (); + } catch (Error e) { + append_log ("[could not detach installer: %s]\n".printf (e.message)); + } } private void load_serial_tail () { @@ -493,6 +514,9 @@ public class MachineStore : Object { case "windows": m.name = "Windows 11"; m.color = 3; break; default: m.name = n; m.color = 1; break; } + if (kf.has_key ("c", "ISO") && kf.get_string ("c", "ISO") != "") { + m.installer = kf.get_string ("c", "ISO"); + } if (kf.has_key ("c", "CPUS")) { m.cpus = kf.get_integer ("c", "CPUS"); } diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index 40efe80a9..c26c86c19 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -19,6 +19,7 @@ struct Preset { const Preset[] presets = { { "Omarchy", "Arch-based tiling desktop", 4, 6, 32, 7, "https://omarchy.org", "omarchy" }, + { "elementary OS", "Pantheon desktop, daily ARM64 build", 4, 4, 32, 0, "https://elementary.io", "elementary" }, { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 24, 4, "https://ubuntu.com/download/server/arm", "" }, { "Debian", "Rock-solid and lightweight", 2, 2, 16, 5, "https://www.debian.org/CD/netinst/", "" }, { "Fedora", "Fresh packages, GNOME", 4, 4, 24, 0, "https://fedoraproject.org/server/download", "" }, @@ -292,6 +293,14 @@ public class MachineLab : Gtk.Application { item.clicked.connect (() => { pop.popdown (); add_machine (preset); }); pbox.append (item); } + var mac_item = new Gtk.Button (); + mac_item.add_css_class ("flat"); + var mac_lbl = new Gtk.Label ("macOS… (not available on Linux)"); + mac_lbl.xalign = 0; + mac_lbl.add_css_class ("ml-machine-sub"); + mac_item.child = mac_lbl; + mac_item.clicked.connect (() => { pop.popdown (); show_macos_info (); }); + pbox.append (mac_item); pop.child = pbox; add.popover = pop; side.append (add); @@ -361,6 +370,33 @@ public class MachineLab : Gtk.Application { grid.attach (card, i % 2, i / 2); i++; } + var mac = new Gtk.Button (); + mac.add_css_class ("flat"); + var mb = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + mb.add_css_class ("ml-gallery-card"); + var mi = new Gtk.Image.from_icon_name ("computer"); + mi.pixel_size = 40; + mi.add_css_class ("ml-row-icon"); + mi.add_css_class ("ml-c-6"); + mb.append (mi); + var mt = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + mt.valign = Gtk.Align.CENTER; + var mn = new Gtk.Label ("macOS"); + mn.xalign = 0; + mn.add_css_class ("ml-spec-value"); + var mbl = new Gtk.Label ("Not possible on this Mac under Linux"); + mbl.xalign = 0; + mbl.add_css_class ("ml-spec-label"); + var mlearn = new Gtk.Label ("Tap to see what works instead"); + mlearn.xalign = 0; + mlearn.add_css_class ("ml-fineprint"); + mt.append (mn); + mt.append (mbl); + mt.append (mlearn); + mb.append (mt); + mac.child = mb; + mac.clicked.connect (() => show_macos_info ()); + grid.attach (mac, i % 2, i / 2); box.append (grid); page.append (box); return page; @@ -407,6 +443,37 @@ public class MachineLab : Gtk.Application { } } + private void show_macos_info () { + var dialog = new Gtk.AlertDialog ("macOS can't run as a guest here"); + dialog.detail = "A macOS guest on Apple Silicon needs Apple's Virtualization framework and virtual hardware, which only exist on macOS hosts. Linux KVM on this Mac can't provide them, and no open implementation boots macOS.\n\nWhat works:\n• Your internal macOS is still installed. Hold the power button at startup and pick it at the boot picker.\n• Run a macOS VM on a Mac that runs macOS (UTM or Tart) and reach it over the network.\n• Use the elementary OS, Omarchy and other Linux machines here for everything else."; + dialog.buttons = { "OK" }; + dialog.show (window); + } + + private string? newest_iso (string pattern) { + string? best = null; + int64 best_time = 0; + try { + var root = Path.build_filename (Machine.vms_dir (), "downloads"); + var top = Dir.open (root); + string? d; + while ((d = top.read_name ()) != null) { + var sub = Path.build_filename (root, d); + if (!FileUtils.test (sub, FileTest.IS_DIR)) continue; + var inner = Dir.open (sub); + string? f; + while ((f = inner.read_name ()) != null) { + if (!PatternSpec.match_simple (pattern, f)) continue; + var path = Path.build_filename (sub, f); + var info = File.new_for_path (path).query_info ("time::modified", FileQueryInfoFlags.NONE); + int64 t = info.get_modification_date_time ().to_unix (); + if (t > best_time) { best_time = t; best = path; } + } + } + } catch (Error e) {} + return best; + } + private void add_machine (Preset preset) { var m = new Machine (); m.id = "machine-%s".printf (Uuid.string_random ().substring (0, 8)); @@ -418,6 +485,11 @@ public class MachineLab : Gtk.Application { m.color = preset.color; m.url = preset.url == "" ? null : preset.url; m.recipe = preset.recipe == "" ? null : preset.recipe; + if (preset.recipe == "elementary") { + // Use the ARM64 ISO mseries-vm already downloaded, if there is one. + m.recipe = null; + m.installer = newest_iso ("elementaryos*arm64*.iso"); + } int port = 2222; bool clash = true; while (clash) { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 885b431aa..3ed356635 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -679,7 +679,7 @@ public class MachinePage : Gtk.Box { var creds = machine.credentials (); creds_card.visible = creds != null; if (creds != null) creds_label.label = creds; - hero_sub.label = managed ? (fresh ? "Imported from mseries-vm · installer not finished" : "Installed · imported from mseries-vm") + hero_sub.label = managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") : fresh ? "New machine · not installed yet" : "Linux ARM64 · installed"; From 60519986006dc885ff9128dfc4bcaa895c454afb Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:42:22 -0400 Subject: [PATCH 079/798] Machine Lab: elementary OS preset, installer auto-detach for imported guests, macOS explainer Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/src/machine.vala | 24 ++++++++++ mseries/machine-lab/src/main.vala | 72 ++++++++++++++++++++++++++++ mseries/machine-lab/src/page.vala | 2 +- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index 4958963e6..f24253dc9 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -400,6 +400,27 @@ public class Machine : Object { if (changed) { state_changed (); } + if (changed && !now && installer != null && installed ()) { + detach_managed_installer.begin (); + } + } + + /* The guest has real data on its disk: stop booting the installer ISO. */ + private async void detach_managed_installer () { + string? tool = find_mseries_vm (); + if (tool == null) { + return; + } + try { + var l = new SubprocessLauncher (SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE); + yield l.spawnv ({ tool, "set", vm_name, "ISO", "" }).wait_async (); + yield l.spawnv ({ tool, "set", vm_name, "BOOT", "disk" }).wait_async (); + installer = null; + installer_detached (); + state_changed (); + } catch (Error e) { + append_log ("[could not detach installer: %s]\n".printf (e.message)); + } } private void load_serial_tail () { @@ -493,6 +514,9 @@ public class MachineStore : Object { case "windows": m.name = "Windows 11"; m.color = 3; break; default: m.name = n; m.color = 1; break; } + if (kf.has_key ("c", "ISO") && kf.get_string ("c", "ISO") != "") { + m.installer = kf.get_string ("c", "ISO"); + } if (kf.has_key ("c", "CPUS")) { m.cpus = kf.get_integer ("c", "CPUS"); } diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index 40efe80a9..c26c86c19 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -19,6 +19,7 @@ struct Preset { const Preset[] presets = { { "Omarchy", "Arch-based tiling desktop", 4, 6, 32, 7, "https://omarchy.org", "omarchy" }, + { "elementary OS", "Pantheon desktop, daily ARM64 build", 4, 4, 32, 0, "https://elementary.io", "elementary" }, { "Ubuntu", "Friendly general-purpose desktop", 4, 4, 24, 4, "https://ubuntu.com/download/server/arm", "" }, { "Debian", "Rock-solid and lightweight", 2, 2, 16, 5, "https://www.debian.org/CD/netinst/", "" }, { "Fedora", "Fresh packages, GNOME", 4, 4, 24, 0, "https://fedoraproject.org/server/download", "" }, @@ -292,6 +293,14 @@ public class MachineLab : Gtk.Application { item.clicked.connect (() => { pop.popdown (); add_machine (preset); }); pbox.append (item); } + var mac_item = new Gtk.Button (); + mac_item.add_css_class ("flat"); + var mac_lbl = new Gtk.Label ("macOS… (not available on Linux)"); + mac_lbl.xalign = 0; + mac_lbl.add_css_class ("ml-machine-sub"); + mac_item.child = mac_lbl; + mac_item.clicked.connect (() => { pop.popdown (); show_macos_info (); }); + pbox.append (mac_item); pop.child = pbox; add.popover = pop; side.append (add); @@ -361,6 +370,33 @@ public class MachineLab : Gtk.Application { grid.attach (card, i % 2, i / 2); i++; } + var mac = new Gtk.Button (); + mac.add_css_class ("flat"); + var mb = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + mb.add_css_class ("ml-gallery-card"); + var mi = new Gtk.Image.from_icon_name ("computer"); + mi.pixel_size = 40; + mi.add_css_class ("ml-row-icon"); + mi.add_css_class ("ml-c-6"); + mb.append (mi); + var mt = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + mt.valign = Gtk.Align.CENTER; + var mn = new Gtk.Label ("macOS"); + mn.xalign = 0; + mn.add_css_class ("ml-spec-value"); + var mbl = new Gtk.Label ("Not possible on this Mac under Linux"); + mbl.xalign = 0; + mbl.add_css_class ("ml-spec-label"); + var mlearn = new Gtk.Label ("Tap to see what works instead"); + mlearn.xalign = 0; + mlearn.add_css_class ("ml-fineprint"); + mt.append (mn); + mt.append (mbl); + mt.append (mlearn); + mb.append (mt); + mac.child = mb; + mac.clicked.connect (() => show_macos_info ()); + grid.attach (mac, i % 2, i / 2); box.append (grid); page.append (box); return page; @@ -407,6 +443,37 @@ public class MachineLab : Gtk.Application { } } + private void show_macos_info () { + var dialog = new Gtk.AlertDialog ("macOS can't run as a guest here"); + dialog.detail = "A macOS guest on Apple Silicon needs Apple's Virtualization framework and virtual hardware, which only exist on macOS hosts. Linux KVM on this Mac can't provide them, and no open implementation boots macOS.\n\nWhat works:\n• Your internal macOS is still installed. Hold the power button at startup and pick it at the boot picker.\n• Run a macOS VM on a Mac that runs macOS (UTM or Tart) and reach it over the network.\n• Use the elementary OS, Omarchy and other Linux machines here for everything else."; + dialog.buttons = { "OK" }; + dialog.show (window); + } + + private string? newest_iso (string pattern) { + string? best = null; + int64 best_time = 0; + try { + var root = Path.build_filename (Machine.vms_dir (), "downloads"); + var top = Dir.open (root); + string? d; + while ((d = top.read_name ()) != null) { + var sub = Path.build_filename (root, d); + if (!FileUtils.test (sub, FileTest.IS_DIR)) continue; + var inner = Dir.open (sub); + string? f; + while ((f = inner.read_name ()) != null) { + if (!PatternSpec.match_simple (pattern, f)) continue; + var path = Path.build_filename (sub, f); + var info = File.new_for_path (path).query_info ("time::modified", FileQueryInfoFlags.NONE); + int64 t = info.get_modification_date_time ().to_unix (); + if (t > best_time) { best_time = t; best = path; } + } + } + } catch (Error e) {} + return best; + } + private void add_machine (Preset preset) { var m = new Machine (); m.id = "machine-%s".printf (Uuid.string_random ().substring (0, 8)); @@ -418,6 +485,11 @@ public class MachineLab : Gtk.Application { m.color = preset.color; m.url = preset.url == "" ? null : preset.url; m.recipe = preset.recipe == "" ? null : preset.recipe; + if (preset.recipe == "elementary") { + // Use the ARM64 ISO mseries-vm already downloaded, if there is one. + m.recipe = null; + m.installer = newest_iso ("elementaryos*arm64*.iso"); + } int port = 2222; bool clash = true; while (clash) { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 885b431aa..3ed356635 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -679,7 +679,7 @@ public class MachinePage : Gtk.Box { var creds = machine.credentials (); creds_card.visible = creds != null; if (creds != null) creds_label.label = creds; - hero_sub.label = managed ? (fresh ? "Imported from mseries-vm · installer not finished" : "Installed · imported from mseries-vm") + hero_sub.label = managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") : fresh ? "New machine · not installed yet" : "Linux ARM64 · installed"; From 0fe14cc5569db4a3ac65082d58395712b7ea0a3a Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:45:06 -0400 Subject: [PATCH 080/798] Reconcile native DRM connector hotplug --- mseries/OS9-HANDOFF.md | 22 +- mseries/ROADMAP.md | 7 +- rust/pantheon-compositor/README.md | 21 +- rust/pantheon-compositor/src/input.rs | 6 +- rust/pantheon-compositor/src/native.rs | 290 +++++++++++++++++- .../pantheon-compositor/src/pantheon_shell.rs | 2 +- 6 files changed, 328 insertions(+), 20 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 0ae06a307..7355e14af 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5505,7 +5505,7 @@ does not steal focus. Default and `native-drm` feature `cargo check --bins` both pass locally. Runtime window launch/focus/close behavior remains unverified. The opt-in native DRM startup path is now implemented but has not been launched on this active desktop; isolated session qualification and -dynamic connector lifecycle remain open. +physical connector lifecycle qualification remain open. ### systemd readiness notification — 2026-10-18 @@ -5533,3 +5533,23 @@ retained to match the local session convention. The payload is not installed; Gala and the active login chooser remain untouched. Next: qualify the native path only in a separate login environment. No native runtime qualification is claimed. + +### Native DRM connector reconciliation — 2026-10-18 + +Added a two-second calloop timer to the native path. It polls connected DRM +connectors while the logind/libseat session is active, initializes new outputs +with unclaimed compatible CRTCs, and removes disconnected outputs. Removal +disables the Wayland `wl_output` global before deleting it two seconds later; +the DRM output handle is dropped so Smithay releases its CRTC compositor. +Output positions and read-only DisplayConfig snapshots are reflowed, output +metadata is updated, panels are repositioned, maximized/fullscreen windows are +reconfigured, and windows outside the remaining logical outputs are moved back +onto the primary output. Polling pauses during session deactivation. The +relative-pointer path now ignores motion while no output is mapped, avoiding +an empty-bounds panic during full disconnect and allowing later recovery. The +`native-drm` binary builds locally; the default build also passes. No hardware +hotplug or native session was run, so connector detection, scanout transitions, +and recovery on the M1 Pro remain unqualified. User-selected layouts and mode +changes are still not implemented. Next: validate this lifecycle in a separate +native session, then add mode/layout application and rollback based on the +actual display workflow. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 891197f63..751e46049 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -59,9 +59,10 @@ libinput, connected outputs on the first usable KMS device, calloop rendering and vblank completion. It prioritizes eDP and places additional outputs to its right. DisplayConfig no longer advertises a fabricated Winit output in native mode. Both build configurations pass locally. Native mode has not been -launched because it deliberately takes over the active display; output hotplug, -user-configured layouts, session switching and actual M-series scanout remain -unqualified. Local `cargo check --bins` and `cargo build --bins` pass both with +launched because it deliberately takes over the active display. Connector +addition/removal reconciliation is implemented with a two-second poll, but +hotplug, user-configured layouts, session switching and actual M-series +scanout remain unqualified. Local `cargo check --bins` and `cargo build --bins` pass both with and without `native-drm`; they establish compilation only, not successful connector modesetting. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 59c1a4ffd..6fde2a137 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -96,11 +96,14 @@ EGL/GBM renderer and a separate connected KMS output, publishes it as a Wayland output, assigns libinput to the active seat, and registers redraw, vblank, and session pause/resume sources with calloop. It initializes all connected outputs with compatible CRTCs on the selected KMS device, puts eDP -first, and places other outputs to its right. Hotplug, changing display -arrangement and modes, session-shell integration, and M-series scanout/input -behavior remain unqualified. Default and feature-enabled `cargo check --bins` and -`cargo build --bins` pass locally; builds do not initialize or take over a -native session. +first, and places other outputs to its right. It polls connector state while +the session is active, adds newly connected outputs, removes disconnected +outputs, updates Wayland/DisplayConfig state, and moves windows back onto a +remaining display when needed. This path is compile-checked only; physical +hotplug, mode changes, user-configured arrangement, session-shell integration, +and M-series scanout/input behavior remain unqualified. Default and +feature-enabled `cargo check --bins` and `cargo build --bins` pass locally; +builds do not initialize or take over a native session. With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices without acquiring DRM master or changing display modes. On the current M-series @@ -115,9 +118,11 @@ does not force a connector reprobe or test modesetting. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output -bounds. This allows the existing xdg move/resize grab implementations to receive -physical pointer motion. Multi-output bounds, pointer constraints, and hardware -input remain unqualified. +bounds. If all outputs are disconnected, relative motion is ignored until a +display returns instead of panicking on empty bounds. This allows the existing +xdg move/resize grab implementations to receive physical pointer motion. +Multi-output bounds, pointer constraints, and hardware input remain +unqualified. Xdg popup requests now resolve their root to a managed toplevel and install Smithay's popup keyboard and pointer grabs, including the serial checks needed diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 627dda4b4..43365add8 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -43,12 +43,14 @@ impl PantheonCompositor { InputEvent::PointerMotion { event, .. } => { let pointer = self.seat.get_pointer().unwrap(); let location = pointer.current_location(); - let output_bounds = self + let Some(output_bounds) = self .space .outputs() .filter_map(|output| self.space.output_geometry(output)) .reduce(|bounds, output| bounds.merge(output)) - .unwrap(); + else { + return; + }; let max_x = (output_bounds.loc.x + output_bounds.size.w - 1) as f64; let max_y = (output_bounds.loc.y + output_bounds.size.h - 1) as f64; let target = location + event.delta(); diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 0087c37ba..51dd8d683 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; use std::path::Path; -use std::time::Duration; +use std::time::{Duration, Instant}; use smithay::{ backend::{ @@ -34,7 +34,11 @@ use smithay::{ }, output::{Mode, Output, PhysicalProperties}, reexports::{ - calloop::{EventLoop, channel, channel::Event as ChannelEvent}, + calloop::{ + EventLoop, channel, + channel::Event as ChannelEvent, + timer::{TimeoutAction, Timer}, + }, drm::control::{Device as DrmControlDevice, ModeTypeFlags, connector}, input::Libinput, rustix::fs::OFlags, @@ -74,6 +78,7 @@ pub struct NativeKmsOutput { pub crtc: smithay::reexports::drm::control::crtc::Handle, pub output: Output, pub drm_output: NativeDrmOutput, + pub global_id: smithay::reexports::wayland_server::backend::GlobalId, pub frame_pending: bool, } @@ -82,12 +87,17 @@ pub struct NativeKmsDevice { pub manager: NativeOutputManager, pub outputs: Vec, pub notifier: Option, + pending_global_removals: Vec<( + smithay::reexports::wayland_server::backend::GlobalId, + Instant, + )>, } pub struct NativeBackend { pub _session: LibSeatSession, pub renderer: NativeRenderer, pub kms: NativeKmsDevice, + session_paused: bool, } struct ConnectedOutput { @@ -188,6 +198,252 @@ impl NativeKmsDevice { Ok(Some(output.output.clone())) } + /// Reconcile connected DRM connectors with the outputs published to the + /// Wayland space. Smithay's DRM notifier reports page flips and failures, + /// so connector state is polled on a short timer instead of assuming a + /// hotplug event will arrive through that source. + fn refresh_connectors( + &mut self, + renderer: &mut NativeRenderer, + state: &mut PantheonCompositor, + ) -> Result> { + let resources = self.manager.device_mut().resource_handles()?; + let mut connected = Vec::new(); + let mut connected_handles = HashSet::new(); + for handle in resources.connectors() { + let info = self.manager.device_mut().get_connector(*handle, false)?; + if info.state() == connector::State::Connected { + connected_handles.insert(info.handle()); + if !info.modes().is_empty() { + connected.push(info); + } + } + } + connected.sort_by_key(|info| { + ( + info.interface() != connector::Interface::EmbeddedDisplayPort, + info.interface_id(), + ) + }); + let disconnected = self + .outputs + .iter() + .filter(|output| !connected_handles.contains(&output.connector)) + .map(|output| output.connector) + .collect::>(); + let mut changed = !disconnected.is_empty(); + if changed { + for output in self + .outputs + .iter() + .filter(|output| !connected_handles.contains(&output.connector)) + { + state + .display_handle + .disable_global::(output.global_id.clone()); + self.pending_global_removals + .push((output.global_id.clone(), Instant::now())); + state.space.unmap_output(&output.output); + state.display_config.remove(output.output.name()); + tracing::info!(output = %output.output.name(), "unmapped disconnected DRM output"); + } + self.outputs + .retain(|output| connected_handles.contains(&output.connector)); + } + + let mut claimed_crtcs = self + .outputs + .iter() + .map(|output| output.crtc) + .collect::>(); + for info in connected { + if self + .outputs + .iter() + .any(|output| output.connector == info.handle()) + { + continue; + } + + let encoder = info + .current_encoder() + .and_then(|handle| self.manager.device_mut().get_encoder(handle).ok()) + .or_else(|| { + info.encoders() + .iter() + .find_map(|handle| self.manager.device_mut().get_encoder(*handle).ok()) + }); + let Some(encoder) = encoder else { + tracing::warn!(connector = ?info.handle(), "connected DRM connector has no usable encoder"); + continue; + }; + let Some(crtc) = resources + .filter_crtcs(encoder.possible_crtcs()) + .into_iter() + .find(|crtc| !claimed_crtcs.contains(crtc)) + else { + tracing::warn!(connector = ?info.handle(), "connected DRM connector has no unclaimed compatible CRTC"); + continue; + }; + + let drm_mode = info + .modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .unwrap_or(info.modes()[0]); + let mode = Mode::from(drm_mode); + let name = format!("{}-{}", info.interface().as_str(), info.interface_id()); + let (physical_width, physical_height) = info.size().unwrap_or((0, 0)); + let output = Output::new( + name.clone(), + PhysicalProperties { + size: (physical_width as i32, physical_height as i32).into(), + subpixel: info.subpixel().into(), + make: "Apple".into(), + model: name.clone(), + serial_number: "Unknown".into(), + }, + ); + output.set_preferred(mode); + output.change_current_state(Some(mode), None, None, Some((0, 0).into())); + + let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; + let render_elements = DrmOutputRenderElements::< + NativeMultiRenderer<'_>, + CursorRenderElement>, + >::default(); + let drm_output = match self.manager.lock().initialize_output( + crtc, + drm_mode, + &[info.handle()], + &output, + None, + &mut gpu_renderer, + &render_elements, + ) { + Ok(output) => output, + Err(error) => { + tracing::warn!(%name, ?error, "could not initialize newly connected KMS output"); + continue; + } + }; + + let global_id = output.create_global::(&state.display_handle); + self.outputs.push(NativeKmsOutput { + connector: info.handle(), + crtc, + output, + drm_output, + global_id, + frame_pending: false, + }); + claimed_crtcs.insert(crtc); + changed = true; + tracing::info!(%name, ?mode, "mapped newly connected DRM output"); + } + + if changed { + self.outputs.sort_by_key(|output| { + ( + !output.output.name().starts_with("eDP-"), + output.output.name(), + ) + }); + let mut x = 0; + for (index, output) in self.outputs.iter().enumerate() { + state.space.map_output(&output.output, (x, 0)); + if let Some(mode) = output.output.current_mode() { + output + .output + .change_current_state(None, None, None, Some((x, 0).into())); + state.display_config.update(OutputSnapshot { + connector: output.output.name(), + vendor: "Apple".into(), + product: output.output.name(), + serial: "Unknown".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + scale: 1.0, + x, + y: 0, + primary: index == 0, + }); + x += mode.size.w; + } + } + + let output_geometries = state + .space + .outputs() + .filter_map(|output| state.space.output_geometry(output)) + .collect::>(); + let fallback = output_geometries.first().copied(); + if let Some(fallback) = fallback { + let windows = state.space.elements().cloned().collect::>(); + for window in windows { + let Some(location) = state.space.element_location(&window) else { + continue; + }; + let size = window.geometry().size; + let center_x = location.x + size.w / 2; + let center_y = location.y + size.h / 2; + let on_output = output_geometries.iter().any(|geometry| { + center_x >= geometry.loc.x + && center_x < geometry.loc.x + geometry.size.w + && center_y >= geometry.loc.y + && center_y < geometry.loc.y + geometry.size.h + }); + if !on_output { + let max_x = fallback.loc.x + (fallback.size.w - size.w).max(0); + let max_y = fallback.loc.y + (fallback.size.h - size.h).max(0); + let x = (fallback.loc.x + (fallback.size.w - size.w) / 2) + .clamp(fallback.loc.x, max_x); + let y = (fallback.loc.y + (fallback.size.h - size.h) / 2) + .clamp(fallback.loc.y, max_y); + state.space.map_element(window, (x, y), false); + } + } + } + + state.last_work_area = None; + let panels = state + .space + .elements() + .filter_map(|window| { + let surface = window.toplevel()?.wl_surface(); + state.is_panel_surface(surface).then(|| surface.clone()) + }) + .collect::>(); + let has_mapped_panels = !panels.is_empty(); + for panel in panels { + state.update_panel(&panel, |_| {}); + } + if !has_mapped_panels && state.space.outputs().next().is_some() { + state.reconfigure_managed_windows(); + } + state.space.refresh(); + let _ = state.display_handle.flush_clients(); + state.request_native_redraw(); + } + + let now = Instant::now(); + self.pending_global_removals + .retain(|(global, disabled_at)| { + if now.duration_since(*disabled_at) >= Duration::from_secs(2) { + state + .display_handle + .remove_global::(global.clone()); + false + } else { + true + } + }); + + Ok(changed) + } + fn open_output_device( session: &LibSeatSession, node: DrmNode, @@ -324,7 +580,7 @@ impl NativeKmsDevice { } }; - output.create_global::(&state.display_handle); + let global_id = output.create_global::(&state.display_handle); state.space.map_output(&output, (x, 0)); state.display_config.update(OutputSnapshot { connector: name.clone(), @@ -345,6 +601,7 @@ impl NativeKmsDevice { crtc: selected.crtc, output, drm_output, + global_id, frame_pending: false, }); } @@ -357,6 +614,7 @@ impl NativeKmsDevice { manager, outputs, notifier: Some(notifier), + pending_global_removals: Vec::new(), }) } } @@ -397,6 +655,7 @@ impl NativeBackend { libinput_context.suspend(); if let Some(backend) = state.native_backend.as_mut() { backend.kms.manager.pause(); + backend.session_paused = true; } tracing::info!("native compositor session paused"); } @@ -406,8 +665,12 @@ impl NativeBackend { tracing::error!(?error, "failed to resume libinput"); } if let Some(backend) = state.native_backend.as_mut() { - if let Err(error) = backend.kms.manager.lock().activate(false) { - tracing::error!(?error, "failed to reactivate DRM output"); + backend.session_paused = true; + match backend.kms.manager.lock().activate(false) { + Ok(()) => backend.session_paused = false, + Err(error) => { + tracing::error!(?error, "failed to reactivate DRM output"); + } } } state.request_native_redraw(); @@ -454,6 +717,7 @@ impl NativeBackend { _session: session, renderer, kms, + session_paused: false, }); event_loop .handle() @@ -469,6 +733,22 @@ impl NativeBackend { } state.native_backend = Some(backend); })?; + + event_loop.handle().insert_source( + Timer::from_duration(Duration::from_secs(2)), + |_, _, state| { + let Some(mut backend) = state.native_backend.take() else { + return TimeoutAction::ToDuration(Duration::from_secs(2)); + }; + if !backend.session_paused + && let Err(error) = backend.kms.refresh_connectors(&mut backend.renderer, state) + { + tracing::warn!(?error, "failed to reconcile native DRM connectors"); + } + state.native_backend = Some(backend); + TimeoutAction::ToDuration(Duration::from_secs(2)) + }, + )?; state.request_native_redraw(); Ok(()) } diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 25dfe0ed9..8e6ed9d3a 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -442,7 +442,7 @@ impl PantheonCompositor { self.space.map_element(window.clone(), target.loc, false); } - fn reconfigure_managed_windows(&mut self) { + pub(crate) fn reconfigure_managed_windows(&mut self) { let mut managed = Vec::new(); for window in self.space.elements() { let Some(surface) = window.toplevel().cloned() else { From 0dfcc725995262f3367cf5a8ef359c6001f7f083 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:52:21 -0400 Subject: [PATCH 081/798] Implement Wayland session lock support --- mseries/OS9-HANDOFF.md | 19 ++++ mseries/ROADMAP.md | 7 ++ rust/pantheon-compositor/src/cursor.rs | 24 ++++ rust/pantheon-compositor/src/handlers/mod.rs | 114 ++++++++++++++++++- rust/pantheon-compositor/src/input.rs | 6 +- rust/pantheon-compositor/src/native.rs | 61 ++++++---- rust/pantheon-compositor/src/state.rs | 70 ++++++++++++ rust/pantheon-compositor/src/winit.rs | 62 ++++++---- 8 files changed, 317 insertions(+), 46 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 7355e14af..99c6ee262 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5553,3 +5553,22 @@ and recovery on the M1 Pro remain unqualified. User-selected layouts and mode changes are still not implemented. Next: validate this lifecycle in a separate native session, then add mode/layout application and rollback based on the actual display workflow. + +### Wayland session lock protocol — 2026-10-18 + +Implemented Smithay's pinned `ext-session-lock-v1` protocol for both nested +Winit and native DRM. A lock request immediately suppresses application +surfaces, clears keyboard focus and active pointer/keyboard grabs, and disables +shell accelerators while locked. Each mapped output renders its lock surface on +black, or black alone until the locker provides a surface; lock success is sent +only after every active output presents a cleared frame. Pointer and keyboard +input are confined to the lock client, and unlock restores the previous live +keyboard focus. Output removal/addition updates pending lock confirmation. +`cargo check` and `cargo build` pass with and without `native-drm`. No session +locker was run against the nested backend and native mode was not launched, so +protocol behavior, input confinement, visual blanking, unlock, locker/PAM +integration and hardware presentation remain unqualified. The compositor does +not supply an authentication UI or policy; a Pantheon locker must implement +the session-lock protocol and authenticate the user. Next: validate a real +locker in a nested session, then qualify login/lock/unlock in an isolated +native session before considering this a swap-in capability. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 751e46049..f5a0ba209 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -66,6 +66,13 @@ scanout remain unqualified. Local `cargo check --bins` and `cargo build --bins` and without `native-drm`; they establish compilation only, not successful connector modesetting. +Smithay's `ext-session-lock-v1` is implemented in both rendering backends: +applications are hidden behind lock surfaces or black, input is confined to +the locker, and lock confirmation waits for each mapped output to present a +cleared frame. Default and `native-drm` builds pass. Real locker behavior, +authentication/PAM integration, nested runtime behavior and native lock/unlock +remain unqualified; the compositor does not provide the lock-screen UI. + The compositor can now optionally send systemd `READY=1` after its Wayland listener and selected backend initialize (`--notify-systemd`). On native startup, readiness follows renderer and connected-output initialization. This diff --git a/rust/pantheon-compositor/src/cursor.rs b/rust/pantheon-compositor/src/cursor.rs index 1af20dd35..5304282ac 100644 --- a/rust/pantheon-compositor/src/cursor.rs +++ b/rust/pantheon-compositor/src/cursor.rs @@ -121,3 +121,27 @@ where } } } + +pub fn render_lock_surface_elements( + renderer: &mut R, + state: &PantheonCompositor, + output: &smithay::output::Output, +) -> Vec> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Texture + Clone + Send + 'static, +{ + let output_name = output.name(); + let Some(surface) = state.lock_surfaces.get(&output_name) else { + return Vec::new(); + }; + let scale = Scale::from(output.current_scale().fractional_scale()); + render_elements_from_surface_tree::>( + renderer, + surface.wl_surface(), + (0, 0), + scale, + 1.0, + Kind::Unspecified, + ) +} diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs index 77405cac0..4006c9c21 100644 --- a/rust/pantheon-compositor/src/handlers/mod.rs +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -11,14 +11,17 @@ use smithay::input::dnd::{DnDGrab, DndGrabHandler, GrabType, Source}; use smithay::input::pointer::Focus; use smithay::input::{Seat, SeatHandler, SeatState}; use smithay::reexports::wayland_server::Resource; -use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; -use smithay::utils::Serial; +use smithay::reexports::wayland_server::protocol::{wl_output::WlOutput, wl_surface::WlSurface}; +use smithay::utils::{IsAlive, Serial}; use smithay::wayland::output::OutputHandler; use smithay::wayland::pointer_constraints::PointerConstraintsHandler; use smithay::wayland::selection::SelectionHandler; use smithay::wayland::selection::data_device::{ DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, }; +use smithay::wayland::session_lock::{ + LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker, +}; impl SeatHandler for PantheonCompositor { type KeyboardFocus = WlSurface; @@ -95,4 +98,111 @@ impl WaylandDndGrabHandler for PantheonCompositor { impl OutputHandler for PantheonCompositor {} +impl SessionLockHandler for PantheonCompositor { + fn lock_state(&mut self) -> &mut SessionLockManagerState { + &mut self.session_lock_state + } + + fn lock(&mut self, confirmation: SessionLocker) { + if self.session_locked || self.lock_confirmation.is_some() { + return; + } + + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.seat.get_pointer().unwrap().unset_grab( + self, + serial, + smithay::backend::input::InputTime::now(), + ); + let keyboard = self.seat.get_keyboard().unwrap(); + keyboard.unset_grab(self); + self.keyboard_focus_before_lock = keyboard.current_focus(); + keyboard.set_focus(self, None, serial); + + self.session_locked = true; + self.lock_owner = Some(confirmation.ext_session_lock().id()); + self.lock_surfaces.clear(); + let pointer = self.seat.get_pointer().unwrap(); + let location = pointer.current_location(); + pointer.motion( + self, + None, + &smithay::input::pointer::MotionEvent { + location, + serial: smithay::utils::SERIAL_COUNTER.next_serial(), + time: smithay::backend::input::InputTime::now(), + }, + ); + pointer.frame(self); + self.lock_confirmation_outputs = self + .space + .outputs() + .map(|output| output.name().to_string()) + .collect(); + self.lock_confirmation = Some(confirmation); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + fn unlock(&mut self) { + self.session_locked = false; + self.lock_owner = None; + self.lock_confirmation = None; + self.lock_confirmation_outputs.clear(); + self.lock_surfaces.clear(); + + let focus = self + .keyboard_focus_before_lock + .take() + .filter(|surface| surface.alive()); + self.seat.get_keyboard().unwrap().set_focus( + self, + focus, + smithay::utils::SERIAL_COUNTER.next_serial(), + ); + + let pointer = self.seat.get_pointer().unwrap(); + let location = pointer.current_location(); + pointer.motion( + self, + self.surface_under(location), + &smithay::input::pointer::MotionEvent { + location, + serial: smithay::utils::SERIAL_COUNTER.next_serial(), + time: smithay::backend::input::InputTime::now(), + }, + ); + pointer.frame(self); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + fn new_surface(&mut self, surface: LockSurface, output: WlOutput) { + let owner = surface.ext_session_lock().id(); + if !self.session_locked || self.lock_owner.as_ref() != Some(&owner) { + return; + } + let Some(output) = smithay::output::Output::from_resource(&output) else { + return; + }; + let Some(geometry) = self.space.output_geometry(&output) else { + return; + }; + surface.with_pending_state(|pending| { + pending.size = + Some((geometry.size.w.max(0) as u32, geometry.size.h.max(0) as u32).into()) + }); + + let output_name = output.name().to_string(); + self.lock_surfaces.insert(output_name, surface.clone()); + self.seat.get_keyboard().unwrap().set_focus( + self, + Some(surface.wl_surface().clone()), + smithay::utils::SERIAL_COUNTER.next_serial(), + ); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } +} + smithay::delegate_dispatch2!(PantheonCompositor); diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 43365add8..923394e4a 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -29,6 +29,7 @@ impl PantheonCompositor { time, |state, modifiers, keysym| { if key_state == smithay::backend::input::KeyState::Pressed + && !state.session_locked && state .shell_compat .activate_key(keysym.modified_sym(), modifiers) @@ -125,7 +126,10 @@ impl PantheonCompositor { let button_state = event.state(); - if ButtonState::Pressed == button_state && !pointer.is_grabbed() { + if !self.session_locked + && ButtonState::Pressed == button_state + && !pointer.is_grabbed() + { if let Some((window, _loc)) = self .space .element_under(pointer.current_location()) diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 51dd8d683..b1b256248 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -48,7 +48,7 @@ use smithay::{ use crate::{ PantheonCompositor, - cursor::{CursorRenderElement, render_cursor_elements}, + cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}, display_config::OutputSnapshot, }; @@ -148,32 +148,42 @@ impl NativeKmsDevice { continue; } let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; - let space_elements = smithay::desktop::space::space_render_elements( - &mut gpu_renderer, - [&state.space], - &output.output, - 1.0, - )?; - let cursor_elements = render_cursor_elements(&mut gpu_renderer, state, &output.output); - let mut elements = space_elements - .into_iter() - .map(|element| match element { - smithay::desktop::space::SpaceRenderElements::Surface(surface) => { - CursorRenderElement::from(surface) - } - smithay::desktop::space::SpaceRenderElements::Element(element) => { - CursorRenderElement::from(element) - } - smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { - unreachable!("SpaceRenderElements generic catcher is never constructed") - } - }) - .collect::>(); - elements.extend(cursor_elements); + let elements = if state.session_locked { + render_lock_surface_elements(&mut gpu_renderer, state, &output.output) + } else { + let space_elements = smithay::desktop::space::space_render_elements( + &mut gpu_renderer, + [&state.space], + &output.output, + 1.0, + )?; + let cursor_elements = + render_cursor_elements(&mut gpu_renderer, state, &output.output); + let mut elements = space_elements + .into_iter() + .map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + }) + .collect::>(); + elements.extend(cursor_elements); + elements + }; let frame = output.drm_output.render_frame( &mut gpu_renderer, &elements, - [0.1, 0.1, 0.1, 1.0], + if state.session_locked { + [0.0, 0.0, 0.0, 1.0] + } else { + [0.1, 0.1, 0.1, 1.0] + }, FrameFlags::DEFAULT, )?; if frame.is_empty { @@ -244,6 +254,7 @@ impl NativeKmsDevice { self.pending_global_removals .push((output.global_id.clone(), Instant::now())); state.space.unmap_output(&output.output); + state.forget_output_during_lock(&output.output.name()); state.display_config.remove(output.output.name()); tracing::info!(output = %output.output.name(), "unmapped disconnected DRM output"); } @@ -353,6 +364,7 @@ impl NativeKmsDevice { let mut x = 0; for (index, output) in self.outputs.iter().enumerate() { state.space.map_output(&output.output, (x, 0)); + state.track_output_for_lock(&output.output); if let Some(mode) = output.output.current_mode() { output .output @@ -693,6 +705,7 @@ impl NativeBackend { return; } }; + state.confirm_lock_output_frame(&output.name()); state.space.elements().for_each(|window| { window.send_frame( &output, diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index af330c9a0..e345d15da 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -18,6 +18,7 @@ use smithay::{ compositor::{CompositorClientState, CompositorState}, output::OutputManagerState, selection::data_device::DataDeviceState, + session_lock::{LockSurface, SessionLockManagerState, SessionLocker}, shell::xdg::XdgShellState, shm::ShmState, socket::ListeningSocketSource, @@ -46,6 +47,13 @@ pub struct PantheonCompositor { pub xdg_shell_state: XdgShellState, pub shm_state: ShmState, pub output_manager_state: OutputManagerState, + pub session_lock_state: SessionLockManagerState, + pub session_locked: bool, + pub lock_surfaces: HashMap, + pub lock_owner: Option, + pub lock_confirmation: Option, + pub lock_confirmation_outputs: HashSet, + pub keyboard_focus_before_lock: Option, pub seat_state: SeatState, pub data_device_state: DataDeviceState, pub pantheon_shell_state: PantheonShellState, @@ -96,6 +104,7 @@ impl PantheonCompositor { let popups = PopupManager::default(); let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + let session_lock_state = SessionLockManagerState::new::(&dh, |_| true); // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); @@ -140,6 +149,13 @@ impl PantheonCompositor { xdg_shell_state, shm_state, output_manager_state, + session_lock_state, + session_locked: false, + lock_surfaces: HashMap::new(), + lock_owner: None, + lock_confirmation: None, + lock_confirmation_outputs: HashSet::new(), + keyboard_focus_before_lock: None, seat_state, data_device_state, pantheon_shell_state, @@ -218,6 +234,29 @@ impl PantheonCompositor { &self, pos: Point, ) -> Option<(WlSurface, Point)> { + if self.session_locked { + for output in self.space.outputs() { + let Some(geometry) = self.space.output_geometry(output) else { + continue; + }; + if !geometry.to_f64().contains(pos) { + continue; + } + let output_name = output.name(); + let Some(lock_surface) = self.lock_surfaces.get(&output_name) else { + return None; + }; + let local = pos - geometry.loc.to_f64(); + return smithay::desktop::utils::under_from_surface_tree( + lock_surface.wl_surface(), + local, + (0, 0), + WindowSurfaceType::ALL, + ) + .map(|(surface, location)| (surface, location.to_f64())); + } + return None; + } self.space .element_under(pos) .and_then(|(window, location)| { @@ -226,6 +265,37 @@ impl PantheonCompositor { .map(|(s, p)| (s, (p + location).to_f64())) }) } + + pub fn track_output_for_lock(&mut self, output: &smithay::output::Output) { + if self.session_locked && self.lock_confirmation.is_some() { + self.lock_confirmation_outputs + .insert(output.name().to_string()); + } + } + + pub fn confirm_lock_output_frame(&mut self, output_name: &str) { + if !self.session_locked || self.lock_confirmation.is_none() { + return; + } + self.lock_confirmation_outputs.remove(output_name); + self.finish_lock_confirmation_if_ready(); + } + + pub fn forget_output_during_lock(&mut self, output_name: &str) { + self.lock_confirmation_outputs.remove(output_name); + self.finish_lock_confirmation_if_ready(); + } + + fn finish_lock_confirmation_if_ready(&mut self) { + if !self.session_locked || self.lock_confirmation.is_none() { + return; + } + if self.lock_confirmation_outputs.is_empty() { + if let Some(confirmation) = self.lock_confirmation.take() { + confirmation.lock(); + } + } + } } /// Data associated with a wayland client that connects to PantheonCompositor. diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 251573082..db3432304 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -11,7 +11,7 @@ use smithay::{ }; use crate::PantheonCompositor; -use crate::cursor::{CursorRenderElement, render_cursor_elements}; +use crate::cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}; use crate::display_config::OutputSnapshot; pub fn init_winit( @@ -71,6 +71,13 @@ pub fn init_winit( refresh: 60_000, }; output.change_current_state(Some(mode), None, None, None); + let output_name = output.name(); + if let Some(lock_surface) = state.lock_surfaces.get(&output_name) { + lock_surface.with_pending_state(|pending| { + pending.size = Some((size.w.max(0) as u32, size.h.max(0) as u32).into()) + }); + lock_surface.send_configure(); + } state.display_config.update(OutputSnapshot { connector: "winit".into(), vendor: "Smithay".into(), @@ -93,27 +100,44 @@ pub fn init_winit( { backend.window().set_cursor_visible(false); let (renderer, mut framebuffer) = backend.bind().unwrap(); - let cursor_elements = render_cursor_elements(renderer, state, &output); - smithay::desktop::space::render_output::< - _, - CursorRenderElement, - _, - _, - >( - &output, - renderer, - &mut framebuffer, - 1.0, - 0, - [&state.space], - &cursor_elements, - &mut damage_tracker, - [0.1, 0.1, 0.1, 1.0], - ) - .unwrap(); + if state.session_locked { + let elements = render_lock_surface_elements(renderer, state, &output); + damage_tracker + .render_output( + renderer, + &mut framebuffer, + 0, + &elements, + [0.0, 0.0, 0.0, 1.0], + ) + .unwrap(); + } else { + let cursor_elements = render_cursor_elements(renderer, state, &output); + smithay::desktop::space::render_output::< + _, + CursorRenderElement, + _, + _, + >( + &output, + renderer, + &mut framebuffer, + 1.0, + 0, + [&state.space], + &cursor_elements, + &mut damage_tracker, + [0.1, 0.1, 0.1, 1.0], + ) + .unwrap(); + } } backend.submit(Some(&[damage])).unwrap(); + if state.session_locked { + state.confirm_lock_output_frame(&output.name()); + } + state.space.elements().for_each(|window| { window.send_frame( &output, From d93d61d4e035d9190731ca9c9e0253e54ddfdb8d Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:52:54 -0400 Subject: [PATCH 082/798] Update compositor lock status docs --- .../EXPERIENCE-REQUIREMENTS.md | 8 ++++++++ rust/pantheon-compositor/README.md | 12 +++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index d52b38f91..ca72bc9ab 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -91,3 +91,11 @@ accelerator registration and activation path: pressing the registered `Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, physical media-key activation and M-series hardware remain unqualified; this is not full session or hardware qualification. + +The compositor now implements Smithay's `ext-session-lock-v1` protocol. Lock +requests suppress application rendering, clear active grabs, route input to +the lock surface, and confirm the lock only after each mapped output presents +a cleared frame. The API and both backend builds are in place, but no Pantheon +locker has been connected; authentication, session action wiring, runtime +input/visual checks, native login and crash recovery remain open acceptance +items. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 6fde2a137..59c62b201 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -82,8 +82,12 @@ area, already maximized windows are reconfigured to its new bounds; unchanged panel repaints do not issue redundant configures. This is only the first shell integration slice. Autohide/overlap tracking, -animated hiding, blur, overview behavior, multi-output work-area updates for -already maximized windows, and session/lock integration are not complete. Widget, +animated hiding, blur, overview behavior and multi-output work-area updates for +already maximized windows are not complete. The compositor now implements +Smithay's `ext-session-lock-v1`: it blanks each output until its lock surface is +ready, confines input to the locker, and restores the prior keyboard focus on +unlock. A Pantheon locker/authentication UI and session action wiring are not +provided, and the protocol has not yet been runtime-qualified. Widget, extended-behavior and greeter protocol requests are accepted but currently have no behavior. Touch input, multi-monitor configuration, accessibility, IBus, Xwayland, screen-sharing portals and production crash recovery also remain @@ -133,7 +137,9 @@ The session bus also exposes the small `org.gnome.Shell` accelerator API used by Wingpanel and its sound indicator. Accelerator registrations are matched through the compositor's active XKB state and reported with `AcceleratorActivated`; `ShowOSD` is currently a no-op, and lock-screen action -modes are not implemented. +modes are not implemented. This is distinct from the compositor's +`ext-session-lock-v1` protocol, which now accepts a locker client but still +needs session-service wiring and runtime qualification. The code is based on Smithay's `smallvil` learning compositor at pinned commit `023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the From 72bc1fd42531eabbd27d2c9830e1263b43c593ee Mon Sep 17 00:00:00 2001 From: sdolbec Date: Sun, 18 Oct 2026 23:56:17 -0400 Subject: [PATCH 083/798] Bridge Pantheon screensaver actions to session lock --- mseries/OS9-HANDOFF.md | 18 ++ .../EXPERIENCE-REQUIREMENTS.md | 15 +- .../session/README.md | 8 + rust/pantheon-compositor/README.md | 12 +- rust/pantheon-compositor/src/handlers/mod.rs | 1 + rust/pantheon-compositor/src/shell_compat.rs | 169 ++++++++++++++++-- rust/pantheon-compositor/src/state.rs | 3 +- 7 files changed, 202 insertions(+), 24 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 99c6ee262..bdd28b8f8 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5572,3 +5572,21 @@ not supply an authentication UI or policy; a Pantheon locker must implement the session-lock protocol and authenticate the user. Next: validate a real locker in a nested session, then qualify login/lock/unlock in an isolated native session before considering this a swap-in capability. + +### Pantheon screensaver action bridge — 2026-10-18 + +Read-only inspection of the running Gala session confirmed that Gala owns +`org.gnome.ScreenSaver` and exports `Lock`, `GetActive`, `SetActive`, +`GetActiveTime` and `ActiveChanged`. Added that D-Bus interface to the Rust +session service. `Lock` and `SetActive(true)` launch `gtklock` by default (or +the executable named by `PANTHEON_LOCK_COMMAND`) with the compositor's own +`WAYLAND_DISPLAY`; the active state and signal follow successful protocol lock +confirmation and authenticated unlock. `SetActive(false)` cannot unlock the +session; only the session-lock client can send `unlock_and_destroy`. Default +and `native-drm` checks/builds pass. The host does not have `gtklock` installed, +and no D-Bus or locker runtime call was made. The experimental session payload +must depend on a locker package; Gtklock is available in the distro package +index and upstream documents its use of `ext-session-lock-v1`, but its UI/PAM +behavior on this Pantheon session remains unqualified. Next: stage the package +in an isolated nested session, verify D-Bus activation and authentication, +then exercise lock/unlock without touching Gala's live session. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index ca72bc9ab..53aeec1e8 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -92,10 +92,11 @@ accelerator registration and activation path: pressing the registered physical media-key activation and M-series hardware remain unqualified; this is not full session or hardware qualification. -The compositor now implements Smithay's `ext-session-lock-v1` protocol. Lock -requests suppress application rendering, clear active grabs, route input to -the lock surface, and confirm the lock only after each mapped output presents -a cleared frame. The API and both backend builds are in place, but no Pantheon -locker has been connected; authentication, session action wiring, runtime -input/visual checks, native login and crash recovery remain open acceptance -items. +The compositor now implements Smithay's `ext-session-lock-v1` protocol and the +observed `org.gnome.ScreenSaver` action interface. Lock requests suppress +application rendering, clear active grabs, route input to the lock surface, +and confirm the lock only after each mapped output presents a cleared frame. +`Lock` starts a configured locker (default `gtklock`), but that package is not +installed by this source tree and no locker has been connected. Authentication, +runtime input/visual checks, native login and crash recovery remain open +acceptance items. diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index d0ecea961..c9f8f9b93 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -31,3 +31,11 @@ Orca and elementary settings-daemon targets. It intentionally does not promise that those services or the compositor are fully compatible; this is an experimental selection for staged qualification, with Gala still available as rollback. + +The compositor's `org.gnome.ScreenSaver.Lock` bridge launches `gtklock` by +default and passes it the session's Wayland socket. A distro package for this +session must include an `ext-session-lock-v1` client with PAM authentication +(the Ubuntu `gtklock` package provides this protocol client); the source tree +does not install it. `PANTHEON_LOCK_COMMAND` can select another installed +client. Authentication, D-Bus activation and session lock/unlock have not yet +been runtime-qualified. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 59c62b201..7bced308b 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -86,8 +86,10 @@ animated hiding, blur, overview behavior and multi-output work-area updates for already maximized windows are not complete. The compositor now implements Smithay's `ext-session-lock-v1`: it blanks each output until its lock surface is ready, confines input to the locker, and restores the prior keyboard focus on -unlock. A Pantheon locker/authentication UI and session action wiring are not -provided, and the protocol has not yet been runtime-qualified. Widget, +unlock. The compositor exports the observed `org.gnome.ScreenSaver` API and +launches `gtklock` on `Lock`/`SetActive(true)`; set `PANTHEON_LOCK_COMMAND` to +select another executable. The locker is not installed by this source tree, and +the D-Bus/session path has not yet been runtime-qualified. Widget, extended-behavior and greeter protocol requests are accepted but currently have no behavior. Touch input, multi-monitor configuration, accessibility, IBus, Xwayland, screen-sharing portals and production crash recovery also remain @@ -137,9 +139,9 @@ The session bus also exposes the small `org.gnome.Shell` accelerator API used by Wingpanel and its sound indicator. Accelerator registrations are matched through the compositor's active XKB state and reported with `AcceleratorActivated`; `ShowOSD` is currently a no-op, and lock-screen action -modes are not implemented. This is distinct from the compositor's -`ext-session-lock-v1` protocol, which now accepts a locker client but still -needs session-service wiring and runtime qualification. +modes are not implemented. Separately, the observed `org.gnome.ScreenSaver` +interface launches the configured session locker; its action and authentication +path still need runtime qualification. The code is based on Smithay's `smallvil` learning compositor at pinned commit `023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs index 4006c9c21..15606f2f9 100644 --- a/rust/pantheon-compositor/src/handlers/mod.rs +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -145,6 +145,7 @@ impl SessionLockHandler for PantheonCompositor { } fn unlock(&mut self) { + self.shell_compat.set_locked(false); self.session_locked = false; self.lock_owner = None; self.lock_confirmation = None; diff --git a/rust/pantheon-compositor/src/shell_compat.rs b/rust/pantheon-compositor/src/shell_compat.rs index a73bd6101..6d66ff8b9 100644 --- a/rust/pantheon-compositor/src/shell_compat.rs +++ b/rust/pantheon-compositor/src/shell_compat.rs @@ -1,8 +1,14 @@ use std::{ collections::HashMap, - sync::{Arc, Mutex, mpsc}, + ffi::OsString, + process::{Child, Command}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + mpsc, + }, thread, - time::Duration, + time::{Duration, Instant}, }; use smithay::input::keyboard::{Keysym, ModifiersState, xkb}; @@ -11,6 +17,15 @@ use zbus::{blocking::Connection, interface, object_server::SignalEmitter, zvaria const BUS_NAME: &str = "org.gnome.Shell"; const OBJECT_PATH: &str = "/org/gnome/Shell"; const INTERFACE: &str = "org.gnome.Shell"; +const SCREENSAVER_BUS_NAME: &str = "org.gnome.ScreenSaver"; +const SCREENSAVER_OBJECT_PATH: &str = "/org/gnome/ScreenSaver"; +const SCREENSAVER_INTERFACE: &str = "org.gnome.ScreenSaver"; + +#[derive(Clone, Copy)] +enum ShellEvent { + AcceleratorActivated(u32), + ActiveChanged(bool), +} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] struct AcceleratorModifiers { @@ -35,21 +50,37 @@ struct KeyBinding { #[derive(Clone)] pub struct ShellCompatHandle { bindings: Arc>>, - activated: mpsc::Sender, + events: mpsc::Sender, + active: Arc, + active_since: Arc>>, } impl ShellCompatHandle { - pub fn start() -> Self { + pub fn start(wayland_display: OsString) -> Self { let bindings = Arc::new(Mutex::new(Vec::new())); - let (activated, receiver) = mpsc::channel(); + let (events, receiver) = mpsc::channel(); + let active = Arc::new(AtomicBool::new(false)); + let active_since = Arc::new(Mutex::new(None)); let service_bindings = bindings.clone(); + let service_active = active.clone(); + let service_active_since = active_since.clone(); thread::Builder::new() .name("pantheon-shell-dbus".into()) - .spawn(move || serve_shell_compat(service_bindings, receiver)) + .spawn(move || { + serve_shell_compat( + service_bindings, + service_active, + service_active_since, + wayland_display, + receiver, + ) + }) .expect("failed to start shell compatibility D-Bus thread"); Self { bindings, - activated, + events, + active, + active_since, } } @@ -67,10 +98,19 @@ impl ShellCompatHandle { return false; } for id in matched { - let _ = self.activated.send(id); + let _ = self.events.send(ShellEvent::AcceleratorActivated(id)); } true } + + pub fn set_locked(&self, locked: bool) { + let previous = self.active.swap(locked, Ordering::AcqRel); + if previous == locked { + return; + } + *self.active_since.lock().expect("lock timer poisoned") = locked.then(Instant::now); + let _ = self.events.send(ShellEvent::ActiveChanged(locked)); + } } struct ShellKeyGrabber { @@ -78,6 +118,14 @@ struct ShellKeyGrabber { next_id: u32, } +struct ScreenSaverCompat { + wayland_display: OsString, + lock_command: OsString, + active: Arc, + active_since: Arc>>, + lock_child: Arc>>, +} + #[interface(name = "org.gnome.Shell")] impl ShellKeyGrabber { fn grab_accelerator(&mut self, accelerator: String, _mode_flags: u32, grab_flags: u32) -> u32 { @@ -140,7 +188,62 @@ impl ShellKeyGrabber { } } -fn serve_shell_compat(bindings: Arc>>, receiver: mpsc::Receiver) { +#[interface(name = "org.gnome.ScreenSaver")] +impl ScreenSaverCompat { + fn lock(&self) -> zbus::fdo::Result<()> { + if self.active.load(Ordering::Acquire) { + return Ok(()); + } + + let mut child = self.lock_child.lock().expect("lock child poisoned"); + if let Some(running) = child.as_mut() { + match running.try_wait() { + Ok(None) => return Ok(()), + Ok(Some(_)) => *child = None, + Err(error) => return Err(zbus::fdo::Error::Failed(error.to_string())), + } + } + + let process = Command::new(&self.lock_command) + .env("WAYLAND_DISPLAY", &self.wayland_display) + .spawn() + .map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?; + *child = Some(process); + Ok(()) + } + + fn get_active(&self) -> bool { + self.active.load(Ordering::Acquire) + } + + fn set_active(&self, active: bool) -> zbus::fdo::Result<()> { + if active { + self.lock()?; + } + // A caller cannot unlock the session through this legacy API; the + // authenticated session-lock client owns unlock_and_destroy. + Ok(()) + } + + fn get_active_time(&self) -> u32 { + self.active_since + .lock() + .expect("lock timer poisoned") + .map(|since| since.elapsed().as_secs().min(u32::MAX as u64) as u32) + .unwrap_or(0) + } + + #[zbus(signal)] + async fn active_changed(emitter: &SignalEmitter<'_>, new_value: bool) -> zbus::Result<()>; +} + +fn serve_shell_compat( + bindings: Arc>>, + active: Arc, + active_since: Arc>>, + wayland_display: OsString, + receiver: mpsc::Receiver, +) { let connection = match Connection::session() { Ok(connection) => connection, Err(error) => { @@ -164,10 +267,43 @@ fn serve_shell_compat(bindings: Arc>>, receiver: mpsc::Rec return; } - tracing::info!("serving the Wingpanel Shell accelerator compatibility interface"); + let lock_command = + std::env::var_os("PANTHEON_LOCK_COMMAND").unwrap_or_else(|| OsString::from("gtklock")); + let lock_child = Arc::new(Mutex::new(None)); + if let Err(error) = connection.object_server().at( + SCREENSAVER_OBJECT_PATH, + ScreenSaverCompat { + wayland_display, + lock_command, + active, + active_since, + lock_child: lock_child.clone(), + }, + ) { + tracing::warn!(%error, "cannot register the ScreenSaver compatibility interface"); + return; + } + if let Err(error) = connection.request_name(SCREENSAVER_BUS_NAME) { + tracing::warn!(%error, "cannot own the ScreenSaver compatibility name"); + return; + } + + tracing::info!("serving Shell accelerator and ScreenSaver compatibility interfaces"); loop { + { + let mut child = lock_child.lock().expect("lock child poisoned"); + if let Some(running) = child.as_mut() { + match running.try_wait() { + Ok(Some(_)) => { + *child = None; + } + Ok(None) => {} + Err(error) => tracing::warn!(%error, "could not reap the session locker"), + } + } + } match receiver.recv_timeout(Duration::from_millis(100)) { - Ok(action) => { + Ok(ShellEvent::AcceleratorActivated(action)) => { let parameters = HashMap::::new(); if let Err(error) = connection.emit_signal( None::<&str>, @@ -179,6 +315,17 @@ fn serve_shell_compat(bindings: Arc>>, receiver: mpsc::Rec tracing::warn!(%error, "could not emit AcceleratorActivated"); } } + Ok(ShellEvent::ActiveChanged(active)) => { + if let Err(error) = connection.emit_signal( + None::<&str>, + SCREENSAVER_OBJECT_PATH, + SCREENSAVER_INTERFACE, + "ActiveChanged", + &(active,), + ) { + tracing::warn!(%error, "could not emit ScreenSaver ActiveChanged"); + } + } Err(mpsc::RecvTimeoutError::Timeout) => {} Err(mpsc::RecvTimeoutError::Disconnected) => return, } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index e345d15da..303fcc28f 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -110,7 +110,6 @@ impl PantheonCompositor { let data_device_state = DataDeviceState::new::(&dh); let pantheon_shell_state = PantheonShellState::new::(&dh); let display_config = DisplayConfigHandle::start(); - let shell_compat = ShellCompatHandle::start(); // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. @@ -133,6 +132,7 @@ impl PantheonCompositor { // Setup a wayland socket that will be used to accept clients let socket_name = Self::init_wayland_listener(display, event_loop)?; + let shell_compat = ShellCompatHandle::start(socket_name.clone()); // Get the loop signal, used to stop the event loop let loop_signal = event_loop.get_signal(); @@ -293,6 +293,7 @@ impl PantheonCompositor { if self.lock_confirmation_outputs.is_empty() { if let Some(confirmation) = self.lock_confirmation.take() { confirmation.lock(); + self.shell_compat.set_locked(true); } } } From 7d5672215d700d99b4fa08000a62579d832e25e3 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:00:25 -0400 Subject: [PATCH 084/798] Report native DRM mode inventory --- mseries/OS9-HANDOFF.md | 13 ++++ mseries/ROADMAP.md | 5 ++ .../EXPERIENCE-REQUIREMENTS.md | 5 +- rust/pantheon-compositor/README.md | 4 +- .../pantheon-compositor/src/display_config.rs | 74 ++++++++++++++----- rust/pantheon-compositor/src/native.rs | 33 ++++++++- rust/pantheon-compositor/src/winit.rs | 14 +++- 7 files changed, 125 insertions(+), 23 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index bdd28b8f8..c395d1d13 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5590,3 +5590,16 @@ index and upstream documents its use of `ext-session-lock-v1`, but its UI/PAM behavior on this Pantheon session remains unqualified. Next: stage the package in an isolated nested session, verify D-Bus activation and authentication, then exercise lock/unlock without touching Gala's live session. + +### Advertise kernel-reported display modes — 2026-10-18 + +`org.gnome.Mutter.DisplayConfig.GetCurrentState` no longer fabricates a single +mode for each native connector. It now lists every mode returned by that +connector's DRM resources, marks the active and preferred modes, and computes +refresh rates from DRM timing data. The nested Winit backend continues to +advertise only its actual current window size. Default and `native-drm` checks +pass. This improves the settings UI's inventory only: `ApplyMonitorsConfig` +is not implemented, mode changes and layout application remain unavailable, +and no live display configuration was changed. Next: validate connector mode +IDs against Pantheon Settings, then implement verified apply/rollback on the +native KMS outputs. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index f5a0ba209..c9408d420 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -66,6 +66,11 @@ scanout remain unqualified. Local `cargo check --bins` and `cargo build --bins` and without `native-drm`; they establish compilation only, not successful connector modesetting. +Read-only DisplayConfig now lists the modes reported by each native DRM +connector, including current/preferred flags and timing-derived refresh rates. +It still cannot apply a requested mode, position, scale or transform; native +mode/layout configuration and rollback are the next display-settings work. + Smithay's `ext-session-lock-v1` is implemented in both rendering backends: applications are hidden behind lock surfaces or black, input is confined to the locker, and lock confirmation waits for each mapped output to present a diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 53aeec1e8..b7d0c210d 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -86,7 +86,10 @@ and `cargo build` pass. Its protocol probe completed under Xvfb over a nested Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland session and rendered SHM buffers. The prototype serves the read-only `org.gnome.Mutter.DisplayConfig.GetCurrentState` query Wingpanel uses, and -Wingpanel completes that call. It also implements the `org.gnome.Shell` +Wingpanel completes that call. Native `GetCurrentState` now lists the modes +reported by each DRM connector and marks current/preferred modes; it remains +read-only and cannot apply mode, layout, scale or transform changes. It also +implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered `Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, physical media-key activation and M-series hardware remain unqualified; this diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 7bced308b..a6016386f 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -157,7 +157,9 @@ sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated and attached shared-memory buffers. Wingpanel requires `org.gnome.Mutter.DisplayConfig`; the compositor now serves the read-only `GetCurrentState` query it calls, with every tracked output's current mode, -logical position, scale, and primary status. Output add/update/remove changes +logical position, scale, and primary status. Native outputs now list all +connector modes reported by DRM and mark active/preferred entries; the nested +backend reports only its current Winit size. Output add/update/remove changes increment the state serial and emit `MonitorsChanged`. The service remains read-only and does not apply monitor modes or layouts. The isolated Wingpanel run no longer reports missing DisplayConfig or Shell keybinding diff --git a/rust/pantheon-compositor/src/display_config.rs b/rust/pantheon-compositor/src/display_config.rs index a6987e428..e4248295d 100644 --- a/rust/pantheon-compositor/src/display_config.rs +++ b/rust/pantheon-compositor/src/display_config.rs @@ -20,12 +20,32 @@ pub struct OutputSnapshot { pub width: i32, pub height: i32, pub refresh_millihz: i32, + pub modes: Vec, pub scale: f64, pub x: i32, pub y: i32, pub primary: bool, } +#[derive(Clone, Debug)] +pub struct OutputModeSnapshot { + pub width: i32, + pub height: i32, + pub refresh_millihz: i32, + pub preferred: bool, +} + +impl OutputModeSnapshot { + pub fn id(&self) -> String { + format!( + "{}x{}@{:.3}", + self.width, + self.height, + f64::from(self.refresh_millihz) / 1000.0 + ) + } +} + #[derive(Clone)] pub struct DisplayConfigHandle { updates: mpsc::Sender, @@ -86,25 +106,41 @@ impl DisplayConfigServer { output.product, output.serial, ); - let mode_id = format!( - "{}x{}@{:.3}", - output.width, - output.height, - f64::from(output.refresh_millihz) / 1000.0 - ); - let mut mode_properties = Properties::new(); - mode_properties.insert("is-current".into(), OwnedValue::from(true)); - mode_properties.insert("is-preferred".into(), OwnedValue::from(true)); - let mode = ( - mode_id, - output.width, - output.height, - f64::from(output.refresh_millihz) / 1000.0, - output.scale, - vec![output.scale], - mode_properties, - ); - monitors.push((monitor_info.clone(), vec![mode], Properties::new())); + let modes = if output.modes.is_empty() { + vec![OutputModeSnapshot { + width: output.width, + height: output.height, + refresh_millihz: output.refresh_millihz, + preferred: true, + }] + } else { + output.modes + }; + let modes = modes + .into_iter() + .map(|mode| { + let mut mode_properties = Properties::new(); + let current = mode.width == output.width + && mode.height == output.height + && mode.refresh_millihz == output.refresh_millihz; + if current { + mode_properties.insert("is-current".into(), OwnedValue::from(true)); + } + if mode.preferred { + mode_properties.insert("is-preferred".into(), OwnedValue::from(true)); + } + ( + mode.id(), + mode.width, + mode.height, + f64::from(mode.refresh_millihz) / 1000.0, + output.scale, + vec![output.scale], + mode_properties, + ) + }) + .collect(); + monitors.push((monitor_info.clone(), modes, Properties::new())); logical_monitors.push(( output.x, output.y, diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index b1b256248..7b68185a7 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -49,7 +49,7 @@ use smithay::{ use crate::{ PantheonCompositor, cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}, - display_config::OutputSnapshot, + display_config::{OutputModeSnapshot, OutputSnapshot}, }; pub type GbmRendererBackend = GbmGlesBackend; @@ -67,6 +67,29 @@ const OUTPUT_FORMATS: &[smithay::backend::allocator::Fourcc] = &[ smithay::backend::allocator::Fourcc::Argb8888, ]; +fn output_mode_snapshots( + modes: &[smithay::reexports::drm::control::Mode], +) -> Vec { + let mut snapshots = modes + .iter() + .map(|drm_mode| { + let mode = Mode::from(*drm_mode); + OutputModeSnapshot { + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + preferred: drm_mode.mode_type().contains(ModeTypeFlags::PREFERRED), + } + }) + .collect::>(); + if !snapshots.iter().any(|mode| mode.preferred) + && let Some(first) = snapshots.first_mut() + { + first.preferred = true; + } + snapshots +} + pub struct NativeRenderer { pub render_node: DrmNode, pub render_gbm: GbmDevice, @@ -76,6 +99,7 @@ pub struct NativeRenderer { pub struct NativeKmsOutput { pub connector: connector::Handle, pub crtc: smithay::reexports::drm::control::crtc::Handle, + pub modes: Vec, pub output: Output, pub drm_output: NativeDrmOutput, pub global_id: smithay::reexports::wayland_server::backend::GlobalId, @@ -304,6 +328,7 @@ impl NativeKmsDevice { .copied() .unwrap_or(info.modes()[0]); let mode = Mode::from(drm_mode); + let available_modes = info.modes().to_vec(); let name = format!("{}-{}", info.interface().as_str(), info.interface_id()); let (physical_width, physical_height) = info.size().unwrap_or((0, 0)); let output = Output::new( @@ -344,6 +369,7 @@ impl NativeKmsDevice { self.outputs.push(NativeKmsOutput { connector: info.handle(), crtc, + modes: available_modes, output, drm_output, global_id, @@ -377,6 +403,7 @@ impl NativeKmsDevice { width: mode.size.w, height: mode.size.h, refresh_millihz: mode.refresh, + modes: output_mode_snapshots(&output.modes), scale: 1.0, x, y: 0, @@ -557,6 +584,8 @@ impl NativeKmsDevice { let mut x = 0; for selected in connected { let mode = Mode::from(selected.mode); + let available_modes = selected.connector.modes().to_vec(); + let mode_snapshots = output_mode_snapshots(&available_modes); let name = format!( "{}-{}", selected.connector.interface().as_str(), @@ -602,6 +631,7 @@ impl NativeKmsDevice { width: mode.size.w, height: mode.size.h, refresh_millihz: mode.refresh, + modes: mode_snapshots, scale: 1.0, x, y: 0, @@ -611,6 +641,7 @@ impl NativeKmsDevice { outputs.push(NativeKmsOutput { connector: selected.connector.handle(), crtc: selected.crtc, + modes: available_modes, output, drm_output, global_id, diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index db3432304..4c51f392c 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -12,7 +12,7 @@ use smithay::{ use crate::PantheonCompositor; use crate::cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}; -use crate::display_config::OutputSnapshot; +use crate::display_config::{OutputModeSnapshot, OutputSnapshot}; pub fn init_winit( event_loop: &mut EventLoop, @@ -53,6 +53,12 @@ pub fn init_winit( width: mode.size.w, height: mode.size.h, refresh_millihz: mode.refresh, + modes: vec![OutputModeSnapshot { + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + preferred: true, + }], scale: 1.0, x: 0, y: 0, @@ -86,6 +92,12 @@ pub fn init_winit( width: mode.size.w, height: mode.size.h, refresh_millihz: mode.refresh, + modes: vec![OutputModeSnapshot { + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + preferred: true, + }], scale: 1.0, x: 0, y: 0, From 6d6858ac865f36d3a958dbd96abbc59f71af38f8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:07:15 -0400 Subject: [PATCH 085/798] Machine Lab: list ARM64 architecture everywhere, clear Connect section (display, console, SSH terminal) Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/src/main.vala | 6 +-- mseries/machine-lab/src/page.vala | 88 +++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index c26c86c19..5a3853298 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -82,7 +82,7 @@ public class MachineRow : Gtk.ListBoxRow { public void update () { name_label.label = machine.name; string state = machine.running ? (machine.paused ? "Paused" : "Running") : machine.is_fresh () ? "Not installed" : "Stopped"; - sub_label.label = "%s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); + sub_label.label = "ARM64 · %s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); for (int i = 0; i < 8; i++) { icon.remove_css_class ("ml-c-%d".printf (i)); } @@ -282,7 +282,7 @@ public class MachineLab : Gtk.Application { var n = new Gtk.Label (p.name); n.xalign = 0; n.add_css_class ("ml-machine-name"); - var d = new Gtk.Label ("%s · %d CPU · %d GiB".printf (p.blurb, p.cpus, p.mem)); + var d = new Gtk.Label ("%s · ARM64 · %d CPU · %d GiB".printf (p.blurb, p.cpus, p.mem)); d.xalign = 0; d.add_css_class ("ml-machine-sub"); tb.append (n); @@ -357,7 +357,7 @@ public class MachineLab : Gtk.Application { var bl = new Gtk.Label (p.blurb); bl.xalign = 0; bl.add_css_class ("ml-spec-label"); - var sp = new Gtk.Label ("%d CPU · %d GiB RAM · %d GiB disk".printf (p.cpus, p.mem, p.disk)); + var sp = new Gtk.Label ("ARM64 · %d CPU · %d GiB RAM · %d GiB disk".printf (p.cpus, p.mem, p.disk)); sp.xalign = 0; sp.add_css_class ("ml-fineprint"); tb.append (n); diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 3ed356635..0355076f6 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -18,11 +18,14 @@ public class MachinePage : Gtk.Box { private Gtk.Label mem_v; private Gtk.Label disk_v; private Gtk.Label net_v; + private Gtk.Label arch_v; private Gtk.Label ssh_cmd; private Gtk.Image hero_icon; private Gtk.Label hero_sub; private Gtk.Box setup_card; private Gtk.Box connect_card; + private Gtk.Box ssh_row; + private Gtk.Label display_detail; private Gtk.MenuButton more_button; private Gtk.Label[] step_badges = {}; private Gtk.Label step2_detail; @@ -260,6 +263,8 @@ public class MachinePage : Gtk.Box { specs.attach (spec_card ("media-optical-symbolic", out mem_v, "Memory"), 1, 0); specs.attach (spec_card ("drive-harddisk-symbolic", out disk_v, "Disk, grows as used"), 0, 1); specs.attach (spec_card ("network-wired-symbolic", out net_v, "Network"), 1, 1); + specs.attach (spec_card ("applications-system-symbolic", out arch_v, "Architecture · native ARM64, not x86-64 / amd64"), 0, 2, 2, 1); + arch_v.label = "ARM64 (aarch64)"; clamp.append (specs); // Readiness @@ -290,27 +295,74 @@ public class MachinePage : Gtk.Box { ready_card.append (recheck_btn); clamp.append (ready_card); - // Connect row - var connect = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + // Connect: the clear way into a running machine. + var connect = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); connect_card = connect; connect.add_css_class ("ml-card"); - connect.add_css_class ("ml-spec"); + var chead = new Gtk.Label ("Connect"); + chead.xalign = 0; + chead.margin_top = 14; + chead.margin_start = 16; + chead.margin_bottom = 4; + chead.add_css_class ("title-4"); + connect.append (chead); + + // Display + var drow = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + drow.add_css_class ("ml-form-row"); + drow.add_css_class ("ml-form-sep"); + var dicon = new Gtk.Image.from_icon_name ("video-display-symbolic"); + dicon.pixel_size = 24; + drow.append (dicon); + var dtext = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + dtext.hexpand = true; + var dtitle = new Gtk.Label ("Display"); + dtitle.xalign = 0; + dtitle.add_css_class ("ml-spec-value"); + display_detail = new Gtk.Label (""); + display_detail.xalign = 0; + display_detail.wrap = true; + display_detail.add_css_class ("ml-spec-label"); + dtext.append (dtitle); + dtext.append (display_detail); + drow.append (dtext); + var console_btn = new Gtk.Button.with_label ("Open Console"); + console_btn.valign = Gtk.Align.CENTER; + console_btn.tooltip_text = "Show the machine's console output"; + console_btn.clicked.connect (() => show_tab ("console")); + drow.append (console_btn); + connect.append (drow); + + // SSH terminal + ssh_row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + ssh_row.add_css_class ("ml-form-row"); var term = new Gtk.Image.from_icon_name ("utilities-terminal-symbolic"); term.pixel_size = 24; - connect.append (term); - var ct = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + ssh_row.append (term); + var ct = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); ct.hexpand = true; - ct.valign = Gtk.Align.CENTER; + var ttitle = new Gtk.Label ("Terminal (SSH)"); + ttitle.xalign = 0; + ttitle.add_css_class ("ml-spec-value"); ssh_cmd = new Gtk.Label (""); ssh_cmd.xalign = 0; ssh_cmd.selectable = true; ssh_cmd.add_css_class ("ml-mono"); - var cl = new Gtk.Label ("Connect once the guest is running"); - cl.xalign = 0; - cl.add_css_class ("ml-spec-label"); + ct.append (ttitle); ct.append (ssh_cmd); - ct.append (cl); - connect.append (ct); + ssh_row.append (ct); + var open_term = new Gtk.Button.with_label ("Open Terminal"); + open_term.valign = Gtk.Align.CENTER; + open_term.clicked.connect (() => { + try { + Process.spawn_async (null, { "io.elementary.terminal", "-e", + "ssh -p %d -o StrictHostKeyChecking=accept-new localhost".printf (machine.ssh_port) }, + null, SpawnFlags.SEARCH_PATH, null, null); + } catch (Error e) { + toast ("Couldn't open Terminal: " + e.message); + } + }); + ssh_row.append (open_term); var copy = new Gtk.Button.from_icon_name ("edit-copy-symbolic"); copy.valign = Gtk.Align.CENTER; copy.add_css_class ("flat"); @@ -319,7 +371,8 @@ public class MachinePage : Gtk.Box { Gdk.Display.get_default ().get_clipboard ().set_text (ssh_cmd.label); toast ("Copied SSH command"); }); - connect.append (copy); + ssh_row.append (copy); + connect.append (ssh_row); clamp.append (connect); // Installer @@ -658,7 +711,12 @@ public class MachinePage : Gtk.Box { ssh_cmd.label = "ssh -p %d localhost".printf (machine.ssh_port); setup_card.visible = fresh && !managed; - connect_card.visible = !managed && !fresh; + connect_card.visible = running || !fresh; + connect_card.sensitive = running; + ssh_row.visible = !managed; + display_detail.label = !running + ? "Start the machine, then its screen opens in its own window on this desktop." + : "Opens in its own window on this desktop. Look for “%s” in the dock, or press Alt+Tab. If it was started without a window, use the console.".printf (machine.name); drop_zone.visible = !managed; more_button.visible = !managed; get_button.visible = machine.url != null || machine.recipe != null; @@ -679,9 +737,9 @@ public class MachinePage : Gtk.Box { var creds = machine.credentials (); creds_card.visible = creds != null; if (creds != null) creds_label.label = creds; - hero_sub.label = managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") + hero_sub.label = "ARM64 · " + (managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") : fresh ? "New machine · not installed yet" - : "Linux ARM64 · installed"; + : "Linux ARM64 · installed"); var problems = Readiness.problems (); bool installing = running && machine.installer != null; From 67b532e5b3140f4b8cb3e0d7b737e7349fdda278 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:07:17 -0400 Subject: [PATCH 086/798] Machine Lab: ARM64 labels, Connect section; handoff: elementary installed-boot finding Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 13 +++++ mseries/machine-lab/src/main.vala | 6 +-- mseries/machine-lab/src/page.vala | 88 +++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 8e76ea88b..c43369824 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6170,3 +6170,16 @@ free-space warning. Verified: builds, Broadway renders, pause/resume against a h Omarchy nightly. Not verified: a complete guest install/boot (~6 GiB download; host had ~19 GiB free) and suspend/resume of the app itself. Next: run the Omarchy automatic install once space is freed, and lift `~/Documents/Codex/2026-10-18/f-2/work/youtube-iso-test` (42 GB, owner decides) if space is needed. + +## 2026-10-19: elementary OS ARM64 install test in Machine Lab's VM + +Booted the CI ISO `elementaryos-9.0-daily-arm64.20261013230825.iso` (SHA-256 verified) in QEMU/KVM, headless, and drove the +installer (Erase Disk and Install, no encryption, no proprietary drivers) to completion on a 32 GiB virtual disk: 11.3 GiB +written, an "elementary OS 9 Early Access" UEFI entry created, the firmware loaded `shimaa64.efi`. **The installed system +then showed no output at all for 5+ minutes** (no serial text, display "not active", QEMU at 100% CPU). So the live ISO +boots and the installer works, but the *installed-system boot is not working on QEMU virt*. Not diagnosed: the installed +kernel is most likely the Fairydust (Apple M-series) kernel, which has no virtio-gpu/console path on QEMU; the +installer's root partition is LVM so the disk could not be inspected without root device-mapper. Next: boot the installed +disk with a console on serial (kernel args `console=ttyAMA0 earlycon`) or boot the live ISO with the disk attached and +read `/boot/grub/grub.cfg` and the initramfs; kernel work belongs to DeepSeek per the coordination rule. +Machine Lab: ARM64 now listed on every machine, plus a Connect section (display, console, SSH terminal). diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index c26c86c19..5a3853298 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -82,7 +82,7 @@ public class MachineRow : Gtk.ListBoxRow { public void update () { name_label.label = machine.name; string state = machine.running ? (machine.paused ? "Paused" : "Running") : machine.is_fresh () ? "Not installed" : "Stopped"; - sub_label.label = "%s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); + sub_label.label = "ARM64 · %s · %d CPU · %d GiB".printf (state, machine.cpus, machine.mem_gib); for (int i = 0; i < 8; i++) { icon.remove_css_class ("ml-c-%d".printf (i)); } @@ -282,7 +282,7 @@ public class MachineLab : Gtk.Application { var n = new Gtk.Label (p.name); n.xalign = 0; n.add_css_class ("ml-machine-name"); - var d = new Gtk.Label ("%s · %d CPU · %d GiB".printf (p.blurb, p.cpus, p.mem)); + var d = new Gtk.Label ("%s · ARM64 · %d CPU · %d GiB".printf (p.blurb, p.cpus, p.mem)); d.xalign = 0; d.add_css_class ("ml-machine-sub"); tb.append (n); @@ -357,7 +357,7 @@ public class MachineLab : Gtk.Application { var bl = new Gtk.Label (p.blurb); bl.xalign = 0; bl.add_css_class ("ml-spec-label"); - var sp = new Gtk.Label ("%d CPU · %d GiB RAM · %d GiB disk".printf (p.cpus, p.mem, p.disk)); + var sp = new Gtk.Label ("ARM64 · %d CPU · %d GiB RAM · %d GiB disk".printf (p.cpus, p.mem, p.disk)); sp.xalign = 0; sp.add_css_class ("ml-fineprint"); tb.append (n); diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 3ed356635..0355076f6 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -18,11 +18,14 @@ public class MachinePage : Gtk.Box { private Gtk.Label mem_v; private Gtk.Label disk_v; private Gtk.Label net_v; + private Gtk.Label arch_v; private Gtk.Label ssh_cmd; private Gtk.Image hero_icon; private Gtk.Label hero_sub; private Gtk.Box setup_card; private Gtk.Box connect_card; + private Gtk.Box ssh_row; + private Gtk.Label display_detail; private Gtk.MenuButton more_button; private Gtk.Label[] step_badges = {}; private Gtk.Label step2_detail; @@ -260,6 +263,8 @@ public class MachinePage : Gtk.Box { specs.attach (spec_card ("media-optical-symbolic", out mem_v, "Memory"), 1, 0); specs.attach (spec_card ("drive-harddisk-symbolic", out disk_v, "Disk, grows as used"), 0, 1); specs.attach (spec_card ("network-wired-symbolic", out net_v, "Network"), 1, 1); + specs.attach (spec_card ("applications-system-symbolic", out arch_v, "Architecture · native ARM64, not x86-64 / amd64"), 0, 2, 2, 1); + arch_v.label = "ARM64 (aarch64)"; clamp.append (specs); // Readiness @@ -290,27 +295,74 @@ public class MachinePage : Gtk.Box { ready_card.append (recheck_btn); clamp.append (ready_card); - // Connect row - var connect = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); + // Connect: the clear way into a running machine. + var connect = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); connect_card = connect; connect.add_css_class ("ml-card"); - connect.add_css_class ("ml-spec"); + var chead = new Gtk.Label ("Connect"); + chead.xalign = 0; + chead.margin_top = 14; + chead.margin_start = 16; + chead.margin_bottom = 4; + chead.add_css_class ("title-4"); + connect.append (chead); + + // Display + var drow = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + drow.add_css_class ("ml-form-row"); + drow.add_css_class ("ml-form-sep"); + var dicon = new Gtk.Image.from_icon_name ("video-display-symbolic"); + dicon.pixel_size = 24; + drow.append (dicon); + var dtext = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + dtext.hexpand = true; + var dtitle = new Gtk.Label ("Display"); + dtitle.xalign = 0; + dtitle.add_css_class ("ml-spec-value"); + display_detail = new Gtk.Label (""); + display_detail.xalign = 0; + display_detail.wrap = true; + display_detail.add_css_class ("ml-spec-label"); + dtext.append (dtitle); + dtext.append (display_detail); + drow.append (dtext); + var console_btn = new Gtk.Button.with_label ("Open Console"); + console_btn.valign = Gtk.Align.CENTER; + console_btn.tooltip_text = "Show the machine's console output"; + console_btn.clicked.connect (() => show_tab ("console")); + drow.append (console_btn); + connect.append (drow); + + // SSH terminal + ssh_row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + ssh_row.add_css_class ("ml-form-row"); var term = new Gtk.Image.from_icon_name ("utilities-terminal-symbolic"); term.pixel_size = 24; - connect.append (term); - var ct = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + ssh_row.append (term); + var ct = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); ct.hexpand = true; - ct.valign = Gtk.Align.CENTER; + var ttitle = new Gtk.Label ("Terminal (SSH)"); + ttitle.xalign = 0; + ttitle.add_css_class ("ml-spec-value"); ssh_cmd = new Gtk.Label (""); ssh_cmd.xalign = 0; ssh_cmd.selectable = true; ssh_cmd.add_css_class ("ml-mono"); - var cl = new Gtk.Label ("Connect once the guest is running"); - cl.xalign = 0; - cl.add_css_class ("ml-spec-label"); + ct.append (ttitle); ct.append (ssh_cmd); - ct.append (cl); - connect.append (ct); + ssh_row.append (ct); + var open_term = new Gtk.Button.with_label ("Open Terminal"); + open_term.valign = Gtk.Align.CENTER; + open_term.clicked.connect (() => { + try { + Process.spawn_async (null, { "io.elementary.terminal", "-e", + "ssh -p %d -o StrictHostKeyChecking=accept-new localhost".printf (machine.ssh_port) }, + null, SpawnFlags.SEARCH_PATH, null, null); + } catch (Error e) { + toast ("Couldn't open Terminal: " + e.message); + } + }); + ssh_row.append (open_term); var copy = new Gtk.Button.from_icon_name ("edit-copy-symbolic"); copy.valign = Gtk.Align.CENTER; copy.add_css_class ("flat"); @@ -319,7 +371,8 @@ public class MachinePage : Gtk.Box { Gdk.Display.get_default ().get_clipboard ().set_text (ssh_cmd.label); toast ("Copied SSH command"); }); - connect.append (copy); + ssh_row.append (copy); + connect.append (ssh_row); clamp.append (connect); // Installer @@ -658,7 +711,12 @@ public class MachinePage : Gtk.Box { ssh_cmd.label = "ssh -p %d localhost".printf (machine.ssh_port); setup_card.visible = fresh && !managed; - connect_card.visible = !managed && !fresh; + connect_card.visible = running || !fresh; + connect_card.sensitive = running; + ssh_row.visible = !managed; + display_detail.label = !running + ? "Start the machine, then its screen opens in its own window on this desktop." + : "Opens in its own window on this desktop. Look for “%s” in the dock, or press Alt+Tab. If it was started without a window, use the console.".printf (machine.name); drop_zone.visible = !managed; more_button.visible = !managed; get_button.visible = machine.url != null || machine.recipe != null; @@ -679,9 +737,9 @@ public class MachinePage : Gtk.Box { var creds = machine.credentials (); creds_card.visible = creds != null; if (creds != null) creds_label.label = creds; - hero_sub.label = managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") + hero_sub.label = "ARM64 · " + (managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") : fresh ? "New machine · not installed yet" - : "Linux ARM64 · installed"; + : "Linux ARM64 · installed"); var problems = Readiness.problems (); bool installing = running && machine.installer != null; From be335a504ed71fa54cf7537bb1cb948e725b1a15 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:16:30 -0400 Subject: [PATCH 087/798] local-ai: omarchy-mlx bump-candidate plan (review only, nothing installed) Co-Authored-By: Claude Sonnet 5.5 --- .../OMARCHY-MLX-BUMP-PLAN-2026-10-19.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md diff --git a/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md b/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md new file mode 100644 index 000000000..6ba6e1531 --- /dev/null +++ b/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md @@ -0,0 +1,50 @@ +# omarchy-mlx bump-candidate plan — October 19, 2026 + +Review only. Nothing was installed, built or pinned. Sources were read from shallow/full clones of public `main`; no kernel, governor, Mesa or running-model changes. + +## Revisions + +| Item | Revision | +|---|---| +| Our pin | v0.7.33, `33630f9c1eeefe5fc985a28bed0585432c2c014e` (still the latest GitHub release on 2026-10-19) | +| Upstream main reviewed | `61495cb66cb9a5fb8e127001c7c1eb959a9d6c22` (#239, 2026-10-18), 36 commits past our pin, none released | +| `mil-hwx-compiler` | main `45f7670` (2026-10-18), CHANGELOG cut 0.3.0; `ane-compiler.lock` in omarchy-mlx pins `a8392e6` | +| `omarchy-ane` | main `94cceca` (2026-10-16), release 0.4.9; MIT | + +## What changed, and what it means for the M1 Pro (T6000, 32 GB, Qwen3.5-4B default) + +| Change | Default | M1 Pro relevance | Verdict | +|---|---|---|---| +| #231 GDN decode: untiled prefetch + native GQA | **On, only when the Vulkan device name contains G14C or G13C** | Needs f32 A_log and Hk != Hv (Qwen3.5-9B). Upstream measured about 8% on M1 Max/M2 Max. Our M1 Pro's device string is not recorded; it may be G13S/G13C-class. Our 4B route eligibility is unchecked. | Test, 9B only; check `vulkaninfo` device name first | +| #229/#225 GDN native GQA opt-in flags | Off | Superseded by #231 for f32; #225 is for bf16 A_log models | Skip unless a model needs it | +| #226 Row-reduce Sum (workgroup per row) | **On** (`MLX_OMARCHY_ROW_REDUCE=0` disables) | Changes summation order for float sums over rows of 256 or more outside fused kernels. Upstream says Qwen3.5-9B and 27B profiles contain no Reduce dispatch. Qwen3.5-4B is not profiled. One Muse NLL check fell outside the registered bound (Apache passage −0.0124 vs 0.01). | Bit-compare 4B greedy output on/off before accepting; keep the kill switch in the profile | +| #223 ArgMax/LogSumExp chunked routes | Off | Possible large-vocab sampling help; separate numerics contract | Optional one-at-a-time test | +| #232 rotating-KV maskless, #233 skip per-token logprob read | Opt-in patch / applied by serve patches | #233 touches mlx-lm server behavior our `mseries-ai` supervisor wraps; #232 targets the 0.32 series (we run mlx-lm 0.31.3) | Review `mlx-lm-server-skip-logprob-read.patch` against our private prefill-256 / thinking-off patch for conflicts. Skip #232 | +| #224/#216/#214 gather_qmm word / mxfp4 kernels | Off / developer switch | MoE only; numerics verdict REJECT upstream | Exclude | +| 6-bit affine quantized_matmul decode (`75cc013b`) | On | Only if a 6-bit model is used | Low value | +| #239 mlx-audio depthwise Metal gate backport | Optional | Not used by our stack | Skip | +| #227 governor doc | n/a | +2.3% for about +2 W idle, M1 Max | Keep current governor (already our decision) | +| `--patch-venv`, versioned patch directories (`patches/mlx-lm-0.32/`) | n/a | `install.sh` refactor: our `mseries/local-ai/install.sh` wraps the upstream installer at v0.7.33 and expects the old flat `patches/` layout | Must re-test our wrapper on the new installer | +| #213 known issue: busy ANE costs 44% of 9B decode (13-inch M1) | n/a | Directly relevant if ANE voice is added; not measured on the Pro | Measure before any ANE+LLM use | + +Net: no commit is a clear win for the 4B default. The only plausible gain is the 9B decode route (#231), which is hardware-name gated and unverified on T6000. + +## ANE side (your two links) + +- **omarchy-ane**: lists M1 Pro (T6000) "on by default", with one passing smoke row. This host already has the driver bound: `/dev/accel/accel0`, driver `ane`, `apple,t6000-ane` on `7.1.12-elementary-aurora20-enh1`. Its source is not recorded in our docs (in-tree aurora driver from aurora-silicon/linux #155 vs DKMS); check that first. The DKMS package is Arch/pacman-oriented; for Ubuntu base, use the in-tree driver or build DKMS manually. No need to replace the working driver. +- **mil-hwx-compiler**: a MIL to ANE-package compiler (H13 = M1 family, targets our chip). It is a build-time input to omarchy-mlx's ANE bundles (`ane-compiler.lock`), not a runtime dependency for chat. Relevant only if we want our own ANE programs or Parakeet encoder bundles; the omarchy-mlx aarch64 wheel already ships the pinned Parakeet bundles. H13 support is fixed-shape fp16 with a limited op set. LICENSE file present (verify terms before redistributing). +- Existing doc `ANE-PREFLIGHT.md` already covers part of this; the ANE+GPU contention risk (#213) is the main gate. + +## Proposed bump procedure (not executed) + +1. Record the exact device string (`vulkaninfo --summary`) and the current driver source for `ane`. +2. Build a candidate **beside** the running runtime (separate prefix, `MSERIES_AI_SOURCES` copy), using main `61495cb` or a release if one appears. Preserve the v0.7.33 runtime; do not touch desktop Mesa. +3. Read the `apply-mlx-lm-patches.sh` diff; confirm our private patch (prefill 256, thinking off, rsqrt/RMSNorm numerics) still applies. +4. Run upstream C++ tests (`omarchy_reduce_row_tests`, `omarchy_gdn_gqa_bf16a`) on the Pro. +5. A/B with workers stopped, one change at a time, medians over repetitions: Qwen3.5-4B greedy token identity with `ROW_REDUCE` on/off; Qwen3.5-9B decode with `GDN_PF_GQA` on/off; record load, temperature and fan. +6. Promote only on token-identical output and no reserve/thermal regression. Otherwise keep v0.7.33 and set `MLX_OMARCHY_ROW_REDUCE=0` if the next release defaults it on. +7. Update `local-ai/install.sh` pin, `UPSTREAM-FEATURE-COMPARISON.md`, `OS9-HANDOFF.md` and `ROADMAP.md` only after the measurements. + +## Not verified + +Device string of this GPU; whether the 4B model reaches the row-reduce or GDN routes; whether the ANE driver here is in-tree or DKMS; license terms for `mil-hwx-compiler` binaries; any benefit on this host (all figures above are upstream's on M1 Max/M2 Max, one boot each). From 864f07f8e9401a3b9f45a824fdce58d3af6275ae Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:17:13 -0400 Subject: [PATCH 088/798] local-ai: bump plan records M1 Pro device string (G13S) and in-tree ANE driver Co-Authored-By: Claude Sonnet 5.5 --- mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md b/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md index 6ba6e1531..621edbaf3 100644 --- a/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md +++ b/mseries/local-ai/OMARCHY-MLX-BUMP-PLAN-2026-10-19.md @@ -15,7 +15,7 @@ Review only. Nothing was installed, built or pinned. Sources were read from shal | Change | Default | M1 Pro relevance | Verdict | |---|---|---|---| -| #231 GDN decode: untiled prefetch + native GQA | **On, only when the Vulkan device name contains G14C or G13C** | Needs f32 A_log and Hk != Hv (Qwen3.5-9B). Upstream measured about 8% on M1 Max/M2 Max. Our M1 Pro's device string is not recorded; it may be G13S/G13C-class. Our 4B route eligibility is unchecked. | Test, 9B only; check `vulkaninfo` device name first | +| #231 GDN decode: untiled prefetch + native GQA | **On, only when the Vulkan device name contains G14C or G13C** | Needs f32 A_log and Hk != Hv (Qwen3.5-9B). Upstream measured about 8% on M1 Max/M2 Max. Our M1 Pro's device string is not recorded; it may be G13S/G13C-class. Our 4B route eligibility is unchecked. | **Does not apply by default here**: the device is `Apple M1 Pro (G13S C0)` (measured 2026-10-19), which contains neither G14C nor G13C. Only the explicit `MLX_OMARCHY_GDN_PF_GQA=1` opt-in could reach it; test that on 9B only | | #229/#225 GDN native GQA opt-in flags | Off | Superseded by #231 for f32; #225 is for bf16 A_log models | Skip unless a model needs it | | #226 Row-reduce Sum (workgroup per row) | **On** (`MLX_OMARCHY_ROW_REDUCE=0` disables) | Changes summation order for float sums over rows of 256 or more outside fused kernels. Upstream says Qwen3.5-9B and 27B profiles contain no Reduce dispatch. Qwen3.5-4B is not profiled. One Muse NLL check fell outside the registered bound (Apache passage −0.0124 vs 0.01). | Bit-compare 4B greedy output on/off before accepting; keep the kill switch in the profile | | #223 ArgMax/LogSumExp chunked routes | Off | Possible large-vocab sampling help; separate numerics contract | Optional one-at-a-time test | @@ -31,13 +31,13 @@ Net: no commit is a clear win for the 4B default. The only plausible gain is the ## ANE side (your two links) -- **omarchy-ane**: lists M1 Pro (T6000) "on by default", with one passing smoke row. This host already has the driver bound: `/dev/accel/accel0`, driver `ane`, `apple,t6000-ane` on `7.1.12-elementary-aurora20-enh1`. Its source is not recorded in our docs (in-tree aurora driver from aurora-silicon/linux #155 vs DKMS); check that first. The DKMS package is Arch/pacman-oriented; for Ubuntu base, use the in-tree driver or build DKMS manually. No need to replace the working driver. +- **omarchy-ane**: lists M1 Pro (T6000) "on by default", with one passing smoke row. This host already has the driver bound: `/dev/accel/accel0`, driver `ane`, `apple,t6000-ane` on `7.1.12-elementary-aurora20-enh1`. Verified it is the in-tree module (`intree: Y`), so the Arch-oriented DKMS package is not needed; do not replace the working driver. - **mil-hwx-compiler**: a MIL to ANE-package compiler (H13 = M1 family, targets our chip). It is a build-time input to omarchy-mlx's ANE bundles (`ane-compiler.lock`), not a runtime dependency for chat. Relevant only if we want our own ANE programs or Parakeet encoder bundles; the omarchy-mlx aarch64 wheel already ships the pinned Parakeet bundles. H13 support is fixed-shape fp16 with a limited op set. LICENSE file present (verify terms before redistributing). - Existing doc `ANE-PREFLIGHT.md` already covers part of this; the ANE+GPU contention risk (#213) is the main gate. ## Proposed bump procedure (not executed) -1. Record the exact device string (`vulkaninfo --summary`) and the current driver source for `ane`. +1. ~~Record device string and ANE driver source~~ Done 2026-10-19, read-only: Vulkan `Apple M1 Pro (G13S C0)`, Honeykrisp, system Mesa 26.0.8-1ubuntu0.3 (the private compute ICD is separate); ANE bound to `ane` (`apple,t6000-ane`), in-tree module `kernel/drivers/accel/ane/ane.ko` (`CONFIG_DRM_ACCEL_ANE=m`, no DKMS). 2. Build a candidate **beside** the running runtime (separate prefix, `MSERIES_AI_SOURCES` copy), using main `61495cb` or a release if one appears. Preserve the v0.7.33 runtime; do not touch desktop Mesa. 3. Read the `apply-mlx-lm-patches.sh` diff; confirm our private patch (prefill 256, thinking off, rsqrt/RMSNorm numerics) still applies. 4. Run upstream C++ tests (`omarchy_reduce_row_tests`, `omarchy_gdn_gqa_bf16a`) on the Pro. @@ -47,4 +47,4 @@ Net: no commit is a clear win for the 4B default. The only plausible gain is the ## Not verified -Device string of this GPU; whether the 4B model reaches the row-reduce or GDN routes; whether the ANE driver here is in-tree or DKMS; license terms for `mil-hwx-compiler` binaries; any benefit on this host (all figures above are upstream's on M1 Max/M2 Max, one boot each). +Whether the 4B model reaches the row-reduce or GDN routes; license terms for `mil-hwx-compiler` binaries; any benefit on this host (all figures above are upstream's on M1 Max/M2 Max, one boot each). From c765cc9f5ae24292876bc77ee9c27a9273b47784 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:17:16 -0400 Subject: [PATCH 089/798] Apply Pantheon monitor configurations --- mseries/OS9-HANDOFF.md | 24 ++ mseries/ROADMAP.md | 8 +- .../EXPERIENCE-REQUIREMENTS.md | 14 +- rust/pantheon-compositor/README.md | 26 +- rust/pantheon-compositor/THIRD-PARTY.md | 11 +- .../pantheon-compositor/src/display_config.rs | 250 +++++++++++++++++- rust/pantheon-compositor/src/native.rs | 209 ++++++++++++++- rust/pantheon-compositor/src/state.rs | 198 +++++++++++++- rust/pantheon-compositor/src/winit.rs | 42 ++- 9 files changed, 721 insertions(+), 61 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c395d1d13..db61dae9f 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5603,3 +5603,27 @@ is not implemented, mode changes and layout application remain unavailable, and no live display configuration was changed. Next: validate connector mode IDs against Pantheon Settings, then implement verified apply/rollback on the native KMS outputs. + +### Apply monitor configuration on the compositor thread — 2026-10-19 + +Implemented `ApplyMonitorsConfig` on the DisplayConfig compatibility service. +Requests validate the state serial, require each connected output exactly once, +check mode IDs and scale/transform values, and reject unsupported mirroring, +output disabling and per-monitor properties. Verify-only requests do not alter +state. Temporary and persistent requests update output positions, scales, +transforms and mode state; the nested Winit backend rejects mode changes. +Native mode changes use Smithay's shared DRM output manager with rendered +contents supplied for every CRTC, and roll back modes already switched if a +later request fails. Persistent requests are written atomically to +`$XDG_CONFIG_HOME/pantheon-rust-compositor/monitors.conf` (or the user's +`~/.config`) and native startup restores saved mode/layout values. The +`MonitorsChanged` update is batched for a complete apply operation. + +Local verification: `cargo check -p pantheon-compositor --bin +pantheon-compositor` and the same command with `--features native-drm` both +pass; touched Rust files pass `rustfmt --check`; `git diff --check` passes. +No native session, mode switch, or live output reconfiguration was attempted, +so DRM behavior and settings-client round trips remain runtime-unqualified. +Next: validate the D-Bus request against Pantheon Settings in an isolated +nested session, then qualify mode rollback and persistent startup on hardware +without replacing the active compositor until a recovery path is ready. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index c9408d420..dd35534da 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -29,7 +29,13 @@ and attached SHM buffers. It reports the absent `org.gnome.Mutter.DisplayConfig` service. The prototype now returns its read-only `GetCurrentState` query using all tracked output modes and logical positions, and increments the state serial on output add/update/remove before emitting `MonitorsChanged`; Wingpanel connects without the -missing-service error. The compositor now serves Wingpanel's `org.gnome.Shell` +missing-service error. `ApplyMonitorsConfig` validates connector assignments, +mode IDs, scale and transform, applies native DRM mode changes through Smithay's +shared output manager with rollback, and applies temporary layout changes. +Method 2 saves a per-user monitor profile and native startup restores it. +Disabling outputs, mirroring, unsupported properties and nested Winit mode +changes are rejected. Both default and native DRM builds compile; live native +mode changes remain unqualified. The compositor now serves Wingpanel's `org.gnome.Shell` accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during an isolated nested run. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index b7d0c210d..606015fef 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -84,11 +84,17 @@ its anchor/size/offset, reserves exclusive panel geometry, and updates already maximized windows when that work area changes. The last local `cargo check` and `cargo build` pass. Its protocol probe completed under Xvfb over a nested Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested -Wayland session and rendered SHM buffers. The prototype serves the read-only +Wayland session and rendered SHM buffers. The prototype serves the `org.gnome.Mutter.DisplayConfig.GetCurrentState` query Wingpanel uses, and -Wingpanel completes that call. Native `GetCurrentState` now lists the modes -reported by each DRM connector and marks current/preferred modes; it remains -read-only and cannot apply mode, layout, scale or transform changes. It also +Wingpanel completes that call. Native `GetCurrentState` lists connector modes +reported by DRM and marks current/preferred modes. `ApplyMonitorsConfig` now +validates serials, output assignments, mode IDs, scale and transform. Native +mode changes use Smithay's shared output manager with rollback; temporary +layout, scale and transform changes update Wayland outputs. Method 2 saves a +per-user profile and native startup restores saved modes and layout. Disabling +outputs, mirroring, unsupported properties and nested Winit mode changes are +rejected. Both default and native DRM builds compile; runtime changes on the +active M-series display remain unqualified. It also implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered `Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index a6016386f..2b0318512 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -155,17 +155,25 @@ requests. Installed Wingpanel 8.0.4 also connected in an isolated D-Bus/Xvfb run forced to Wayland. Its client trace confirms it bound the shell global, sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated and attached shared-memory buffers. Wingpanel requires -`org.gnome.Mutter.DisplayConfig`; the compositor now serves the read-only -`GetCurrentState` query it calls, with every tracked output's current mode, -logical position, scale, and primary status. Native outputs now list all -connector modes reported by DRM and mark active/preferred entries; the nested -backend reports only its current Winit size. Output add/update/remove changes -increment the state serial and emit `MonitorsChanged`. The service remains -read-only and does not apply monitor modes or layouts. The isolated +`org.gnome.Mutter.DisplayConfig`; the compositor serves its `GetCurrentState` +query with every tracked output's current mode, logical position, scale, and +primary status. Native outputs list connector modes reported by DRM and mark +active/preferred entries; the nested backend reports its actual Winit size. +`ApplyMonitorsConfig` validates serials, connector assignments, mode IDs, scale +and transform. Native DRM mode changes use Smithay's shared output manager and +roll back earlier changes if a later mode fails. Temporary layout, scale and +transform changes update the Wayland outputs. Method 2 saves a per-user profile +at `$XDG_CONFIG_HOME/pantheon-rust-compositor/monitors.conf` (or +`~/.config/pantheon-rust-compositor/monitors.conf`); native startup restores +saved modes and layout. Output disabling, mirroring, unsupported properties and +nested Winit mode changes are rejected. Output and configuration updates +increment the serial and emit `MonitorsChanged`. Default and native DRM builds +compile, but no native session or live monitor change has been run from this +development session. The isolated Wingpanel run no longer reports missing DisplayConfig or Shell keybinding services. Pressing `Super+N` in the nested window produced the registered `AcceleratorActivated` signal. The sound indicator's media-key registrations also completed without its prior D-Bus error; physical media-key activation has not been checked. This is evidence of panel compatibility at the -protocol/rendering path, not full session qualification. Applying monitor -modes, Dock behavior and M-series DRM/KMS remain unqualified. +protocol/rendering path, not full session qualification. Monitor configuration +runtime, Dock behavior and M-series DRM/KMS remain unqualified. diff --git a/rust/pantheon-compositor/THIRD-PARTY.md b/rust/pantheon-compositor/THIRD-PARTY.md index 2d55c24a9..817e6cc2d 100644 --- a/rust/pantheon-compositor/THIRD-PARTY.md +++ b/rust/pantheon-compositor/THIRD-PARTY.md @@ -11,13 +11,14 @@ LGPL-2.1-or-later. This copy advertises protocol version 2 and includes the M-series `set_offset` request already used by this repository's Gala/Dock patches. The source XML retains its copyright and SPDX notice. -The read-only `org.gnome.Mutter.DisplayConfig.GetCurrentState` compatibility -reply follows the public D-Bus contract in GNOME Mutter's +The `org.gnome.Mutter.DisplayConfig` compatibility interface follows the public +D-Bus contract in GNOME Mutter's `data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml` (). -The Rust implementation is a fresh subset for Wingpanel's current read-only -layout query; it does not copy Mutter implementation code or provide mode -application. +The Rust implementation is original code for the current output inventory and +the supported subset of `ApplyMonitorsConfig`; it does not copy Mutter +implementation code. Mirroring, output disabling, and other unsupported +configuration properties return errors. The `org.gnome.Shell` accelerator compatibility surface follows the client interfaces in elementary/Wingpanel `lib/KeybindingHelper.vala` at diff --git a/rust/pantheon-compositor/src/display_config.rs b/rust/pantheon-compositor/src/display_config.rs index e4248295d..8bb726ea2 100644 --- a/rust/pantheon-compositor/src/display_config.rs +++ b/rust/pantheon-compositor/src/display_config.rs @@ -1,5 +1,7 @@ use std::{ collections::HashMap, + fs, io, + path::PathBuf, sync::{Arc, Mutex, mpsc}, thread, time::Duration, @@ -22,6 +24,7 @@ pub struct OutputSnapshot { pub refresh_millihz: i32, pub modes: Vec, pub scale: f64, + pub transform: u32, pub x: i32, pub y: i32, pub primary: bool, @@ -35,6 +38,135 @@ pub struct OutputModeSnapshot { pub preferred: bool, } +pub fn transform_to_dbus(transform: smithay::utils::Transform) -> u32 { + match transform { + smithay::utils::Transform::Normal => 0, + smithay::utils::Transform::_90 => 1, + smithay::utils::Transform::_180 => 2, + smithay::utils::Transform::_270 => 3, + smithay::utils::Transform::Flipped => 4, + smithay::utils::Transform::Flipped90 => 5, + smithay::utils::Transform::Flipped180 => 6, + smithay::utils::Transform::Flipped270 => 7, + } +} + +#[derive(Clone, Debug)] +pub struct SavedMonitorConfiguration { + pub connector: String, + pub mode: String, + pub x: i32, + pub y: i32, + pub scale: f64, + pub transform: u32, + pub primary: bool, +} + +fn monitor_configuration_path() -> Option { + std::env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config"))) + .map(|base| base.join("pantheon-rust-compositor/monitors.conf")) +} + +pub fn load_saved_monitor_configurations() -> HashMap { + let Some(path) = monitor_configuration_path() else { + return HashMap::new(); + }; + let Ok(contents) = fs::read_to_string(path) else { + return HashMap::new(); + }; + let configurations = contents + .lines() + .filter_map(|line| { + let mut fields = line.split('\t'); + let [ + Some(connector), + Some(mode), + Some(x), + Some(y), + Some(scale), + Some(transform), + Some(primary), + ] = [ + fields.next(), + fields.next(), + fields.next(), + fields.next(), + fields.next(), + fields.next(), + fields.next(), + ] + else { + return None; + }; + let scale = scale.parse::().ok()?; + let transform = transform.parse::().ok()?; + if !scale.is_finite() || !(0.5..=4.0).contains(&scale) || transform > 7 { + return None; + } + Some(SavedMonitorConfiguration { + connector: connector.to_string(), + mode: mode.to_string(), + x: x.parse().ok()?, + y: y.parse().ok()?, + scale, + transform, + primary: primary == "1", + }) + }) + .map(|configuration| (configuration.connector.clone(), configuration)) + .collect::>(); + if configurations + .values() + .filter(|configuration| configuration.primary) + .count() + == 1 + { + configurations + } else { + HashMap::new() + } +} + +pub fn save_monitor_configurations(configurations: &[SavedMonitorConfiguration]) -> io::Result<()> { + let path = monitor_configuration_path().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "no user configuration directory is available", + ) + })?; + let parent = path + .parent() + .expect("monitor configuration path has a parent"); + fs::create_dir_all(parent)?; + let temporary_path = path.with_extension("conf.tmp"); + let mut contents = String::new(); + for configuration in configurations { + if configuration.connector.contains(['\t', '\n']) + || configuration.mode.contains(['\t', '\n']) + || !configuration.scale.is_finite() + { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid monitor configuration value", + )); + } + contents.push_str(&format!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\n", + configuration.connector, + configuration.mode, + configuration.x, + configuration.y, + configuration.scale, + configuration.transform, + u8::from(configuration.primary), + )); + } + fs::write(&temporary_path, contents)?; + fs::rename(temporary_path, path) +} + impl OutputModeSnapshot { pub fn id(&self) -> String { format!( @@ -51,25 +183,43 @@ pub struct DisplayConfigHandle { updates: mpsc::Sender, } +pub struct ApplyRequest { + pub serial: u32, + pub method: u32, + pub logical_monitors: Vec, + pub response: mpsc::SyncSender>, +} + enum DisplayConfigUpdate { Upsert(OutputSnapshot), + UpsertMany(Vec), Remove(String), } impl DisplayConfigHandle { - pub fn start() -> Self { + pub fn start() -> (Self, mpsc::Receiver) { let (updates, receiver) = mpsc::channel(); + let (apply_requests, apply_receiver) = mpsc::channel(); + let apply_requests_for_service = apply_requests.clone(); thread::Builder::new() .name("pantheon-display-config-dbus".into()) - .spawn(move || serve_display_config(receiver)) + .spawn(move || serve_display_config(receiver, apply_requests_for_service)) .expect("failed to start display config D-Bus thread"); - Self { updates } + (Self { updates }, apply_receiver) } pub fn update(&self, snapshot: OutputSnapshot) { let _ = self.updates.send(DisplayConfigUpdate::Upsert(snapshot)); } + pub fn update_many(&self, snapshots: Vec) { + if !snapshots.is_empty() { + let _ = self + .updates + .send(DisplayConfigUpdate::UpsertMany(snapshots)); + } + } + pub fn remove(&self, connector: impl Into) { let _ = self .updates @@ -77,13 +227,18 @@ impl DisplayConfigHandle { } } -struct DisplayConfigServer(Arc)>>); - type Properties = HashMap; type MonitorInfo = (String, String, String, String); type Mode = (String, i32, i32, f64, f64, Vec, Properties); type Monitor = (MonitorInfo, Vec, Properties); type LogicalMonitor = (i32, i32, f64, u32, bool, Vec, Properties); +pub type ApplyMonitor = (String, String, Properties); +pub type ApplyLogicalMonitor = (i32, i32, f64, u32, bool, Vec, Properties); + +struct DisplayConfigServer( + Arc)>>, + mpsc::Sender, +); #[interface(name = "org.gnome.Mutter.DisplayConfig")] impl DisplayConfigServer { @@ -119,6 +274,11 @@ impl DisplayConfigServer { let modes = modes .into_iter() .map(|mode| { + let mut scales = vec![0.5, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0]; + if !scales.contains(&output.scale) { + scales.push(output.scale); + scales.sort_by(f64::total_cmp); + } let mut mode_properties = Properties::new(); let current = mode.width == output.width && mode.height == output.height @@ -135,7 +295,7 @@ impl DisplayConfigServer { mode.height, f64::from(mode.refresh_millihz) / 1000.0, output.scale, - vec![output.scale], + scales, mode_properties, ) }) @@ -145,7 +305,7 @@ impl DisplayConfigServer { output.x, output.y, output.scale, - 0, + output.transform, output.primary, vec![monitor_info], Properties::new(), @@ -156,11 +316,65 @@ impl DisplayConfigServer { (serial, monitors, logical_monitors, properties) } + fn apply_monitors_config( + &self, + serial: u32, + method: u32, + logical_monitors: Vec, + _properties: Properties, + ) -> zbus::fdo::Result<()> { + let current_serial = self + .0 + .lock() + .map_err(|_| { + zbus::fdo::Error::Failed("display configuration state is unavailable".into()) + })? + .0; + if serial != current_serial { + return Err(zbus::fdo::Error::Failed(format!( + "stale display configuration serial {serial}; current serial is {current_serial}" + ))); + } + if !matches!(method, 0..=2) { + return Err(zbus::fdo::Error::InvalidArgs(format!( + "unknown ApplyMonitorsConfig method {method}" + ))); + } + + let (response, result) = mpsc::sync_channel(1); + self.1 + .send(ApplyRequest { + serial, + method, + logical_monitors, + response, + }) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into()))?; + match result.recv_timeout(Duration::from_secs(10)) { + Ok(Ok(())) => Ok(()), + Ok(Err(error)) => Err(zbus::fdo::Error::Failed(error)), + Err(mpsc::RecvTimeoutError::Timeout) => Err(zbus::fdo::Error::Timeout( + "compositor did not apply the display configuration in time".into(), + )), + Err(mpsc::RecvTimeoutError::Disconnected) => Err(zbus::fdo::Error::Failed( + "compositor stopped before applying the display configuration".into(), + )), + } + } + + #[zbus(property)] + fn apply_monitors_config_allowed(&self) -> bool { + true + } + #[zbus(signal)] async fn monitors_changed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; } -fn serve_display_config(receiver: mpsc::Receiver) { +fn serve_display_config( + receiver: mpsc::Receiver, + apply_requests: mpsc::Sender, +) { let connection = match Connection::session() { Ok(connection) => connection, Err(error) => { @@ -173,10 +387,10 @@ fn serve_display_config(receiver: mpsc::Receiver) { // fabricated Winit monitor when native DRM is selected. let initial = HashMap::new(); let snapshot = Arc::new(Mutex::new((1, initial))); - if let Err(error) = connection - .object_server() - .at(OBJECT_PATH, DisplayConfigServer(snapshot.clone())) - { + if let Err(error) = connection.object_server().at( + OBJECT_PATH, + DisplayConfigServer(snapshot.clone(), apply_requests), + ) { tracing::warn!(%error, "cannot register the display config compatibility interface"); return; } @@ -186,7 +400,7 @@ fn serve_display_config(receiver: mpsc::Receiver) { return; } - tracing::info!("serving the read-only Mutter display config query used by Wingpanel"); + tracing::info!("serving Mutter display configuration queries and apply requests"); loop { match receiver.recv_timeout(Duration::from_millis(100)) { Ok(update) => { @@ -200,6 +414,16 @@ fn serve_display_config(receiver: mpsc::Receiver) { } state.1.insert(output.connector.clone(), output); } + DisplayConfigUpdate::UpsertMany(outputs) => { + for output in &outputs { + if output.primary { + for current in state.1.values_mut() { + current.primary = false; + } + } + state.1.insert(output.connector.clone(), output.clone()); + } + } DisplayConfigUpdate::Remove(connector) => { state.1.remove(&connector); } diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 7b68185a7..89139bd08 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -5,7 +5,7 @@ //! device owns the physical connectors. Output allocation and scanout are //! attached by the native output backend built on top of this manager. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::Path; use std::time::{Duration, Instant}; @@ -49,7 +49,9 @@ use smithay::{ use crate::{ PantheonCompositor, cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}, - display_config::{OutputModeSnapshot, OutputSnapshot}, + display_config::{ + OutputModeSnapshot, OutputSnapshot, load_saved_monitor_configurations, transform_to_dbus, + }, }; pub type GbmRendererBackend = GbmGlesBackend; @@ -131,6 +133,126 @@ struct ConnectedOutput { } impl NativeKmsDevice { + /// Apply output modes as one transaction. Smithay coordinates the shared + /// DRM device while retaining rendered content for every CRTC; if a mode + /// fails, previously switched outputs are restored before returning. + pub fn apply_modes( + &mut self, + renderer: &mut NativeRenderer, + state: &PantheonCompositor, + requested: &HashMap, + ) -> Result<(), String> { + let mut gpu_renderer = renderer + .gpus + .single_renderer(&renderer.render_node) + .map_err(|error| format!("could not activate the display renderer: {error:?}"))?; + let mut render_elements = DrmOutputRenderElements::< + NativeMultiRenderer<'_>, + CursorRenderElement>, + >::new(); + for output in &self.outputs { + let elements = if state.session_locked { + render_lock_surface_elements(&mut gpu_renderer, state, &output.output) + } else { + let space_elements = smithay::desktop::space::space_render_elements( + &mut gpu_renderer, + [&state.space], + &output.output, + output.output.current_scale().fractional_scale() as f32, + ) + .map_err(|error| format!("could not prepare current output contents: {error:?}"))?; + let cursor_elements = + render_cursor_elements(&mut gpu_renderer, state, &output.output); + let mut elements = space_elements + .into_iter() + .map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + }) + .collect::>(); + elements.extend(cursor_elements); + elements + }; + render_elements.add_output(&output.crtc, [0.1, 0.1, 0.1, 1.0].into(), elements); + } + + let mut changes = Vec::new(); + for output in &self.outputs { + let Some(mode) = requested.get(&output.output.name()) else { + return Err(format!("no requested mode for {}", output.output.name())); + }; + let target = output + .modes + .iter() + .find(|drm_mode| Mode::from(**drm_mode) == *mode) + .copied() + .ok_or_else(|| { + format!( + "requested mode is not supported by {}", + output.output.name() + ) + })?; + let current = output.output.current_mode().ok_or_else(|| { + format!("{} has no current mode to restore", output.output.name()) + })?; + let original = output + .modes + .iter() + .find(|drm_mode| Mode::from(**drm_mode) == current) + .copied() + .ok_or_else(|| { + format!( + "current mode is missing from {}'s DRM inventory", + output.output.name() + ) + })?; + if target != original { + changes.push((output.crtc, target, original)); + } + } + + let mut applied = Vec::new(); + for (crtc, target, original) in changes { + let result = { + let mut manager = self.manager.lock(); + manager.use_mode(&crtc, target, &mut gpu_renderer, &render_elements) + }; + if let Err(error) = result { + let mut rollback_errors = Vec::new(); + for (rollback_crtc, rollback_mode) in applied.into_iter().rev() { + let rollback_result = { + let mut manager = self.manager.lock(); + manager.use_mode( + &rollback_crtc, + rollback_mode, + &mut gpu_renderer, + &render_elements, + ) + }; + if let Err(rollback_error) = rollback_result { + rollback_errors.push(format!("{rollback_error:?}")); + } + } + if rollback_errors.is_empty() { + return Err(format!("DRM rejected a requested display mode: {error:?}")); + } + return Err(format!( + "DRM rejected a display mode ({error:?}) and rollback also failed: {}", + rollback_errors.join("; ") + )); + } + applied.push((crtc, original)); + } + Ok(()) + } + /// Open the first usable KMS device on the active libseat and initialize /// every connected connector with a compatible, unclaimed CRTC. Each /// output receives its own scanout surface while sharing the device, @@ -179,7 +301,7 @@ impl NativeKmsDevice { &mut gpu_renderer, [&state.space], &output.output, - 1.0, + output.output.current_scale().fractional_scale() as f32, )?; let cursor_elements = render_cursor_elements(&mut gpu_renderer, state, &output.output); @@ -329,6 +451,12 @@ impl NativeKmsDevice { .unwrap_or(info.modes()[0]); let mode = Mode::from(drm_mode); let available_modes = info.modes().to_vec(); + let preferred_mode = available_modes + .iter() + .find(|candidate| candidate.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .map(Mode::from) + .unwrap_or(mode); let name = format!("{}-{}", info.interface().as_str(), info.interface_id()); let (physical_width, physical_height) = info.size().unwrap_or((0, 0)); let output = Output::new( @@ -341,7 +469,10 @@ impl NativeKmsDevice { serial_number: "Unknown".into(), }, ); - output.set_preferred(mode); + output.set_preferred(preferred_mode); + for available_mode in &available_modes { + output.add_mode(Mode::from(*available_mode)); + } output.change_current_state(Some(mode), None, None, Some((0, 0).into())); let mut gpu_renderer = renderer.gpus.single_renderer(&renderer.render_node)?; @@ -405,6 +536,7 @@ impl NativeKmsDevice { refresh_millihz: mode.refresh, modes: output_mode_snapshots(&output.modes), scale: 1.0, + transform: 0, x, y: 0, primary: index == 0, @@ -499,6 +631,7 @@ impl NativeKmsDevice { let (drm, notifier) = DrmDevice::new(drm_fd.clone(), true)?; let gbm = GbmDevice::new(drm_fd)?; let resources = drm.resource_handles()?; + let saved_configurations = load_saved_monitor_configurations(); let mut discovered = Vec::new(); for handle in resources.connectors() { @@ -536,12 +669,32 @@ impl NativeKmsDevice { continue; }; claimed_crtcs.insert(crtc); - let mode = connector + let preferred_mode = connector .modes() .iter() .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) .copied() .unwrap_or(connector.modes()[0]); + let name = format!( + "{}-{}", + connector.interface().as_str(), + connector.interface_id() + ); + let mode = saved_configurations + .get(&name) + .and_then(|saved| { + connector.modes().iter().find(|mode| { + let mode = Mode::from(**mode); + format!( + "{}x{}@{:.3}", + mode.size.w, + mode.size.h, + f64::from(mode.refresh) / 1000.0 + ) == saved.mode + }) + }) + .copied() + .unwrap_or(preferred_mode); connected.push(ConnectedOutput { connector, crtc, @@ -581,7 +734,7 @@ impl NativeKmsDevice { CursorRenderElement>, >::default(); let mut outputs = Vec::with_capacity(connected.len()); - let mut x = 0; + let mut next_x = 0; for selected in connected { let mode = Mode::from(selected.mode); let available_modes = selected.connector.modes().to_vec(); @@ -591,6 +744,21 @@ impl NativeKmsDevice { selected.connector.interface().as_str(), selected.connector.interface_id() ); + let saved = saved_configurations.get(&name); + let x = saved.map_or(next_x, |saved| saved.x); + let y = saved.map_or(0, |saved| saved.y); + let scale = saved.map_or(1.0, |saved| saved.scale); + let primary = saved.map_or(outputs.is_empty(), |saved| saved.primary); + let transform = match saved.map_or(0, |saved| saved.transform) { + 1 => smithay::utils::Transform::_90, + 2 => smithay::utils::Transform::_180, + 3 => smithay::utils::Transform::_270, + 4 => smithay::utils::Transform::Flipped, + 5 => smithay::utils::Transform::Flipped90, + 6 => smithay::utils::Transform::Flipped180, + 7 => smithay::utils::Transform::Flipped270, + _ => smithay::utils::Transform::Normal, + }; let (physical_width, physical_height) = selected.connector.size().unwrap_or((0, 0)); let output = Output::new( name.clone(), @@ -602,8 +770,22 @@ impl NativeKmsDevice { serial_number: "Unknown".into(), }, ); - output.set_preferred(mode); - output.change_current_state(Some(mode), None, None, Some((x, 0).into())); + let preferred_mode = available_modes + .iter() + .find(|candidate| candidate.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .map(Mode::from) + .unwrap_or(mode); + output.set_preferred(preferred_mode); + for available_mode in &available_modes { + output.add_mode(Mode::from(*available_mode)); + } + output.change_current_state( + Some(mode), + Some(transform), + Some(smithay::output::Scale::Fractional(scale)), + Some((x, y).into()), + ); let drm_output = match manager.lock().initialize_output( selected.crtc, @@ -622,7 +804,7 @@ impl NativeKmsDevice { }; let global_id = output.create_global::(&state.display_handle); - state.space.map_output(&output, (x, 0)); + state.space.map_output(&output, (x, y)); state.display_config.update(OutputSnapshot { connector: name.clone(), vendor: "Apple".into(), @@ -632,12 +814,13 @@ impl NativeKmsDevice { height: mode.size.h, refresh_millihz: mode.refresh, modes: mode_snapshots, - scale: 1.0, + scale, + transform: transform_to_dbus(transform), x, - y: 0, - primary: outputs.is_empty(), + y, + primary, }); - x += mode.size.w; + next_x = next_x.max(x + (mode.size.w as f64 / scale).round() as i32); outputs.push(NativeKmsOutput { connector: selected.connector.handle(), crtc: selected.crtc, diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 303fcc28f..6932fa39b 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -1,12 +1,16 @@ use std::collections::{HashMap, HashSet}; -use std::{ffi::OsString, sync::Arc}; +use std::{ffi::OsString, sync::Arc, time::Duration}; use smithay::{ backend::renderer::element::memory::MemoryRenderBuffer, desktop::{PopupManager, Space, Window, WindowSurfaceType}, input::{Seat, SeatState, pointer::CursorImageStatus}, reexports::{ - calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, + calloop::{ + EventLoop, Interest, LoopSignal, Mode, PostAction, + generic::Generic, + timer::{TimeoutAction, Timer}, + }, wayland_server::{ Display, DisplayHandle, backend::{ClientData, ClientId, DisconnectReason}, @@ -26,7 +30,10 @@ use smithay::{ }; use crate::cursor::default_cursor_buffer; -use crate::display_config::DisplayConfigHandle; +use crate::display_config::{ + ApplyRequest, DisplayConfigHandle, OutputModeSnapshot, OutputSnapshot, + SavedMonitorConfiguration, save_monitor_configurations, transform_to_dbus, +}; #[cfg(feature = "native-drm")] use crate::native::NativeBackend; use crate::pantheon_shell::{PanelState, PantheonShellState}; @@ -109,7 +116,17 @@ impl PantheonCompositor { // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); let pantheon_shell_state = PantheonShellState::new::(&dh); - let display_config = DisplayConfigHandle::start(); + let (display_config, apply_requests) = DisplayConfigHandle::start(); + event_loop.handle().insert_source( + Timer::from_duration(Duration::from_millis(25)), + move |_, _, state| { + while let Ok(request) = apply_requests.try_recv() { + let result = state.apply_display_configuration(&request); + let _ = request.response.send(result); + } + TimeoutAction::ToDuration(Duration::from_millis(25)) + }, + )?; // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. @@ -183,6 +200,179 @@ impl PantheonCompositor { } } + fn apply_display_configuration(&mut self, request: &ApplyRequest) -> Result<(), String> { + if request.logical_monitors.is_empty() { + return Err("at least one logical monitor is required".into()); + } + if self.session_locked { + return Err("monitor configuration cannot change while the session is locked".into()); + } + + let outputs = self.space.outputs().cloned().collect::>(); + if request.logical_monitors.len() != outputs.len() { + return Err("disabling outputs and monitor mirroring are not supported yet".into()); + } + + let mut configurations = HashMap::new(); + let mut saved_configurations = Vec::new(); + let mut primary_count = 0; + for (x, y, scale, transform, primary, monitors, properties) in &request.logical_monitors { + if !scale.is_finite() || !(0.5..=4.0).contains(scale) { + return Err("monitor scale must be between 0.5 and 4.0".into()); + } + if !properties.is_empty() { + return Err("logical monitor properties are not supported".into()); + } + let transform_value = *transform; + let transform = match transform_value { + 0 => smithay::utils::Transform::Normal, + 1 => smithay::utils::Transform::_90, + 2 => smithay::utils::Transform::_180, + 3 => smithay::utils::Transform::_270, + 4 => smithay::utils::Transform::Flipped, + 5 => smithay::utils::Transform::Flipped90, + 6 => smithay::utils::Transform::Flipped180, + 7 => smithay::utils::Transform::Flipped270, + _ => return Err(format!("unknown monitor transform {transform_value}")), + }; + if monitors.len() != 1 { + return Err("mirrored monitor groups are not supported yet".into()); + } + let (connector, mode_id, monitor_properties) = &monitors[0]; + if !monitor_properties.is_empty() { + return Err("monitor configuration properties are not supported".into()); + } + let Some(output) = outputs.iter().find(|output| output.name() == *connector) else { + return Err(format!("monitor {connector} is no longer connected")); + }; + let Some(mode) = output.modes().into_iter().find(|mode| { + format!( + "{}x{}@{:.3}", + mode.size.w, + mode.size.h, + f64::from(mode.refresh) / 1000.0 + ) == *mode_id + }) else { + return Err(format!("mode {mode_id} is unavailable on {connector}")); + }; + if configurations + .insert( + connector.clone(), + (*x, *y, *scale, transform, *primary, mode), + ) + .is_some() + { + return Err(format!("monitor {connector} appears more than once")); + } + if *primary { + primary_count += 1; + } + saved_configurations.push(SavedMonitorConfiguration { + connector: connector.clone(), + mode: mode_id.clone(), + x: *x, + y: *y, + scale: *scale, + transform: transform_value, + primary: *primary, + }); + } + if configurations.len() != outputs.len() { + return Err("every connected output must appear exactly once".into()); + } + if primary_count != 1 { + return Err("exactly one primary monitor is required".into()); + } + + if request.method == 0 { + return Ok(()); + } + + let requested_modes = configurations + .iter() + .map(|(name, (_, _, _, _, _, mode))| (name.clone(), *mode)) + .collect::>(); + if outputs + .iter() + .any(|output| output.current_mode() != requested_modes.get(&output.name()).copied()) + { + #[cfg(feature = "native-drm")] + { + let Some(mut backend) = self.native_backend.take() else { + return Err("the nested backend cannot change display modes".into()); + }; + let result = backend + .kms + .apply_modes(&mut backend.renderer, self, &requested_modes); + self.native_backend = Some(backend); + result?; + } + #[cfg(not(feature = "native-drm"))] + return Err("this build cannot change native display modes".into()); + } + + let mut snapshots = Vec::with_capacity(outputs.len()); + for output in outputs { + let (x, y, scale, transform, primary, mode) = configurations[&output.name()]; + output.change_current_state( + Some(mode), + Some(transform), + Some(smithay::output::Scale::Fractional(scale)), + Some((x, y).into()), + ); + self.space.map_output(&output, (x, y)); + let physical = output.physical_properties(); + snapshots.push(OutputSnapshot { + connector: output.name(), + vendor: physical.make, + product: physical.model, + serial: physical.serial_number, + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + modes: output + .modes() + .into_iter() + .map(|mode| OutputModeSnapshot { + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + preferred: output.preferred_mode() == Some(mode), + }) + .collect(), + scale, + transform: transform_to_dbus(transform), + x, + y, + primary, + }); + } + self.display_config.update_many(snapshots); + + self.last_work_area = None; + let panels = self + .space + .elements() + .filter_map(|window| { + let surface = window.toplevel()?.wl_surface(); + self.is_panel_surface(surface).then(|| surface.clone()) + }) + .collect::>(); + for panel in panels { + self.update_panel(&panel, |_| {}); + } + self.reconfigure_managed_windows(); + self.space.refresh(); + let _ = self.display_handle.flush_clients(); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + if request.method == 2 { + save_monitor_configurations(&saved_configurations) + .map_err(|error| format!("could not save monitor configuration: {error}"))?; + } + Ok(()) + } + fn init_wayland_listener( display: Display, event_loop: &mut EventLoop, diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 4c51f392c..9e46cbf32 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -12,7 +12,9 @@ use smithay::{ use crate::PantheonCompositor; use crate::cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}; -use crate::display_config::{OutputModeSnapshot, OutputSnapshot}; +use crate::display_config::{ + OutputModeSnapshot, OutputSnapshot, load_saved_monitor_configurations, transform_to_dbus, +}; pub fn init_winit( event_loop: &mut EventLoop, @@ -36,15 +38,29 @@ pub fn init_winit( }, ); let _global = output.create_global::(&state.display_handle); + let saved = load_saved_monitor_configurations().remove("winit"); + let x = saved.as_ref().map_or(0, |saved| saved.x); + let y = saved.as_ref().map_or(0, |saved| saved.y); + let scale = saved.as_ref().map_or(1.0, |saved| saved.scale); + let transform = match saved.as_ref().map_or(7, |saved| saved.transform) { + 0 => Transform::Normal, + 1 => Transform::_90, + 2 => Transform::_180, + 3 => Transform::_270, + 4 => Transform::Flipped, + 5 => Transform::Flipped90, + 6 => Transform::Flipped180, + _ => Transform::Flipped270, + }; output.change_current_state( Some(mode), - Some(Transform::Flipped180), - None, - Some((0, 0).into()), + Some(transform), + Some(smithay::output::Scale::Fractional(scale)), + Some((x, y).into()), ); output.set_preferred(mode); - state.space.map_output(&output, (0, 0)); + state.space.map_output(&output, (x, y)); state.display_config.update(OutputSnapshot { connector: "winit".into(), vendor: "Smithay".into(), @@ -59,9 +75,10 @@ pub fn init_winit( refresh_millihz: mode.refresh, preferred: true, }], - scale: 1.0, - x: 0, - y: 0, + scale: output.current_scale().fractional_scale(), + transform: transform_to_dbus(output.current_transform()), + x: output.current_location().x, + y: output.current_location().y, primary: true, }); @@ -98,9 +115,10 @@ pub fn init_winit( refresh_millihz: mode.refresh, preferred: true, }], - scale: 1.0, - x: 0, - y: 0, + scale: output.current_scale().fractional_scale(), + transform: transform_to_dbus(output.current_transform()), + x: output.current_location().x, + y: output.current_location().y, primary: true, }); } @@ -134,7 +152,7 @@ pub fn init_winit( &output, renderer, &mut framebuffer, - 1.0, + output.current_scale().fractional_scale() as f32, 0, [&state.space], &cursor_elements, From 13f5885addf6db75cc0e79d40b57e8706894e9d5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:18:12 -0400 Subject: [PATCH 090/798] Handoff: diagnose silent installed elementary boot on QEMU (Fairydust kernel lacks PCI_HOST_GENERIC and SYSFB) Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c43369824..90070a325 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6183,3 +6183,21 @@ installer's root partition is LVM so the disk could not be inspected without roo disk with a console on serial (kernel args `console=ttyAMA0 earlycon`) or boot the live ISO with the disk attached and read `/boot/grub/grub.cfg` and the initramfs; kernel work belongs to DeepSeek per the coordination rule. Machine Lab: ARM64 now listed on every machine, plus a Connect section (display, console, SSH terminal). + +## 2026-10-19: why the installed elementary OS is silent on QEMU (diagnosed, not fixed) + +Booted the same ISO live with the installed disk attached and read the guest's files (read-only mount, no kernel or +image changes). The installed system has two kernels: generic `7.0.0-38-generic` (what the live ISO uses, boots fine) and +`7.1.13-elementary-fairydust-usb4`. GRUB's default entry picks the newest, the Fairydust kernel, with a hidden menu +(`GRUB_TIMEOUT_STYLE=hidden`, timeout 0) and `quiet splash`. The Fairydust config has: + +- `# CONFIG_PCI_HOST_GENERIC is not set`: no driver for QEMU virt's generic PCIe host, so no PCI devices at all + (virtio-pci is a module, `CONFIG_VIRTIO_PCI=m`, and cannot find a bus; the root LV on the virtio disk is unreachable). +- `# CONFIG_SYSFB_SIMPLEFB is not set` and `# CONFIG_FB_EFI is not set`: no EFI framebuffer, so no display either. +- `CONFIG_DRM_VIRTIO_GPU=m`: not usable without the PCI bus. + +The generic 7.0.0-38 kernel has `PCI_HOST_GENERIC=y`, `SYSFB_SIMPLEFB=y`, `VIRTIO_PCI=y`. Root is ext4 on LVM (`data-root`), +swap is `/dev/mapper/cryptswap` in fstab. Result: it is the Apple-only Fairydust config, not a broken install. +Options (kernel work belongs to DeepSeek): add `PCI_HOST_GENERIC=y` and `SYSFB_SIMPLEFB=y` (or `FB_EFI`) to the Fairydust +config so it also boots on QEMU virt, or make the VM install default to the generic kernel (GRUB_DEFAULT set to the +7.0.0-38 entry). Not tried: either fix, because changing the guest's boot config was outside this task's scope. From dacbe5dd31134535f873ed778f7f9bde40396ed7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:19:09 -0400 Subject: [PATCH 091/798] local-ai: draft issue for omarchy-mlx bump and image-gen evaluation Co-Authored-By: Claude Sonnet 5.5 --- ...UE-DRAFT-omarchy-mlx-bump-and-image-gen.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 mseries/local-ai/ISSUE-DRAFT-omarchy-mlx-bump-and-image-gen.md diff --git a/mseries/local-ai/ISSUE-DRAFT-omarchy-mlx-bump-and-image-gen.md b/mseries/local-ai/ISSUE-DRAFT-omarchy-mlx-bump-and-image-gen.md new file mode 100644 index 000000000..1127f631e --- /dev/null +++ b/mseries/local-ai/ISSUE-DRAFT-omarchy-mlx-bump-and-image-gen.md @@ -0,0 +1,39 @@ +# Desired: evaluate omarchy-mlx main and a Draw Things-style local image app on the M1 Pro + +Draft, not posted. Companion to [OMARCHY-MLX-BUMP-PLAN-2026-10-19.md](OMARCHY-MLX-BUMP-PLAN-2026-10-19.md). + +## Summary + +Qualify (or reject) omarchy-mlx `main` against our v0.7.33 pin on the 32 GB M1 Pro, and decide whether a local image-generation app comparable to Draw Things is feasible on this stack. + +## Why + +- omarchy-mlx main is 36 commits past v0.7.33 with no release. Several routes changed defaults (row-reduce Sum on; GDN decode route gated to G14C/G13C devices). +- This host reports `Apple M1 Pro (G13S C0)`, so the new GDN default does not apply here. Any gain is unmeasured on this chip. +- Owner interest in an app like Draw Things (local, GPU-accelerated image generation with a native UI). + +## Draw Things reference (what is known) + +- Draw Things is an Apple-platform app (iPhone, iPad, Mac). No native Linux desktop app was found. +- The project publishes a Docker image, `drawthingsai/draw-things-grpc-server-cli`, a CUDA gRPC server for the app. Listed builds are linux/amd64 only, so it does not run on this ARM64 Apple GPU. Official docs and the GitHub repo were not read; treat this as unconfirmed until they are. +- So a Draw Things-like experience here means a different stack: an MLX image model (diffusion) on the Honeykrisp Vulkan backend, behind a simple local UI. Feasibility, model licenses, and memory fit (6 GiB desktop reserve plus the resident assistant) are unknown. + +## Scope + +1. Bump track (from the plan): candidate build beside the running runtime; Qwen3.5-4B token identity with `MLX_OMARCHY_ROW_REDUCE` on/off; Qwen3.5-9B decode with `MLX_OMARCHY_GDN_PF_GQA` on/off; check our private patch and `local-ai/install.sh` wrapper against the new installer layout. +2. Image-gen track: survey MLX diffusion models that run on omarchy-mlx's Vulkan backend; measure memory and time per image; confirm licenses; no resident worker alongside the assistant without admission control. +3. ANE: no ANE+LLM concurrency until the busy-ANE decode loss (-44% on a 13-inch M1, upstream KNOWN-ISSUES) is measured on this Pro. `ane` is already bound in-tree (`apple,t6000-ane`). + +## Acceptance + +- Medians over repetitions, one change at a time, workers stopped, load/temperature/fan recorded. +- Token-identical greedy output or a documented difference; no reserve or thermal regression. +- Handoff, roadmap and pin docs updated only after measurements. + +## Out of scope + +Kernel, bootloader, governor, desktop Mesa and audio changes. No new always-resident model. + +## Not verified + +Whether the 4B model reaches the changed routes; any image model's fit or speed here; Draw Things' current Linux status beyond the Docker listing. From 2151eac0b1d8e0d484e9609612baa6df5349673f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:20:19 -0400 Subject: [PATCH 092/798] local-ai: receipt for omarchy-ane smoke on T6000 and 0.32-only patch dry-run Co-Authored-By: Claude Sonnet 5.5 --- .../2026-10-19-ane-smoke-and-patch-dryrun.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 mseries/local-ai/receipts/2026-10-19-ane-smoke-and-patch-dryrun.md diff --git a/mseries/local-ai/receipts/2026-10-19-ane-smoke-and-patch-dryrun.md b/mseries/local-ai/receipts/2026-10-19-ane-smoke-and-patch-dryrun.md new file mode 100644 index 000000000..95e51c329 --- /dev/null +++ b/mseries/local-ai/receipts/2026-10-19-ane-smoke-and-patch-dryrun.md @@ -0,0 +1,17 @@ +# ANE smoke and mlx-lm patch dry-run — 2026-10-19 (M1 Pro, T6000) + +Read-only checks from scratch builds; nothing installed, no driver, kernel or runtime changed. + +## omarchy-ane (main 94cceca), host tools built in scratch with `make all` + +- `omarchy-ane-check`: SoC node `apple,t6000-ane`; `driver_source=intree` (`ane.ko`, `CONFIG_DRM_ACCEL_ANE=m`); loaded and bound to `285c04000.ane`. **One FAIL**: `/dev/accel/accel0` is `crw-rw---- root:render` (mode 0660). The owner is in `render`, so this is a note for other users, not a blocker; not changed (system setting). +- `omarchy-ane-smoke` (shipped H13 add program, 20 calls): **20/20 bit-exact**, min 0.036 ms, median 0.046 ms, errors 0, golden `5ad7eccd…0dd6`. One boot, one run, an add program only: it shows the in-tree driver executes, not that any model runs on the ANE. + +## omarchy-mlx main (61495cb) mlx-lm patches + +- Installed mlx-lm is 0.31.3. `mlx-lm-server-skip-logprob-read.patch` exists only in `patches/mlx-lm-0.32/` and targets `mlx_lm/server.py` of the 0.32 API (`StopSequences`). It does not apply to our installed tree (dry-run on a copy: no file to patch). Not a candidate until we move to the 0.32 line. +- `scripts/patch-mlx-lm-rotkv-maskless.py` is likewise 0.32-series only. + +## Implication + +Nothing from main is portable to the running 0.31.3 stack without a runtime rebuild. The remaining gains need the candidate-build A/B in the bump plan. From 6f225809835c019b981b1c131aaf8c08b8af6d05 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:20:20 -0400 Subject: [PATCH 093/798] Advertise fractional output scaling --- mseries/OS9-HANDOFF.md | 5 +++ mseries/ROADMAP.md | 5 ++- .../EXPERIENCE-REQUIREMENTS.md | 5 ++- mseries/pantheon-rust-compositor/README.md | 11 +++-- .../session/README.md | 4 +- rust/pantheon-compositor/README.md | 6 ++- rust/pantheon-compositor/src/native.rs | 1 + .../pantheon-compositor/src/pantheon_shell.rs | 2 +- rust/pantheon-compositor/src/state.rs | 45 +++++++++++++++++++ rust/pantheon-compositor/src/winit.rs | 1 + 10 files changed, 76 insertions(+), 9 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index db61dae9f..4d5c9f284 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5618,6 +5618,11 @@ later request fails. Persistent requests are written atomically to `$XDG_CONFIG_HOME/pantheon-rust-compositor/monitors.conf` (or the user's `~/.config`) and native startup restores saved mode/layout values. The `MonitorsChanged` update is batched for a complete apply operation. +The compositor now advertises `wp_fractional_scale_v1` and `wp_viewporter`; +new window surfaces receive the preferred scale of their first mapped output, +and output scale preferences refresh during rendering and after configuration +changes. Cross-output movement and fractional rendering still require a nested +client check. Local verification: `cargo check -p pantheon-compositor --bin pantheon-compositor` and the same command with `--features native-drm` both diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index dd35534da..accc4e5ce 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -35,7 +35,10 @@ shared output manager with rollback, and applies temporary layout changes. Method 2 saves a per-user monitor profile and native startup restores it. Disabling outputs, mirroring, unsupported properties and nested Winit mode changes are rejected. Both default and native DRM builds compile; live native -mode changes remain unqualified. The compositor now serves Wingpanel's `org.gnome.Shell` +mode changes remain unqualified. The compositor advertises +`wp_fractional_scale_v1` and `wp_viewporter`, updating window surface scale +preferences from mapped outputs. Moving surfaces across differently scaled +outputs still needs runtime verification. The compositor also serves Wingpanel's `org.gnome.Shell` accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during an isolated nested run. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 606015fef..dd90e08b7 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -94,7 +94,10 @@ layout, scale and transform changes update Wayland outputs. Method 2 saves a per-user profile and native startup restores saved modes and layout. Disabling outputs, mirroring, unsupported properties and nested Winit mode changes are rejected. Both default and native DRM builds compile; runtime changes on the -active M-series display remain unqualified. It also +active M-series display remain unqualified. The compositor advertises +`wp_fractional_scale_v1` and `wp_viewporter`, setting each window surface's +preferred scale from its first mapped output. Runtime checks for moving a +surface between differently scaled outputs remain open. It also implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered `Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 5e4828a3b..847a2cba5 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -22,10 +22,13 @@ required by the M-series/Pantheon session, in independently qualified slices. ## Current prototype -[`rust/pantheon-compositor`](../../rust/pantheon-compositor/README.md) is an -opt-in nested Smithay/Winit prototype. It serves a separate Wayland socket from -an existing desktop and does not own DRM/KMS or change the login session. It is -the first development harness, not a finished compositor or a session swap-in. +[`rust/pantheon-compositor`](../../rust/pantheon-compositor/README.md) has an +opt-in nested Smithay/Winit backend for development and an explicit native DRM +backend for M-series qualification. It supports the Pantheon shell contract, +window basics, monitor configuration and lock bridge, but native display, +session, input and recovery behavior still need on-device qualification. The +staged login-session files are separate from the active Gala session and are +not installed or selected by default. ## Dock HDMI boundary diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index c9f8f9b93..5edc09cdd 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -38,4 +38,6 @@ session must include an `ext-session-lock-v1` client with PAM authentication (the Ubuntu `gtklock` package provides this protocol client); the source tree does not install it. `PANTHEON_LOCK_COMMAND` can select another installed client. Authentication, D-Bus activation and session lock/unlock have not yet -been runtime-qualified. +been runtime-qualified. The compositor advertises fractional-scale and +viewporter globals, but rendering quality and moving windows between outputs +with different scales still need nested-session qualification. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 2b0318512..56af8fd59 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -169,7 +169,11 @@ saved modes and layout. Output disabling, mirroring, unsupported properties and nested Winit mode changes are rejected. Output and configuration updates increment the serial and emit `MonitorsChanged`. Default and native DRM builds compile, but no native session or live monitor change has been run from this -development session. The isolated +development session. The compositor also advertises `wp_fractional_scale_v1` +and `wp_viewporter`, setting window surfaces' preferred scale from their first +mapped output and refreshing preferences during rendering and after monitor +configuration changes. Cross-output moves and fractional rendering quality +still need an isolated client/runtime check. The isolated Wingpanel run no longer reports missing DisplayConfig or Shell keybinding services. Pressing `Super+N` in the nested window produced the registered `AcceleratorActivated` signal. The sound indicator's media-key registrations diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 89139bd08..22babec8b 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -288,6 +288,7 @@ impl NativeKmsDevice { renderer: &mut NativeRenderer, state: &PantheonCompositor, ) -> Result> { + state.update_fractional_scale_preferences(); let mut rendered = 0; for output in &mut self.outputs { if output.frame_pending { diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 8e6ed9d3a..00827ee00 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -305,7 +305,7 @@ impl PantheonCompositor { true } - fn find_window_for_surface(&self, surface: &WlSurface) -> Option { + pub(crate) fn find_window_for_surface(&self, surface: &WlSurface) -> Option { self.space .elements() .find(|window| { diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 6932fa39b..4a5b03ef7 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -20,12 +20,16 @@ use smithay::{ utils::{Logical, Point, Size}, wayland::{ compositor::{CompositorClientState, CompositorState}, + fractional_scale::{ + FractionalScaleHandler, FractionalScaleManagerState, with_fractional_scale, + }, output::OutputManagerState, selection::data_device::DataDeviceState, session_lock::{LockSurface, SessionLockManagerState, SessionLocker}, shell::xdg::XdgShellState, shm::ShmState, socket::ListeningSocketSource, + viewporter::ViewporterState, }, }; @@ -54,6 +58,8 @@ pub struct PantheonCompositor { pub xdg_shell_state: XdgShellState, pub shm_state: ShmState, pub output_manager_state: OutputManagerState, + pub fractional_scale_manager_state: FractionalScaleManagerState, + pub viewporter_state: ViewporterState, pub session_lock_state: SessionLockManagerState, pub session_locked: bool, pub lock_surfaces: HashMap, @@ -111,6 +117,8 @@ impl PantheonCompositor { let popups = PopupManager::default(); let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + let fractional_scale_manager_state = FractionalScaleManagerState::new::(&dh); + let viewporter_state = ViewporterState::new::(&dh); let session_lock_state = SessionLockManagerState::new::(&dh, |_| true); // Data device is responsible for clipboard and drag-and-drop @@ -166,6 +174,8 @@ impl PantheonCompositor { xdg_shell_state, shm_state, output_manager_state, + fractional_scale_manager_state, + viewporter_state, session_lock_state, session_locked: false, lock_surfaces: HashMap::new(), @@ -363,6 +373,7 @@ impl PantheonCompositor { } self.reconfigure_managed_windows(); self.space.refresh(); + self.update_fractional_scale_preferences(); let _ = self.display_handle.flush_clients(); #[cfg(feature = "native-drm")] self.request_native_redraw(); @@ -373,6 +384,20 @@ impl PantheonCompositor { Ok(()) } + pub fn update_fractional_scale_preferences(&self) { + for window in self.space.elements() { + let Some(output) = self.space.outputs_for_element(window).first().cloned() else { + continue; + }; + let scale = output.current_scale().fractional_scale(); + window.with_surfaces(|_, states| { + with_fractional_scale(states, |fractional_scale| { + fractional_scale.set_preferred_scale(scale); + }); + }); + } + } + fn init_wayland_listener( display: Display, event_loop: &mut EventLoop, @@ -489,6 +514,26 @@ impl PantheonCompositor { } } +impl FractionalScaleHandler for PantheonCompositor { + fn new_fractional_scale(&mut self, surface: WlSurface) { + let mut root = surface.clone(); + while let Some(parent) = smithay::wayland::compositor::get_parent(&root) { + root = parent; + } + let output = self + .find_window_for_surface(&root) + .and_then(|window| self.space.outputs_for_element(&window).first().cloned()) + .or_else(|| self.space.outputs().next().cloned()); + if let Some(output) = output { + smithay::wayland::compositor::with_states(&surface, |states| { + with_fractional_scale(states, |fractional_scale| { + fractional_scale.set_preferred_scale(output.current_scale().fractional_scale()); + }); + }); + } + } +} + /// Data associated with a wayland client that connects to PantheonCompositor. /// One instance of this type per client. #[derive(Default)] diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 9e46cbf32..2361a3c12 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -124,6 +124,7 @@ pub fn init_winit( } WinitEvent::Input(event) => state.process_input_event(event), WinitEvent::Redraw => { + state.update_fractional_scale_preferences(); let size = backend.window_size(); let damage = Rectangle::from_size(size); From 6f5e269a929d595d92564a9d9a31a46c6b6aca1e Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:22:44 -0400 Subject: [PATCH 094/798] Support XDG window activation --- mseries/OS9-HANDOFF.md | 7 ++ mseries/ROADMAP.md | 5 +- .../EXPERIENCE-REQUIREMENTS.md | 5 +- rust/pantheon-compositor/README.md | 6 +- rust/pantheon-compositor/src/state.rs | 73 +++++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4d5c9f284..f5e83cf7f 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5624,6 +5624,13 @@ and output scale preferences refresh during rendering and after configuration changes. Cross-output movement and fractional rendering still require a nested client check. +The compositor also now serves Smithay's `xdg-activation-v1`. Client tokens +must carry a recent serial from this compositor's seat; tokens expire after a +minute if unused and are consumed on activation. Recent activation requests +raise/focus an already mapped target window, except while the session is +locked. The new app-launch path has only been compile-checked, not exercised by +a client. + Local verification: `cargo check -p pantheon-compositor --bin pantheon-compositor` and the same command with `--features native-drm` both pass; touched Rust files pass `rustfmt --check`; `git diff --check` passes. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index accc4e5ce..7fb783594 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -40,7 +40,10 @@ mode changes remain unqualified. The compositor advertises preferences from mapped outputs. Moving surfaces across differently scaled outputs still needs runtime verification. The compositor also serves Wingpanel's `org.gnome.Shell` accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during -an isolated nested run. `ShowOSD`, lock-screen action modes and physical media +an isolated nested run. `xdg-activation-v1` now validates recent seat-backed +tokens, consumes them after use and raises/focuses mapped target windows when +the session is unlocked; app launch flows remain unverified. +`ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; xdg popup requests now install Smithay keyboard/pointer grabs for managed diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index dd90e08b7..0fab7926d 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -97,7 +97,10 @@ rejected. Both default and native DRM builds compile; runtime changes on the active M-series display remain unqualified. The compositor advertises `wp_fractional_scale_v1` and `wp_viewporter`, setting each window surface's preferred scale from its first mapped output. Runtime checks for moving a -surface between differently scaled outputs remain open. It also +surface between differently scaled outputs remain open. The compositor now +serves `xdg-activation-v1`, requiring a recent token associated with this seat +and using it to raise/focus mapped windows outside a locked session. App-launch +activation remains runtime-unverified. It also implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered `Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 56af8fd59..152866a13 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -173,7 +173,11 @@ development session. The compositor also advertises `wp_fractional_scale_v1` and `wp_viewporter`, setting window surfaces' preferred scale from their first mapped output and refreshing preferences during rendering and after monitor configuration changes. Cross-output moves and fractional rendering quality -still need an isolated client/runtime check. The isolated +still need an isolated client/runtime check. The compositor implements +`xdg-activation-v1`: it accepts recent tokens tied to the compositor seat, +consumes tokens on use, and raises/focuses mapped target windows unless the +session is locked. Activation launch flows still need an isolated client check. +The isolated Wingpanel run no longer reports missing DisplayConfig or Shell keybinding services. Pressing `Super+N` in the nested window produced the registered `AcceleratorActivated` signal. The sound indicator's media-key registrations diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 4a5b03ef7..6544d9c70 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -30,6 +30,9 @@ use smithay::{ shm::ShmState, socket::ListeningSocketSource, viewporter::ViewporterState, + xdg_activation::{ + XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, + }, }, }; @@ -60,6 +63,7 @@ pub struct PantheonCompositor { pub output_manager_state: OutputManagerState, pub fractional_scale_manager_state: FractionalScaleManagerState, pub viewporter_state: ViewporterState, + pub xdg_activation_state: XdgActivationState, pub session_lock_state: SessionLockManagerState, pub session_locked: bool, pub lock_surfaces: HashMap, @@ -119,6 +123,7 @@ impl PantheonCompositor { let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); let fractional_scale_manager_state = FractionalScaleManagerState::new::(&dh); let viewporter_state = ViewporterState::new::(&dh); + let xdg_activation_state = XdgActivationState::new::(&dh); let session_lock_state = SessionLockManagerState::new::(&dh, |_| true); // Data device is responsible for clipboard and drag-and-drop @@ -135,6 +140,15 @@ impl PantheonCompositor { TimeoutAction::ToDuration(Duration::from_millis(25)) }, )?; + event_loop.handle().insert_source( + Timer::from_duration(Duration::from_secs(10)), + |_, _, state| { + state + .xdg_activation_state + .retain_tokens(|_, token| token.timestamp.elapsed() < Duration::from_secs(60)); + TimeoutAction::ToDuration(Duration::from_secs(10)) + }, + )?; // A seat is a group of keyboards, pointer and touch devices. // A seat typically has a pointer and maintains a keyboard focus and a pointer focus. @@ -176,6 +190,7 @@ impl PantheonCompositor { output_manager_state, fractional_scale_manager_state, viewporter_state, + xdg_activation_state, session_lock_state, session_locked: false, lock_surfaces: HashMap::new(), @@ -534,6 +549,64 @@ impl FractionalScaleHandler for PantheonCompositor { } } +impl XdgActivationHandler for PantheonCompositor { + fn activation_state(&mut self) -> &mut XdgActivationState { + &mut self.xdg_activation_state + } + + fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool { + self.xdg_activation_state + .retain_tokens(|_, token| token.timestamp.elapsed() < Duration::from_secs(60)); + let Some((serial, seat)) = data.serial else { + return false; + }; + let Some(keyboard) = self.seat.get_keyboard() else { + return false; + }; + Seat::from_resource(&seat) == Some(self.seat.clone()) + && keyboard + .last_enter() + .is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) + } + + fn request_activation( + &mut self, + token: XdgActivationToken, + token_data: XdgActivationTokenData, + surface: WlSurface, + ) { + self.xdg_activation_state.remove_token(&token); + if self.session_locked || token_data.timestamp.elapsed() > Duration::from_secs(10) { + return; + } + + let mut root = surface; + while let Some(parent) = smithay::wayland::compositor::get_parent(&root) { + root = parent; + } + let Some(window) = self.find_window_for_surface(&root) else { + return; + }; + self.space.raise_element(&window, true); + if let Some(surface) = window + .toplevel() + .map(|toplevel| toplevel.wl_surface().clone()) + { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + if let Some(keyboard) = self.seat.get_keyboard() { + keyboard.set_focus(self, Some(surface), serial); + } + self.space.elements().for_each(|window| { + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } + }); + } + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } +} + /// Data associated with a wayland client that connects to PantheonCompositor. /// One instance of this type per client. #[derive(Default)] From 4a3e094edea888aa97744ad25742c6028719d5d5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:25:06 -0400 Subject: [PATCH 095/798] Honor Wayland minimize requests --- mseries/OS9-HANDOFF.md | 12 ++ mseries/ROADMAP.md | 4 +- .../EXPERIENCE-REQUIREMENTS.md | 4 +- rust/pantheon-compositor/README.md | 4 +- .../src/handlers/xdg_shell.rs | 121 +++++++++++++----- rust/pantheon-compositor/src/state.rs | 15 ++- 6 files changed, 122 insertions(+), 38 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index f5e83cf7f..c7e6d82d5 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5639,3 +5639,15 @@ so DRM behavior and settings-client round trips remain runtime-unqualified. Next: validate the D-Bus request against Pantheon Settings in an isolated nested session, then qualify mode rollback and persistent startup on hardware without replacing the active compositor until a recovery path is ready. + +### Retain and restore minimized windows — 2026-10-19 + +Implemented Smithay's `XdgShellHandler::minimize_request`: requests outside a +locked session remove non-panel windows from the visible `Space` while retaining +their `Window` and previous position. If the minimized window held keyboard +focus, focus moves to the next visible application window. A later valid +`xdg-activation-v1` request restores a retained window to its former location, +raises it and focuses it; destroying a minimized toplevel drops the retained +entry. Default and `native-drm` compositor builds pass. This code has not been +exercised with an app client; task-switcher discovery and runtime restoration +remain unverified. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 7fb783594..833748d56 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -42,7 +42,9 @@ outputs still needs runtime verification. The compositor also serves Wingpanel's accelerator API and matched `Super+N` to an `AcceleratorActivated` signal during an isolated nested run. `xdg-activation-v1` now validates recent seat-backed tokens, consumes them after use and raises/focuses mapped target windows when -the session is unlocked; app launch flows remain unverified. +the session is unlocked; app launch flows remain unverified. The +`xdg_toplevel.set_minimized` request now unmaps and retains windows, which valid activation requests +can restore at their previous position; this is not runtime-qualified. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 0fab7926d..2da2d8403 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -100,7 +100,9 @@ preferred scale from its first mapped output. Runtime checks for moving a surface between differently scaled outputs remain open. The compositor now serves `xdg-activation-v1`, requiring a recent token associated with this seat and using it to raise/focus mapped windows outside a locked session. App-launch -activation remains runtime-unverified. It also +activation remains runtime-unverified. `xdg_toplevel.set_minimized` now hides and +retains a window, and a later valid activation request restores it. Minimize / +restore behavior remains runtime-unverified. It also implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered `Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 152866a13..1b8f6cb28 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -176,7 +176,9 @@ configuration changes. Cross-output moves and fractional rendering quality still need an isolated client/runtime check. The compositor implements `xdg-activation-v1`: it accepts recent tokens tied to the compositor seat, consumes tokens on use, and raises/focuses mapped target windows unless the -session is locked. Activation launch flows still need an isolated client check. +session is locked. `xdg_toplevel.set_minimized` unmaps and retains windows; a later +valid activation request restores them at their previous location. Activation +and minimize/restore flows still need an isolated client check. The isolated Wingpanel run no longer reports missing DisplayConfig or Shell keybinding services. Pressing `Super+N` in the nested window produced the registered diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 72ef0b207..834f74522 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -145,6 +145,44 @@ impl XdgShellHandler for PantheonCompositor { self.configure_window_mode(surface, false, false); } + fn minimize_request(&mut self, surface: ToplevelSurface) { + if self.session_locked { + return; + } + let wl_surface = surface.wl_surface(); + if self.is_panel_surface(wl_surface) { + return; + } + let Some(window) = self + .space + .elements() + .find(|window| { + window + .toplevel() + .is_some_and(|top| top.wl_surface() == wl_surface) + }) + .cloned() + else { + return; + }; + let Some(location) = self.space.element_location(&window) else { + return; + }; + let id = wl_surface.id(); + let was_focused = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .is_some_and(|focused| surface_has_root(&focused, wl_surface)); + self.space.unmap_elem(&window); + self.minimized_windows.insert(id, (window, location)); + if was_focused { + self.focus_next_window(); + } + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { let wl_surface = surface.wl_surface(); let id = wl_surface.id(); @@ -152,9 +190,10 @@ impl XdgShellHandler for PantheonCompositor { .seat .get_keyboard() .and_then(|keyboard| keyboard.current_focus()) - .is_some_and(|focused| focused == *wl_surface); + .is_some_and(|focused| surface_has_root(&focused, wl_surface)); self.initially_positioned.remove(&id); self.managed_windows.remove(&id); + self.minimized_windows.remove(&id); let window = self .space @@ -170,38 +209,7 @@ impl XdgShellHandler for PantheonCompositor { } if was_focused { - let next_window = self - .space - .elements() - .rev() - .find(|window| { - window - .toplevel() - .is_some_and(|top| !self.is_panel_surface(top.wl_surface())) - }) - .cloned(); - if let Some(window) = next_window { - self.space.raise_element(&window, true); - if let Some(surface) = window.toplevel().map(|top| top.wl_surface().clone()) { - let serial = smithay::utils::SERIAL_COUNTER.next_serial(); - self.seat - .get_keyboard() - .unwrap() - .set_focus(self, Some(surface), serial); - self.space.elements().for_each(|window| { - if let Some(toplevel) = window.toplevel() { - toplevel.send_pending_configure(); - } - }); - } - } else { - let serial = smithay::utils::SERIAL_COUNTER.next_serial(); - self.seat.get_keyboard().unwrap().set_focus( - self, - Option::::None, - serial, - ); - } + self.focus_next_window(); } } @@ -249,6 +257,45 @@ impl XdgShellHandler for PantheonCompositor { } } +impl PantheonCompositor { + pub(crate) fn focus_next_window(&mut self) { + if self.session_locked { + return; + } + let next_window = self + .space + .elements() + .rev() + .find(|window| { + window + .toplevel() + .is_some_and(|top| !self.is_panel_surface(top.wl_surface())) + }) + .cloned(); + if let Some(window) = next_window { + self.space.raise_element(&window, true); + if let Some(surface) = window.toplevel().map(|top| top.wl_surface().clone()) { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.seat + .get_keyboard() + .unwrap() + .set_focus(self, Some(surface), serial); + self.space.elements().for_each(|window| { + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } + }); + } + } else { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.seat + .get_keyboard() + .unwrap() + .set_focus(self, Option::::None, serial); + } + } +} + fn check_grab( seat: &Seat, surface: &WlSurface, @@ -272,6 +319,14 @@ fn check_grab( Some(start_data) } +fn surface_has_root(surface: &WlSurface, expected_root: &WlSurface) -> bool { + let mut root = surface.clone(); + while let Some(parent) = smithay::wayland::compositor::get_parent(&root) { + root = parent; + } + root == *expected_root +} + /// Should be called on `WlSurface::commit` after `Window::on_commit`. pub fn handle_commit(state: &mut PantheonCompositor, surface: &WlSurface) { // Handle toplevel commits. diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 6544d9c70..e85cf861f 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -12,7 +12,7 @@ use smithay::{ timer::{TimeoutAction, Timer}, }, wayland_server::{ - Display, DisplayHandle, + Display, DisplayHandle, Resource, backend::{ClientData, ClientId, DisconnectReason}, protocol::wl_surface::WlSurface, }, @@ -80,6 +80,10 @@ pub struct PantheonCompositor { pub last_work_area: Option>, pub managed_windows: HashMap, + pub minimized_windows: HashMap< + smithay::reexports::wayland_server::backend::ObjectId, + (Window, Point), + >, pub initially_positioned: HashSet, pub popups: PopupManager, pub cursor_status: CursorImageStatus, @@ -206,6 +210,7 @@ impl PantheonCompositor { panels: HashMap::new(), last_work_area: None, managed_windows: HashMap::new(), + minimized_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, cursor_status: CursorImageStatus::default_named(), @@ -584,7 +589,13 @@ impl XdgActivationHandler for PantheonCompositor { while let Some(parent) = smithay::wayland::compositor::get_parent(&root) { root = parent; } - let Some(window) = self.find_window_for_surface(&root) else { + let root_id = root.id(); + let window = self.find_window_for_surface(&root).or_else(|| { + let (window, location) = self.minimized_windows.remove(&root_id)?; + self.space.map_element(window.clone(), location, false); + Some(window) + }); + let Some(window) = window else { return; }; self.space.raise_element(&window, true); From 47362874770b7999a55dac109fb6bf0e4f56d77f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:25:10 -0400 Subject: [PATCH 096/798] Handoff: installed elementary OS boots to the desktop on QEMU with the generic kernel Co-Authored-By: Claude Sonnet 5.5 --- docs/images/os9-ci-installed-qemu-desktop.png | Bin 0 -> 160643 bytes mseries/OS9-HANDOFF.md | 11 +++++++++++ 2 files changed, 11 insertions(+) create mode 100644 docs/images/os9-ci-installed-qemu-desktop.png diff --git a/docs/images/os9-ci-installed-qemu-desktop.png b/docs/images/os9-ci-installed-qemu-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..cc2fa0dc447995dd90168d363f9c5ded2019bda4 GIT binary patch literal 160643 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

    =bW_##CCUBSS99v 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

    E+t?RhIk%)V^2EJmtD>audNapH3I_IX-0uro zMD{ZGDdeqp;Lstm1xciKE=MCtw?#x%akHdo=YvyYi|et{gt)89v*e2&V-4_#LLzy1$i-o6+OKjst-T6m;Y`!&EXkY?8*lk`pBFdej%0(qR>M=F>$u zQ+80uyc#7_p)#58O2Q}kuI~DoFWFpPqDxgml4VT6+D%&2cO23+Oc|B#Ji%2A+LlGT zO9-)1@la>iG!i2ArSaxxM_M|6RCmA4UYA3kxkeHSvO~TLr2V=Q?YOJ1Szu9=Q40mJ z^g$9$b$%8%JPOR$`vcK_<5R0ZomtrsN1VXIM#(9sfoAy;FKeJ2emWir2KTT80%dT6 z?WFISuk29xMKoU+)PQNGPMGdUn=|*{XDB0oq^E`x)R8D;*w3sh`w2J5qM8hX4~kX+ z$&(K{rRh@#{n4sg@KHp|{W0&Wf0tYk-l0Os+|f~OzEMj1|EPM)fGE2zY;@=pkdhK5 z1cojp1w>F9Wavg6x}*f@l2kytL%Lf8r7>tpK?EeF6eXlNYo7OfC+3%mOx&~fT379T zF==bxKYO zLyvVY)b~>nxlZbr;Y?Mi9djvY$RQHULs~(r*z!u^Ol>xD<$mFso^~0ENQH}@^Q5HW z?j9M`_iF{z=$XS{GhD4~aojKMkH}MsqUMtB6bVU*9yc_77)~7hS*WI8{f}J#1O_h-1~YuQ?jx;HiPU1kxHPTyY)AG2e-@5FZ>{!H z>1(Ll6gEK{JBdnqvbMOkG7rdzIR5tBDKWor@Vfc2vx0AAATWZSo<4SVetU1d<#%lM z))xs*9%Jm_FKDywYs8`0A1+v^<5+|L)|jFL?^F6UaJ~9!O^h4FMeyKr{=2_#{)jOA zx{Ht7Q!$|RPsHuus*Lxs`}W;-84)(NyA=o$+!ruvkz@i{6V89*Y`oS*!v8--MO)eZ z)rQ3hO70C-(KCac;J^uioP{_WD&2OOkh?0_*hO+Z0|Nu6r>C-W-m6Oi7w3;B*3GiM z?Qsp@*oWCpy}$gr+GhN*n7Z?cO8Q}FBD2hc_s{z0#)|E8+REMh{r#=1iYPj;nF0y+ z`-!{={f{bHG$x$OByh>A&teI#xm!H_ymwQHmg9Cvc)$6Fx<^U9cct*#zXhk^jD1;l zRbr3ljA(=lb!fp~gTMA;{K^7NXk1K8%w(l;Y-^3bOZ$#BkA`34>heY9SAG3!$~POi zguArcog`#iD<&G*>Sti5e2d3sOblr)p3!yjg5jC!ZGp~i%WbjJ1(tWB0y*aU-+HF} zy#oo}NKX-FAXWt<4sdLah0qjIIi}QHN)jc-UczdjyYh$tEto%w_{l47w(gmNQ1(0KII%woF*vgnTw|?1uajKZO%A%r1$FYXI!7+Xx^20fZMv8M#YIYN*FbxBU9{o zQIGBMUZ3}r7?-%oRPo_jz2xq02mj&VJ2j=)?_s{be8~{VSi#$LJ8t*!p3AdP5?iuA zZbpCV%4UDt^7d0y(s{Fj!I3IUQKEK)(!FBdae*gGi6mu1$k#;jRyyjW+!_xY_3Xze z*)(x+bTcxYjQYY*yz%VVfw)_y-~EbRHLBhExI?HpUa6G~P+$9KFGN80QlnH>BpYcl zs=Z2IND&MCD(#G?XrAkXZN<1DC5x_#c|iMYqW2?)L=moMy~DQ<|yo;Gr=cQ>vm` zOK2e#$S%t-n6A!QQt1 zoP?^#iV`Y#AeD3vE2CRi$#ykSf`GBu-JMbfMaofnfUvw`&JrW~DQ07;MSV$}A~H1x z`N0AAa;H~W`d>?GbA$y)TgFdC$Fauw)fWw=;R}(+`tpm(+HC@1*Kk)=sLI%|u&{!x z2;1&IX`3!&P_DlEg{dvCR0sX~+H;@4GndPJ0&fc{uq9CKYQ`wu`eV0w`c1Ro>zz7A zaJ+o0WaGSM0vCbiPHD1l)mG>LUb(-YR1hva(nY_1SS+}AX)>%(xS@VV(n;;IwAOdi zRLLC*Ghv#aEVTb#giMht*KQ<;s)^

    izzK{&ZZ7+u4n}@bO!914>?yf=Rc#?Y6 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

    Famsx>Quydp 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%0A%U7UjS`;=xT} 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 literal 0 HcmV?d00001 diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 90070a325..359f98991 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6201,3 +6201,14 @@ swap is `/dev/mapper/cryptswap` in fstab. Result: it is the Apple-only Fairydust Options (kernel work belongs to DeepSeek): add `PCI_HOST_GENERIC=y` and `SYSFB_SIMPLEFB=y` (or `FB_EFI`) to the Fairydust config so it also boots on QEMU virt, or make the VM install default to the generic kernel (GRUB_DEFAULT set to the 7.0.0-38 entry). Not tried: either fix, because changing the guest's boot config was outside this task's scope. + +### Result of the VM-only fix (2026-10-19) + +In the test VM only, from the live ISO: backed up `/etc/default/grub` to `/etc/default/grub.before-machinelab`, set +`GRUB_DEFAULT` to the generic `7.0.0-38-generic` entry, `GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=ttyAMA0,115200"`, +and ran `update-grub` in a chroot. Booting the installed disk then reached the serial login prompt, the Initial Setup +wizard (language, keyboard, account), the greeter and the elementary desktop (Welcome app, panel, dock): see +[`os9-ci-installed-qemu-desktop.png`](../docs/images/os9-ci-installed-qemu-desktop.png). So the CI ISO's installed system +**boots and works on QEMU/KVM when it runs the generic kernel**; the Fairydust kernel still cannot boot on QEMU virt +(see above). Test account in the VM: `~/vms/elementary/credentials.txt` (owner-only, VM only). Not verified: the Fairydust +kernel on QEMU, audio, networking inside the guest, suspend, or any Apple hardware path. From 79cde08b495dbf257fab49f2c9ebd561c9552c3f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:27:43 -0400 Subject: [PATCH 097/798] local-ai: receipt for omarchy-mlx main candidate build and C++ tests Co-Authored-By: Claude Sonnet 5.5 --- .../2026-10-19-omarchy-mlx-candidate-build.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-build.md diff --git a/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-build.md b/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-build.md new file mode 100644 index 000000000..b5f732d76 --- /dev/null +++ b/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-build.md @@ -0,0 +1,27 @@ +# omarchy-mlx main candidate build and C++ tests — 2026-10-19 (M1 Pro, G13S C0) + +Candidate: omarchy-mlx `61495cb` (main, unreleased), MLX 0.32.3 pin `9c3d355`. Built in `~/.cache/omarchy-mlx-candidate-61495cb` (disk, not tmpfs), `nice -n 19`, `ninja -j2`. Nothing installed; the v0.7.33 runtime and its workers were untouched and kept running. + +## Configuration + +`prepare-mlx.sh` then cmake Release with `MLX_BUILD_OMARCHY=ON MLX_BUILD_TESTS=ON MLX_BUILD_CPU=OFF`, no Python bindings, no wheel (the wheel build also needs the 458 MB pinned whole-encoder bundle, not fetched). `MLX_BUILD_CPU=ON` failed to configure: no BLAS/LAPACK dev packages (`lapacke.h` absent). I did not install them. + +## Result: 1350/1350 build steps linked + +Tests were run against the private Honeykrisp ICD (`VK_DRIVER_FILES=~/.local/share/mlx-omarchy/vulkan/honeykrisp_icd.aarch64.json`), concurrently with the normal desktop load. Only 5 of the 52 test binaries were built. + +| Binary | Cases | Result | +|---|---|---| +| omarchy_runtime_tests | 59 | pass (27,057 assertions) | +| omarchy_reduce_row_tests (row-reduce Sum, now default) | 6 | pass (216) | +| omarchy_gdn_gqa_bf16a_tests (GDN native GQA) | 6 | pass (93) | +| omarchy_vocab_tail_tests (chunked ArgMax/LogSumExp) | 4 | pass (6,942) | +| omarchy_matmul_family_tests | 43 | 41 pass, **2 fail**, 83,471,241 assertions passed, 0 failed | + +The 2 failures are `gather_qmm tile route stays off below four rows per expert` and `gather_qmm sorted-expert tile route matches the f32 reference…`, each throwing `vector::_M_range_check` on an empty vector (lines 6084 and 6128). These tests build an f32 reference on the CPU stream; other cases in the same binary print `Skipping: no CPU backend`. The likely cause is my CPU-off build, **not confirmed**. They cover MoE gather_qmm only, which the dense Qwen3.5-4B default does not use. + +## What this does not show + +- No model run: no Qwen3.5-4B token-identity check with `MLX_OMARCHY_ROW_REDUCE`, no 9B decode A/B. The test passes are unit-level, one boot, one run. +- Not tested with CPU=ON, the sanitizer flags, or the other 47 binaries. +- No wheel was built, so the Python side (mlx-lm patches against 0.31.3) is unchanged from the dry-run receipt. From 4d75752a1884fe9d0179a92c86d624b4c281918e Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:29:47 -0400 Subject: [PATCH 098/798] Machine Lab: Display tab shows the guest desktop in the app (screendump frames, mouse and keyboard over QMP); machines start headless Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/README.md | 3 +- mseries/machine-lab/data/style.css | 7 + mseries/machine-lab/launcher/run-vm.sh | 7 +- mseries/machine-lab/meson.build | 2 +- mseries/machine-lab/src/machine.vala | 149 +++++++++-- mseries/machine-lab/src/page.vala | 15 ++ mseries/machine-lab/src/viewer.vala | 337 +++++++++++++++++++++++++ 7 files changed, 497 insertions(+), 23 deletions(-) create mode 100644 mseries/machine-lab/src/viewer.vala diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md index ca557f35b..b9566f7ca 100644 --- a/mseries/machine-lab/README.md +++ b/mseries/machine-lab/README.md @@ -16,6 +16,7 @@ free-space check, toasts, light/dark and accent following the system. QEMU. Sign-in details are shown in the app and saved with owner-only permissions. - Own ISO path: drop or choose any ARM64 `.iso`; once the disk holds real data the installer detaches itself. - **Pause / Resume** over QEMU's QMP socket (`qmp.sock` in the machine folder), plus Shut Down / Force Stop. +- **Display tab**: the machine's desktop inside the app (QEMU `screendump` frames over QMP, mouse and keyboard sent back; click the screen to capture, Ctrl+Alt+G to release; Ctrl+Alt+Del and screenshot buttons). Machines start headless so the screen lives in the app, not a separate QEMU window. - Live console from the launcher output, or the guest serial log for imported machines. ```sh @@ -27,5 +28,5 @@ Layout: `src/machine.vala` (model, QMP, process control, discovery), `src/page.v `src/main.vala` (window, sidebar, presets), `launcher/` (QEMU launcher and Omarchy updater). **Not verified:** a full guest install and boot end to end (the Omarchy nightly is ~6 GiB and the host had -~19 GiB free). Pause/resume was verified against a headless QEMU through the app. Visual checks used GTK Broadway. +~19 GiB free). Pause/resume was verified against a headless QEMU through the app. The Display tab was verified in GTK Broadway against the installed elementary guest (frames, mouse, keyboard); the Wayland keyboard path uses evdev codes and is unverified. Visual checks used GTK Broadway. Provenance: started as the Codex `native-vm-poc` spike; its launcher and Omarchy updater are merged here. diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css index e8f363d18..472644101 100644 --- a/mseries/machine-lab/data/style.css +++ b/mseries/machine-lab/data/style.css @@ -156,3 +156,10 @@ textview.ml-console, textview.ml-console text { .ml-gallery-card:hover { box-shadow: 0 0 0 2px @accent_color, 0 2px 8px alpha(black, 0.18); } .ml-pill.paused { color: #946800; background: alpha(#f9c440, 0.35); } + +.ml-screen-frame { + border-radius: 10px; + background: #000; + box-shadow: 0 0 0 1px alpha(@fg_color, 0.2), 0 2px 8px alpha(black, 0.25); +} +.ml-screen-frame label, .ml-screen-frame image { color: #d8dde6; } diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh index ade2a912b..3ebb0183b 100755 --- a/mseries/machine-lab/launcher/run-vm.sh +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -3,7 +3,7 @@ # run-vm.sh ISO [VM_DIR] [CIDATA_ISO] boot an installer (ISO "-" = none) # run-vm.sh --disk-only VM_DIR boot the installed disk # Hardware comes from the environment: MACHINE_CPUS, MACHINE_MEM_GIB, -# MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME. A QMP socket (qmp.sock in +# MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME, MACHINE_DISPLAY (gtk|none). A QMP socket (qmp.sock in # VM_DIR) lets Machine Lab pause and resume the guest. set -euo pipefail @@ -67,6 +67,9 @@ if [ ! -e "$VM_DIR/disk.qcow2" ]; then fi rm -f "$VM_DIR/qmp.sock" +DISPLAY_ARGS=(-display gtk,gl=off) +[ "${MACHINE_DISPLAY:-gtk}" = none ] && DISPLAY_ARGS=(-display none) + QEMU_ARGS=( -name "$VM_NAME" -machine virt,gic-version=3,msi=gicv2m,acpi=off @@ -105,7 +108,7 @@ QEMU_ARGS+=( -device virtio-gpu-pci -device virtio-rng-pci -qmp unix:"$VM_DIR/qmp.sock",server=on,wait=off - -display gtk,gl=off + "${DISPLAY_ARGS[@]}" -boot menu=on ) diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index 6012b34c0..94eaa35c1 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -16,7 +16,7 @@ resources = gnome.compile_resources('machine-lab-resources', add_project_arguments(['--pkg', 'posix'], language: 'vala') executable('machine-lab', - 'src/main.vala', 'src/machine.vala', 'src/page.vala', + 'src/main.vala', 'src/machine.vala', 'src/page.vala', 'src/viewer.vala', resources, dependencies: [gtk, granite], install: true diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index f24253dc9..b8fe55856 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -106,33 +106,143 @@ public class Machine : Object { // ---- QMP: pause and resume ---- - private string? qmp (string command) { + // One persistent QMP connection per machine, shared by the poller (main + // thread) and the display worker thread, so access is serialized. + private Mutex qmp_lock = Mutex (); + private SocketConnection? qmp_conn = null; + private DataInputStream? qmp_in = null; + + private bool qmp_open () { + if (qmp_conn != null) { + return true; + } var sock = Path.build_filename (dir, "qmp.sock"); if (!FileUtils.test (sock, FileTest.EXISTS)) { - return null; + return false; } try { var client = new SocketClient (); - client.timeout = 2; - var conn = client.connect (new UnixSocketAddress (sock)); - var input = new DataInputStream (conn.input_stream); - var output = conn.output_stream; - input.read_line (); // greeting - output.write ("{\"execute\":\"qmp_capabilities\"}\n".data); - input.read_line (); - output.write ("{\"execute\":\"%s\"}\n".printf (command).data); - string? reply = input.read_line (); - // Events can arrive before the return value. - while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { - reply = input.read_line (); - } - conn.close (); - return reply; + client.timeout = 3; + qmp_conn = client.connect (new UnixSocketAddress (sock)); + qmp_in = new DataInputStream (qmp_conn.input_stream); + qmp_in.read_line (); // greeting + qmp_conn.output_stream.write ("{\"execute\":\"qmp_capabilities\"}\n".data); + qmp_in.read_line (); + return true; } catch (Error e) { - return null; + qmp_close (); + return false; } } + private void qmp_close () { + try { + if (qmp_conn != null) { + qmp_conn.close (); + } + } catch (Error e) {} + qmp_conn = null; + qmp_in = null; + } + + /* Send one QMP command (a full JSON object) and return its reply line. */ + public string? qmp_raw (string json) { + qmp_lock.lock (); + string? reply = null; + for (int attempt = 0; attempt < 2 && reply == null; attempt++) { + if (!qmp_open ()) { + break; + } + try { + qmp_conn.output_stream.write ((json + "\n").data); + reply = qmp_in.read_line (); + // Events can arrive before the return value. + while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { + reply = qmp_in.read_line (); + } + } catch (Error e) { + reply = null; + } + if (reply == null) { + qmp_close (); // stale socket (machine restarted): reconnect once + } + } + qmp_lock.unlock (); + return reply; + } + + private string? qmp (string command) { + return qmp_raw ("{\"execute\":\"%s\"}".printf (command)); + } + + // ---- Display: frames and input over QMP ---- + + public signal void frame_ready (string path); + + private AsyncQueue? input_queue = null; + private bool frame_busy = false; + + public string frame_path () { + var d = Path.build_filename (Environment.get_user_runtime_dir (), "machine-lab"); + DirUtils.create_with_parents (d, 0700); + return Path.build_filename (d, id + ".png"); + } + + /* Ask QEMU for the current screen. At most one request is in flight. */ + public void request_frame () { + if (!running || frame_busy) { + return; + } + frame_busy = true; + string path = frame_path (); + new Thread ("ml-frame", () => { + var reply = qmp_raw ("{\"execute\":\"screendump\",\"arguments\":{\"filename\":\"%s\",\"format\":\"png\"}}".printf (path)); + bool ok = reply != null && reply.contains ("\"return\""); + Idle.add (() => { + frame_busy = false; + if (ok) { + frame_ready (path); + } + return Source.REMOVE; + }); + }); + } + + /* Queue an input event; a worker thread sends them in order. */ + public void send_input (string events_json) { + if (!running) { + return; + } + if (input_queue == null) { + input_queue = new AsyncQueue (); + var q = input_queue; + new Thread ("ml-input", () => { + while (true) { + string cmd = q.pop (); + if (cmd == "") { + break; + } + qmp_raw ("{\"execute\":\"input-send-event\",\"arguments\":{\"events\":%s}}".printf (cmd)); + } + }); + } + input_queue.push (events_json); + } + + public void send_pointer (double fx, double fy) { + int x = (int) (fx.clamp (0.0, 1.0) * 32767); + int y = (int) (fy.clamp (0.0, 1.0) * 32767); + send_input ("[{\"type\":\"abs\",\"data\":{\"axis\":\"x\",\"value\":%d}},{\"type\":\"abs\",\"data\":{\"axis\":\"y\",\"value\":%d}}]".printf (x, y)); + } + + public void send_button (string button, bool down) { + send_input ("[{\"type\":\"btn\",\"data\":{\"button\":\"%s\",\"down\":%s}}]".printf (button, down ? "true" : "false")); + } + + public void send_key (string qcode, bool down) { + send_input ("[{\"type\":\"key\",\"data\":{\"down\":%s,\"key\":{\"type\":\"qcode\",\"data\":\"%s\"}}}]".printf (down ? "true" : "false", qcode)); + } + public void pause () { if (running && qmp ("stop") != null) { sync_paused (); @@ -214,13 +324,14 @@ public class Machine : Object { var l = new SubprocessLauncher (SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); string[] argv; if (managed) { - argv = { launcher, "start", vm_name, "--daemon" }; + argv = { launcher, "start", vm_name, "--headless" }; } else { l.setenv ("MACHINE_CPUS", cpus.to_string (), true); l.setenv ("MACHINE_MEM_GIB", mem_gib.to_string (), true); l.setenv ("MACHINE_DISK_GIB", disk_gib.to_string (), true); l.setenv ("MACHINE_SSH_PORT", ssh_port.to_string (), true); l.setenv ("MACHINE_NAME", name, true); + l.setenv ("MACHINE_DISPLAY", "none", true); // the app shows the screen itself if (recipe == "omarchy" && installer == null) { string? updater = find_runtime ("machine-lab-prepare-omarchy", "prepare-latest-omarchy.py"); if (updater == null) { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 0355076f6..67f319648 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -49,6 +49,8 @@ public class MachinePage : Gtk.Box { private Gtk.Button start_button; private Gtk.Button stop_button; private Gtk.Stack stack; + private Viewer viewer; + private bool was_running = false; private Gtk.Box settings_form; private Gtk.Label settings_note; private Gtk.Entry name_entry; @@ -72,6 +74,9 @@ public class MachinePage : Gtk.Box { stack.transition_type = Gtk.StackTransitionType.CROSSFADE; stack.vexpand = true; stack.add_titled (build_overview (), "overview", "Overview"); + viewer = new Viewer (machine); + viewer.toast.connect ((m) => toast (m)); + stack.add_titled (viewer, "display", "Display"); stack.add_titled (build_console (), "console", "Console"); stack.add_titled (build_settings (), "settings", "Settings"); @@ -97,7 +102,17 @@ public class MachinePage : Gtk.Box { append (handle); append (stack); + stack.notify["visible-child-name"].connect (() => { + viewer.set_active (stack.visible_child_name == "display"); + }); machine.state_changed.connect (refresh); + machine.state_changed.connect (() => { + // Take the user to the screen as soon as a machine starts. + if (machine.running && !was_running) { + show_tab ("display"); + } + was_running = machine.running; + }); machine.finished.connect ((code) => { if (code > 0) { show_tab ("console"); diff --git a/mseries/machine-lab/src/viewer.vala b/mseries/machine-lab/src/viewer.vala new file mode 100644 index 000000000..43e801d9b --- /dev/null +++ b/mseries/machine-lab/src/viewer.vala @@ -0,0 +1,337 @@ +/* The machine's screen inside Machine Lab: frames from QEMU (screendump), + * mouse and keyboard sent back over QMP. */ +using Gtk; + +public class Viewer : Gtk.Box { + public Machine machine { get; construct; } + public signal void toast (string message); + + private Gtk.Picture picture; + private Gtk.Stack stack; + private Gtk.Label hint; + private Gtk.Label size_label; + private uint timer = 0; + private bool active = false; + private bool captured = false; + private int tex_w = 0; + private int tex_h = 0; + private bool ctrl_down = false; + private bool alt_down = false; + private double last_x = -1; + private double last_y = -1; + private int64 last_move = 0; + + // evdev scancode (GDK keycode - 8) -> QEMU qcode + private static HashTable? keymap = null; + + public Viewer (Machine machine) { + Object (machine: machine, orientation: Gtk.Orientation.VERTICAL, spacing: 0); + } + + construct { + build_keymap (); + margin_start = 24; + margin_end = 24; + margin_bottom = 16; + margin_top = 4; + + picture = new Gtk.Picture (); + picture.content_fit = Gtk.ContentFit.CONTAIN; + picture.can_shrink = true; + picture.hexpand = true; + picture.vexpand = true; + picture.focusable = true; + picture.add_css_class ("ml-screen"); + + // Placeholder shown while the machine is off. + var off = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); + off.valign = Gtk.Align.CENTER; + off.halign = Gtk.Align.CENTER; + var icon = new Gtk.Image.from_icon_name ("video-display-symbolic"); + icon.pixel_size = 64; + icon.opacity = 0.5; + off.append (icon); + var t = new Gtk.Label ("The machine is off"); + t.add_css_class ("title-3"); + off.append (t); + var d = new Gtk.Label ("Start it and its desktop appears here."); + d.add_css_class ("dim-label"); + off.append (d); + + stack = new Gtk.Stack (); + stack.vexpand = true; + stack.add_named (off, "off"); + stack.add_named (picture, "screen"); + stack.add_css_class ("ml-screen-frame"); + append (stack); + + var bar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + bar.margin_top = 10; + hint = new Gtk.Label ("Click the screen to use the keyboard and mouse in the machine. Ctrl+Alt+G gives them back."); + hint.hexpand = true; + hint.xalign = 0; + hint.wrap = true; + hint.add_css_class ("ml-fineprint"); + bar.append (hint); + size_label = new Gtk.Label (""); + size_label.add_css_class ("ml-fineprint"); + bar.append (size_label); + var cad = new Gtk.Button.with_label ("Ctrl+Alt+Del"); + cad.clicked.connect (() => { + foreach (var k in new string[] { "ctrl", "alt", "delete" }) machine.send_key (k, true); + foreach (var k in new string[] { "delete", "alt", "ctrl" }) machine.send_key (k, false); + }); + bar.append (cad); + var shot = new Gtk.Button.from_icon_name ("camera-photo-symbolic"); + shot.tooltip_text = "Save a screenshot to Pictures"; + shot.clicked.connect (save_screenshot); + bar.append (shot); + append (bar); + + // Mouse + var click = new Gtk.GestureClick (); + click.button = 0; + click.pressed.connect ((n, x, y) => { + picture.grab_focus (); + captured = true; + update_hint (); + move_pointer (x, y, true); + machine.send_button (button_name (click.get_current_button ()), true); + }); + click.released.connect ((n, x, y) => { + machine.send_button (button_name (click.get_current_button ()), false); + }); + picture.add_controller (click); + + var motion = new Gtk.EventControllerMotion (); + motion.motion.connect ((x, y) => move_pointer (x, y, false)); + picture.add_controller (motion); + + var scroll = new Gtk.EventControllerScroll (Gtk.EventControllerScrollFlags.VERTICAL); + scroll.scroll.connect ((dx, dy) => { + string b = dy < 0 ? "wheel-up" : "wheel-down"; + machine.send_button (b, true); + machine.send_button (b, false); + return true; + }); + picture.add_controller (scroll); + + // Keyboard: capture phase so window shortcuts don't steal keys while captured. + var keys = new Gtk.EventControllerKey (); + keys.propagation_phase = Gtk.PropagationPhase.CAPTURE; + keys.key_pressed.connect ((keyval, keycode, state) => handle_key (keyval, keycode, true)); + keys.key_released.connect ((keyval, keycode, state) => { handle_key (keyval, keycode, false); }); + picture.add_controller (keys); + + var focus = new Gtk.EventControllerFocus (); + focus.leave.connect (() => { captured = false; release_modifiers (); update_hint (); }); + picture.add_controller (focus); + + machine.frame_ready.connect (on_frame); + machine.state_changed.connect (sync_state); + sync_state (); + } + + ~Viewer () { + if (timer != 0) { + Source.remove (timer); + } + } + + /* Only fetch frames while this tab is on screen. */ + public void set_active (bool on) { + active = on; + if (on && timer == 0) { + timer = Timeout.add (90, () => { + if (active && machine.running) { + machine.request_frame (); + } + return Source.CONTINUE; + }); + machine.request_frame (); + } else if (!on && timer != 0) { + Source.remove (timer); + timer = 0; + } + } + + private void sync_state () { + stack.visible_child_name = machine.running ? "screen" : "off"; + if (!machine.running) { + captured = false; + update_hint (); + } + } + + private void on_frame (string path) { + try { + var tex = Gdk.Texture.from_filename (path); + tex_w = tex.get_width (); + tex_h = tex.get_height (); + picture.paintable = tex; + size_label.label = "%d × %d".printf (tex_w, tex_h); + stack.visible_child_name = "screen"; + } catch (Error e) { + // A frame can be caught mid-write; the next one will do. + } + } + + private void update_hint () { + hint.label = captured + ? "Keyboard and mouse are in the machine. Press Ctrl+Alt+G to get them back." + : "Click the screen to use the keyboard and mouse in the machine. Ctrl+Alt+G gives them back."; + } + + // Pointer position inside the picture's displayed image, as 0..1. + private void move_pointer (double x, double y, bool force) { + if (tex_w == 0 || tex_h == 0) { + return; + } + int64 now = get_monotonic_time (); + if (!force && now - last_move < 16000) { + return; // ~60 Hz is plenty + } + last_move = now; + double w = picture.get_width (); + double h = picture.get_height (); + double scale = double.min (w / tex_w, h / tex_h); + double iw = tex_w * scale; + double ih = tex_h * scale; + double ox = (w - iw) / 2; + double oy = (h - ih) / 2; + double fx = (x - ox) / iw; + double fy = (y - oy) / ih; + if (fx < 0 || fx > 1 || fy < 0 || fy > 1) { + return; + } + machine.send_pointer (fx, fy); + } + + private string button_name (uint b) { + switch (b) { + case 2: return "middle"; + case 3: return "right"; + case 8: return "side"; + case 9: return "extra"; + default: return "left"; + } + } + + // Broadway reports browser key codes, not evdev, so map by key name there. + private static string? qcode_from_keyval (uint keyval) { + string? n = Gdk.keyval_name (keyval); + if (n == null) return null; + n = n.down (); + if (n.length == 1 && (n[0].isalnum ())) return n; + switch (n) { + case "return": return "ret"; + case "space": return "spc"; + case "backspace": return "backspace"; + case "tab": return "tab"; + case "escape": return "esc"; + case "minus": return "minus"; + case "equal": return "equal"; + case "comma": return "comma"; + case "period": return "dot"; + case "slash": return "slash"; + case "semicolon": return "semicolon"; + case "apostrophe": return "apostrophe"; + case "up": return "up"; + case "down": return "down"; + case "left": return "left"; + case "right": return "right"; + case "delete": return "delete"; + case "home": return "home"; + case "end": return "end"; + case "shift_l": return "shift"; + case "shift_r": return "shift_r"; + case "control_l": return "ctrl"; + case "control_r": return "ctrl_r"; + case "alt_l": return "alt"; + case "alt_r": return "alt_r"; + default: return null; + } + } + + private bool is_broadway () { + return get_display ().get_class ().get_name ().contains ("Broadway"); + } + + private bool handle_key (uint keyval, uint keycode, bool down) { + int evdev = (int) keycode - 8; + if (is_broadway ()) { + string? q = qcode_from_keyval (keyval); + if (!captured || q == null) return false; + machine.send_key (q, down); + return true; + } + if (evdev == 29 || evdev == 97) ctrl_down = down; + if (evdev == 56 || evdev == 100) alt_down = down; + if (!captured) { + return false; + } + // Ctrl+Alt+G releases the keyboard back to Machine Lab. + if (evdev == 34 && ctrl_down && alt_down) { + if (down) { + release_modifiers (); + captured = false; + update_hint (); + ((Gtk.Widget) this.get_root ()).grab_focus (); + } + return true; + } + string? q = keymap[evdev]; + if (q == null) { + return false; + } + machine.send_key (q, down); + return true; + } + + private void release_modifiers () { + foreach (var k in new string[] { "ctrl", "alt", "shift", "shift_r", "ctrl_r", "alt_r", "meta_l" }) { + if (machine.running) machine.send_key (k, false); + } + ctrl_down = false; + alt_down = false; + } + + private void save_screenshot () { + string path = machine.frame_path (); + if (!FileUtils.test (path, FileTest.EXISTS)) { + toast ("No screen to save yet"); + return; + } + var pictures = Environment.get_user_special_dir (UserDirectory.PICTURES) ?? Environment.get_home_dir (); + var stamp = new DateTime.now_local ().format ("%Y-%m-%d %H.%M.%S"); + var dest = Path.build_filename (pictures, "%s %s.png".printf (machine.name, stamp)); + try { + File.new_for_path (path).copy (File.new_for_path (dest), FileCopyFlags.OVERWRITE); + toast ("Saved to Pictures"); + } catch (Error e) { + toast ("Couldn't save: " + e.message); + } + } + + private static void build_keymap () { + if (keymap != null) { + return; + } + keymap = new HashTable (direct_hash, direct_equal); + string[] row = { + "1:esc", "2:1", "3:2", "4:3", "5:4", "6:5", "7:6", "8:7", "9:8", "10:9", "11:0", "12:minus", "13:equal", + "14:backspace", "15:tab", "16:q", "17:w", "18:e", "19:r", "20:t", "21:y", "22:u", "23:i", "24:o", "25:p", + "26:bracket_left", "27:bracket_right", "28:ret", "29:ctrl", "30:a", "31:s", "32:d", "33:f", "34:g", + "35:h", "36:j", "37:k", "38:l", "39:semicolon", "40:apostrophe", "41:grave_accent", "42:shift", + "43:backslash", "44:z", "45:x", "46:c", "47:v", "48:b", "49:n", "50:m", "51:comma", "52:dot", + "53:slash", "54:shift_r", "55:kp_multiply", "56:alt", "57:spc", "58:caps_lock", "59:f1", "60:f2", + "61:f3", "62:f4", "63:f5", "64:f6", "65:f7", "66:f8", "67:f9", "68:f10", "87:f11", "88:f12", + "97:ctrl_r", "100:alt_r", "102:home", "103:up", "104:pgup", "105:left", "106:right", "107:end", + "108:down", "109:pgdn", "110:insert", "111:delete", "125:meta_l", "126:meta_r", "127:menu" + }; + foreach (var e in row) { + var parts = e.split (":"); + keymap[int.parse (parts[0])] = parts[1]; + } + } +} From d790d0988ed5219b0584b5fce768638a74a011b8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:30:16 -0400 Subject: [PATCH 099/798] Add native Pantheon window snapping --- mseries/OS9-HANDOFF.md | 11 +- mseries/ROADMAP.md | 6 + mseries/pantheon-rust-compositor/README.md | 11 + .../src/handlers/xdg_shell.rs | 3 + rust/pantheon-compositor/src/input.rs | 7 +- rust/pantheon-compositor/src/main.rs | 1 + .../pantheon-compositor/src/pantheon_shell.rs | 4 + rust/pantheon-compositor/src/state.rs | 20 ++ rust/pantheon-compositor/src/tiling.rs | 293 ++++++++++++++++++ 9 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 rust/pantheon-compositor/src/tiling.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c7e6d82d5..c30575611 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -26,7 +26,16 @@ unimplemented/unqualified. Implemented relative pointer motion, pointer focus updates, and dispatch through active window move/resize grabs. Added xdg popup keyboard/pointer grabs rooted at managed toplevels, including nested serial validation; local `cargo check --bins` and `cargo build --bins` pass. Popup -behavior is not yet runtime-qualified. Applying monitor modes, Dock behavior and +behavior is not yet runtime-qualified. Added native `Super+Left/Right` half +snapping and `Super+Alt+U/I/J/K` plus configured Super+keypad quarter aliases. +Targets use each focused window's output work area and reflow with panel/output +changes; compositor move/resize and maximize requests clear the managed tile. +`Super+Shift+Left/Right` now moves a window to an adjacent output, preserving +its relative work-area offset or tile position. The snapping and monitor-move +code compile but have not been runtime-qualified. Drag-to-corner preview, +configurable shortcut bindings, multi-output behavior, and client minimum-size +handling remain to be implemented or verified. Applying +monitor modes, Dock behavior and M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` are connected while `HDMI-A-1` is disconnected. No modeset was attempted. Added diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 833748d56..8c758b324 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -45,6 +45,12 @@ tokens, consumes them after use and raises/focuses mapped target windows when the session is unlocked; app launch flows remain unverified. The `xdg_toplevel.set_minimized` request now unmaps and retains windows, which valid activation requests can restore at their previous position; this is not runtime-qualified. +The compositor now handles the existing half-snap and quarter-snap keyboard +chords, uses the focused window's output work area, and reflows tracked tiles +when work areas change. `Super+Shift+Left/Right` moves a window to the adjacent +output while preserving its work-area offset or tile position. This code is +compile-checked only. Drag-to-corner preview, configurable Rust-side bindings, +multi-output behavior, and runtime snapping qualification remain open. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 847a2cba5..23968a765 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -14,6 +14,17 @@ Anything beyond those needs is conditional on a named app or workflow. The prioritized list and upstream feature radar are in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md). +The nested prototype now handles the existing half-snap chords (`Super+Left` +and `Super+Right`) and quarter-snap chords (`Super+Alt+U/I/J/K` plus the +existing Super+keypad aliases). It computes rectangles from the focused +window's output work area, sends an xdg configure, and reflows managed tiles +when panel/output work areas change. Starting a compositor move/resize or +maximizing the window ends its managed tile. `Super+Shift+Left/Right` moves a +window to the adjacent output while preserving its work-area offset (or tile +position). These keyboard paths are compile checked only: runtime shortcut and +multi-output behavior, drag-to-corner preview, and configurable Rust-side +bindings remain open requirements. + The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That explains why swapping out Mutter underneath Gala is not a drop-in operation. diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 834f74522..b5dd668bb 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -65,6 +65,7 @@ impl XdgShellHandler for PantheonCompositor { let wl_surface = surface.wl_surface(); if let Some(start_data) = check_grab(&seat, wl_surface, serial) { + self.clear_tiling_for_surface(wl_surface); let pointer = seat.get_pointer().unwrap(); let window = self @@ -97,6 +98,7 @@ impl XdgShellHandler for PantheonCompositor { let wl_surface = surface.wl_surface(); if let Some(start_data) = check_grab(&seat, wl_surface, serial) { + self.clear_tiling_for_surface(wl_surface); let pointer = seat.get_pointer().unwrap(); let window = self @@ -193,6 +195,7 @@ impl XdgShellHandler for PantheonCompositor { .is_some_and(|focused| surface_has_root(&focused, wl_surface)); self.initially_positioned.remove(&id); self.managed_windows.remove(&id); + self.tiled_windows.remove(&id); self.minimized_windows.remove(&id); let window = self diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 923394e4a..da268ff92 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -30,9 +30,10 @@ impl PantheonCompositor { |state, modifiers, keysym| { if key_state == smithay::backend::input::KeyState::Pressed && !state.session_locked - && state - .shell_compat - .activate_key(keysym.modified_sym(), modifiers) + && (state.handle_tiling_shortcut(keysym.modified_sym(), modifiers) + || state + .shell_compat + .activate_key(keysym.modified_sym(), modifiers)) { FilterResult::Intercept(()) } else { diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 4906b5da4..08c33ec3b 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -11,6 +11,7 @@ mod pantheon_shell; mod protocols; mod shell_compat; mod state; +mod tiling; mod winit; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 00827ee00..88a90be1c 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -380,6 +380,7 @@ impl PantheonCompositor { }; let surface_id = wl_surface.id(); + self.clear_tiling_for_surface(wl_surface); let restore = *self .managed_windows .entry(surface_id.clone()) @@ -457,11 +458,14 @@ impl PantheonCompositor { for (surface, window, state) in managed { self.apply_window_mode(&surface, &window, state, state.maximized, state.fullscreen); } + self.reconfigure_tiled_windows(); } } pub fn handle_surface_commit(state: &mut PantheonCompositor, surface: &WlSurface) { if state.is_panel_surface(surface) { state.update_panel(surface, |_| {}); + } else { + state.update_tiling_after_commit(surface); } } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index e85cf861f..d76824eb1 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -80,6 +80,7 @@ pub struct PantheonCompositor { pub last_work_area: Option>, pub managed_windows: HashMap, + pub tiled_windows: HashMap, pub minimized_windows: HashMap< smithay::reexports::wayland_server::backend::ObjectId, (Window, Point), @@ -104,6 +105,24 @@ pub struct ManagedWindow { pub fullscreen: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TilePosition { + Left, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +#[derive(Clone)] +pub struct TiledWindow { + pub position: TilePosition, + pub output: smithay::output::Output, + pub rect: smithay::utils::Rectangle, + pub committed_size: Size, +} + impl PantheonCompositor { pub fn new( event_loop: &mut EventLoop, @@ -210,6 +229,7 @@ impl PantheonCompositor { panels: HashMap::new(), last_work_area: None, managed_windows: HashMap::new(), + tiled_windows: HashMap::new(), minimized_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, diff --git a/rust/pantheon-compositor/src/tiling.rs b/rust/pantheon-compositor/src/tiling.rs new file mode 100644 index 000000000..8125dfec1 --- /dev/null +++ b/rust/pantheon-compositor/src/tiling.rs @@ -0,0 +1,293 @@ +use smithay::{ + desktop::Window, + input::keyboard::{Keysym, ModifiersState, xkb}, + reexports::wayland_server::Resource, + utils::{Logical, Rectangle, Size}, + wayland::shell::xdg::ToplevelSurface, +}; + +use crate::state::{PantheonCompositor, TilePosition, TiledWindow}; + +impl PantheonCompositor { + pub fn handle_tiling_shortcut(&mut self, keysym: Keysym, modifiers: &ModifiersState) -> bool { + if self.session_locked || !modifiers.logo || modifiers.ctrl { + return false; + } + + if modifiers.shift && !modifiers.alt { + let direction = if keysym == xkb::keysyms::KEY_Left.into() { + Some(-1) + } else if keysym == xkb::keysyms::KEY_Right.into() { + Some(1) + } else { + None + }; + if let Some(direction) = direction { + self.move_focused_window_to_output(direction); + return true; + } + return false; + } + + let position = if !modifiers.alt && !modifiers.shift { + if keysym == xkb::keysyms::KEY_Left.into() { + Some(TilePosition::Left) + } else if keysym == xkb::keysyms::KEY_Right.into() { + Some(TilePosition::Right) + } else { + None + } + } else if !modifiers.shift { + match keysym.raw() { + k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_u)) + || (!modifiers.alt + && (k == u32::from(xkb::keysyms::KEY_KP_Home) + || k == u32::from(xkb::keysyms::KEY_KP_7))) => + { + Some(TilePosition::TopLeft) + } + k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_i)) + || (!modifiers.alt + && (k == u32::from(xkb::keysyms::KEY_KP_Prior) + || k == u32::from(xkb::keysyms::KEY_KP_9))) => + { + Some(TilePosition::TopRight) + } + k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_j)) + || (!modifiers.alt + && (k == u32::from(xkb::keysyms::KEY_KP_End) + || k == u32::from(xkb::keysyms::KEY_KP_1))) => + { + Some(TilePosition::BottomLeft) + } + k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_k)) + || (!modifiers.alt + && (k == u32::from(xkb::keysyms::KEY_KP_Next) + || k == u32::from(xkb::keysyms::KEY_KP_3))) => + { + Some(TilePosition::BottomRight) + } + _ => None, + } + } else { + None + }; + + let Some(position) = position else { + return false; + }; + self.tile_focused_window(position); + true + } + + fn tile_focused_window(&mut self, position: TilePosition) { + let Some(focused) = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + else { + return; + }; + let Some(window) = self.find_window_for_surface(&focused) else { + return; + }; + let Some(surface) = window.toplevel().cloned() else { + return; + }; + if self.is_panel_surface(surface.wl_surface()) { + return; + } + let Some(output) = self.space.outputs_for_element(&window).first().cloned() else { + return; + }; + let Some(output_geometry) = self.space.output_geometry(&output) else { + return; + }; + let rect = tile_rect(self.work_area_for_output(output_geometry), position); + self.managed_windows.remove(&surface.wl_surface().id()); + self.apply_tile(&surface, &window, output, position, rect); + } + + fn move_focused_window_to_output(&mut self, direction: i32) { + let Some(focused) = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + else { + return; + }; + let Some(window) = self.find_window_for_surface(&focused) else { + return; + }; + let Some(surface) = window.toplevel().cloned() else { + return; + }; + let Some(current_output) = self.space.outputs_for_element(&window).first().cloned() else { + return; + }; + let mut outputs = self + .space + .outputs() + .filter_map(|output| { + self.space + .output_geometry(output) + .map(|geometry| (output.clone(), geometry)) + }) + .collect::>(); + outputs.sort_by_key(|(_, geometry)| (geometry.loc.x, geometry.loc.y)); + let Some(current_index) = outputs + .iter() + .position(|(output, _)| output.name() == current_output.name()) + else { + return; + }; + let target_index = current_index as i32 + direction; + if !(0..outputs.len() as i32).contains(&target_index) { + return; + } + let (target_output, target_geometry) = outputs.swap_remove(target_index as usize); + let target_work_area = self.work_area_for_output(target_geometry); + if let Some(position) = self + .tiled_windows + .get(&surface.wl_surface().id()) + .map(|placement| placement.position) + { + let rect = tile_rect(target_work_area, position); + self.apply_tile(&surface, &window, target_output, position, rect); + } else if let Some(current_geometry) = self.space.element_geometry(&window) { + let current_output_geometry = self.space.output_geometry(¤t_output).unwrap(); + let current_work_area = self.work_area_for_output(current_output_geometry); + let offset = current_geometry.loc - current_work_area.loc; + let max_x = (target_work_area.loc.x + target_work_area.size.w + - current_geometry.size.w) + .max(target_work_area.loc.x); + let max_y = (target_work_area.loc.y + target_work_area.size.h + - current_geometry.size.h) + .max(target_work_area.loc.y); + let location: smithay::utils::Point = ( + (target_work_area.loc.x + offset.x).clamp(target_work_area.loc.x, max_x), + (target_work_area.loc.y + offset.y).clamp(target_work_area.loc.y, max_y), + ) + .into(); + self.space.map_element(window, location, false); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + } + + fn apply_tile( + &mut self, + surface: &ToplevelSurface, + window: &Window, + output: smithay::output::Output, + position: TilePosition, + rect: Rectangle, + ) { + surface.with_pending_state(|state| { + state.states.unset(smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::Maximized); + state.states.unset(smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::Fullscreen); + state.size = Some(rect.size); + }); + surface.send_pending_configure(); + self.space.map_element(window.clone(), rect.loc, false); + self.tiled_windows.insert( + surface.wl_surface().id(), + TiledWindow { + position, + output, + rect, + committed_size: window.geometry().size, + }, + ); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + pub(crate) fn reconfigure_tiled_windows(&mut self) { + let mut tiled = Vec::new(); + for window in self.space.elements() { + let Some(surface) = window.toplevel().cloned() else { + continue; + }; + let Some(placement) = self.tiled_windows.get(&surface.wl_surface().id()) else { + continue; + }; + tiled.push((surface, (*window).clone(), placement.clone())); + } + + for (surface, window, placement) in tiled { + let Some(output_geometry) = self.space.output_geometry(&placement.output) else { + self.tiled_windows.remove(&surface.wl_surface().id()); + continue; + }; + let rect = tile_rect( + self.work_area_for_output(output_geometry), + placement.position, + ); + self.apply_tile( + &surface, + &window, + placement.output, + placement.position, + rect, + ); + } + } + + pub(crate) fn clear_tiling_for_surface( + &mut self, + surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, + ) { + self.tiled_windows.remove(&surface.id()); + } + + pub(crate) fn update_tiling_after_commit( + &mut self, + surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, + ) { + let Some(window) = self.find_window_for_surface(surface) else { + return; + }; + let Some(placement) = self.tiled_windows.get(&surface.id()).cloned() else { + return; + }; + let committed_size = window.geometry().size; + if committed_size == placement.rect.size { + if let Some(placement) = self.tiled_windows.get_mut(&surface.id()) { + placement.committed_size = committed_size; + } + } else if committed_size != placement.committed_size { + // A client that commits a different size declined the tile (for + // example because its minimum-size hints do not fit the work area). + self.tiled_windows.remove(&surface.id()); + } + } +} + +fn tile_rect(area: Rectangle, position: TilePosition) -> Rectangle { + let half_width = area.size.w / 2; + let half_height = area.size.h / 2; + let (right, bottom, width, height) = match position { + TilePosition::Left => (false, false, half_width, area.size.h), + TilePosition::Right => (true, false, area.size.w - half_width, area.size.h), + TilePosition::TopLeft => (false, false, half_width, half_height), + TilePosition::TopRight => (true, false, area.size.w - half_width, half_height), + TilePosition::BottomLeft => (false, true, half_width, area.size.h - half_height), + TilePosition::BottomRight => ( + true, + true, + area.size.w - half_width, + area.size.h - half_height, + ), + }; + let min_width = 1.min(width); + let min_height = 1.min(height); + Rectangle::new( + ( + area.loc.x + if right { half_width } else { 0 }, + area.loc.y + if bottom { half_height } else { 0 }, + ) + .into(), + Size::from((width.max(min_width), height.max(min_height))), + ) +} From 9bc437b8908348b25d25abe8b83ea364e5a6dc5c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:33:57 -0400 Subject: [PATCH 100/798] Load compositor shortcuts from GSettings --- mseries/OS9-HANDOFF.md | 8 +- mseries/ROADMAP.md | 6 +- mseries/pantheon-rust-compositor/README.md | 8 +- rust/Cargo.lock | 189 +++++++++++++++++++ rust/pantheon-compositor/Cargo.toml | 1 + rust/pantheon-compositor/src/shell_compat.rs | 6 +- rust/pantheon-compositor/src/state.rs | 3 + rust/pantheon-compositor/src/tiling.rs | 183 ++++++++++++------ 8 files changed, 331 insertions(+), 73 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c30575611..6ada79955 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -32,9 +32,11 @@ Targets use each focused window's output work area and reflow with panel/output changes; compositor move/resize and maximize requests clear the managed tile. `Super+Shift+Left/Right` now moves a window to an adjacent output, preserving its relative work-area offset or tile position. The snapping and monitor-move -code compile but have not been runtime-qualified. Drag-to-corner preview, -configurable shortcut bindings, multi-output behavior, and client minimum-size -handling remain to be implemented or verified. Applying +code compile but have not been runtime-qualified. Shortcuts load from the +existing Mutter, Pantheon and GNOME GSettings schemas at session startup, with +built-in defaults if a schema is unavailable. Drag-to-corner preview, live +binding reload, multi-output behavior, and client minimum-size handling remain +to be implemented or verified. Applying monitor modes, Dock behavior and M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 8c758b324..4f70164a6 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -49,8 +49,10 @@ The compositor now handles the existing half-snap and quarter-snap keyboard chords, uses the focused window's output work area, and reflows tracked tiles when work areas change. `Super+Shift+Left/Right` moves a window to the adjacent output while preserving its work-area offset or tile position. This code is -compile-checked only. Drag-to-corner preview, configurable Rust-side bindings, -multi-output behavior, and runtime snapping qualification remain open. +compile-checked only. It loads shortcut arrays from the installed Mutter, +Pantheon and GNOME GSettings schemas at session startup, with built-in defaults +when those schemas are unavailable. Drag-to-corner preview, live binding +reload, multi-output behavior, and runtime snapping qualification remain open. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 23968a765..94dcb4b85 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -21,9 +21,11 @@ window's output work area, sends an xdg configure, and reflows managed tiles when panel/output work areas change. Starting a compositor move/resize or maximizing the window ends its managed tile. `Super+Shift+Left/Right` moves a window to the adjacent output while preserving its work-area offset (or tile -position). These keyboard paths are compile checked only: runtime shortcut and -multi-output behavior, drag-to-corner preview, and configurable Rust-side -bindings remain open requirements. +position). Shortcuts load at session startup from the existing Mutter half-tile, +Pantheon quarter-tile, and GNOME monitor-move GSettings keys; built-in defaults +apply if those schemas or keys are unavailable. Restart the session after +changing bindings. These keyboard paths are compile checked only: runtime +shortcut and multi-output behavior plus drag-to-corner preview remain open. The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e9a5578c9..db5103f4a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -303,6 +303,16 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-expr" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba9e9ec16c447027685b1f897b720e18e9a8afd00bd7332c483537e38086c9f" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.5" @@ -538,12 +548,32 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.34" @@ -563,6 +593,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "futures-task" version = "0.3.34" @@ -576,6 +617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -625,6 +667,36 @@ dependencies = [ "rand_core", ] +[[package]] +name = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -642,12 +714,73 @@ version = "0.33.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4" +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1211,6 +1344,7 @@ name = "pantheon-compositor" version = "0.1.0" dependencies = [ "bitflags", + "gio", "smithay", "tracing", "tracing-subscriber", @@ -1546,6 +1680,15 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1727,6 +1870,25 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -1808,6 +1970,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "1.1.2+spec-1.1.0" @@ -1838,6 +2015,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + [[package]] name = "tracing" version = "0.1.44" @@ -1970,6 +2153,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index 0064e751a..bbef9835a 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -40,6 +40,7 @@ wayland-backend = "0.3.17" wayland-client = "0.31.11" wayland-server = "0.31.14" zbus = "=5.19.0" +gio = "0.20" [dependencies.smithay] git = "https://github.com/Smithay/smithay.git" diff --git a/rust/pantheon-compositor/src/shell_compat.rs b/rust/pantheon-compositor/src/shell_compat.rs index 6d66ff8b9..2c96dffea 100644 --- a/rust/pantheon-compositor/src/shell_compat.rs +++ b/rust/pantheon-compositor/src/shell_compat.rs @@ -36,7 +36,7 @@ struct AcceleratorModifiers { } #[derive(Clone, Copy, Debug)] -struct ParsedAccelerator { +pub(crate) struct ParsedAccelerator { keysym: Keysym, modifiers: AcceleratorModifiers, } @@ -332,7 +332,7 @@ fn serve_shell_compat( } } -fn parse_accelerator(value: &str, _grab_flags: u32) -> Option { +pub(crate) fn parse_accelerator(value: &str, _grab_flags: u32) -> Option { let mut remaining = value.trim(); let mut modifiers = AcceleratorModifiers::default(); while remaining.starts_with('<') { @@ -359,7 +359,7 @@ fn parse_accelerator(value: &str, _grab_flags: u32) -> Option Some(ParsedAccelerator { keysym, modifiers }) } -fn matches_accelerator( +pub(crate) fn matches_accelerator( accelerator: ParsedAccelerator, keysym: Keysym, modifiers: &ModifiersState, diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index d76824eb1..db9446555 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -45,6 +45,7 @@ use crate::display_config::{ use crate::native::NativeBackend; use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::shell_compat::ShellCompatHandle; +use crate::tiling::TilingBindings; #[cfg(feature = "native-drm")] use smithay::reexports::calloop::channel::Sender; @@ -81,6 +82,7 @@ pub struct PantheonCompositor { pub managed_windows: HashMap, pub tiled_windows: HashMap, + pub tiling_bindings: TilingBindings, pub minimized_windows: HashMap< smithay::reexports::wayland_server::backend::ObjectId, (Window, Point), @@ -230,6 +232,7 @@ impl PantheonCompositor { last_work_area: None, managed_windows: HashMap::new(), tiled_windows: HashMap::new(), + tiling_bindings: TilingBindings::load(), minimized_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, diff --git a/rust/pantheon-compositor/src/tiling.rs b/rust/pantheon-compositor/src/tiling.rs index 8125dfec1..5accb813c 100644 --- a/rust/pantheon-compositor/src/tiling.rs +++ b/rust/pantheon-compositor/src/tiling.rs @@ -1,82 +1,141 @@ +use gio::{Settings, SettingsBackend, SettingsSchemaSource, prelude::SettingsExtManual}; use smithay::{ desktop::Window, - input::keyboard::{Keysym, ModifiersState, xkb}, + input::keyboard::{Keysym, ModifiersState}, reexports::wayland_server::Resource, utils::{Logical, Rectangle, Size}, wayland::shell::xdg::ToplevelSurface, }; +use crate::shell_compat::{ParsedAccelerator, matches_accelerator, parse_accelerator}; use crate::state::{PantheonCompositor, TilePosition, TiledWindow}; -impl PantheonCompositor { - pub fn handle_tiling_shortcut(&mut self, keysym: Keysym, modifiers: &ModifiersState) -> bool { - if self.session_locked || !modifiers.logo || modifiers.ctrl { - return false; - } +#[derive(Clone, Copy)] +enum TilingAction { + Tile(TilePosition), + MoveOutput(i32), +} - if modifiers.shift && !modifiers.alt { - let direction = if keysym == xkb::keysyms::KEY_Left.into() { - Some(-1) - } else if keysym == xkb::keysyms::KEY_Right.into() { - Some(1) - } else { - None - }; - if let Some(direction) = direction { - self.move_focused_window_to_output(direction); - return true; +#[derive(Clone, Copy)] +struct TilingBinding { + accelerator: ParsedAccelerator, + action: TilingAction, +} + +pub struct TilingBindings { + bindings: Vec, +} + +impl TilingBindings { + pub fn load() -> Self { + let mut bindings = Vec::new(); + for (key, action, fallback) in [ + ( + "top-left", + TilingAction::Tile(TilePosition::TopLeft), + vec!["KP_Home", "KP_7", "u"], + ), + ( + "top-right", + TilingAction::Tile(TilePosition::TopRight), + vec!["KP_Prior", "KP_9", "i"], + ), + ( + "bottom-left", + TilingAction::Tile(TilePosition::BottomLeft), + vec!["KP_End", "KP_1", "j"], + ), + ( + "bottom-right", + TilingAction::Tile(TilePosition::BottomRight), + vec!["KP_Next", "KP_3", "k"], + ), + ] { + for accelerator in settings_shortcuts("org.techmore.mseries.snapping", key, &fallback) { + push_binding(&mut bindings, &accelerator, action); } - return false; } - let position = if !modifiers.alt && !modifiers.shift { - if keysym == xkb::keysyms::KEY_Left.into() { - Some(TilePosition::Left) - } else if keysym == xkb::keysyms::KEY_Right.into() { - Some(TilePosition::Right) - } else { - None + for (schema_id, key, action, fallback) in [ + ( + "org.gnome.mutter.keybindings", + "toggle-tiled-left", + TilingAction::Tile(TilePosition::Left), + vec!["Left"], + ), + ( + "org.gnome.mutter.keybindings", + "toggle-tiled-right", + TilingAction::Tile(TilePosition::Right), + vec!["Right"], + ), + ( + "org.gnome.desktop.wm.keybindings", + "move-to-monitor-left", + TilingAction::MoveOutput(-1), + vec!["Left"], + ), + ( + "org.gnome.desktop.wm.keybindings", + "move-to-monitor-right", + TilingAction::MoveOutput(1), + vec!["Right"], + ), + ] { + for accelerator in settings_shortcuts(schema_id, key, &fallback) { + push_binding(&mut bindings, &accelerator, action); } - } else if !modifiers.shift { - match keysym.raw() { - k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_u)) - || (!modifiers.alt - && (k == u32::from(xkb::keysyms::KEY_KP_Home) - || k == u32::from(xkb::keysyms::KEY_KP_7))) => - { - Some(TilePosition::TopLeft) - } - k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_i)) - || (!modifiers.alt - && (k == u32::from(xkb::keysyms::KEY_KP_Prior) - || k == u32::from(xkb::keysyms::KEY_KP_9))) => - { - Some(TilePosition::TopRight) - } - k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_j)) - || (!modifiers.alt - && (k == u32::from(xkb::keysyms::KEY_KP_End) - || k == u32::from(xkb::keysyms::KEY_KP_1))) => - { - Some(TilePosition::BottomLeft) - } - k if (modifiers.alt && k == u32::from(xkb::keysyms::KEY_k)) - || (!modifiers.alt - && (k == u32::from(xkb::keysyms::KEY_KP_Next) - || k == u32::from(xkb::keysyms::KEY_KP_3))) => - { - Some(TilePosition::BottomRight) - } - _ => None, - } - } else { - None - }; + } + + Self { bindings } + } +} - let Some(position) = position else { +fn settings_shortcuts(schema_id: &str, key: &str, fallback: &[&str]) -> Vec { + let Some(source) = SettingsSchemaSource::default() else { + return fallback.iter().map(|value| (*value).to_owned()).collect(); + }; + let Some(schema) = source.lookup(schema_id, true) else { + return fallback.iter().map(|value| (*value).to_owned()).collect(); + }; + if !schema.has_key(key) { + return fallback.iter().map(|value| (*value).to_owned()).collect(); + } + let settings = Settings::new_full(&schema, None::<&SettingsBackend>, None); + settings + .strv(key) + .iter() + .map(|value| value.to_string()) + .collect() +} + +fn push_binding(bindings: &mut Vec, accelerator: &str, action: TilingAction) { + if let Some(accelerator) = parse_accelerator(accelerator, 0) { + bindings.push(TilingBinding { + accelerator, + action, + }); + } +} + +impl PantheonCompositor { + pub fn handle_tiling_shortcut(&mut self, keysym: Keysym, modifiers: &ModifiersState) -> bool { + if self.session_locked { + return false; + } + let Some(binding) = self + .tiling_bindings + .bindings + .iter() + .find(|binding| matches_accelerator(binding.accelerator, keysym, modifiers)) + .copied() + else { return false; }; - self.tile_focused_window(position); + match binding.action { + TilingAction::Tile(position) => self.tile_focused_window(position), + TilingAction::MoveOutput(direction) => self.move_focused_window_to_output(direction), + } true } From de649e0704a19bbcc28b8227441eacb71ef9aca0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:36:45 -0400 Subject: [PATCH 101/798] Add drag corner snapping preview --- mseries/OS9-HANDOFF.md | 4 +- mseries/ROADMAP.md | 3 + mseries/pantheon-rust-compositor/README.md | 5 +- rust/pantheon-compositor/src/cursor.rs | 19 +++- .../src/grabs/move_grab.rs | 25 ++++- .../src/handlers/xdg_shell.rs | 3 + rust/pantheon-compositor/src/input.rs | 8 ++ rust/pantheon-compositor/src/state.rs | 8 +- rust/pantheon-compositor/src/tiling.rs | 103 +++++++++++++++++- 9 files changed, 167 insertions(+), 11 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 6ada79955..33ba689bc 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -36,7 +36,9 @@ code compile but have not been runtime-qualified. Shortcuts load from the existing Mutter, Pantheon and GNOME GSettings schemas at session startup, with built-in defaults if a schema is unavailable. Drag-to-corner preview, live binding reload, multi-output behavior, and client minimum-size handling remain -to be implemented or verified. Applying +to be runtime-qualified. Implemented drag previews in the 40-logical-pixel +output corners, Escape cancellation, fixed-size exclusion, and snap-on-release. +Applying monitor modes, Dock behavior and M-series DRM/KMS remain unqualified. Read-only host enumeration sees `/dev/dri/card1` on the `asahi` driver and `/dev/dri/card2` on `apple-drm`; `eDP-1` and `DP-2` diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 4f70164a6..81cad9d8c 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -53,6 +53,9 @@ compile-checked only. It loads shortcut arrays from the installed Mutter, Pantheon and GNOME GSettings schemas at session startup, with built-in defaults when those schemas are unavailable. Drag-to-corner preview, live binding reload, multi-output behavior, and runtime snapping qualification remain open. +Quarter drag previews now appear in the 40-logical-pixel output corners, +Escape cancels the snap, fixed-size windows are excluded, and release applies +the selected quarter. This path is compile-checked only. `ShowOSD`, lock-screen action modes and physical media key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 94dcb4b85..02a19ecd5 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -25,7 +25,10 @@ position). Shortcuts load at session startup from the existing Mutter half-tile, Pantheon quarter-tile, and GNOME monitor-move GSettings keys; built-in defaults apply if those schemas or keys are unavailable. Restart the session after changing bindings. These keyboard paths are compile checked only: runtime -shortcut and multi-output behavior plus drag-to-corner preview remain open. +shortcut and multi-output behavior remain open. During a compositor move, the +40-logical-pixel output corners show a translucent quarter preview; releasing +snaps the window, and Escape cancels the snap. Fixed-size windows are excluded. +The drag preview and release path also need runtime qualification. The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That diff --git a/rust/pantheon-compositor/src/cursor.rs b/rust/pantheon-compositor/src/cursor.rs index 5304282ac..bd4c46337 100644 --- a/rust/pantheon-compositor/src/cursor.rs +++ b/rust/pantheon-compositor/src/cursor.rs @@ -6,6 +6,7 @@ use smithay::{ element::{ Kind, Wrap, memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + solid::{SolidColorBuffer, SolidColorRenderElement}, surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, }, }, @@ -22,6 +23,7 @@ render_elements! { pub CursorRenderElement where R: ImportAll + ImportMem; Surface=WaylandSurfaceRenderElement, Memory=MemoryRenderBufferRenderElement, + Solid=SolidColorRenderElement, WrappedSurface=Wrap>, } @@ -77,7 +79,7 @@ where } let scale = Scale::from(output.current_scale().fractional_scale()); - match &state.cursor_status { + let mut elements = match &state.cursor_status { CursorImageStatus::Hidden => Vec::new(), CursorImageStatus::Surface(surface) if surface.alive() => { let hotspot = with_states(surface, |states| { @@ -119,7 +121,22 @@ where .into_iter() .collect() } + }; + + if let Some(preview) = state.corner_snap_preview.as_ref() + && preview.output.name() == output.name() + { + let buffer = SolidColorBuffer::new(preview.rect.size, [0.18, 0.46, 0.92, 1.0]); + let location = (preview.rect.loc - output_geometry.loc) + .to_f64() + .to_physical(scale) + .to_i32_round(); + let element = + SolidColorRenderElement::from_buffer(&buffer, location, scale, 0.28, Kind::Unspecified); + elements.push(element.into()); } + + elements } pub fn render_lock_surface_elements( diff --git a/rust/pantheon-compositor/src/grabs/move_grab.rs b/rust/pantheon-compositor/src/grabs/move_grab.rs index a55bec9bb..741cad67c 100644 --- a/rust/pantheon-compositor/src/grabs/move_grab.rs +++ b/rust/pantheon-compositor/src/grabs/move_grab.rs @@ -8,9 +8,9 @@ use smithay::{ desktop::Window, input::pointer::{ AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, - GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, - GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, - PointerInnerHandle, RelativeMotionEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, + GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, + MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, }, reexports::wayland_server::protocol::wl_surface::WlSurface, utils::{Logical, Point}, @@ -37,6 +37,7 @@ impl PointerGrab for MoveSurfaceGrab { let new_location = self.initial_window_location.to_f64() + delta; data.space .map_element(self.window.clone(), new_location.to_i32_round(), true); + data.update_corner_snap_preview(&self.window, event.location); } fn relative_motion( @@ -62,8 +63,16 @@ impl PointerGrab for MoveSurfaceGrab { const BTN_LEFT: u32 = 0x110; if !handle.current_pressed().contains(&BTN_LEFT) { + let snap = (!data.corner_snap_cancelled) + .then(|| data.corner_snap_preview.clone()) + .flatten(); + let window = self.window.clone(); // No more buttons are pressed, release the grab. handle.unset_grab(self, data, event.serial, event.time, true); + data.finish_move_snap(); + if let Some(snap) = snap { + data.commit_move_snap(&window, snap); + } } } @@ -76,7 +85,11 @@ impl PointerGrab for MoveSurfaceGrab { handle.axis(data, details) } - fn frame(&mut self, data: &mut PantheonCompositor, handle: &mut PointerInnerHandle<'_, PantheonCompositor>) { + fn frame( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + ) { handle.frame(data); } @@ -156,5 +169,7 @@ impl PointerGrab for MoveSurfaceGrab { &self.start_data } - fn unset(&mut self, _data: &mut PantheonCompositor) {} + fn unset(&mut self, data: &mut PantheonCompositor) { + data.finish_move_snap(); + } } diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index b5dd668bb..d64fa781a 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -66,6 +66,9 @@ impl XdgShellHandler for PantheonCompositor { if let Some(start_data) = check_grab(&seat, wl_surface, serial) { self.clear_tiling_for_surface(wl_surface); + self.move_grab_active = true; + self.corner_snap_cancelled = false; + self.corner_snap_preview = None; let pointer = seat.get_pointer().unwrap(); let window = self diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index da268ff92..6006871f2 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -28,6 +28,14 @@ impl PantheonCompositor { serial, time, |state, modifiers, keysym| { + if key_state == smithay::backend::input::KeyState::Pressed + && state.move_grab_active + && keysym.modified_sym() + == smithay::input::keyboard::xkb::keysyms::KEY_Escape.into() + { + state.cancel_move_snap(); + return FilterResult::Intercept(()); + } if key_state == smithay::backend::input::KeyState::Pressed && !state.session_locked && (state.handle_tiling_shortcut(keysym.modified_sym(), modifiers) diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index db9446555..6ef3f2776 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -45,7 +45,7 @@ use crate::display_config::{ use crate::native::NativeBackend; use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::shell_compat::ShellCompatHandle; -use crate::tiling::TilingBindings; +use crate::tiling::{CornerSnapPreview, TilingBindings}; #[cfg(feature = "native-drm")] use smithay::reexports::calloop::channel::Sender; @@ -83,6 +83,9 @@ pub struct PantheonCompositor { HashMap, pub tiled_windows: HashMap, pub tiling_bindings: TilingBindings, + pub corner_snap_preview: Option, + pub corner_snap_cancelled: bool, + pub move_grab_active: bool, pub minimized_windows: HashMap< smithay::reexports::wayland_server::backend::ObjectId, (Window, Point), @@ -233,6 +236,9 @@ impl PantheonCompositor { managed_windows: HashMap::new(), tiled_windows: HashMap::new(), tiling_bindings: TilingBindings::load(), + corner_snap_preview: None, + corner_snap_cancelled: false, + move_grab_active: false, minimized_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, diff --git a/rust/pantheon-compositor/src/tiling.rs b/rust/pantheon-compositor/src/tiling.rs index 5accb813c..693372aec 100644 --- a/rust/pantheon-compositor/src/tiling.rs +++ b/rust/pantheon-compositor/src/tiling.rs @@ -3,8 +3,11 @@ use smithay::{ desktop::Window, input::keyboard::{Keysym, ModifiersState}, reexports::wayland_server::Resource, - utils::{Logical, Rectangle, Size}, - wayland::shell::xdg::ToplevelSurface, + utils::{Logical, Point, Rectangle, Size}, + wayland::{ + compositor, + shell::xdg::{SurfaceCachedState, ToplevelSurface}, + }, }; use crate::shell_compat::{ParsedAccelerator, matches_accelerator, parse_accelerator}; @@ -22,6 +25,13 @@ struct TilingBinding { action: TilingAction, } +#[derive(Clone)] +pub struct CornerSnapPreview { + pub output: smithay::output::Output, + pub position: TilePosition, + pub rect: Rectangle, +} + pub struct TilingBindings { bindings: Vec, } @@ -167,6 +177,95 @@ impl PantheonCompositor { self.apply_tile(&surface, &window, output, position, rect); } + pub(crate) fn update_corner_snap_preview( + &mut self, + window: &Window, + pointer: Point, + ) { + if self.corner_snap_cancelled { + return; + } + let Some(surface) = window.toplevel() else { + self.corner_snap_preview = None; + return; + }; + if self.is_panel_surface(surface.wl_surface()) + || self + .managed_windows + .get(&surface.wl_surface().id()) + .is_some_and(|managed| managed.fullscreen) + { + self.corner_snap_preview = None; + return; + } + let (min_size, max_size) = compositor::with_states(surface.wl_surface(), |states| { + let mut cached = states.cached_state.get::(); + let current = cached.current(); + (current.min_size, current.max_size) + }); + let fixed_width = min_size.w > 0 && max_size.w == min_size.w; + let fixed_height = min_size.h > 0 && max_size.h == min_size.h; + if fixed_width && fixed_height { + self.corner_snap_preview = None; + return; + } + + self.corner_snap_preview = self.space.outputs().find_map(|output| { + let geometry = self.space.output_geometry(output)?; + if !geometry.to_f64().contains(pointer) { + return None; + } + let near_left = pointer.x < (geometry.loc.x + 40) as f64; + let near_right = pointer.x >= (geometry.loc.x + geometry.size.w - 40) as f64; + let near_top = pointer.y < (geometry.loc.y + 40) as f64; + let near_bottom = pointer.y >= (geometry.loc.y + geometry.size.h - 40) as f64; + let position = match (near_left, near_right, near_top, near_bottom) { + (true, _, true, _) => TilePosition::TopLeft, + (_, true, true, _) => TilePosition::TopRight, + (true, _, _, true) => TilePosition::BottomLeft, + (_, true, _, true) => TilePosition::BottomRight, + _ => return None, + }; + let rect = tile_rect(self.work_area_for_output(geometry), position); + Some(CornerSnapPreview { + output: output.clone(), + position, + rect, + }) + }); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + pub(crate) fn cancel_move_snap(&mut self) { + self.corner_snap_cancelled = true; + self.corner_snap_preview = None; + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + pub(crate) fn finish_move_snap(&mut self) { + self.move_grab_active = false; + self.corner_snap_cancelled = false; + self.corner_snap_preview = None; + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + pub(crate) fn commit_move_snap(&mut self, window: &Window, preview: CornerSnapPreview) { + let Some(surface) = window.toplevel().cloned() else { + return; + }; + self.managed_windows.remove(&surface.wl_surface().id()); + self.apply_tile( + &surface, + window, + preview.output, + preview.position, + preview.rect, + ); + } + fn move_focused_window_to_output(&mut self, direction: i32) { let Some(focused) = self .seat From 05f0f7396ced0b3e7bde8463b937638b7e414e20 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:39:19 -0400 Subject: [PATCH 102/798] Track Pantheon work areas per output --- mseries/OS9-HANDOFF.md | 5 +- mseries/ROADMAP.md | 4 +- mseries/pantheon-rust-compositor/README.md | 5 ++ .../src/handlers/xdg_shell.rs | 14 ++++- rust/pantheon-compositor/src/native.rs | 2 +- .../pantheon-compositor/src/pantheon_shell.rs | 56 ++++++++++++++----- rust/pantheon-compositor/src/state.rs | 6 +- 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 33ba689bc..4489b0939 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5,7 +5,10 @@ On branch `codex/rust-mutter-pantheon`, recent changes reconfigure already maximized windows when a committed Pantheon panel change alters its exclusive work area; unchanged panel repaints do not trigger redundant xdg configure -events. Added `pantheon-shell-probe`, which binds shell/panel v2 and sends +events. Work-area state now tracks each active output, maximized/fullscreen +windows and popup constraints use their current output, and removing a panel +reflows managed windows. Multi-output panel placement remains unqualified. +Added `pantheon-shell-probe`, which binds shell/panel v2 and sends `get_panel`, anchor, size, hide-mode, and offset requests. Local `cargo check` and `cargo build --bins` pass. The 12-second Xvfb nested run completed the probe over Wayland using Mesa llvmpipe. Then installed Wingpanel 8.0.4 ran against the diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 81cad9d8c..031bf1079 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -19,7 +19,9 @@ advertises Pantheon's protocol v2, places panel surfaces using anchor/size/offset, and applies exclusive panel geometry when configuring maximized windows. A changed panel work area now reconfigures already maximized windows, while unchanged panel repaints do not send redundant -configures. New committed toplevels are centered and cascaded on the first +configures. Work-area caches now cover every active output; maximized/fullscreen +windows and popup constraints use the output containing their window, and panel +removal triggers work-area reflow. New committed toplevels are centered and cascaded on the first output, activated, and focused; absolute pointer events map to all logical outputs. These new behaviors are compile-checked but not runtime-qualified. The v2 protocol probe completed a nested Xvfb run and sent panel requests over the diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 02a19ecd5..19f0f8bb8 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -30,6 +30,11 @@ shortcut and multi-output behavior remain open. During a compositor move, the snaps the window, and Escape cancels the snap. Fixed-size windows are excluded. The drag preview and release path also need runtime qualification. +Panel reservations and managed maximize/tile layouts are tracked per output +connector. Maximized/fullscreen windows and popup constraints use the window's +current output, and removing a panel releases its work area. Multi-output +placement and panel behavior remain runtime-unqualified. + The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That explains why swapping out Mutter underneath Gala is not a drop-in operation. diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index d64fa781a..d3601210e 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -428,8 +428,18 @@ impl PantheonCompositor { return; }; - let output = self.space.outputs().next().unwrap(); - let output_geo = self.space.output_geometry(output).unwrap(); + let Some(output) = self + .space + .outputs_for_element(window) + .first() + .cloned() + .or_else(|| self.space.outputs().next().cloned()) + else { + return; + }; + let Some(output_geo) = self.space.output_geometry(&output) else { + return; + }; let window_geo = self.space.element_geometry(window).unwrap(); // The target geometry for the positioner should be relative to its parent's geometry, so diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 22babec8b..2d3de58e2 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -579,7 +579,7 @@ impl NativeKmsDevice { } } - state.last_work_area = None; + state.last_work_areas.clear(); let panels = state .space .elements() diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 88a90be1c..6c19a5d3d 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use smithay::{ desktop::Window, reexports::wayland_server::{ @@ -37,8 +39,11 @@ pub struct ShellSurfaceData { pub surface: WlSurface, } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct PanelState { + /// Connector name selected for this panel. Missing or disconnected names + /// fall back to the first active output and are updated on placement. + pub output_name: Option, /// Pantheon protocol values: top=0, bottom=1, left=2, right=3. pub anchor: Option, /// Non-positive dimensions use the committed surface size. @@ -227,13 +232,16 @@ impl PantheonCompositor { pub fn remove_panel(&mut self, surface: &WlSurface) { self.panels.remove(&surface.id()); + if self.refresh_work_areas() { + self.reconfigure_managed_windows(); + } } pub fn update_panel(&mut self, surface: &WlSurface, update: impl FnOnce(&mut PanelState)) { let panel = self.panels.entry(surface.id()).or_default(); update(panel); self.position_panel(surface); - if self.refresh_work_area() { + if self.refresh_work_areas() { self.reconfigure_managed_windows(); } } @@ -290,18 +298,19 @@ impl PantheonCompositor { ) } - fn refresh_work_area(&mut self) -> bool { - let Some(output) = self.space.outputs().next().cloned() else { - return false; - }; - let Some(output_geometry) = self.space.output_geometry(&output) else { - return false; - }; - let work_area = self.work_area_for_output(output_geometry); - if self.last_work_area == Some(work_area) { + fn refresh_work_areas(&mut self) -> bool { + let work_areas = self + .space + .outputs() + .filter_map(|output| { + let geometry = self.space.output_geometry(output)?; + Some((output.name(), self.work_area_for_output(geometry))) + }) + .collect::>(); + if self.last_work_areas == work_areas { return false; } - self.last_work_area = Some(work_area); + self.last_work_areas = work_areas; true } @@ -317,7 +326,7 @@ impl PantheonCompositor { } fn position_panel(&mut self, surface: &WlSurface) { - let Some(panel) = self.panels.get(&surface.id()).copied() else { + let Some(mut panel) = self.panels.get(&surface.id()).cloned() else { return; }; let Some(anchor) = panel.anchor else { @@ -326,12 +335,23 @@ impl PantheonCompositor { let Some(window) = self.find_window_for_surface(surface) else { return; }; - let Some(output) = self.space.outputs().next().cloned() else { + let Some(output) = panel + .output_name + .as_deref() + .and_then(|name| self.space.outputs().find(|output| output.name() == name)) + .cloned() + .or_else(|| self.space.outputs_for_element(&window).first().cloned()) + .or_else(|| self.space.outputs().next().cloned()) + else { return; }; let Some(output_geometry) = self.space.output_geometry(&output) else { return; }; + if panel.output_name.as_deref() != Some(output.name().as_str()) { + panel.output_name = Some(output.name()); + self.panels.insert(surface.id(), panel.clone()); + } let surface_size = window.geometry().size; let width = if panel.width > 0 { @@ -411,7 +431,13 @@ impl PantheonCompositor { maximized: bool, fullscreen: bool, ) { - let Some(output) = self.space.outputs().next().cloned() else { + let Some(output) = self + .space + .outputs_for_element(window) + .first() + .cloned() + .or_else(|| self.space.outputs().next().cloned()) + else { return; }; let Some(output_geometry) = self.space.output_geometry(&output) else { diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 6ef3f2776..e1aa52701 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -78,7 +78,7 @@ pub struct PantheonCompositor { pub display_config: DisplayConfigHandle, pub shell_compat: ShellCompatHandle, pub panels: HashMap, - pub last_work_area: Option>, + pub last_work_areas: HashMap>, pub managed_windows: HashMap, pub tiled_windows: HashMap, @@ -232,7 +232,7 @@ impl PantheonCompositor { display_config, shell_compat, panels: HashMap::new(), - last_work_area: None, + last_work_areas: HashMap::new(), managed_windows: HashMap::new(), tiled_windows: HashMap::new(), tiling_bindings: TilingBindings::load(), @@ -408,7 +408,7 @@ impl PantheonCompositor { } self.display_config.update_many(snapshots); - self.last_work_area = None; + self.last_work_areas.clear(); let panels = self .space .elements() From 6ae925e09ddd691521e77cd579434a27cd3ff8c2 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:40:24 -0400 Subject: [PATCH 103/798] Machine Lab display: PPM frames (~30 fps), dedicated qmp-view socket so the viewer never blocks control commands; mseries-vm gets the view socket Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/README.md | 2 +- mseries/machine-lab/launcher/run-vm.sh | 8 +- mseries/machine-lab/src/machine.vala | 116 ++++++++++++++----------- mseries/machine-lab/src/viewer.vala | 91 ++++++++++++++----- mseries/vms/mseries-vm | 5 +- 5 files changed, 144 insertions(+), 78 deletions(-) diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md index b9566f7ca..a5b84f366 100644 --- a/mseries/machine-lab/README.md +++ b/mseries/machine-lab/README.md @@ -16,7 +16,7 @@ free-space check, toasts, light/dark and accent following the system. QEMU. Sign-in details are shown in the app and saved with owner-only permissions. - Own ISO path: drop or choose any ARM64 `.iso`; once the disk holds real data the installer detaches itself. - **Pause / Resume** over QEMU's QMP socket (`qmp.sock` in the machine folder), plus Shut Down / Force Stop. -- **Display tab**: the machine's desktop inside the app (QEMU `screendump` frames over QMP, mouse and keyboard sent back; click the screen to capture, Ctrl+Alt+G to release; Ctrl+Alt+Del and screenshot buttons). Machines start headless so the screen lives in the app, not a separate QEMU window. +- **Display tab**: the machine's desktop inside the app (QEMU `screendump` frames as uncompressed PPM over QMP, about 30 fps while the screen changes; mouse and keyboard sent back over a dedicated `qmp-view.sock` so the viewer never blocks `mseries-vm` control commands; click the screen to capture, Ctrl+Alt+G to release; Ctrl+Alt+Del and screenshot buttons). Machines start headless so the screen lives in the app, not a separate QEMU window. - Live console from the launcher output, or the guest serial log for imported machines. ```sh diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh index 3ebb0183b..e667a81be 100755 --- a/mseries/machine-lab/launcher/run-vm.sh +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -3,8 +3,9 @@ # run-vm.sh ISO [VM_DIR] [CIDATA_ISO] boot an installer (ISO "-" = none) # run-vm.sh --disk-only VM_DIR boot the installed disk # Hardware comes from the environment: MACHINE_CPUS, MACHINE_MEM_GIB, -# MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME, MACHINE_DISPLAY (gtk|none). A QMP socket (qmp.sock in -# VM_DIR) lets Machine Lab pause and resume the guest. +# MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME, MACHINE_DISPLAY (gtk|none). Two QMP sockets in +# VM_DIR: qmp.sock (control: pause, resume, mseries-vm) and qmp-view.sock (the +# Machine Lab display) so a viewer never blocks control commands. set -euo pipefail CPUS=${MACHINE_CPUS:-4} @@ -65,7 +66,7 @@ fi if [ ! -e "$VM_DIR/disk.qcow2" ]; then "$QEMU_IMG" create -f qcow2 "$VM_DIR/disk.qcow2" "${DISK_GIB}G" fi -rm -f "$VM_DIR/qmp.sock" +rm -f "$VM_DIR/qmp.sock" "$VM_DIR/qmp-view.sock" DISPLAY_ARGS=(-display gtk,gl=off) [ "${MACHINE_DISPLAY:-gtk}" = none ] && DISPLAY_ARGS=(-display none) @@ -108,6 +109,7 @@ QEMU_ARGS+=( -device virtio-gpu-pci -device virtio-rng-pci -qmp unix:"$VM_DIR/qmp.sock",server=on,wait=off + -qmp unix:"$VM_DIR/qmp-view.sock",server=on,wait=off "${DISPLAY_ARGS[@]}" -boot menu=on ) diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index b8fe55856..7bab7db4d 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -106,65 +106,79 @@ public class Machine : Object { // ---- QMP: pause and resume ---- - // One persistent QMP connection per machine, shared by the poller (main - // thread) and the display worker thread, so access is serialized. + // QEMU serves one QMP client per socket, so a connection held open would + // block `mseries-vm stop`, key, screenshot and friends. Control calls + // (status, pause) therefore connect, ask and hang up. The display uses its + // own socket (qmp-view.sock) and may stay connected; guests started before + // that socket existed fall back to short connections on qmp.sock. private Mutex qmp_lock = Mutex (); - private SocketConnection? qmp_conn = null; - private DataInputStream? qmp_in = null; + private SocketConnection? view_conn = null; + private DataInputStream? view_in = null; - private bool qmp_open () { - if (qmp_conn != null) { - return true; - } - var sock = Path.build_filename (dir, "qmp.sock"); - if (!FileUtils.test (sock, FileTest.EXISTS)) { - return false; - } + private void view_close () { try { - var client = new SocketClient (); - client.timeout = 3; - qmp_conn = client.connect (new UnixSocketAddress (sock)); - qmp_in = new DataInputStream (qmp_conn.input_stream); - qmp_in.read_line (); // greeting - qmp_conn.output_stream.write ("{\"execute\":\"qmp_capabilities\"}\n".data); - qmp_in.read_line (); - return true; - } catch (Error e) { - qmp_close (); - return false; + if (view_conn != null) { + view_conn.close (); + } + } catch (Error e) {} + view_conn = null; + view_in = null; + } + + private string? qmp_exchange (SocketConnection conn, DataInputStream din, string json) throws Error { + conn.output_stream.write ((json + "\n").data); + string? reply = din.read_line (); + // Events can arrive before the return value. + while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { + reply = din.read_line (); } + return reply; } - private void qmp_close () { - try { - if (qmp_conn != null) { - qmp_conn.close (); - } - } catch (Error e) {} - qmp_conn = null; - qmp_in = null; + private bool qmp_handshake (SocketConnection conn, DataInputStream din) throws Error { + din.read_line (); // greeting + conn.output_stream.write ("{\"execute\":\"qmp_capabilities\"}\n".data); + din.read_line (); + return true; } - /* Send one QMP command (a full JSON object) and return its reply line. */ - public string? qmp_raw (string json) { + /* Send one QMP command (a full JSON object) and return its reply line. + * With view = true, use the display's dedicated socket when there is one. */ + public string? qmp_raw (string json, bool view = false) { qmp_lock.lock (); string? reply = null; - for (int attempt = 0; attempt < 2 && reply == null; attempt++) { - if (!qmp_open ()) { - break; - } - try { - qmp_conn.output_stream.write ((json + "\n").data); - reply = qmp_in.read_line (); - // Events can arrive before the return value. - while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { - reply = qmp_in.read_line (); + string view_sock = Path.build_filename (dir, "qmp-view.sock"); + bool dedicated = view && FileUtils.test (view_sock, FileTest.EXISTS); + string sock = dedicated ? view_sock : Path.build_filename (dir, "qmp.sock"); + if (FileUtils.test (sock, FileTest.EXISTS)) { + for (int attempt = 0; attempt < 2 && reply == null; attempt++) { + SocketConnection? conn = null; + DataInputStream? din = null; + try { + if (dedicated && view_conn != null) { + conn = view_conn; + din = view_in; + } else { + var client = new SocketClient (); + client.timeout = 3; + conn = client.connect (new UnixSocketAddress (sock)); + din = new DataInputStream (conn.input_stream); + qmp_handshake (conn, din); + if (dedicated) { + view_conn = conn; + view_in = din; + } + } + reply = qmp_exchange (conn, din, json); + } catch (Error e) { + reply = null; + } + if (reply == null && dedicated) { + view_close (); // stale socket (machine restarted): reconnect once + } + if (!dedicated && conn != null) { + try { conn.close (); } catch (Error e) {} } - } catch (Error e) { - reply = null; - } - if (reply == null) { - qmp_close (); // stale socket (machine restarted): reconnect once } } qmp_lock.unlock (); @@ -185,7 +199,7 @@ public class Machine : Object { public string frame_path () { var d = Path.build_filename (Environment.get_user_runtime_dir (), "machine-lab"); DirUtils.create_with_parents (d, 0700); - return Path.build_filename (d, id + ".png"); + return Path.build_filename (d, id + ".ppm"); } /* Ask QEMU for the current screen. At most one request is in flight. */ @@ -196,7 +210,7 @@ public class Machine : Object { frame_busy = true; string path = frame_path (); new Thread ("ml-frame", () => { - var reply = qmp_raw ("{\"execute\":\"screendump\",\"arguments\":{\"filename\":\"%s\",\"format\":\"png\"}}".printf (path)); + var reply = qmp_raw ("{\"execute\":\"screendump\",\"arguments\":{\"filename\":\"%s\",\"format\":\"ppm\"}}".printf (path), true); bool ok = reply != null && reply.contains ("\"return\""); Idle.add (() => { frame_busy = false; @@ -222,7 +236,7 @@ public class Machine : Object { if (cmd == "") { break; } - qmp_raw ("{\"execute\":\"input-send-event\",\"arguments\":{\"events\":%s}}".printf (cmd)); + qmp_raw ("{\"execute\":\"input-send-event\",\"arguments\":{\"events\":%s}}".printf (cmd), true); } }); } diff --git a/mseries/machine-lab/src/viewer.vala b/mseries/machine-lab/src/viewer.vala index 43e801d9b..ceedeef64 100644 --- a/mseries/machine-lab/src/viewer.vala +++ b/mseries/machine-lab/src/viewer.vala @@ -138,23 +138,32 @@ public class Viewer : Gtk.Box { } } - /* Only fetch frames while this tab is on screen. */ + /* Only fetch frames while this tab is on screen: ~30 fps while the screen + * changes, ~7 fps once it has been still for a moment. */ public void set_active (bool on) { active = on; if (on && timer == 0) { - timer = Timeout.add (90, () => { - if (active && machine.running) { - machine.request_frame (); - } - return Source.CONTINUE; - }); - machine.request_frame (); + schedule (0); } else if (!on && timer != 0) { Source.remove (timer); timer = 0; } } + private void schedule (uint ms) { + timer = Timeout.add (ms, () => { + timer = 0; + if (!active) { + return Source.REMOVE; + } + if (machine.running) { + machine.request_frame (); + } + schedule (still > 20 ? 150 : 33); + return Source.REMOVE; + }); + } + private void sync_state () { stack.visible_child_name = machine.running ? "screen" : "off"; if (!machine.running) { @@ -163,17 +172,58 @@ public class Viewer : Gtk.Box { } } + private Bytes? last_frame = null; + private int still = 0; + + /* Frames arrive as binary PPM (P6): no compression on either end, which is + * far cheaper than PNG and lets the view run near 30 fps. */ private void on_frame (string path) { + uint8[] data; try { - var tex = Gdk.Texture.from_filename (path); - tex_w = tex.get_width (); - tex_h = tex.get_height (); - picture.paintable = tex; - size_label.label = "%d × %d".printf (tex_w, tex_h); - stack.visible_child_name = "screen"; + FileUtils.get_data (path, out data); } catch (Error e) { - // A frame can be caught mid-write; the next one will do. + return; + } + if (data.length < 16 || data[0] != 'P' || data[1] != '6') { + return; + } + // Header: "P6 " then one whitespace byte, then RGB. + int pos = 2; + int[] header = {}; + while (header.length < 3 && pos < data.length) { + while (pos < data.length && (data[pos] == ' ' || data[pos] == '\n' || data[pos] == '\r' || data[pos] == '\t')) pos++; + if (pos < data.length && data[pos] == '#') { + while (pos < data.length && data[pos] != '\n') pos++; + continue; + } + int v = 0; + int start = pos; + while (pos < data.length && data[pos] >= '0' && data[pos] <= '9') { + v = v * 10 + (data[pos] - '0'); + pos++; + } + if (pos == start) return; + header += v; + } + pos++; + int w = header[0]; + int h = header[1]; + if (w <= 0 || h <= 0 || data.length - pos < w * h * 3) { + return; // caught mid-write; the next frame will do } + var whole = new Bytes (data); + if (last_frame != null && last_frame.compare (whole) == 0) { + still++; // nothing changed: let the poller slow down + return; + } + last_frame = whole; + still = 0; + tex_w = w; + tex_h = h; + picture.paintable = new Gdk.MemoryTexture (w, h, Gdk.MemoryFormat.R8G8B8, + whole.slice (pos, pos + w * h * 3), w * 3); + size_label.label = "%d × %d".printf (w, h); + stack.visible_child_name = "screen"; } private void update_hint () { @@ -297,19 +347,18 @@ public class Viewer : Gtk.Box { } private void save_screenshot () { - string path = machine.frame_path (); - if (!FileUtils.test (path, FileTest.EXISTS)) { + var tex = picture.paintable as Gdk.Texture; + if (tex == null) { toast ("No screen to save yet"); return; } var pictures = Environment.get_user_special_dir (UserDirectory.PICTURES) ?? Environment.get_home_dir (); var stamp = new DateTime.now_local ().format ("%Y-%m-%d %H.%M.%S"); var dest = Path.build_filename (pictures, "%s %s.png".printf (machine.name, stamp)); - try { - File.new_for_path (path).copy (File.new_for_path (dest), FileCopyFlags.OVERWRITE); + if (tex.save_to_png (dest)) { toast ("Saved to Pictures"); - } catch (Error e) { - toast ("Couldn't save: " + e.message); + } else { + toast ("Couldn't save the screenshot"); } } diff --git a/mseries/vms/mseries-vm b/mseries/vms/mseries-vm index 70916d639..8f9690f5c 100755 --- a/mseries/vms/mseries-vm +++ b/mseries/vms/mseries-vm @@ -372,7 +372,7 @@ cmd_start() { rotate_file() { [[ -f $1 ]] && mv -f "$1" "$1.prev"; return 0; } rotate_file "$dir/serial.log" rotate_file "$dir/qemu.log" - rm -f "$dir/qmp.sock" + rm -f "$dir/qmp.sock" "$dir/qmp-view.sock" local machine="virt,accel=kvm" @@ -430,6 +430,7 @@ cmd_start() { fi cmd+=(-serial "file:$dir/serial.log" -monitor none -qmp "unix:$dir/qmp.sock,server=on,wait=off" + -qmp "unix:$dir/qmp-view.sock,server=on,wait=off" -msg timestamp=on) if (( headless )); then cmd+=(-display none) @@ -495,7 +496,7 @@ cmd_stop() { kill -KILL "$pid" 2>/dev/null || true sleep 1 fi - rm -f "$dir/qemu.pid" "$dir/qmp.sock" + rm -f "$dir/qemu.pid" "$dir/qmp.sock" "$dir/qmp-view.sock" stop_swtpm "$name" echo "VM '$name' stopped." } From 2e2f3e78943ef3e750a388d3f7e682c931a702ec Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:40:34 -0400 Subject: [PATCH 104/798] Place new windows on active output --- mseries/OS9-HANDOFF.md | 3 + mseries/ROADMAP.md | 5 +- mseries/pantheon-rust-compositor/README.md | 5 ++ .../src/handlers/xdg_shell.rs | 58 +++++++++++++------ 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4489b0939..de0e1b135 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -8,6 +8,9 @@ work area; unchanged panel repaints do not trigger redundant xdg configure events. Work-area state now tracks each active output, maximized/fullscreen windows and popup constraints use their current output, and removing a panel reflows managed windows. Multi-output panel placement remains unqualified. +New toplevels center and cascade in the focused window's work area, falling +back to the pointer output and then the primary output. This placement path +builds but remains runtime-unqualified. Added `pantheon-shell-probe`, which binds shell/panel v2 and sends `get_panel`, anchor, size, hide-mode, and offset requests. Local `cargo check` and `cargo build --bins` pass. The 12-second Xvfb nested run completed the probe diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 031bf1079..bc6ef431e 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -21,8 +21,9 @@ when configuring maximized windows. A changed panel work area now reconfigures already maximized windows, while unchanged panel repaints do not send redundant configures. Work-area caches now cover every active output; maximized/fullscreen windows and popup constraints use the output containing their window, and panel -removal triggers work-area reflow. New committed toplevels are centered and cascaded on the first -output, activated, and focused; absolute pointer events map to all logical +removal triggers work-area reflow. New committed toplevels are centered and +cascaded in the focused window's work area, falling back to the pointer output +and then the primary output; they are activated and focused. Absolute pointer events map to all logical outputs. These new behaviors are compile-checked but not runtime-qualified. The v2 protocol probe completed a nested Xvfb run and sent panel requests over the Wayland socket. Installed Wingpanel 8.0.4 also connected in an diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 19f0f8bb8..c2356d746 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -35,6 +35,11 @@ connector. Maximized/fullscreen windows and popup constraints use the window's current output, and removing a panel releases its work area. Multi-output placement and panel behavior remain runtime-unqualified. +New toplevels center and cascade in the focused window's work area; when there +is no focused toplevel, placement follows the pointer's output, then falls back +to the primary output. This launch placement compiles in both renderer builds +but has not been runtime-qualified. + The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That explains why swapping out Mutter underneath Gala is not a drop-in operation. diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index d3601210e..990cfc9ed 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -356,27 +356,51 @@ pub fn handle_commit(state: &mut PantheonCompositor, surface: &WlSurface) { window.toplevel().unwrap().send_configure(); } - // The first committed buffer makes the requested window size known. - // Center new app windows on the first available output and offset later - // launches so they remain visible and easy to find. + // Place new app windows on the focused window's output, falling back + // to the pointer output and then the primary output. Use its work area + // so the panel/dock keeps its reserved space. let surface_id = surface.id(); let size = window.geometry().size; if size.w > 0 && size.h > 0 && !state.initially_positioned.contains(&surface_id) { let cascade = (state.space.elements().count().saturating_sub(1) % 10) as i32 * 24; - let placement = state - .space - .outputs() - .next() - .and_then(|output| state.space.output_geometry(output)) - .map(|output_geometry| { - let max_x = output_geometry.loc.x + (output_geometry.size.w - size.w).max(0); - let max_y = output_geometry.loc.y + (output_geometry.size.h - size.h).max(0); - let x = - (output_geometry.loc.x + (output_geometry.size.w - size.w) / 2 + cascade) - .clamp(output_geometry.loc.x, max_x); - let y = - (output_geometry.loc.y + (output_geometry.size.h - size.h) / 2 + cascade) - .clamp(output_geometry.loc.y, max_y); + let focused_output = state + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .map(|mut focused| { + while let Some(parent) = smithay::wayland::compositor::get_parent(&focused) { + focused = parent; + } + focused + }) + .and_then(|focused| state.find_window_for_surface(&focused)) + .and_then(|focused| state.space.outputs_for_element(&focused).first().cloned()); + let pointer_location = state + .seat + .get_pointer() + .map(|pointer| pointer.current_location()); + let pointer_output = pointer_location.and_then(|pointer| { + state.space.outputs().find_map(|output| { + state + .space + .output_geometry(output) + .filter(|geometry| geometry.to_f64().contains(pointer)) + .map(|_| output.clone()) + }) + }); + let output = focused_output + .or(pointer_output) + .or_else(|| state.space.outputs().next().cloned()); + let placement = output + .and_then(|output| state.space.output_geometry(&output)) + .map(|output_geometry| state.work_area_for_output(output_geometry)) + .map(|work_area| { + let max_x = work_area.loc.x + (work_area.size.w - size.w).max(0); + let max_y = work_area.loc.y + (work_area.size.h - size.h).max(0); + let x = (work_area.loc.x + (work_area.size.w - size.w) / 2 + cascade) + .clamp(work_area.loc.x, max_x); + let y = (work_area.loc.y + (work_area.size.h - size.h) / 2 + cascade) + .clamp(work_area.loc.y, max_y); (x, y) }); From e41ba8fa389cc362534aa2c9b722f9e129ff88c6 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:42:49 -0400 Subject: [PATCH 105/798] Honor configured primary output --- mseries/OS9-HANDOFF.md | 4 ++- mseries/ROADMAP.md | 4 ++- mseries/pantheon-rust-compositor/README.md | 5 +++ .../src/handlers/xdg_shell.rs | 4 +-- rust/pantheon-compositor/src/native.rs | 33 +++++++++++++++++-- .../pantheon-compositor/src/pantheon_shell.rs | 4 +-- rust/pantheon-compositor/src/state.rs | 18 +++++++++- rust/pantheon-compositor/src/winit.rs | 1 + 8 files changed, 63 insertions(+), 10 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index de0e1b135..7cbfb8b5a 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -10,7 +10,9 @@ windows and popup constraints use their current output, and removing a panel reflows managed windows. Multi-output panel placement remains unqualified. New toplevels center and cascade in the focused window's work area, falling back to the pointer output and then the primary output. This placement path -builds but remains runtime-unqualified. +builds but remains runtime-unqualified. The primary connector now follows +saved monitor profiles and `ApplyMonitorsConfig`; hotplug preserves it while +connected, otherwise preferring eDP and then the first available output. Added `pantheon-shell-probe`, which binds shell/panel v2 and sends `get_panel`, anchor, size, hide-mode, and offset requests. Local `cargo check` and `cargo build --bins` pass. The 12-second Xvfb nested run completed the probe diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index bc6ef431e..ddc8dc7e9 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -24,7 +24,9 @@ windows and popup constraints use the output containing their window, and panel removal triggers work-area reflow. New committed toplevels are centered and cascaded in the focused window's work area, falling back to the pointer output and then the primary output; they are activated and focused. Absolute pointer events map to all logical -outputs. These new behaviors are compile-checked but not runtime-qualified. The +outputs. The configured primary connector is retained through monitor profile +application and hotplug while it remains connected, falling back to eDP or the +first available output. These new behaviors are compile-checked but not runtime-qualified. The v2 protocol probe completed a nested Xvfb run and sent panel requests over the Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland run, sent `get_panel`, anchor and hide-mode requests, diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index c2356d746..22d78fa53 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -40,6 +40,11 @@ is no focused toplevel, placement follows the pointer's output, then falls back to the primary output. This launch placement compiles in both renderer builds but has not been runtime-qualified. +The configured primary connector is retained from monitor profiles and +`ApplyMonitorsConfig`; native hotplug preserves it while connected, then falls +back to the internal panel or first available output. Fallback placement and +hotplug primary selection remain runtime-unqualified. + The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That explains why swapping out Mutter underneath Gala is not a drop-in operation. diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 990cfc9ed..f3a4549d4 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -390,7 +390,7 @@ pub fn handle_commit(state: &mut PantheonCompositor, surface: &WlSurface) { }); let output = focused_output .or(pointer_output) - .or_else(|| state.space.outputs().next().cloned()); + .or_else(|| state.primary_output()); let placement = output .and_then(|output| state.space.output_geometry(&output)) .map(|output_geometry| state.work_area_for_output(output_geometry)) @@ -457,7 +457,7 @@ impl PantheonCompositor { .outputs_for_element(window) .first() .cloned() - .or_else(|| self.space.outputs().next().cloned()) + .or_else(|| self.primary_output()) else { return; }; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 2d3de58e2..f27615e9e 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -519,8 +519,25 @@ impl NativeKmsDevice { output.output.name(), ) }); + let primary_output_name = state + .primary_output_name + .as_ref() + .filter(|name| { + self.outputs + .iter() + .any(|output| &output.output.name() == *name) + }) + .cloned() + .or_else(|| { + self.outputs + .iter() + .find(|output| output.output.name().starts_with("eDP-")) + .map(|output| output.output.name()) + }) + .or_else(|| self.outputs.first().map(|output| output.output.name())); + state.primary_output_name = primary_output_name.clone(); let mut x = 0; - for (index, output) in self.outputs.iter().enumerate() { + for output in &self.outputs { state.space.map_output(&output.output, (x, 0)); state.track_output_for_lock(&output.output); if let Some(mode) = output.output.current_mode() { @@ -540,7 +557,8 @@ impl NativeKmsDevice { transform: 0, x, y: 0, - primary: index == 0, + primary: primary_output_name.as_deref() + == Some(output.output.name().as_str()), }); x += mode.size.w; } @@ -551,7 +569,10 @@ impl NativeKmsDevice { .outputs() .filter_map(|output| state.space.output_geometry(output)) .collect::>(); - let fallback = output_geometries.first().copied(); + let fallback = state + .primary_output() + .and_then(|output| state.space.output_geometry(&output)) + .or_else(|| output_geometries.first().copied()); if let Some(fallback) = fallback { let windows = state.space.elements().cloned().collect::>(); for window in windows { @@ -806,6 +827,9 @@ impl NativeKmsDevice { let global_id = output.create_global::(&state.display_handle); state.space.map_output(&output, (x, y)); + if primary { + state.primary_output_name = Some(name.clone()); + } state.display_config.update(OutputSnapshot { connector: name.clone(), vendor: "Apple".into(), @@ -835,6 +859,9 @@ impl NativeKmsDevice { if outputs.is_empty() { return Err("could not initialize any connected KMS output".into()); } + if state.primary_output_name.is_none() { + state.primary_output_name = outputs.first().map(|output| output.output.name()); + } Ok(Self { node, diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 6c19a5d3d..26c0ce4f7 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -341,7 +341,7 @@ impl PantheonCompositor { .and_then(|name| self.space.outputs().find(|output| output.name() == name)) .cloned() .or_else(|| self.space.outputs_for_element(&window).first().cloned()) - .or_else(|| self.space.outputs().next().cloned()) + .or_else(|| self.primary_output()) else { return; }; @@ -436,7 +436,7 @@ impl PantheonCompositor { .outputs_for_element(window) .first() .cloned() - .or_else(|| self.space.outputs().next().cloned()) + .or_else(|| self.primary_output()) else { return; }; diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index e1aa52701..a44b1e8f1 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -77,6 +77,7 @@ pub struct PantheonCompositor { pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, pub shell_compat: ShellCompatHandle, + pub primary_output_name: Option, pub panels: HashMap, pub last_work_areas: HashMap>, pub managed_windows: @@ -231,6 +232,7 @@ impl PantheonCompositor { pantheon_shell_state, display_config, shell_compat, + primary_output_name: None, panels: HashMap::new(), last_work_areas: HashMap::new(), managed_windows: HashMap::new(), @@ -259,6 +261,14 @@ impl PantheonCompositor { } } + pub fn primary_output(&self) -> Option { + self.primary_output_name + .as_deref() + .and_then(|name| self.space.outputs().find(|output| output.name() == name)) + .cloned() + .or_else(|| self.space.outputs().next().cloned()) + } + fn apply_display_configuration(&mut self, request: &ApplyRequest) -> Result<(), String> { if request.logical_monitors.is_empty() { return Err("at least one logical monitor is required".into()); @@ -347,6 +357,11 @@ impl PantheonCompositor { return Ok(()); } + let primary_output_name = outputs + .iter() + .find(|output| configurations[&output.name()].4) + .map(|output| output.name()); + let requested_modes = configurations .iter() .map(|(name, (_, _, _, _, _, mode))| (name.clone(), *mode)) @@ -406,6 +421,7 @@ impl PantheonCompositor { primary, }); } + self.primary_output_name = primary_output_name; self.display_config.update_many(snapshots); self.last_work_areas.clear(); @@ -572,7 +588,7 @@ impl FractionalScaleHandler for PantheonCompositor { let output = self .find_window_for_surface(&root) .and_then(|window| self.space.outputs_for_element(&window).first().cloned()) - .or_else(|| self.space.outputs().next().cloned()); + .or_else(|| self.primary_output()); if let Some(output) = output { smithay::wayland::compositor::with_states(&surface, |states| { with_fractional_scale(states, |fractional_scale| { diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 2361a3c12..cc1272d49 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -61,6 +61,7 @@ pub fn init_winit( output.set_preferred(mode); state.space.map_output(&output, (x, y)); + state.primary_output_name = Some(output.name()); state.display_config.update(OutputSnapshot { connector: "winit".into(), vendor: "Smithay".into(), From 5522ea3b3696bd63b661de56af7a7f5ecef43591 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:44:26 -0400 Subject: [PATCH 106/798] Preserve output layout on hotplug --- mseries/OS9-HANDOFF.md | 14 ++++++ mseries/ROADMAP.md | 10 ++-- mseries/pantheon-rust-compositor/README.md | 6 ++- rust/pantheon-compositor/src/native.rs | 55 +++++++++++++++++----- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 7cbfb8b5a..3fb8e5559 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5672,3 +5672,17 @@ raises it and focuses it; destroying a minimized toplevel drops the retained entry. Default and `native-drm` compositor builds pass. This code has not been exercised with an app client; task-switcher discovery and runtime restoration remain unverified. + +### Preserve output layout during native hotplug — 2026-10-19 + +Native connector reconciliation now captures existing logical output +positions before removing disconnected outputs. Connected outputs retain those +positions; newly discovered outputs are added to the right of the current +layout, with their logical width accounting for scale. Connector snapshots +retain the matching x/y positions and scale. This avoids resetting a user's +multi-monitor arrangement whenever a connector changes. Local default +`cargo check`/`cargo build` and the `native-drm` build pass. A repository-wide +`cargo fmt --check` still reports unrelated formatting differences in +`mseries-power` and `grabs/resize_grab.rs`; the touched native compositor +source was formatted directly. No display session was launched and hotplug +behavior remains unqualified on hardware. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ddc8dc7e9..0159431f8 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -90,10 +90,12 @@ and vblank completion. It prioritizes eDP and places additional outputs to its right. DisplayConfig no longer advertises a fabricated Winit output in native mode. Both build configurations pass locally. Native mode has not been launched because it deliberately takes over the active display. Connector -addition/removal reconciliation is implemented with a two-second poll, but -hotplug, user-configured layouts, session switching and actual M-series -scanout remain unqualified. Local `cargo check --bins` and `cargo build --bins` pass both with -and without `native-drm`; they establish compilation only, not successful +addition/removal reconciliation is implemented with a two-second poll. On +hotplug, connected outputs keep their current positions and new outputs are +placed after the existing layout. Hotplug, saved-layout restore, session +switching and actual M-series scanout remain unqualified. Local +`cargo check --bins` and `cargo build --bins` pass with and without +`native-drm`; they establish compilation only, not successful connector modesetting. Read-only DisplayConfig now lists the modes reported by each native DRM diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 22d78fa53..ca459ead3 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -42,8 +42,10 @@ but has not been runtime-qualified. The configured primary connector is retained from monitor profiles and `ApplyMonitorsConfig`; native hotplug preserves it while connected, then falls -back to the internal panel or first available output. Fallback placement and -hotplug primary selection remain runtime-unqualified. +back to the internal panel or first available output. Native hotplug also keeps +the positions of outputs that remain connected and places newly discovered +outputs beyond the current layout. Fallback placement and hotplug behavior +remain runtime-unqualified. The pinned Gala 8.6.2 audit found 79 Vala files directly using `Meta.*` across 24,534 lines of Gala core source, with Clutter/Cogl/Mtk dependencies. That diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index f27615e9e..8f70e0b4e 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -382,6 +382,19 @@ impl NativeKmsDevice { info.interface_id(), ) }); + // Keep the user's current layout for outputs that remain connected. + // Repacking every connector from x=0 here would silently erase a + // configured vertical or offset arrangement on any hotplug. + let existing_locations = self + .outputs + .iter() + .filter_map(|output| { + state + .space + .output_geometry(&output.output) + .map(|geometry| (output.output.name(), geometry.loc)) + }) + .collect::>(); let disconnected = self .outputs .iter() @@ -536,31 +549,51 @@ impl NativeKmsDevice { }) .or_else(|| self.outputs.first().map(|output| output.output.name())); state.primary_output_name = primary_output_name.clone(); - let mut x = 0; + let mut next_x = state + .space + .outputs() + .filter_map(|output| state.space.output_geometry(output)) + .map(|geometry| geometry.loc.x + geometry.size.w) + .max() + .unwrap_or(0); for output in &self.outputs { - state.space.map_output(&output.output, (x, 0)); + let name = output.output.name(); + let location = existing_locations.get(&name).copied().unwrap_or_else(|| { + let location = (next_x, 0).into(); + if let Some(mode) = output.output.current_mode() { + let scale = output.output.current_scale().fractional_scale() as f64; + next_x += (mode.size.w as f64 / scale).round() as i32; + } + location + }); + output + .output + .change_current_state(None, None, None, Some(location)); + state.space.map_output(&output.output, location); state.track_output_for_lock(&output.output); if let Some(mode) = output.output.current_mode() { - output - .output - .change_current_state(None, None, None, Some((x, 0).into())); + let scale = output.output.current_scale().fractional_scale() as f64; + let scale = if scale.is_finite() && scale > 0.0 { + scale + } else { + 1.0 + }; state.display_config.update(OutputSnapshot { - connector: output.output.name(), + connector: name.clone(), vendor: "Apple".into(), - product: output.output.name(), + product: name, serial: "Unknown".into(), width: mode.size.w, height: mode.size.h, refresh_millihz: mode.refresh, modes: output_mode_snapshots(&output.modes), - scale: 1.0, + scale, transform: 0, - x, - y: 0, + x: location.x, + y: location.y, primary: primary_output_name.as_deref() == Some(output.output.name().as_str()), }); - x += mode.size.w; } } From 3cba9073b3c344aae95e433adc28d07703b1aea4 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:49:07 -0400 Subject: [PATCH 107/798] Render shell compatibility OSDs --- mseries/OS9-HANDOFF.md | 13 ++ mseries/ROADMAP.md | 5 +- rust/pantheon-compositor/README.md | 11 +- rust/pantheon-compositor/src/cursor.rs | 150 ++++++++++++++++++- rust/pantheon-compositor/src/shell_compat.rs | 98 +++++++++++- rust/pantheon-compositor/src/state.rs | 13 +- 6 files changed, 279 insertions(+), 11 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 3fb8e5559..8420653dd 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5686,3 +5686,16 @@ multi-monitor arrangement whenever a connector changes. Local default `mseries-power` and `grabs/resize_grab.rs`; the touched native compositor source was formatted directly. No display session was launched and hotplug behavior remains unqualified on hardware. + +### Render the Shell `ShowOSD` compatibility call — 2026-10-19 + +The `org.gnome.Shell.ShowOSD(a{sv})` method now retains the current icon, +level, optional maximum level, connector and legacy monitor index. Both render +paths draw a brief volume or brightness card with an icon and level meter on +the selected connector, falling back to the primary output. The native backend +receives redraw requests when the OSD appears and expires. This provides +feedback for the existing Shell accelerator clients without relying on Gala. +It does not draw caller-supplied text labels or reproduce Pantheon's complete +OSD styling; physical key activation and visual runtime behavior still need +qualification. Local default `cargo check`/`cargo build` and the +`native-drm` build pass. No tests or display session were run. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 0159431f8..aeb67f03d 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -61,8 +61,9 @@ reload, multi-output behavior, and runtime snapping qualification remain open. Quarter drag previews now appear in the 40-logical-pixel output corners, Escape cancels the snap, fixed-size windows are excluded, and release applies the selected quarter. This path is compile-checked only. -`ShowOSD`, lock-screen action modes and physical media -key activation remain unimplemented or unqualified. Relative pointer motion now +`ShowOSD` renders a minimal brightness/volume indicator with icon and level +meter; text labels, full Pantheon styling, lock-screen action modes and physical +media-key activation remain unimplemented or unqualified. Relative pointer motion now updates pointer focus and drives the compositor's existing move/resize grabs; xdg popup requests now install Smithay keyboard/pointer grabs for managed toplevel roots. Both features build locally; popup behavior has not been diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 1b8f6cb28..ac340bf1d 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -138,10 +138,13 @@ it has not yet been exercised with a real application menu. The session bus also exposes the small `org.gnome.Shell` accelerator API used by Wingpanel and its sound indicator. Accelerator registrations are matched through the compositor's active XKB state and reported with -`AcceleratorActivated`; `ShowOSD` is currently a no-op, and lock-screen action -modes are not implemented. Separately, the observed `org.gnome.ScreenSaver` -interface launches the configured session locker; its action and authentication -path still need runtime qualification. +`AcceleratorActivated`. `ShowOSD` now renders a short brightness or volume +indicator, including the requested output connector and normalized level when +provided. It uses compositor-drawn icons and a meter; text labels and the full +Pantheon OSD styling are not implemented. Lock-screen action modes remain +unimplemented. Separately, the observed `org.gnome.ScreenSaver` interface +launches the configured session locker; its action and authentication path +still need runtime qualification. The code is based on Smithay's `smallvil` learning compositor at pinned commit `023b78b1c5a05ca512767cacc07efca55fae8be4`; see `SMITHAY-LICENSE.txt`. Keep the diff --git a/rust/pantheon-compositor/src/cursor.rs b/rust/pantheon-compositor/src/cursor.rs index bd4c46337..31acd2741 100644 --- a/rust/pantheon-compositor/src/cursor.rs +++ b/rust/pantheon-compositor/src/cursor.rs @@ -13,7 +13,7 @@ use smithay::{ }, input::pointer::{CursorImageAttributes, CursorImageStatus}, render_elements, - utils::{IsAlive, Scale, Transform}, + utils::{IsAlive, Logical, Point, Rectangle, Scale, Size, Transform}, wayland::compositor::with_states, }; @@ -136,6 +136,154 @@ where elements.push(element.into()); } + elements.extend(render_shell_osd_elements(renderer, state, output)); + + elements +} + +fn render_shell_osd_elements( + _renderer: &mut R, + state: &PantheonCompositor, + output: &smithay::output::Output, +) -> Vec> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Texture + Clone + Send + 'static, +{ + let Some(osd) = state.shell_compat.active_osd() else { + return Vec::new(); + }; + let outputs = state.space.outputs().collect::>(); + let target_name = osd + .connector + .as_ref() + .filter(|connector| { + outputs + .iter() + .any(|candidate| candidate.name().as_str() == connector.as_str()) + }) + .cloned() + .or_else(|| { + osd.monitor + .and_then(|index| outputs.get(index as usize).map(|target| target.name())) + }) + .or_else(|| state.primary_output().map(|target| target.name())); + if target_name.as_deref() != Some(output.name().as_str()) { + return Vec::new(); + } + let Some(geometry) = state.space.output_geometry(output) else { + return Vec::new(); + }; + + let width = 304.min(geometry.size.w.saturating_sub(32).max(120)); + let height = 76.min(geometry.size.h.saturating_sub(32).max(48)); + let card = Rectangle::::new( + Point::from(( + geometry.loc.x + (geometry.size.w - width) / 2, + geometry.loc.y + (geometry.size.h - height) / 2, + )), + Size::from((width, height)), + ); + let scale = Scale::from(output.current_scale().fractional_scale()); + let mut elements = Vec::new(); + let mut append = |rect: Rectangle, color: [f32; 4], opacity: f32| { + let location = (rect.loc - geometry.loc) + .to_f64() + .to_physical(scale) + .to_i32_round(); + let size = rect.size; + if size.w > 0 && size.h > 0 { + let buffer = SolidColorBuffer::new(size, color); + let element = SolidColorRenderElement::from_buffer( + &buffer, + location, + scale, + opacity, + Kind::Unspecified, + ); + elements.push(element.into()); + } + }; + + append(card, [0.055, 0.065, 0.085, 1.0], 0.94); + let icon_x = card.loc.x + 22; + let icon_y = card.loc.y + 25; + if osd + .icon + .as_deref() + .is_some_and(|icon| icon.contains("brightness")) + { + // A simple sun glyph keeps the brightness OSD recognizable without + // pulling a shell toolkit or image stack into the compositor. + append( + Rectangle::new((icon_x + 7, icon_y + 7).into(), (12, 12).into()), + [1.0, 0.86, 0.49, 1.0], + 1.0, + ); + for (x, y, w, h) in [ + (icon_x + 11, icon_y, 4, 5), + (icon_x + 11, icon_y + 21, 4, 5), + (icon_x, icon_y + 11, 5, 4), + (icon_x + 21, icon_y + 11, 5, 4), + (icon_x + 3, icon_y + 3, 4, 4), + (icon_x + 19, icon_y + 3, 4, 4), + (icon_x + 3, icon_y + 19, 4, 4), + (icon_x + 19, icon_y + 19, 4, 4), + ] { + append( + Rectangle::new((x, y).into(), (w, h).into()), + [1.0, 0.86, 0.49, 1.0], + 1.0, + ); + } + } else { + // The sound indicator uses the same endpoint for volume and mute. + append( + Rectangle::new((icon_x, icon_y + 8).into(), (8, 12).into()), + [0.76, 0.85, 1.0, 1.0], + 1.0, + ); + append( + Rectangle::new((icon_x + 8, icon_y + 3).into(), (7, 22).into()), + [0.76, 0.85, 1.0, 1.0], + 1.0, + ); + if !osd + .icon + .as_deref() + .is_some_and(|icon| icon.contains("muted")) + { + append( + Rectangle::new((icon_x + 18, icon_y + 8).into(), (4, 12).into()), + [0.76, 0.85, 1.0, 1.0], + 1.0, + ); + } + } + + if let Some(level) = osd.level { + let max_level = osd.max_level.filter(|value| *value > 0.0).unwrap_or(1.0); + if level.is_finite() && max_level.is_finite() { + let bar_width = (width - 76).max(32); + let bar_height = 8; + let bar_x = card.loc.x + 62; + let bar_y = card.loc.y + (height - bar_height) / 2; + append( + Rectangle::new((bar_x, bar_y).into(), (bar_width, bar_height).into()), + [0.28, 0.31, 0.37, 1.0], + 1.0, + ); + let fill_width = + ((level / max_level).clamp(0.0, 1.0) * f64::from(bar_width)).round() as i32; + if fill_width > 0 { + append( + Rectangle::new((bar_x, bar_y).into(), (fill_width, bar_height).into()), + [0.45, 0.67, 1.0, 1.0], + 1.0, + ); + } + } + } elements } diff --git a/rust/pantheon-compositor/src/shell_compat.rs b/rust/pantheon-compositor/src/shell_compat.rs index 2c96dffea..7678800e0 100644 --- a/rust/pantheon-compositor/src/shell_compat.rs +++ b/rust/pantheon-compositor/src/shell_compat.rs @@ -12,6 +12,7 @@ use std::{ }; use smithay::input::keyboard::{Keysym, ModifiersState, xkb}; +use smithay::reexports::calloop::channel::Sender; use zbus::{blocking::Connection, interface, object_server::SignalEmitter, zvariant::OwnedValue}; const BUS_NAME: &str = "org.gnome.Shell"; @@ -41,6 +42,22 @@ pub(crate) struct ParsedAccelerator { modifiers: AcceleratorModifiers, } +#[derive(Clone, Debug)] +pub(crate) struct ShellOsd { + pub icon: Option, + pub level: Option, + pub max_level: Option, + pub connector: Option, + pub monitor: Option, + expires_at: Instant, +} + +impl ShellOsd { + pub fn visible(&self) -> bool { + Instant::now() < self.expires_at + } +} + #[derive(Clone, Copy, Debug)] struct KeyBinding { id: u32, @@ -53,10 +70,11 @@ pub struct ShellCompatHandle { events: mpsc::Sender, active: Arc, active_since: Arc>>, + osd: Arc>>, } impl ShellCompatHandle { - pub fn start(wayland_display: OsString) -> Self { + pub fn start(wayland_display: OsString, redraw_sender: Sender<()>) -> Self { let bindings = Arc::new(Mutex::new(Vec::new())); let (events, receiver) = mpsc::channel(); let active = Arc::new(AtomicBool::new(false)); @@ -64,6 +82,14 @@ impl ShellCompatHandle { let service_bindings = bindings.clone(); let service_active = active.clone(); let service_active_since = active_since.clone(); + let osd = Arc::new(Mutex::new(None)); + let service_osd = osd.clone(); + let (osd_expiry, expiry_receiver) = mpsc::channel(); + let expiry_redraw_sender = redraw_sender.clone(); + thread::Builder::new() + .name("pantheon-osd-expiry".into()) + .spawn(move || run_osd_expiry(expiry_receiver, expiry_redraw_sender)) + .expect("failed to start OSD expiry worker"); thread::Builder::new() .name("pantheon-shell-dbus".into()) .spawn(move || { @@ -71,6 +97,9 @@ impl ShellCompatHandle { service_bindings, service_active, service_active_since, + service_osd, + redraw_sender, + osd_expiry, wayland_display, receiver, ) @@ -81,6 +110,17 @@ impl ShellCompatHandle { events, active, active_since, + osd, + } + } + + pub(crate) fn active_osd(&self) -> Option { + let mut osd = self.osd.lock().expect("OSD state lock poisoned"); + if osd.as_ref().is_some_and(ShellOsd::visible) { + osd.clone() + } else { + *osd = None; + None } } @@ -115,6 +155,9 @@ impl ShellCompatHandle { struct ShellKeyGrabber { bindings: Arc>>, + osd: Arc>>, + redraw_sender: Sender<()>, + osd_expiry: mpsc::Sender, next_id: u32, } @@ -148,7 +191,32 @@ impl ShellKeyGrabber { removed == actions.len() } - fn show_osd(&self, _parameters: HashMap) {} + fn show_osd(&self, parameters: HashMap) { + let value = |key: &str| parameters.get(key); + let icon = value("icon") + .and_then(|value| value.try_clone().ok()) + .and_then(|value| String::try_from(value).ok()); + let connector = value("connector") + .and_then(|value| value.try_clone().ok()) + .and_then(|value| String::try_from(value).ok()); + let level = value("level").and_then(|value| f64::try_from(value).ok()); + let max_level = value("maxLevel").and_then(|value| f64::try_from(value).ok()); + let monitor = value("monitor") + .and_then(|value| i32::try_from(value).ok()) + .and_then(|value| u32::try_from(value).ok()); + *self.osd.lock().expect("OSD state lock poisoned") = Some(ShellOsd { + icon, + level, + max_level, + connector, + monitor, + expires_at: Instant::now() + Duration::from_millis(1400), + }); + let _ = self.redraw_sender.send(()); + let _ = self + .osd_expiry + .send(Instant::now() + Duration::from_millis(1450)); + } #[zbus(signal)] async fn accelerator_activated( @@ -241,6 +309,9 @@ fn serve_shell_compat( bindings: Arc>>, active: Arc, active_since: Arc>>, + osd: Arc>>, + redraw_sender: Sender<()>, + osd_expiry: mpsc::Sender, wayland_display: OsString, receiver: mpsc::Receiver, ) { @@ -256,6 +327,9 @@ fn serve_shell_compat( OBJECT_PATH, ShellKeyGrabber { bindings, + osd, + redraw_sender, + osd_expiry, next_id: 1, }, ) { @@ -332,6 +406,26 @@ fn serve_shell_compat( } } +fn run_osd_expiry(receiver: mpsc::Receiver, redraw_sender: Sender<()>) { + loop { + let mut deadline = match receiver.recv() { + Ok(deadline) => deadline, + Err(_) => return, + }; + loop { + let wait = deadline.saturating_duration_since(Instant::now()); + match receiver.recv_timeout(wait) { + Ok(next_deadline) => deadline = next_deadline, + Err(mpsc::RecvTimeoutError::Timeout) => { + let _ = redraw_sender.send(()); + break; + } + Err(mpsc::RecvTimeoutError::Disconnected) => return, + } + } + } +} + pub(crate) fn parse_accelerator(value: &str, _grab_flags: u32) -> Option { let mut remaining = value.trim(); let mut modifiers = AcceleratorModifiers::default(); diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index a44b1e8f1..92063b407 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -7,7 +7,7 @@ use smithay::{ input::{Seat, SeatState, pointer::CursorImageStatus}, reexports::{ calloop::{ - EventLoop, Interest, LoopSignal, Mode, PostAction, + EventLoop, Interest, LoopSignal, Mode, PostAction, channel, generic::Generic, timer::{TimeoutAction, Timer}, }, @@ -200,7 +200,16 @@ impl PantheonCompositor { // Setup a wayland socket that will be used to accept clients let socket_name = Self::init_wayland_listener(display, event_loop)?; - let shell_compat = ShellCompatHandle::start(socket_name.clone()); + let (osd_redraw_sender, osd_redraw_receiver) = channel::channel(); + event_loop + .handle() + .insert_source(osd_redraw_receiver, |event, _, _state| { + if matches!(event, channel::Event::Msg(())) { + #[cfg(feature = "native-drm")] + _state.request_native_redraw(); + } + })?; + let shell_compat = ShellCompatHandle::start(socket_name.clone(), osd_redraw_sender); // Get the loop signal, used to stop the event loop let loop_signal = event_loop.get_signal(); From cab3784bd3d129bacbaf33b79b1fa24e6a20413e Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:53:01 -0400 Subject: [PATCH 108/798] Load live keyboard settings from GSettings --- mseries/OS9-HANDOFF.md | 12 +++ mseries/ROADMAP.md | 6 ++ .../EXPERIENCE-REQUIREMENTS.md | 5 ++ mseries/pantheon-rust-compositor/README.md | 8 ++ rust/pantheon-compositor/src/input.rs | 88 ++++++++++++++++++- rust/pantheon-compositor/src/state.rs | 49 ++++++++++- 6 files changed, 164 insertions(+), 4 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 8420653dd..e61418f64 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5699,3 +5699,15 @@ It does not draw caller-supplied text labels or reproduce Pantheon's complete OSD styling; physical key activation and visual runtime behavior still need qualification. Local default `cargo check`/`cargo build` and the `native-drm` build pass. No tests or display session were run. + +### Apply the configured XKB keymap and repeat settings — 2026-10-19 + +The compositor now initializes its XKB model, current XKB source, XKB options, +and keyboard repeat settings from `org.gnome.desktop.input-sources` and +`org.gnome.desktop.peripherals.keyboard`. A one-second poll applies live +changes to the keymap and Wayland repeat info. Current host settings are model +`pc105+inet`, US layout, `ctrl:swap_lwin_lctl`, repeat enabled, 500 ms delay, +and 30 ms interval; this preserves the desktop's Command/Ctrl mapping. The +configured `mozc-jp` IBus source is installed but not selected, and no IBus +bridge was added. Default and native DRM builds pass. No live key injection or +layout change was run, so physical mapping and reload remain runtime-unverified. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index aeb67f03d..1816a54c0 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -58,6 +58,12 @@ compile-checked only. It loads shortcut arrays from the installed Mutter, Pantheon and GNOME GSettings schemas at session startup, with built-in defaults when those schemas are unavailable. Drag-to-corner preview, live binding reload, multi-output behavior, and runtime snapping qualification remain open. +The keyboard's XKB model, active layout, options, and repeat settings now load +from the user's GNOME input-source and keyboard schemas. A one-second settings +poll applies layout/option and repeat changes to connected Wayland clients. +Live behavior has only been compile-checked. The M1 Pro currently selects US +XKB with `ctrl:swap_lwin_lctl`; its installed `mozc-jp` IBus source is inactive +and remains conditional. Quarter drag previews now appear in the 40-logical-pixel output corners, Escape cancels the snap, fixed-size windows are excluded, and release applies the selected quarter. This path is compile-checked only. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 2da2d8403..af8b8c941 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -33,6 +33,11 @@ They are important capabilities in a general-purpose desktop, but their presence in Mutter is not by itself a reason to reproduce them in the first cut. +On the current host, the configured sources are US XKB and IBus `mozc-jp`, with +US selected. `ibus` and `ibus-mozc` are installed. This confirms the input +method is available but does not establish it as an active daily workflow; keep +the IBus bridge conditional until that workflow is confirmed. + Keep notification UI, app launchers, panel/dock rendering, settings surfaces, wallpaper and other shell UI in their existing Pantheon components unless a documented requirement shows they cannot work through the compositor contract. diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index ca459ead3..aad34ce98 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -30,6 +30,14 @@ shortcut and multi-output behavior remain open. During a compositor move, the snaps the window, and Escape cancels the snap. Fixed-size windows are excluded. The drag preview and release path also need runtime qualification. +At startup, the keyboard now loads the user's XKB model, active XKB layout, +options, and repeat settings from GNOME GSettings. This applies the M-series +`ctrl:swap_lwin_lctl` Command/Ctrl mapping. The compositor polls settings +once per second and applies layout, option, and repeat changes live; this path +is build-checked but not runtime-qualified. The current source is US; +`mozc-jp` is installed but inactive, so IBus remains a conditional workflow +rather than a first-session requirement. + Panel reservations and managed maximize/tile layouts are tracked per output connector. Maximized/fullscreen windows and popup constraints use the window's current output, and removing a panel releases its work area. Multi-output diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 6006871f2..98714e34c 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -1,10 +1,14 @@ +use gio::{ + Settings, SettingsBackend, SettingsSchemaSource, + prelude::{SettingsExt, SettingsExtManual}, +}; use smithay::{ backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, }, input::{ - keyboard::FilterResult, + keyboard::{FilterResult, XkbConfig}, pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent}, }, reexports::wayland_server::protocol::wl_surface::WlSurface, @@ -13,6 +17,88 @@ use smithay::{ use crate::state::PantheonCompositor; +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct StartupKeyboardConfig { + pub model: String, + pub layout: String, + pub variant: String, + pub options: Option, + pub repeat_delay: i32, + pub repeat_rate: i32, +} + +pub(crate) fn startup_keyboard_config() -> StartupKeyboardConfig { + let mut config = StartupKeyboardConfig { + model: String::new(), + layout: String::new(), + variant: String::new(), + options: None, + repeat_delay: 500, + repeat_rate: 33, + }; + let Some(source) = SettingsSchemaSource::default() else { + return config; + }; + + if let Some(schema) = source.lookup("org.gnome.desktop.input-sources", true) { + let settings = Settings::new_full(&schema, None::<&SettingsBackend>, None); + config.model = settings.string("xkb-model").to_string(); + config.options = Some( + settings + .strv("xkb-options") + .iter() + .map(|option| option.to_string()) + .collect::>() + .join(","), + ); + let sources = settings + .value("sources") + .get::>() + .unwrap_or_default(); + let current_index = settings.uint("current") as usize; + let current = sources.get(current_index); + let selected = current + .filter(|(kind, _)| kind == "xkb") + .or_else(|| sources.iter().find(|(kind, _)| kind == "xkb")); + if let Some((_, source_id)) = selected { + if let Some((layout, variant)) = source_id.split_once('+') { + config.layout = layout.to_owned(); + config.variant = variant.to_owned(); + } else { + config.layout.clone_from(source_id); + } + } + } + + if let Some(schema) = source.lookup("org.gnome.desktop.peripherals.keyboard", true) { + let settings = Settings::new_full(&schema, None::<&SettingsBackend>, None); + if !settings.boolean("repeat") { + config.repeat_delay = 0; + config.repeat_rate = 0; + } else { + config.repeat_delay = settings.uint("delay").min(i32::MAX as u32) as i32; + let interval = settings.uint("repeat-interval"); + config.repeat_rate = if interval == 0 { + 33 + } else { + (1000 / interval).min(i32::MAX as u32) as i32 + }; + } + } + + config +} + +pub(crate) fn xkb_config(config: &StartupKeyboardConfig) -> XkbConfig<'_> { + XkbConfig { + model: &config.model, + layout: &config.layout, + variant: &config.variant, + options: config.options.clone(), + ..XkbConfig::default() + } +} + impl PantheonCompositor { pub fn process_input_event(&mut self, event: InputEvent) { match event { diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 92063b407..89273eff3 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -73,6 +73,7 @@ pub struct PantheonCompositor { pub lock_confirmation_outputs: HashSet, pub keyboard_focus_before_lock: Option, pub seat_state: SeatState, + pub(crate) keyboard_config: crate::input::StartupKeyboardConfig, pub data_device_state: DataDeviceState, pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, @@ -184,9 +185,50 @@ impl PantheonCompositor { let mut seat_state = SeatState::new(); let mut seat: Seat = seat_state.new_wl_seat(&dh, seat_name); - // Notify clients that we have a keyboard, for the sake of the example we assume that keyboard is always present. - // You may want to track keyboard hot-plug in real compositor. - seat.add_keyboard(Default::default(), 200, 25).unwrap(); + // Initialize the physical keyboard with the user's XKB layout, + // Command/Ctrl options, and repeat preferences when the desktop + // settings schemas are available. + let keyboard_config = crate::input::startup_keyboard_config(); + if let Err(error) = seat.add_keyboard( + crate::input::xkb_config(&keyboard_config), + keyboard_config.repeat_delay, + keyboard_config.repeat_rate, + ) { + tracing::warn!( + ?error, + "configured XKB keymap failed; using environment defaults" + ); + seat.add_keyboard( + Default::default(), + keyboard_config.repeat_delay, + keyboard_config.repeat_rate, + )?; + } + event_loop.handle().insert_source( + Timer::from_duration(Duration::from_secs(1)), + |_, _, state| { + let config = crate::input::startup_keyboard_config(); + if let Some(keyboard) = state.seat.get_keyboard() { + let keymap_changed = config.model != state.keyboard_config.model + || config.layout != state.keyboard_config.layout + || config.variant != state.keyboard_config.variant + || config.options != state.keyboard_config.options; + if keymap_changed + && let Err(error) = + keyboard.set_xkb_config(state, crate::input::xkb_config(&config)) + { + tracing::warn!(?error, "could not apply changed XKB settings"); + } + if config.repeat_delay != state.keyboard_config.repeat_delay + || config.repeat_rate != state.keyboard_config.repeat_rate + { + keyboard.change_repeat_info(config.repeat_rate, config.repeat_delay); + } + } + state.keyboard_config = config; + TimeoutAction::ToDuration(Duration::from_secs(1)) + }, + )?; // Notify clients that we have a pointer (mouse) // Here we assume that there is always pointer plugged in @@ -237,6 +279,7 @@ impl PantheonCompositor { lock_confirmation_outputs: HashSet::new(), keyboard_focus_before_lock: None, seat_state, + keyboard_config, data_device_state, pantheon_shell_state, display_config, From be5747ad9aa72b42640f9e66f7cebdcb240fa357 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:54:18 -0400 Subject: [PATCH 109/798] Bound experimental session crash recovery --- mseries/OS9-HANDOFF.md | 12 ++++++++++++ mseries/ROADMAP.md | 8 +++++--- mseries/pantheon-rust-compositor/README.md | 6 ++++++ mseries/pantheon-rust-compositor/session/README.md | 7 +++++++ .../systemd/user/pantheon-rust-compositor@.service | 6 +++++- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e61418f64..66ca7e648 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5711,3 +5711,15 @@ and 30 ms interval; this preserves the desktop's Command/Ctrl mapping. The configured `mozc-jp` IBus source is installed but not selected, and no IBus bridge was added. Default and native DRM builds pass. No live key injection or layout change was run, so physical mapping and reload remain runtime-unverified. + +### Bound experimental-session crash recovery — 2026-10-19 + +The staged `pantheon-rust-compositor@.service` now uses `Restart=on-failure` +with a two-second delay and a three-start/60-second limit. A hung startup has a +90-second readiness timeout. After the limit, the existing +`OnFailure=gnome-session-shutdown.target` path stops the experimental session; +normal session stops are not restarted. `systemd-analyze verify` parsed both +staged units and reported only expected unresolved install-time dependencies +(`gnome-session-initialized.target`) and the not-yet-installed +`/usr/bin/pantheon-compositor` path. The service remains uninstalled; no live +crash, restart, login, or rollback was exercised. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 1816a54c0..e954f28f2 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -128,9 +128,11 @@ A separate `pantheon-rust-wayland` session payload now mirrors the installed GNOME session/systemd contract: a unique login entry, a compositor component hidden under systemd, a notify service with an explicit Wayland socket, and a session-target drop-in preserving the installed Pantheon support services. -It is repository-only and has not been installed or runtime-qualified. The -next gate is native-session qualification in an isolated login, including -socket/environment handoff, ordinary apps, lock/logout and failure recovery. +The service retries unexpected exits after two seconds, capped at three starts +per minute, then requests the session shutdown target. It is repository-only +and has not been installed or runtime-qualified. The next gate is native-session +qualification in an isolated login, including socket/environment handoff, +ordinary apps, lock/logout and failure recovery. ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index aad34ce98..b38d789b3 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -71,6 +71,12 @@ session, input and recovery behavior still need on-device qualification. The staged login-session files are separate from the active Gala session and are not installed or selected by default. +The staged systemd service now retries an unexpected compositor failure with +a two-second delay, capped at three starts per minute. After that limit, its +`OnFailure` action requests the GNOME session shutdown target rather than +looping indefinitely. This is configuration only; a crash/restart has not been +exercised in an isolated login. + ## Dock HDMI boundary The WD22TB4 HDMI goal remains in diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 5edc09cdd..3c91c7fcc 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -18,6 +18,13 @@ the explicit socket; without that variable the nested development backend continues to allocate an automatic socket, and the native backend can also use automatic allocation for manual development launches. +The experimental service retries an unexpected compositor failure, with a +two-second delay and a limit of three starts within one minute. Once the limit +is reached, its existing `OnFailure=gnome-session-shutdown.target` path ends +the failed experimental session instead of restarting indefinitely. A normal +session stop is not restarted. These recovery settings are staged only and +have not been exercised in a live GNOME session. + The directory layout mirrors install paths below `usr/`. A distro package can stage the payload under its package root and install it with the compositor binary at `/usr/bin/pantheon-compositor`. Do not add these files to diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service index 3860d12bf..5e66bc081 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service @@ -11,15 +11,19 @@ Requisite=gnome-session-initialized.target PartOf=gnome-session-initialized.target Before=gnome-session-initialized.target ConditionEnvironment=XDG_SESSION_TYPE=%I +StartLimitIntervalSec=60s +StartLimitBurst=3 [Service] Slice=session.slice Type=notify +TimeoutStartSec=90s Environment=WAYLAND_DISPLAY=wayland-0 Environment=PANTHEON_WAYLAND_DISPLAY=wayland-0 ExecStart=/usr/bin/pantheon-compositor --backend native --notify-systemd ExecStartPost=/usr/bin/systemctl --user import-environment WAYLAND_DISPLAY ExecStopPost=-/usr/bin/systemctl --user unset-environment WAYLAND_DISPLAY -Restart=no +Restart=on-failure +RestartSec=2s TimeoutStopSec=5 OOMScoreAdjust=-1000 From 08db3e27b062998f791263c2ae66de8c04333c31 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 00:58:23 -0400 Subject: [PATCH 110/798] Package the experimental Pantheon compositor --- mseries/OS9-HANDOFF.md | 12 ++++ mseries/pantheon-rust-compositor/README.md | 7 ++ mseries/pantheon-rust-compositor/build-deb.sh | 69 +++++++++++++++++++ .../session/README.md | 3 +- 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100755 mseries/pantheon-rust-compositor/build-deb.sh diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 66ca7e648..015162af4 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5723,3 +5723,15 @@ staged units and reported only expected unresolved install-time dependencies (`gnome-session-initialized.target`) and the not-yet-installed `/usr/bin/pantheon-compositor` path. The service remains uninstalled; no live crash, restart, login, or rollback was exercised. + +### Build an installable arm64 package without selecting the session — 2026-10-19 + +Added `mseries/pantheon-rust-compositor/build-deb.sh`. On the local arm64 host +it built the optimized `native-drm` executable and an arm64 Debian package +containing the opt-in session files. `dpkg-shlibdeps` derived shared-library +dependencies; the package also requires `gnome-session`, `gtklock`, `libegl1` +and `libgles2`. `dpkg-deb --info` and `--contents` confirm the package metadata, +binary and session units. Artifact: +`/tmp/pantheon-rust-compositor-deb/pantheon-rust-compositor_0.1.0+gitbe5747a_arm64.deb` +(SHA-256 `976823c9bc75c6f5f07cebe5967b4cab59ab38457f2196df7cef1fc8910a1038`). +The package was not installed and the experimental session was not selected. diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index b38d789b3..0015e5121 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -77,6 +77,13 @@ a two-second delay, capped at three starts per minute. After that limit, its looping indefinitely. This is configuration only; a crash/restart has not been exercised in an isolated login. +`build-deb.sh` builds an arm64 release binary with `native-drm` and packages it +with the separate experimental session files. It derives shared-library +dependencies and requires `gnome-session` and `gtklock`. Build it locally with +`OUTDIR=/tmp/pantheon-rust-compositor-deb mseries/pantheon-rust-compositor/build-deb.sh`. +The resulting package is for isolated qualification; it is not installed by +the build script. + ## Dock HDMI boundary The WD22TB4 HDMI goal remains in diff --git a/mseries/pantheon-rust-compositor/build-deb.sh b/mseries/pantheon-rust-compositor/build-deb.sh new file mode 100755 index 000000000..19c20aa97 --- /dev/null +++ b/mseries/pantheon-rust-compositor/build-deb.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +OUTDIR=${OUTDIR:?set OUTDIR to the directory for the generated .deb} +mkdir -p "$OUTDIR" +OUTDIR=$(cd -- "$OUTDIR" && pwd) +ARCH=$(dpkg --print-architecture) +if [[ $ARCH != arm64 ]]; then + echo "error: this native compositor package is for arm64; host is $ARCH" >&2 + exit 1 +fi + +mkdir -p "$OUTDIR/debian/pantheon-rust-compositor" +PACKAGE_ROOT="$OUTDIR/debian/pantheon-rust-compositor" +VERSION=${DEB_VERSION:-0.1.0+git$(git -C "$ROOT" rev-parse --short HEAD)} + +cargo build --release --manifest-path "$ROOT/rust/Cargo.toml" \ + -p pantheon-compositor --bin pantheon-compositor --features native-drm + +BIN="$ROOT/rust/target/release/pantheon-compositor" +[[ -x $BIN ]] || { echo "error: compositor build produced no executable" >&2; exit 1; } +if ldd "$BIN" | grep -q 'not found'; then + echo "error: compositor binary has unresolved shared libraries" >&2 + exit 1 +fi + +install -D -m 0755 "$BIN" "$PACKAGE_ROOT/usr/bin/pantheon-compositor" +cp -a "$ROOT/mseries/pantheon-rust-compositor/session/usr/." "$PACKAGE_ROOT/usr/" +find "$PACKAGE_ROOT" -type d -exec chmod 0755 {} + +find "$PACKAGE_ROOT" -type f -exec chmod 0644 {} + +chmod 0755 "$PACKAGE_ROOT/usr/bin/pantheon-compositor" + +cat > "$OUTDIR/debian/control" <<'EOF' +Source: pantheon-rust-compositor +Section: x11 +Priority: optional +Maintainer: techmore +Standards-Version: 4.6.2 + +Package: pantheon-rust-compositor +Architecture: arm64 +Description: experimental Rust compositor for the Pantheon M-series session + An opt-in Smithay Wayland compositor and a separate experimental GNOME session. +EOF + +SHLIB_DEPENDS=$(cd "$OUTDIR" && dpkg-shlibdeps -O \ + --package=pantheon-rust-compositor -Sdebian/pantheon-rust-compositor \ + debian/pantheon-rust-compositor/usr/bin/pantheon-compositor \ + | sed -n 's/^shlibs:Depends=//p') +if [[ -z $SHLIB_DEPENDS ]]; then + echo "error: dpkg-shlibdeps returned no runtime dependencies" >&2 + exit 1 +fi + +mkdir -p "$PACKAGE_ROOT/DEBIAN" +cat > "$PACKAGE_ROOT/DEBIAN/control" < +Depends: $SHLIB_DEPENDS, gnome-session, gtklock, libegl1, libgles2 +Description: experimental Rust compositor for the Pantheon M-series session + An opt-in Smithay Wayland compositor and a separate experimental GNOME session. +EOF + +dpkg-deb --build --root-owner-group "$PACKAGE_ROOT" \ + "$OUTDIR/pantheon-rust-compositor_${VERSION}_arm64.deb" +dpkg-deb --info "$OUTDIR/pantheon-rust-compositor_${VERSION}_arm64.deb" diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 3c91c7fcc..0dd9d2031 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -27,7 +27,8 @@ have not been exercised in a live GNOME session. The directory layout mirrors install paths below `usr/`. A distro package can stage the payload under its package root and install it with the compositor -binary at `/usr/bin/pantheon-compositor`. Do not add these files to +binary at `/usr/bin/pantheon-compositor`; the repository's +`../build-deb.sh` creates that arm64 package. Do not add these files to the active host session until the native backend, login, app startup, lock, logout and recovery have been qualified on a separate session. Native DRM startup takes over the display device and has not been runtime-qualified on From 2c8422f0c2b50f9f31daece122a8c88e98628105 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 01:03:41 -0400 Subject: [PATCH 111/798] Honor Pantheon panel hide modes --- mseries/OS9-HANDOFF.md | 16 +- .../EXPERIENCE-REQUIREMENTS.md | 10 +- rust/pantheon-compositor/README.md | 10 +- .../src/handlers/xdg_shell.rs | 2 + rust/pantheon-compositor/src/input.rs | 4 + .../pantheon-compositor/src/pantheon_shell.rs | 221 +++++++++++++++++- rust/pantheon-compositor/src/state.rs | 3 +- 7 files changed, 257 insertions(+), 9 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 015162af4..f70bd7b4f 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5732,6 +5732,20 @@ containing the opt-in session files. `dpkg-shlibdeps` derived shared-library dependencies; the package also requires `gnome-session`, `gtklock`, `libegl1` and `libgles2`. `dpkg-deb --info` and `--contents` confirm the package metadata, binary and session units. Artifact: -`/tmp/pantheon-rust-compositor-deb/pantheon-rust-compositor_0.1.0+gitbe5747a_arm64.deb` +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-rust-compositor_0.1.0+gitbe5747a_arm64.deb` (SHA-256 `976823c9bc75c6f5f07cebe5967b4cab59ab38457f2196df7cef1fc8910a1038`). The package was not installed and the experimental session was not selected. + +### Honor Pantheon Dock hide modes — 2026-10-19 + +The active `io.elementary.dock` setting is `autohide-mode='overlapping-window'`. +The compositor now evaluates Pantheon panel hide modes 0–4 against managed +maximize state, focused-window overlap, and any application-window overlap. +Hidden panels move outside their output; pointer movement within 2 logical +pixels of that output's panel edge reveals them. A visible panel reserves its +work area, and hiding releases it and reconfigures maximized/tiled windows. +This is an immediate transition without Gala's animation. Default and +`native-drm` `cargo check` pass, and touched files were formatted. No automated +tests or Dock runtime session were run; overlap timing, edge reveal and work +area behavior still need an isolated Wingpanel/Dock session before hardware +qualification. The active Gala session remains untouched. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index af8b8c941..86488c14f 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -85,8 +85,14 @@ planning against a release. acceptance checks. Keep Gala available as rollback until owner acceptance. The nested prototype serves the Pantheon shell protocol v2, places a panel from -its anchor/size/offset, reserves exclusive panel geometry, and updates already -maximized windows when that work area changes. The last local `cargo check` +its anchor/size/offset, and honors hide modes by tracking maximized/focused or +overlapping windows. Pointer motion at the output edge reveals hidden panels; +work-area reservations are released while hidden and restored while visible, +with managed windows reconfigured on transitions. Hiding currently moves the +panel immediately without an animation. The active host Dock setting is +`overlapping-window`; the code has compile checks but no Dock runtime +qualification yet. It updates already-maximized windows when work areas change. +The last local `cargo check` and `cargo build` pass. Its protocol probe completed under Xvfb over a nested Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested Wayland session and rendered SHM buffers. The prototype serves the diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index ac340bf1d..283dab4a7 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -81,9 +81,13 @@ available to protocol-v2 clients. When panel commits change the exclusive work area, already maximized windows are reconfigured to its new bounds; unchanged panel repaints do not issue redundant configures. -This is only the first shell integration slice. Autohide/overlap tracking, -animated hiding, blur, overview behavior and multi-output work-area updates for -already maximized windows are not complete. The compositor now implements +The Dock's existing hide modes now drive compositor visibility: maximize and +overlap modes hide while their condition is true, `always` starts hidden, and +pointer motion at the panel's output edge reveals it. Hiding releases the +panel's work area; revealing it restores that area and reconfigures managed +windows. The active host Dock setting is `overlapping-window`. The transition +is immediate; animated hiding and runtime Dock interaction remain unqualified. +Blur, overview behavior and multi-output Dock behavior also remain open. The compositor now implements Smithay's `ext-session-lock-v1`: it blanks each output until its lock surface is ready, confines input to the locker, and restores the prior keyboard focus on unlock. The compositor exports the observed `org.gnome.ScreenSaver` API and diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index f3a4549d4..973468261 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -184,6 +184,7 @@ impl XdgShellHandler for PantheonCompositor { if was_focused { self.focus_next_window(); } + self.refresh_panel_visibility(); #[cfg(feature = "native-drm")] self.request_native_redraw(); } @@ -299,6 +300,7 @@ impl PantheonCompositor { .unwrap() .set_focus(self, Option::::None, serial); } + self.refresh_panel_visibility(); } } diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 98714e34c..1d0ef8265 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -135,6 +135,7 @@ impl PantheonCompositor { } }, ); + self.refresh_panel_visibility(); } InputEvent::PointerMotion { event, .. } => { let pointer = self.seat.get_pointer().unwrap(); @@ -175,6 +176,7 @@ impl PantheonCompositor { time: event.time(), }, ); + self.refresh_panel_visibility(); pointer.frame(self); } InputEvent::PointerMotionAbsolute { event, .. } => { @@ -209,6 +211,7 @@ impl PantheonCompositor { time: event.time(), }, ); + self.refresh_panel_visibility(); pointer.frame(self); } InputEvent::PointerButton { event, .. } => { @@ -258,6 +261,7 @@ impl PantheonCompositor { time: event.time(), }, ); + self.refresh_panel_visibility(); pointer.frame(self); } InputEvent::PointerAxis { event, .. } => { diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 26c0ce4f7..a04e4dc71 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -53,6 +53,8 @@ pub struct PanelState { /// Pantheon protocol values: never=0, maximized-focused=1, /// overlapping-focused=2, overlapping-any=3, always=4. pub hide_mode: u32, + /// Whether the compositor has moved this panel out of view for its hide mode. + pub hidden: bool, pub visible_in_multitasking_view: bool, } @@ -235,15 +237,18 @@ impl PantheonCompositor { if self.refresh_work_areas() { self.reconfigure_managed_windows(); } + self.request_native_redraw(); } pub fn update_panel(&mut self, surface: &WlSurface, update: impl FnOnce(&mut PanelState)) { let panel = self.panels.entry(surface.id()).or_default(); update(panel); self.position_panel(surface); + self.refresh_panel_visibility(); if self.refresh_work_areas() { self.reconfigure_managed_windows(); } + self.request_native_redraw(); } pub fn is_panel_surface(&self, surface: &WlSurface) -> bool { @@ -256,7 +261,7 @@ impl PantheonCompositor { ) -> Rectangle { let (mut top, mut bottom, mut left, mut right) = (0, 0, 0, 0); for (surface_id, panel) in &self.panels { - if panel.anchor.is_none() || panel.hide_mode != 0 { + if panel.anchor.is_none() || panel.hidden { continue; } let Some(window) = self.space.elements().find(|window| { @@ -365,7 +370,7 @@ impl PantheonCompositor { surface_size.h }; let offset = panel.offset; - let location = match anchor { + let mut location = match anchor { 0 => ( output_geometry.loc.x + (output_geometry.size.w - width) / 2 + offset, output_geometry.loc.y, @@ -384,10 +389,220 @@ impl PantheonCompositor { ), _ => return, }; + if panel.hidden { + match anchor { + 0 => location.1 -= height, + 1 => location.1 += height, + 2 => location.0 -= width, + 3 => location.0 += width, + _ => return, + } + } self.space.map_element(window.clone(), location, false); self.space.raise_element(&window, false); } + /// Apply the hide mode requested by the Pantheon panel protocol. The + /// current Dock uses `overlapping-window`; edge motion reveals it again. + pub fn refresh_panel_visibility(&mut self) { + const EDGE_REVEAL_DISTANCE: f64 = 2.0; + + let pointer = self + .seat + .get_pointer() + .map(|pointer| pointer.current_location()); + let panels = self + .panels + .iter() + .map(|(id, panel)| (id.clone(), panel.clone())) + .collect::>(); + let mut changed = false; + + for (surface_id, mut panel) in panels { + if panel.anchor.is_none() { + continue; + } + let Some(window) = self.space.elements().find(|window| { + window + .toplevel() + .is_some_and(|top| top.wl_surface().id() == surface_id) + }) else { + continue; + }; + let Some(output) = panel + .output_name + .as_deref() + .and_then(|name| self.space.outputs().find(|output| output.name() == name)) + .cloned() + .or_else(|| self.space.outputs_for_element(window).first().cloned()) + .or_else(|| self.primary_output()) + else { + continue; + }; + let Some(output_rect) = self.space.output_geometry(&output) else { + continue; + }; + let surface_size = window.geometry().size; + let width = if panel.width > 0 { + panel.width + } else { + surface_size.w + }; + let height = if panel.height > 0 { + panel.height + } else { + surface_size.h + }; + let base_location = match panel.anchor.unwrap() { + 0 => ( + output_rect.loc.x + (output_rect.size.w - width) / 2 + panel.offset, + output_rect.loc.y, + ), + 1 => ( + output_rect.loc.x + (output_rect.size.w - width) / 2 + panel.offset, + output_rect.loc.y + output_rect.size.h - height, + ), + 2 => ( + output_rect.loc.x, + output_rect.loc.y + (output_rect.size.h - height) / 2 + panel.offset, + ), + 3 => ( + output_rect.loc.x + output_rect.size.w - width, + output_rect.loc.y + (output_rect.size.h - height) / 2 + panel.offset, + ), + _ => continue, + }; + let panel_rect = + Rectangle::new(Point::from(base_location), Size::from((width, height))); + let pointer_over_panel = !panel.hidden + && pointer.is_some_and(|position| panel_rect.to_f64().contains(position)); + let pointer_at_edge = pointer.is_some_and(|position| { + output_rect.to_f64().contains(position) + && match panel.anchor.unwrap() { + 0 => position.y <= (output_rect.loc.y as f64 + EDGE_REVEAL_DISTANCE), + 1 => { + position.y + >= (output_rect.loc.y + output_rect.size.h) as f64 + - EDGE_REVEAL_DISTANCE + } + 2 => position.x <= (output_rect.loc.x as f64 + EDGE_REVEAL_DISTANCE), + 3 => { + position.x + >= (output_rect.loc.x + output_rect.size.w) as f64 + - EDGE_REVEAL_DISTANCE + } + _ => false, + } + }); + let pointer_reveals_panel = pointer_over_panel || pointer_at_edge; + + let focused_window = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .map(|mut focused| { + while let Some(parent) = smithay::wayland::compositor::get_parent(&focused) { + focused = parent; + } + focused + }) + .and_then(|focused| self.find_window_for_surface(&focused)); + let focused_window = focused_window.filter(|focused| { + focused + .toplevel() + .is_none_or(|top| !self.is_panel_surface(top.wl_surface())) + }); + let focused_maximized = focused_window.as_ref().is_some_and(|focused| { + focused + .toplevel() + .and_then(|top| self.managed_windows.get(&top.wl_surface().id())) + .is_some_and(|managed| managed.maximized || managed.fullscreen) + }); + let focused_overlaps = focused_window.as_ref().is_some_and(|focused| { + self.window_overlaps_panel(focused, panel_rect, panel.anchor.unwrap()) + }); + let any_window_overlaps = self.space.elements().any(|other| { + let Some(top) = other.toplevel() else { + return false; + }; + if self.is_panel_surface(top.wl_surface()) { + return false; + } + self.window_overlaps_panel(other, panel_rect, panel.anchor.unwrap()) + }); + let should_hide = match panel.hide_mode { + 0 => false, + 1 => focused_maximized, + 2 => focused_overlaps, + 3 => any_window_overlaps, + 4 => true, + _ => false, + }; + let hidden = should_hide && !pointer_reveals_panel; + if panel.hidden != hidden { + panel.hidden = hidden; + self.panels.insert(surface_id.clone(), panel); + if let Some(surface) = self + .panels + .get(&surface_id) + .and_then(|_| { + self.space.elements().find(|window| { + window + .toplevel() + .is_some_and(|top| top.wl_surface().id() == surface_id) + }) + }) + .and_then(|window| window.toplevel().map(|top| top.wl_surface().clone())) + { + self.position_panel(&surface); + } + changed = true; + } + } + + if changed { + if self.refresh_work_areas() { + self.reconfigure_managed_windows(); + } + self.request_native_redraw(); + } + } + + fn window_overlaps_panel( + &self, + window: &Window, + panel_rect: Rectangle, + anchor: u32, + ) -> bool { + let collision_rect = match anchor { + 0 => Rectangle::new( + panel_rect.loc, + Size::from((panel_rect.size.w, panel_rect.size.h + 1)), + ), + 1 => Rectangle::new( + Point::from((panel_rect.loc.x, panel_rect.loc.y - 1)), + Size::from((panel_rect.size.w, panel_rect.size.h + 1)), + ), + 2 => Rectangle::new( + panel_rect.loc, + Size::from((panel_rect.size.w + 1, panel_rect.size.h)), + ), + 3 => Rectangle::new( + Point::from((panel_rect.loc.x - 1, panel_rect.loc.y)), + Size::from((panel_rect.size.w + 1, panel_rect.size.h)), + ), + _ => panel_rect, + }; + let geometry = self.space.element_geometry(window).unwrap_or_else(|| { + let size = window.geometry().size; + Rectangle::new( + self.space.element_location(window).unwrap_or_default(), + size, + ) + }); + collision_rect.intersection(geometry).is_some() + } + pub fn configure_window_mode( &mut self, surface: ToplevelSurface, @@ -417,6 +632,7 @@ impl PantheonCompositor { } self.apply_window_mode(&surface, &window, restore, maximized, fullscreen); + self.refresh_panel_visibility(); if !maximized && !fullscreen { self.managed_windows.remove(&surface_id); @@ -493,5 +709,6 @@ pub fn handle_surface_commit(state: &mut PantheonCompositor, surface: &WlSurface state.update_panel(surface, |_| {}); } else { state.update_tiling_after_commit(surface); + state.refresh_panel_visibility(); } } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 89273eff3..5c5eb2719 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -306,8 +306,8 @@ impl PantheonCompositor { }) } - #[cfg(feature = "native-drm")] pub fn request_native_redraw(&self) { + #[cfg(feature = "native-drm")] if let Some(sender) = &self.native_redraw_sender { let _ = sender.send(()); } @@ -710,6 +710,7 @@ impl XdgActivationHandler for PantheonCompositor { } }); } + self.refresh_panel_visibility(); #[cfg(feature = "native-drm")] self.request_native_redraw(); } From 3840094d80dc4b45e6f2d2ae06d4413999cd463c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 01:04:26 -0400 Subject: [PATCH 112/798] Record current compositor qualification package --- mseries/OS9-HANDOFF.md | 7 ++++--- mseries/pantheon-rust-compositor/README.md | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index f70bd7b4f..f1dc40772 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5731,9 +5731,10 @@ it built the optimized `native-drm` executable and an arm64 Debian package containing the opt-in session files. `dpkg-shlibdeps` derived shared-library dependencies; the package also requires `gnome-session`, `gtklock`, `libegl1` and `libgles2`. `dpkg-deb --info` and `--contents` confirm the package metadata, -binary and session units. Artifact: -`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-rust-compositor_0.1.0+gitbe5747a_arm64.deb` -(SHA-256 `976823c9bc75c6f5f07cebe5967b4cab59ab38457f2196df7cef1fc8910a1038`). +binary and session units. The package was rebuilt from commit `2c8422f` after +implementing Dock hide modes. Current artifact: +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-rust-compositor_0.1.0+git2c8422f_arm64.deb` +(SHA-256 `613415f318dcc7c3421d7eb3823b7e2f35525a1f75201f31677646f71e3f6ce8`). The package was not installed and the experimental session was not selected. ### Honor Pantheon Dock hide modes — 2026-10-19 diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 0015e5121..7295a8815 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -80,9 +80,11 @@ exercised in an isolated login. `build-deb.sh` builds an arm64 release binary with `native-drm` and packages it with the separate experimental session files. It derives shared-library dependencies and requires `gnome-session` and `gtklock`. Build it locally with -`OUTDIR=/tmp/pantheon-rust-compositor-deb mseries/pantheon-rust-compositor/build-deb.sh`. -The resulting package is for isolated qualification; it is not installed by -the build script. +`OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The +current package from commit `2c8422f` is saved at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-rust-compositor_0.1.0+git2c8422f_arm64.deb` +(SHA-256 `613415f318dcc7c3421d7eb3823b7e2f35525a1f75201f31677646f71e3f6ce8`). +The package is for isolated qualification; the build script does not install it. ## Dock HDMI boundary From 99b1cb03fb90177254a97b3471e013a989ed5d16 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 01:06:36 -0400 Subject: [PATCH 113/798] Probe cross-device GBM buffer imports --- mseries/OS9-HANDOFF.md | 12 ++ rust/pantheon-compositor/README.md | 10 +- .../src/bin/pantheon-drm-probe.rs | 119 +++++++++++++++++- 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index f1dc40772..020233699 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5750,3 +5750,15 @@ This is an immediate transition without Gala's animation. Default and tests or Dock runtime session were run; overlap timing, edge reveal and work area behavior still need an isolated Wingpanel/Dock session before hardware qualification. The active Gala session remains untouched. + +### Preflight Asahi-to-Apple DMA-BUF import without modesetting — 2026-10-19 + +Extended `pantheon-drm-probe` to allocate 64×64 GBM buffers with render and +scanout usage on `/dev/dri/renderD128`, export each as a DMA-BUF, and import +into the connected Apple KMS device's GBM implementation with scanout usage. +The current M1 Pro succeeded for implicit-modifier formats AB30, AR30, AB24 +and AR24. This confirms DMA-BUF allocation/export/import for these formats; it +does not prove DRM framebuffer creation or physical scanout. The probe opens +the devices but does not acquire DRM master, create framebuffers, or modeset. +The next hardware gate remains a controlled native compositor start in an +isolated session. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 283dab4a7..1558dd94c 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -121,10 +121,12 @@ host it finds EGL rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, Asahi) but KMS resource queries there return `Operation not supported`. The Apple display-controller `/dev/dri/card2` owns KMS: `eDP-1` is connected at 3024×1964@120 Hz and `DP-2` is connected with 3840×2160@30 Hz and 1920×1080@60 -Hz modes; `HDMI-A-1` is disconnected. Card2 has no EGL device. The native backend -therefore needs Smithay's multi-GPU render/import path from Asahi to Apple KMS; -single-node setup is not enough. This probe reads connector state and modes but -does not force a connector reprobe or test modesetting. +Hz modes; `HDMI-A-1` is disconnected. Card2 has no EGL device. The probe now +allocates 64×64 render/scanout GBM buffers on Asahi and attempts GBM DMA-BUF +imports on Apple KMS. On this host, implicit-modifier imports succeeded for +AB30, AR30, AB24 and AR24. This supports the chosen cross-device path, but does +not prove DRM framebuffer creation, atomic modesetting, or display scanout. The +probe does not acquire DRM master, force a connector reprobe, or change modes. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs index 1b8845d15..44de200ee 100644 --- a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -1,9 +1,17 @@ use std::{ - fs::File, + fs::{File, OpenOptions}, os::fd::{AsFd, BorrowedFd}, + path::PathBuf, }; -use smithay::backend::egl::EGLDevice; +use smithay::backend::{ + allocator::{ + Allocator, Buffer, Fourcc, Modifier, + dmabuf::AsDmabuf, + gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + }, + egl::EGLDevice, +}; struct ReadOnlyDrmDevice(File); @@ -19,6 +27,7 @@ impl smithay::reexports::drm::control::Device for ReadOnlyDrmDevice {} fn main() -> Result<(), Box> { let devices = EGLDevice::enumerate()?; let mut count = 0; + let mut hardware_render_path = None; for (index, device) in devices.enumerate() { count += 1; @@ -37,6 +46,9 @@ fn main() -> Result<(), Box> { .unwrap_or_else(|| "unavailable".to_owned()) }) .unwrap_or_else(|error| format!("unavailable ({error})")); + if !device.is_software() { + hardware_render_path = device.render_device_path().ok(); + } println!("EGL device {index}:"); println!(" DRM node: {drm_path}"); @@ -49,12 +61,20 @@ fn main() -> Result<(), Box> { println!("No EGL devices found."); } - inspect_kms_connectors()?; + let kms_path = inspect_kms_connectors(hardware_render_path.as_deref())?; + match (hardware_render_path, kms_path) { + (Some(render_path), Some(kms_path)) => probe_cross_gpu_import(&render_path, &kms_path), + _ => println!( + "Cross-GPU GBM import: skipped (could not identify a hardware renderer and connected KMS device)." + ), + } Ok(()) } -fn inspect_kms_connectors() -> Result<(), Box> { +fn inspect_kms_connectors( + render_path: Option<&std::path::Path>, +) -> Result, Box> { use smithay::reexports::drm::control::Device as _; let mut cards = std::fs::read_dir("/dev/dri")? @@ -70,6 +90,7 @@ fn inspect_kms_connectors() -> Result<(), Box> { }) .collect::>(); cards.sort(); + let mut connected_kms = None; for path in cards { println!("KMS device {}:", path.display()); @@ -92,6 +113,18 @@ fn inspect_kms_connectors() -> Result<(), Box> { match device.get_connector(*handle, false) { Ok(connector) => { println!(" {}: {:?}", connector, connector.state()); + let is_separate_device = render_path.is_some_and(|render_path| { + smithay::backend::drm::DrmNode::from_path(render_path) + .ok() + .zip(smithay::backend::drm::DrmNode::from_path(&path).ok()) + .is_some_and(|(render, kms)| render.dev_id() != kms.dev_id()) + }); + if is_separate_device + && connector.state() + == smithay::reexports::drm::control::connector::State::Connected + { + connected_kms.get_or_insert_with(|| path.clone()); + } for mode in connector.modes().iter().take(8) { let (width, height) = mode.size(); println!(" {width}x{height}@{}Hz", mode.vrefresh()); @@ -105,5 +138,81 @@ fn inspect_kms_connectors() -> Result<(), Box> { } } - Ok(()) + Ok(connected_kms) +} + +/// Check whether the render GPU can allocate a scanout-capable DMA-BUF that +/// the connected KMS device's GBM implementation can import. This does not +/// create a DRM framebuffer, acquire DRM master, or change an output mode. +fn probe_cross_gpu_import(render_path: &PathBuf, kms_path: &PathBuf) { + println!("Cross-GPU GBM import preflight:"); + println!(" render node: {}", render_path.display()); + println!(" KMS device: {}", kms_path.display()); + + let render_file = match OpenOptions::new().read(true).write(true).open(render_path) { + Ok(file) => file, + Err(error) => { + println!(" cannot open render node read/write: {error}"); + return; + } + }; + let render_gbm = match GbmDevice::new(render_file) { + Ok(gbm) => gbm, + Err(error) => { + println!(" cannot create render GBM device: {error}"); + return; + } + }; + let kms_file = match OpenOptions::new().read(true).write(true).open(kms_path) { + Ok(file) => file, + Err(error) => { + println!(" cannot open KMS device read/write: {error}"); + return; + } + }; + let kms_gbm = match GbmDevice::new(kms_file) { + Ok(gbm) => gbm, + Err(error) => { + println!(" cannot create KMS GBM device: {error}"); + return; + } + }; + + let mut allocator = GbmAllocator::new( + render_gbm, + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ); + let formats = [ + Fourcc::Abgr2101010, + Fourcc::Argb2101010, + Fourcc::Abgr8888, + Fourcc::Argb8888, + ]; + for format in formats { + let buffer = match allocator.create_buffer(64, 64, format, &[Modifier::Invalid]) { + Ok(buffer) => buffer, + Err(error) => { + println!(" {format:?}: render allocation failed: {error}"); + continue; + } + }; + let dmabuf = match buffer.export() { + Ok(dmabuf) => dmabuf, + Err(error) => { + println!(" {format:?}: DMA-BUF export failed: {error}"); + continue; + } + }; + match dmabuf.import_to(&kms_gbm, GbmBufferFlags::SCANOUT) { + Ok(_) => println!( + " {format:?} modifier {:?}: KMS GBM import succeeded", + dmabuf.format().modifier + ), + Err(error) => println!( + " {format:?} modifier {:?}: KMS GBM import failed: {error}", + dmabuf.format().modifier + ), + } + } + println!(" framebuffer creation and modesetting were not attempted."); } From b72d60fb18cc01ca36c992ccfdb32a53e3ae9657 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:16:07 -0400 Subject: [PATCH 114/798] Add rootless XWayland support to Pantheon compositor --- .../EXPERIENCE-REQUIREMENTS.md | 4 +- mseries/pantheon-rust-compositor/README.md | 8 + mseries/pantheon-rust-compositor/build-deb.sh | 2 +- rust/Cargo.lock | 35 ++ rust/pantheon-compositor/Cargo.toml | 2 +- .../src/grabs/resize_grab.rs | 44 ++- .../src/handlers/compositor.rs | 2 +- rust/pantheon-compositor/src/handlers/mod.rs | 56 ++- .../src/handlers/xdg_shell.rs | 38 +- rust/pantheon-compositor/src/input.rs | 32 +- rust/pantheon-compositor/src/main.rs | 19 +- rust/pantheon-compositor/src/state.rs | 16 + rust/pantheon-compositor/src/xwayland.rs | 338 ++++++++++++++++++ 13 files changed, 539 insertions(+), 57 deletions(-) create mode 100644 rust/pantheon-compositor/src/xwayland.rs diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 86488c14f..ffbae5727 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -16,6 +16,7 @@ it does not imply that the current prototype supports it. | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | +| Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus and clipboard exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard and clipboard paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | @@ -23,8 +24,7 @@ it does not imply that the current prototype supports it. ## Conditional work, driven by an observed need Add these only when an M-series workflow depends on them, and keep each as a -separate acceptance item: Xwayland for a required X11 app; IBus/input-method -support for the installed input method; accessibility protocols/actions used by +separate acceptance item: IBus/input-method support for the installed input method; accessibility protocols/actions used by the owner's setup; screencast/remote-desktop portal support for screen sharing; touch/gestures if the selected hardware and workflow use them. The measured M1 Pro topology already requires cross-device buffer sharing for native display diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 7295a8815..0732326ff 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -38,6 +38,14 @@ is build-checked but not runtime-qualified. The current source is US; `mozc-jp` is installed but inactive, so IBus remains a conditional workflow rather than a first-session requirement. +The prototype now starts Smithay's rootless XWayland window manager, maps X11 +windows into the shared desktop space, routes keyboard input to focused X11 +clients, and bridges clipboard ownership and transfers. Native startup exports +the selected `DISPLAY` through D-Bus and systemd activation environments; the +`--run` client waits for XWayland readiness. XWayland keyboard, clipboard, +session activation, and X11 window behavior compile but still need runtime +qualification. The package depends on the `xwayland` executable. + Panel reservations and managed maximize/tile layouts are tracked per output connector. Maximized/fullscreen windows and popup constraints use the window's current output, and removing a panel releases its work area. Multi-output diff --git a/mseries/pantheon-rust-compositor/build-deb.sh b/mseries/pantheon-rust-compositor/build-deb.sh index 19c20aa97..7ec4b37ce 100755 --- a/mseries/pantheon-rust-compositor/build-deb.sh +++ b/mseries/pantheon-rust-compositor/build-deb.sh @@ -59,7 +59,7 @@ Package: pantheon-rust-compositor Version: $VERSION Architecture: arm64 Maintainer: techmore -Depends: $SHLIB_DEPENDS, gnome-session, gtklock, libegl1, libgles2 +Depends: $SHLIB_DEPENDS, gnome-session, gtklock, libegl1, libgles2, xwayland Description: experimental Rust compositor for the Pantheon M-series session An opt-in Smithay Wayland compositor and a separate experimental GNOME session. EOF diff --git a/rust/Cargo.lock b/rust/Cargo.lock index db5103f4a..f016285bd 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -355,6 +355,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.3.1" @@ -458,6 +464,20 @@ dependencies = [ "linux-raw-sys 0.9.4", ] +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "endi" version = "1.1.1" @@ -1168,6 +1188,12 @@ dependencies = [ name = "mseries-power" version = "0.1.0" +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "ndk" version = "0.9.0" @@ -1633,6 +1659,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -1764,6 +1796,7 @@ dependencies = [ "drm-ffi", "drm-fourcc", "drm-sys", + "encoding_rs", "errno", "gbm", "gl_generator", @@ -1779,6 +1812,7 @@ dependencies = [ "rand", "reis", "rustix 1.1.5", + "scopeguard", "siphasher", "smallvec", "tempfile", @@ -1793,6 +1827,7 @@ dependencies = [ "wayland-protocols-wlr", "wayland-server", "winit", + "x11rb", "xkbcommon", ] diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index bbef9835a..2f240b928 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -46,4 +46,4 @@ gio = "0.20" git = "https://github.com/Smithay/smithay.git" rev = "023b78b1c5a05ca512767cacc07efca55fae8be4" default-features = false -features = ["backend_winit", "wayland_frontend", "desktop"] +features = ["backend_winit", "wayland_frontend", "desktop", "xwayland"] diff --git a/rust/pantheon-compositor/src/grabs/resize_grab.rs b/rust/pantheon-compositor/src/grabs/resize_grab.rs index aa72c03ea..f6a0c9314 100644 --- a/rust/pantheon-compositor/src/grabs/resize_grab.rs +++ b/rust/pantheon-compositor/src/grabs/resize_grab.rs @@ -8,12 +8,13 @@ use smithay::{ desktop::{Space, Window}, input::pointer::{ AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, - GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, - GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, - PointerInnerHandle, RelativeMotionEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, + GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, + MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, }, reexports::{ - wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::protocol::wl_surface::WlSurface, + wayland_protocols::xdg::shell::server::xdg_toplevel, + wayland_server::protocol::wl_surface::WlSurface, }, utils::{Logical, Point, Rectangle, Size}, wayland::{compositor, shell::xdg::SurfaceCachedState}, @@ -63,7 +64,10 @@ impl ResizeSurfaceGrab { let initial_rect = initial_window_rect; ResizeSurfaceState::with(window.toplevel().unwrap().wl_surface(), |state| { - *state = ResizeSurfaceState::Resizing { edges, initial_rect }; + *state = ResizeSurfaceState::Resizing { + edges, + initial_rect, + }; }); Self { @@ -118,8 +122,16 @@ impl PointerGrab for ResizeSurfaceGrab { let min_width = min_size.w.max(1); let min_height = min_size.h.max(1); - let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w }; - let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h }; + let max_width = if max_size.w == 0 { + i32::MAX + } else { + max_size.w + }; + let max_height = if max_size.h == 0 { + i32::MAX + } else { + max_size.h + }; self.last_window_size = Size::from(( new_window_width.max(min_width).min(max_width), @@ -187,7 +199,11 @@ impl PointerGrab for ResizeSurfaceGrab { handle.axis(data, details) } - fn frame(&mut self, data: &mut PantheonCompositor, handle: &mut PointerInnerHandle<'_, PantheonCompositor>) { + fn frame( + &mut self, + data: &mut PantheonCompositor, + handle: &mut PointerInnerHandle<'_, PantheonCompositor>, + ) { handle.frame(data); } @@ -306,8 +322,14 @@ impl ResizeSurfaceState { fn commit(&mut self) -> Option<(ResizeEdge, Rectangle)> { match *self { - Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)), - Self::WaitingForLastCommit { edges, initial_rect } => { + Self::Resizing { + edges, + initial_rect, + } => Some((edges, initial_rect)), + Self::WaitingForLastCommit { + edges, + initial_rect, + } => { // The resize is done, let's go back to idle *self = Self::Idle; @@ -322,7 +344,7 @@ impl ResizeSurfaceState { pub fn handle_commit(space: &mut Space, surface: &WlSurface) -> Option<()> { let window = space .elements() - .find(|w| w.toplevel().unwrap().wl_surface() == surface) + .find(|w| w.toplevel().is_some_and(|top| top.wl_surface() == surface)) .cloned()?; let mut window_loc = space.element_location(&window)?; diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index 675abc888..807437c5d 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -36,7 +36,7 @@ impl CompositorHandler for PantheonCompositor { if let Some(window) = self .space .elements() - .find(|w| w.toplevel().unwrap().wl_surface() == &root) + .find(|w| w.toplevel().is_some_and(|top| top.wl_surface() == &root)) { window.on_commit(); } diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs index 15606f2f9..2310aa0dc 100644 --- a/rust/pantheon-compositor/src/handlers/mod.rs +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -15,10 +15,10 @@ use smithay::reexports::wayland_server::protocol::{wl_output::WlOutput, wl_surfa use smithay::utils::{IsAlive, Serial}; use smithay::wayland::output::OutputHandler; use smithay::wayland::pointer_constraints::PointerConstraintsHandler; -use smithay::wayland::selection::SelectionHandler; use smithay::wayland::selection::data_device::{ DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, }; +use smithay::wayland::selection::{SelectionHandler, SelectionSource, SelectionTarget}; use smithay::wayland::session_lock::{ LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker, }; @@ -57,6 +57,39 @@ impl PointerConstraintsHandler for PantheonCompositor {} impl SelectionHandler for PantheonCompositor { type SelectionUserData = (); + + fn new_selection( + &mut self, + selection: SelectionTarget, + source: Option, + _seat: Seat, + ) { + if let Some(xwm) = self.xwm.as_mut() + && let Err(error) = + xwm.new_selection(selection, source.map(|source| source.mime_types())) + { + tracing::warn!( + ?selection, + ?error, + "failed to forward Wayland selection to XWayland" + ); + } + } + + fn send_selection( + &mut self, + selection: SelectionTarget, + mime_type: String, + fd: std::os::fd::OwnedFd, + _seat: Seat, + _user_data: &Self::SelectionUserData, + ) { + if let Some(xwm) = self.xwm.as_mut() + && let Err(error) = xwm.send_selection(selection, mime_type, fd) + { + tracing::warn!(?selection, ?error, "failed to send selection to XWayland"); + } + } } impl DataDeviceHandler for PantheonCompositor { @@ -117,6 +150,8 @@ impl SessionLockHandler for PantheonCompositor { let keyboard = self.seat.get_keyboard().unwrap(); keyboard.unset_grab(self); self.keyboard_focus_before_lock = keyboard.current_focus(); + self.x11_keyboard_focus_before_lock = self.x11_keyboard_focus.clone(); + self.set_x11_keyboard_focus(None, serial); keyboard.set_focus(self, None, serial); self.session_locked = true; @@ -156,11 +191,19 @@ impl SessionLockHandler for PantheonCompositor { .keyboard_focus_before_lock .take() .filter(|surface| surface.alive()); - self.seat.get_keyboard().unwrap().set_focus( - self, - focus, - smithay::utils::SERIAL_COUNTER.next_serial(), - ); + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + if let Some(x11_focus) = self + .x11_keyboard_focus_before_lock + .take() + .filter(IsAlive::alive) + { + self.set_x11_keyboard_focus(Some(x11_focus), serial); + } else { + self.seat + .get_keyboard() + .unwrap() + .set_focus(self, focus, serial); + } let pointer = self.seat.get_pointer().unwrap(); let location = pointer.current_location(); @@ -196,6 +239,7 @@ impl SessionLockHandler for PantheonCompositor { let output_name = output.name().to_string(); self.lock_surfaces.insert(output_name, surface.clone()); + self.set_x11_keyboard_focus(None, smithay::utils::SERIAL_COUNTER.next_serial()); self.seat.get_keyboard().unwrap().set_focus( self, Some(surface.wl_surface().clone()), diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 973468261..1754163ff 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -74,7 +74,10 @@ impl XdgShellHandler for PantheonCompositor { let window = self .space .elements() - .find(|w| w.toplevel().unwrap().wl_surface() == wl_surface) + .find(|w| { + w.toplevel() + .is_some_and(|top| top.wl_surface() == wl_surface) + }) .unwrap() .clone(); let initial_window_location = self.space.element_location(&window).unwrap(); @@ -107,7 +110,10 @@ impl XdgShellHandler for PantheonCompositor { let window = self .space .elements() - .find(|w| w.toplevel().unwrap().wl_surface() == wl_surface) + .find(|w| { + w.toplevel() + .is_some_and(|top| top.wl_surface() == wl_surface) + }) .unwrap() .clone(); let initial_window_location = self.space.element_location(&window).unwrap(); @@ -276,29 +282,25 @@ impl PantheonCompositor { .find(|window| { window .toplevel() - .is_some_and(|top| !self.is_panel_surface(top.wl_surface())) + .is_none_or(|top| !self.is_panel_surface(top.wl_surface())) }) .cloned(); if let Some(window) = next_window { self.space.raise_element(&window, true); - if let Some(surface) = window.toplevel().map(|top| top.wl_surface().clone()) { - let serial = smithay::utils::SERIAL_COUNTER.next_serial(); - self.seat - .get_keyboard() - .unwrap() - .set_focus(self, Some(surface), serial); - self.space.elements().for_each(|window| { - if let Some(toplevel) = window.toplevel() { - toplevel.send_pending_configure(); - } - }); - } + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.focus_window_keyboard(&window, serial); + self.space.elements().for_each(|window| { + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } + }); } else { let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + self.set_x11_keyboard_focus(None, serial); self.seat .get_keyboard() .unwrap() - .set_focus(self, Option::::None, serial); + .set_focus(self, None, serial); } self.refresh_panel_visibility(); } @@ -341,7 +343,7 @@ pub fn handle_commit(state: &mut PantheonCompositor, surface: &WlSurface) { let window = state .space .elements() - .find(|w| w.toplevel().unwrap().wl_surface() == surface) + .find(|w| w.toplevel().is_some_and(|top| top.wl_surface() == surface)) .cloned(); if let Some(window) = window { let initial_configure_sent = with_states(surface, |states| { @@ -449,7 +451,7 @@ impl PantheonCompositor { let Some(window) = self .space .elements() - .find(|w| w.toplevel().unwrap().wl_surface() == &root) + .find(|w| w.toplevel().is_some_and(|top| top.wl_surface() == &root)) else { return; }; diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index 1d0ef8265..fec19daad 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -8,10 +8,9 @@ use smithay::{ KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, }, input::{ - keyboard::{FilterResult, XkbConfig}, + keyboard::{FilterResult, KeyboardTarget, XkbConfig}, pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent}, }, - reexports::wayland_server::protocol::wl_surface::WlSurface, utils::SERIAL_COUNTER, }; @@ -106,6 +105,8 @@ impl PantheonCompositor { let serial = SERIAL_COUNTER.next_serial(); let time = Event::time(&event); let key_state = event.state(); + let x11_focus = self.x11_keyboard_focus.clone(); + let seat = self.seat.clone(); self.seat.get_keyboard().unwrap().input::<(), _>( self, @@ -130,6 +131,14 @@ impl PantheonCompositor { .activate_key(keysym.modified_sym(), modifiers)) { FilterResult::Intercept(()) + } else if let Some(focus) = x11_focus.as_ref() { + >::key( + focus, &seat, state, keysym, key_state, serial, time, + ); + >::modifiers( + focus, &seat, state, *modifiers, serial, + ); + FilterResult::Intercept(()) } else { FilterResult::Forward } @@ -233,22 +242,29 @@ impl PantheonCompositor { .element_under(pointer.current_location()) .map(|(w, l)| (w.clone(), l)) { - let surface = window.toplevel().unwrap().wl_surface().clone(); - if self.is_panel_surface(&surface) { + let panel_surface = window + .toplevel() + .is_some_and(|top| self.is_panel_surface(top.wl_surface())); + if panel_surface { self.space.raise_element(&window, false); } else { self.space.raise_element(&window, true); - keyboard.set_focus(self, Some(surface), serial); + self.focus_window_keyboard(&window, serial); } self.space.elements().for_each(|window| { - window.toplevel().unwrap().send_pending_configure(); + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } }); } else { + self.set_x11_keyboard_focus(None, serial); self.space.elements().for_each(|window| { window.set_activated(false); - window.toplevel().unwrap().send_pending_configure(); + if let Some(toplevel) = window.toplevel() { + toplevel.send_pending_configure(); + } }); - keyboard.set_focus(self, Option::::None, serial); + keyboard.set_focus(self, None, serial); } }; diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 08c33ec3b..fe6e6b612 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -13,6 +13,7 @@ mod shell_compat; mod state; mod tiling; mod winit; +mod xwayland; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; pub use state::PantheonCompositor; @@ -61,17 +62,17 @@ fn main() -> Result<(), Box> { } }; + crate::xwayland::start( + &mut event_loop, + &mut state, + matches!(backend, BackendChoice::Native), + )?; + state.pending_command = command; + state.notify_systemd_when_ready = notify_systemd; + // Launch requested clients against the compositor socket selected above. unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; - if notify_systemd { - notify_systemd_ready()?; - } - - if let Some((program, args)) = command { - std::process::Command::new(program).args(args).spawn()?; - } - event_loop.run(None, &mut state, move |_| {})?; Ok(()) } @@ -87,7 +88,7 @@ fn init_logging() { /// Tell a systemd user service that the compositor has initialized its /// Wayland socket and selected backend. Native startup only reaches this point /// after the renderer and connected KMS outputs have been initialized. -fn notify_systemd_ready() -> io::Result<()> { +pub(crate) fn notify_systemd_ready() -> io::Result<()> { let socket = std::env::var_os("NOTIFY_SOCKET") .ok_or_else(|| io::Error::other("--notify-systemd requires NOTIFY_SOCKET"))?; let encoded_socket = socket.as_encoded_bytes(); diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 5c5eb2719..108e0c308 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -33,6 +33,7 @@ use smithay::{ xdg_activation::{ XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, }, + xwayland_shell::XWaylandShellState, }, }; @@ -72,6 +73,13 @@ pub struct PantheonCompositor { pub lock_confirmation: Option, pub lock_confirmation_outputs: HashSet, pub keyboard_focus_before_lock: Option, + pub x11_keyboard_focus_before_lock: Option, + pub x11_keyboard_focus: Option, + pub xwayland_shell_state: XWaylandShellState, + pub xwm: Option, + pub xdisplay: Option, + pub pending_command: Option<(String, Vec)>, + pub notify_systemd_when_ready: bool, pub seat_state: SeatState, pub(crate) keyboard_config: crate::input::StartupKeyboardConfig, pub data_device_state: DataDeviceState, @@ -155,6 +163,7 @@ impl PantheonCompositor { let viewporter_state = ViewporterState::new::(&dh); let xdg_activation_state = XdgActivationState::new::(&dh); let session_lock_state = SessionLockManagerState::new::(&dh, |_| true); + let xwayland_shell_state = XWaylandShellState::new::(&dh); // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); @@ -278,6 +287,13 @@ impl PantheonCompositor { lock_confirmation: None, lock_confirmation_outputs: HashSet::new(), keyboard_focus_before_lock: None, + x11_keyboard_focus_before_lock: None, + x11_keyboard_focus: None, + xwayland_shell_state, + xwm: None, + xdisplay: None, + pending_command: None, + notify_systemd_when_ready: false, seat_state, keyboard_config, data_device_state, diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs new file mode 100644 index 000000000..3f2bc7452 --- /dev/null +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -0,0 +1,338 @@ +use std::process::Stdio; + +use smithay::{ + desktop::Window, + input::keyboard::KeyboardTarget, + reexports::calloop::EventLoop, + utils::{Logical, Point, Rectangle}, + wayland::{ + selection::{ + SelectionTarget, + data_device::{ + clear_data_device_selection, current_data_device_selection_userdata, + request_data_device_client_selection, set_data_device_selection, + }, + }, + xwayland_shell::XWaylandShellHandler, + }, + xwayland::{ + X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler, + xwm::{Reorder, ResizeEdge, XwmId}, + }, +}; + +use crate::PantheonCompositor; + +pub fn start( + event_loop: &mut EventLoop<'static, PantheonCompositor>, + state: &mut PantheonCompositor, + export_session_environment: bool, +) -> Result<(), Box> { + let (source, client) = XWayland::spawn( + &state.display_handle, + None, + std::iter::empty::<(String, String)>(), + std::iter::empty::(), + true, + Stdio::null(), + Stdio::null(), + |_| (), + )?; + let display_handle = state.display_handle.clone(); + let loop_handle = event_loop.handle(); + event_loop + .handle() + .insert_source(source, move |event, _, state| { + match event { + XWaylandEvent::Ready { + x11_socket, + display_number, + } => { + match X11Wm::start_wm( + loop_handle.clone(), + &display_handle, + x11_socket, + client.clone(), + ) { + Ok(wm) => { + state.xwm = Some(wm); + state.xdisplay = Some(display_number); + let display = format!(":{display_number}"); + // SAFETY: compositor startup owns the session environment before + // launching any requested clients. + unsafe { std::env::set_var("DISPLAY", &display) }; + if export_session_environment { + let activation_display = format!("DISPLAY={display}"); + match std::process::Command::new( + "dbus-update-activation-environment", + ) + .args(["--systemd", &activation_display]) + .status() + { + Ok(status) if status.success() => {} + Ok(status) => tracing::warn!( + ?status, + "could not update desktop DISPLAY environment for XWayland" + ), + Err(error) => tracing::warn!( + ?error, + "could not update desktop DISPLAY environment for XWayland" + ), + } + } + if state.notify_systemd_when_ready { + state.notify_systemd_when_ready = false; + if let Err(error) = crate::notify_systemd_ready() { + tracing::error!( + ?error, + "could not notify systemd that Pantheon is ready" + ); + } + } + if let Some((program, args)) = state.pending_command.take() + && let Err(error) = + std::process::Command::new(program).args(args).spawn() + { + tracing::error!(?error, "could not launch requested client"); + } + tracing::info!(display_number, "XWayland is ready"); + } + Err(error) => { + tracing::error!(?error, "could not attach the XWayland window manager") + } + } + } + XWaylandEvent::Error => tracing::error!("XWayland failed to start"), + } + })?; + Ok(()) +} + +impl XWaylandShellHandler for PantheonCompositor { + fn xwayland_shell_state( + &mut self, + ) -> &mut smithay::wayland::xwayland_shell::XWaylandShellState { + &mut self.xwayland_shell_state + } +} + +impl XwmHandler for PantheonCompositor { + fn xwm_state(&mut self, xwm: XwmId) -> &mut X11Wm { + let wm = self + .xwm + .as_mut() + .expect("XWayland window manager is not ready"); + debug_assert_eq!(wm.id(), xwm); + wm + } + + fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + + fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + + fn map_window_request(&mut self, _xwm: XwmId, surface: X11Surface) { + if let Err(error) = surface.set_mapped(true) { + tracing::warn!(?error, "could not map X11 window"); + return; + } + let geometry = surface.last_configure(); + let location = self + .space + .outputs() + .find_map(|output| { + let rect = self.space.output_geometry(output)?; + Some(Point::from(( + rect.loc.x + (rect.size.w - geometry.size.w).max(0) / 2, + rect.loc.y + (rect.size.h - geometry.size.h).max(0) / 2, + ))) + }) + .unwrap_or(Point::from((0, 0))); + let window = Window::new_x11_window(surface.clone()); + self.space.map_element(window.clone(), location, true); + let configured = Rectangle::new(location, geometry.size); + if let Err(error) = surface.configure(Some(configured)) { + tracing::warn!(?error, "could not configure mapped X11 window"); + } + window.set_activated(true); + if !self.session_locked { + self.focus_window_keyboard(&window, smithay::utils::SERIAL_COUNTER.next_serial()); + } + } + + fn mapped_override_redirect_window(&mut self, _xwm: XwmId, surface: X11Surface) { + let location = surface.last_configure().loc; + self.space + .map_element(Window::new_x11_window(surface), location, true); + } + + fn unmapped_window(&mut self, _xwm: XwmId, surface: X11Surface) { + let window = { + self.space + .elements() + .find(|window| { + window + .x11_surface() + .is_some_and(|candidate| candidate == &surface) + }) + .cloned() + }; + if let Some(window) = window { + self.space.unmap_elem(&window); + } + if !surface.is_override_redirect() + && let Err(error) = surface.set_mapped(false) + { + tracing::warn!(?error, "could not unmap X11 window"); + } + } + + fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + + fn configure_request( + &mut self, + _xwm: XwmId, + surface: X11Surface, + x: Option, + y: Option, + width: Option, + height: Option, + _reorder: Option, + ) { + let mut geometry = surface.last_configure(); + if let Some(x) = x { + geometry.loc.x = x; + } + if let Some(y) = y { + geometry.loc.y = y; + } + if let Some(width) = width { + geometry.size.w = width as i32; + } + if let Some(height) = height { + geometry.size.h = height as i32; + } + if let Err(error) = surface.configure(Some(geometry)) { + tracing::warn!(?error, "could not honor X11 configure request"); + } + } + + fn configure_notify( + &mut self, + _xwm: XwmId, + surface: X11Surface, + geometry: Rectangle, + _above: Option, + ) { + let window = { + self.space + .elements() + .find(|window| { + window + .x11_surface() + .is_some_and(|candidate| candidate == &surface) + }) + .cloned() + }; + if let Some(window) = window { + self.space.map_element(window, geometry.loc, false); + } + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + fn resize_request( + &mut self, + _xwm: XwmId, + _window: X11Surface, + _button: u32, + _edge: ResizeEdge, + ) { + } + + fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {} + + fn allow_selection_access(&mut self, xwm: XwmId, selection: SelectionTarget) -> bool { + selection == SelectionTarget::Clipboard + && self + .x11_keyboard_focus + .as_ref() + .is_some_and(|surface| surface.xwm_id() == Some(xwm)) + } + + fn send_selection( + &mut self, + _xwm: XwmId, + selection: SelectionTarget, + mime_type: String, + fd: std::os::fd::OwnedFd, + ) { + if selection == SelectionTarget::Clipboard + && let Err(error) = request_data_device_client_selection(&self.seat, mime_type, fd) + { + tracing::warn!( + ?error, + "could not transfer the Wayland clipboard to XWayland" + ); + } + } + + fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec) { + if selection == SelectionTarget::Clipboard { + set_data_device_selection(&self.display_handle, &self.seat, mime_types, ()); + } + } + + fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) { + if selection == SelectionTarget::Clipboard + && current_data_device_selection_userdata(&self.seat).is_some() + { + clear_data_device_selection(&self.display_handle, &self.seat); + } + } +} + +impl PantheonCompositor { + pub(crate) fn set_x11_keyboard_focus( + &mut self, + focus: Option, + serial: smithay::utils::Serial, + ) { + if self.x11_keyboard_focus == focus { + return; + } + let seat = self.seat.clone(); + if let Some(previous) = self.x11_keyboard_focus.take() { + >::leave( + &previous, &seat, self, serial, + ); + } + self.x11_keyboard_focus = focus.clone(); + if let Some(focus) = focus { + if let Some(keyboard) = self.seat.get_keyboard() { + keyboard.set_focus(self, None, serial); + } + >::enter( + &focus, + &seat, + self, + Vec::new(), + serial, + ); + } + } + + pub(crate) fn focus_window_keyboard( + &mut self, + window: &Window, + serial: smithay::utils::Serial, + ) { + if let Some(surface) = window.x11_surface() { + self.set_x11_keyboard_focus(Some(surface.clone()), serial); + } else if let Some(toplevel) = window.toplevel() { + self.set_x11_keyboard_focus(None, serial); + if let Some(keyboard) = self.seat.get_keyboard() { + keyboard.set_focus(self, Some(toplevel.wl_surface().clone()), serial); + } + } + } +} From b1b9f60a50037e73db936ea759b7b8a1360a2192 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:18:21 -0400 Subject: [PATCH 115/798] Handle X11 window move and resize requests --- mseries/pantheon-rust-compositor/README.md | 5 +- .../src/grabs/resize_grab.rs | 82 +++++++----- rust/pantheon-compositor/src/xwayland.rs | 119 ++++++++++++++++-- 3 files changed, 164 insertions(+), 42 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 0732326ff..84a3755d5 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -39,8 +39,9 @@ is build-checked but not runtime-qualified. The current source is US; rather than a first-session requirement. The prototype now starts Smithay's rootless XWayland window manager, maps X11 -windows into the shared desktop space, routes keyboard input to focused X11 -clients, and bridges clipboard ownership and transfers. Native startup exports +windows into the shared desktop space, routes move/resize and activation +requests, sends keyboard input to focused X11 clients, and bridges clipboard +ownership and transfers. Native startup exports the selected `DISPLAY` through D-Bus and systemd activation environments; the `--run` client waits for XWayland readiness. XWayland keyboard, clipboard, session activation, and X11 window behavior compile but still need runtime diff --git a/rust/pantheon-compositor/src/grabs/resize_grab.rs b/rust/pantheon-compositor/src/grabs/resize_grab.rs index f6a0c9314..b17e37ef9 100644 --- a/rust/pantheon-compositor/src/grabs/resize_grab.rs +++ b/rust/pantheon-compositor/src/grabs/resize_grab.rs @@ -63,12 +63,14 @@ impl ResizeSurfaceGrab { ) -> Self { let initial_rect = initial_window_rect; - ResizeSurfaceState::with(window.toplevel().unwrap().wl_surface(), |state| { - *state = ResizeSurfaceState::Resizing { - edges, - initial_rect, - }; - }); + if let Some(toplevel) = window.toplevel() { + ResizeSurfaceState::with(toplevel.wl_surface(), |state| { + *state = ResizeSurfaceState::Resizing { + edges, + initial_rect, + }; + }); + } Self { start_data, @@ -113,11 +115,15 @@ impl PointerGrab for ResizeSurfaceGrab { } let (min_size, max_size) = - compositor::with_states(self.window.toplevel().unwrap().wl_surface(), |states| { - let mut guard = states.cached_state.get::(); - let data = guard.current(); - (data.min_size, data.max_size) - }); + self.window + .toplevel() + .map_or((Size::from((1, 1)), Size::from((0, 0))), |toplevel| { + compositor::with_states(toplevel.wl_surface(), |states| { + let mut guard = states.cached_state.get::(); + let data = guard.current(); + (data.min_size, data.max_size) + }) + }); let min_width = min_size.w.max(1); let min_height = min_size.h.max(1); @@ -138,13 +144,26 @@ impl PointerGrab for ResizeSurfaceGrab { new_window_height.max(min_height).min(max_height), )); - let xdg = self.window.toplevel().unwrap(); - xdg.with_pending_state(|state| { - state.states.set(xdg_toplevel::State::Resizing); - state.size = Some(self.last_window_size); - }); - - xdg.send_pending_configure(); + if let Some(xdg) = self.window.toplevel() { + xdg.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + xdg.send_pending_configure(); + } else if let Some(x11) = self.window.x11_surface() { + let mut rect = self.initial_rect; + rect.size = self.last_window_size; + if self.edges.intersects(ResizeEdge::LEFT) { + rect.loc.x += self.initial_rect.size.w - rect.size.w; + } + if self.edges.intersects(ResizeEdge::TOP) { + rect.loc.y += self.initial_rect.size.h - rect.size.h; + } + if let Err(error) = x11.configure(Some(rect)) { + tracing::warn!(?error, "could not resize X11 window"); + } + data.space.map_element(self.window.clone(), rect.loc, false); + } } fn relative_motion( @@ -173,20 +192,19 @@ impl PointerGrab for ResizeSurfaceGrab { // No more buttons are pressed, release the grab. handle.unset_grab(self, data, event.serial, event.time, true); - let xdg = self.window.toplevel().unwrap(); - xdg.with_pending_state(|state| { - state.states.unset(xdg_toplevel::State::Resizing); - state.size = Some(self.last_window_size); - }); - - xdg.send_pending_configure(); - - ResizeSurfaceState::with(xdg.wl_surface(), |state| { - *state = ResizeSurfaceState::WaitingForLastCommit { - edges: self.edges, - initial_rect: self.initial_rect, - }; - }); + if let Some(xdg) = self.window.toplevel() { + xdg.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Resizing); + state.size = Some(self.last_window_size); + }); + xdg.send_pending_configure(); + ResizeSurfaceState::with(xdg.wl_surface(), |state| { + *state = ResizeSurfaceState::WaitingForLastCommit { + edges: self.edges, + initial_rect: self.initial_rect, + }; + }); + } } } diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs index 3f2bc7452..ef9461133 100644 --- a/rust/pantheon-compositor/src/xwayland.rs +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -3,6 +3,7 @@ use std::process::Stdio; use smithay::{ desktop::Window, input::keyboard::KeyboardTarget, + input::pointer::Focus, reexports::calloop::EventLoop, utils::{Logical, Point, Rectangle}, wayland::{ @@ -22,6 +23,9 @@ use smithay::{ }; use crate::PantheonCompositor; +use crate::grabs::{ + MoveSurfaceGrab, ResizeSurfaceGrab, resize_grab::ResizeEdge as PantheonResizeEdge, +}; pub fn start( event_loop: &mut EventLoop<'static, PantheonCompositor>, @@ -184,6 +188,10 @@ impl XwmHandler for PantheonCompositor { { tracing::warn!(?error, "could not unmap X11 window"); } + if self.x11_keyboard_focus.as_ref() == Some(&surface) { + self.set_x11_keyboard_focus(None, smithay::utils::SERIAL_COUNTER.next_serial()); + self.focus_next_window(); + } } fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {} @@ -240,16 +248,13 @@ impl XwmHandler for PantheonCompositor { self.request_native_redraw(); } - fn resize_request( - &mut self, - _xwm: XwmId, - _window: X11Surface, - _button: u32, - _edge: ResizeEdge, - ) { + fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, edge: ResizeEdge) { + self.begin_x11_resize(&window, edge); } - fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {} + fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) { + self.begin_x11_move(&window); + } fn allow_selection_access(&mut self, xwm: XwmId, selection: SelectionTarget) -> bool { selection == SelectionTarget::Clipboard @@ -289,9 +294,107 @@ impl XwmHandler for PantheonCompositor { clear_data_device_selection(&self.display_handle, &self.seat); } } + + fn active_window_request( + &mut self, + _xwm: XwmId, + surface: X11Surface, + _timestamp: u32, + _currently_active_window: Option, + ) { + if self.session_locked { + return; + } + let Some(window) = self.find_x11_window(&surface) else { + return; + }; + self.space.raise_element(&window, true); + self.focus_window_keyboard(&window, smithay::utils::SERIAL_COUNTER.next_serial()); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } } impl PantheonCompositor { + fn find_x11_window(&self, surface: &X11Surface) -> Option { + self.space + .elements() + .find(|window| { + window + .x11_surface() + .is_some_and(|candidate| candidate == surface) + }) + .cloned() + } + + fn begin_x11_move(&mut self, surface: &X11Surface) { + let Some(pointer) = self.seat.get_pointer() else { + return; + }; + let Some(start_data) = pointer.grab_start_data() else { + return; + }; + let Some(window) = self.find_x11_window(surface) else { + return; + }; + let Some(initial_window_location) = self.space.element_location(&window) else { + return; + }; + if let Some(wl_surface) = surface.wl_surface() { + self.clear_tiling_for_surface(&wl_surface); + } + self.move_grab_active = true; + self.corner_snap_cancelled = false; + self.corner_snap_preview = None; + let grab = MoveSurfaceGrab { + start_data, + window, + initial_window_location, + }; + pointer.set_grab( + self, + grab, + smithay::utils::SERIAL_COUNTER.next_serial(), + Focus::Clear, + ); + } + + fn begin_x11_resize(&mut self, surface: &X11Surface, edge: ResizeEdge) { + let Some(pointer) = self.seat.get_pointer() else { + return; + }; + let Some(start_data) = pointer.grab_start_data() else { + return; + }; + let Some(window) = self.find_x11_window(surface) else { + return; + }; + let Some(location) = self.space.element_location(&window) else { + return; + }; + if let Some(wl_surface) = surface.wl_surface() { + self.clear_tiling_for_surface(&wl_surface); + } + let edges = match edge { + ResizeEdge::Top => PantheonResizeEdge::TOP, + ResizeEdge::Bottom => PantheonResizeEdge::BOTTOM, + ResizeEdge::Left => PantheonResizeEdge::LEFT, + ResizeEdge::TopLeft => PantheonResizeEdge::TOP_LEFT, + ResizeEdge::BottomLeft => PantheonResizeEdge::BOTTOM_LEFT, + ResizeEdge::Right => PantheonResizeEdge::RIGHT, + ResizeEdge::TopRight => PantheonResizeEdge::TOP_RIGHT, + ResizeEdge::BottomRight => PantheonResizeEdge::BOTTOM_RIGHT, + }; + let rect = Rectangle::new(location, window.geometry().size); + let grab = ResizeSurfaceGrab::start(start_data, window, edges, rect); + pointer.set_grab( + self, + grab, + smithay::utils::SERIAL_COUNTER.next_serial(), + Focus::Clear, + ); + } + pub(crate) fn set_x11_keyboard_focus( &mut self, focus: Option, From 22fee828abcd6fa9c852832e1d33bc27fbfe5a12 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:19:14 -0400 Subject: [PATCH 116/798] Handle X11 window state requests --- mseries/pantheon-rust-compositor/README.md | 8 +- rust/pantheon-compositor/src/state.rs | 4 + rust/pantheon-compositor/src/xwayland.rs | 116 ++++++++++++++++++++- 3 files changed, 122 insertions(+), 6 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 84a3755d5..46b21858c 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -39,10 +39,10 @@ is build-checked but not runtime-qualified. The current source is US; rather than a first-session requirement. The prototype now starts Smithay's rootless XWayland window manager, maps X11 -windows into the shared desktop space, routes move/resize and activation -requests, sends keyboard input to focused X11 clients, and bridges clipboard -ownership and transfers. Native startup exports -the selected `DISPLAY` through D-Bus and systemd activation environments; the +windows into the shared desktop space, routes move/resize, maximize, +fullscreen, minimize, restore, and activation requests, sends keyboard input to +focused X11 clients, and bridges clipboard ownership and transfers. Native +startup exports the selected `DISPLAY` through D-Bus and systemd activation environments; the `--run` client waits for XWayland readiness. XWayland keyboard, clipboard, session activation, and X11 window behavior compile but still need runtime qualification. The package depends on the `xwayland` executable. diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 108e0c308..2de741293 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -91,6 +91,8 @@ pub struct PantheonCompositor { pub last_work_areas: HashMap>, pub managed_windows: HashMap, + pub x11_managed_windows: HashMap, + pub x11_minimized_windows: HashMap)>, pub tiled_windows: HashMap, pub tiling_bindings: TilingBindings, pub corner_snap_preview: Option, @@ -304,6 +306,8 @@ impl PantheonCompositor { panels: HashMap::new(), last_work_areas: HashMap::new(), managed_windows: HashMap::new(), + x11_managed_windows: HashMap::new(), + x11_minimized_windows: HashMap::new(), tiled_windows: HashMap::new(), tiling_bindings: TilingBindings::load(), corner_snap_preview: None, diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs index ef9461133..a4d7099c7 100644 --- a/rust/pantheon-compositor/src/xwayland.rs +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -22,10 +22,10 @@ use smithay::{ }, }; -use crate::PantheonCompositor; use crate::grabs::{ MoveSurfaceGrab, ResizeSurfaceGrab, resize_grab::ResizeEdge as PantheonResizeEdge, }; +use crate::{PantheonCompositor, state::ManagedWindow}; pub fn start( event_loop: &mut EventLoop<'static, PantheonCompositor>, @@ -194,7 +194,61 @@ impl XwmHandler for PantheonCompositor { } } - fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + fn destroyed_window(&mut self, _xwm: XwmId, surface: X11Surface) { + self.x11_managed_windows.remove(&surface.window_id()); + self.x11_minimized_windows.remove(&surface.window_id()); + } + + fn maximize_request(&mut self, _xwm: XwmId, surface: X11Surface) { + self.configure_x11_window_mode(&surface, true, false); + } + + fn unmaximize_request(&mut self, _xwm: XwmId, surface: X11Surface) { + self.configure_x11_window_mode(&surface, false, false); + } + + fn fullscreen_request(&mut self, _xwm: XwmId, surface: X11Surface) { + self.configure_x11_window_mode(&surface, false, true); + } + + fn unfullscreen_request(&mut self, _xwm: XwmId, surface: X11Surface) { + self.configure_x11_window_mode(&surface, false, false); + } + + fn minimize_request(&mut self, _xwm: XwmId, surface: X11Surface) { + let Some(window) = self.find_x11_window(&surface) else { + return; + }; + let Some(location) = self.space.element_location(&window) else { + return; + }; + self.x11_minimized_windows + .insert(surface.window_id(), (window.clone(), location)); + self.space.unmap_elem(&window); + if let Err(error) = surface.set_mapped(false) { + tracing::warn!(?error, "could not minimize X11 window"); + } + if self.x11_keyboard_focus.as_ref() == Some(&surface) { + self.set_x11_keyboard_focus(None, smithay::utils::SERIAL_COUNTER.next_serial()); + self.focus_next_window(); + } + } + + fn unminimize_request(&mut self, _xwm: XwmId, surface: X11Surface) { + let Some((window, location)) = self.x11_minimized_windows.remove(&surface.window_id()) + else { + return; + }; + if let Err(error) = surface.set_mapped(true) { + tracing::warn!(?error, "could not restore minimized X11 window"); + self.x11_minimized_windows + .insert(surface.window_id(), (window, location)); + return; + } + self.space.map_element(window.clone(), location, true); + window.set_activated(true); + self.focus_window_keyboard(&window, smithay::utils::SERIAL_COUNTER.next_serial()); + } fn configure_request( &mut self, @@ -327,6 +381,64 @@ impl PantheonCompositor { .cloned() } + fn configure_x11_window_mode( + &mut self, + surface: &X11Surface, + maximized: bool, + fullscreen: bool, + ) { + let Some(window) = self.find_x11_window(surface) else { + return; + }; + let id = surface.window_id(); + let restore = *self + .x11_managed_windows + .entry(id) + .or_insert_with(|| ManagedWindow { + restore_location: self.space.element_location(&window).unwrap_or_default(), + restore_size: window.geometry().size, + maximized: false, + fullscreen: false, + }); + if let Some(managed) = self.x11_managed_windows.get_mut(&id) { + managed.maximized = maximized; + managed.fullscreen = fullscreen; + } + let output = self + .space + .outputs_for_element(&window) + .first() + .cloned() + .or_else(|| self.primary_output()); + let Some(output_geometry) = output.and_then(|output| self.space.output_geometry(&output)) + else { + return; + }; + let target = if fullscreen { + output_geometry + } else if maximized { + self.work_area_for_output(output_geometry) + } else { + smithay::utils::Rectangle::new(restore.restore_location, restore.restore_size) + }; + if let Err(error) = surface.set_maximized(maximized) { + tracing::warn!(?error, "could not update X11 maximized state"); + } + if let Err(error) = surface.set_fullscreen(fullscreen) { + tracing::warn!(?error, "could not update X11 fullscreen state"); + } + if let Err(error) = surface.configure(Some(target)) { + tracing::warn!(?error, "could not configure X11 window state"); + } + self.space.map_element(window, target.loc, false); + if !maximized && !fullscreen { + self.x11_managed_windows.remove(&id); + } + self.refresh_panel_visibility(); + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + fn begin_x11_move(&mut self, surface: &X11Surface) { let Some(pointer) = self.seat.get_pointer() else { return; From df9379cde391f95cfe9b2c85b1f2829db65e7425 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:19:55 -0400 Subject: [PATCH 117/798] Keep X11 focus and activation state consistent --- rust/pantheon-compositor/src/handlers/xdg_shell.rs | 7 +++++-- rust/pantheon-compositor/src/xwayland.rs | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 1754163ff..914fc3e7f 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -281,8 +281,11 @@ impl PantheonCompositor { .rev() .find(|window| { window - .toplevel() - .is_none_or(|top| !self.is_panel_surface(top.wl_surface())) + .x11_surface() + .is_none_or(|surface| !surface.is_override_redirect()) + && window + .toplevel() + .is_none_or(|top| !self.is_panel_surface(top.wl_surface())) }) .cloned(); if let Some(window) = next_window { diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs index a4d7099c7..685b4867c 100644 --- a/rust/pantheon-compositor/src/xwayland.rs +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -224,6 +224,7 @@ impl XwmHandler for PantheonCompositor { }; self.x11_minimized_windows .insert(surface.window_id(), (window.clone(), location)); + window.set_activated(false); self.space.unmap_elem(&window); if let Err(error) = surface.set_mapped(false) { tracing::warn!(?error, "could not minimize X11 window"); @@ -541,6 +542,9 @@ impl PantheonCompositor { window: &Window, serial: smithay::utils::Serial, ) { + for candidate in self.space.elements() { + candidate.set_activated(candidate == window); + } if let Some(surface) = window.x11_surface() { self.set_x11_keyboard_focus(Some(surface.clone()), serial); } else if let Some(toplevel) = window.toplevel() { From 734b5698dc44a00b39b5f855cdd397499618d026 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:21:22 -0400 Subject: [PATCH 118/798] Record local XWayland compositor package --- mseries/pantheon-rust-compositor/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 46b21858c..0c15bc2b3 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -90,10 +90,11 @@ exercised in an isolated login. with the separate experimental session files. It derives shared-library dependencies and requires `gnome-session` and `gtklock`. Build it locally with `OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The -current package from commit `2c8422f` is saved at -`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-rust-compositor_0.1.0+git2c8422f_arm64.deb` -(SHA-256 `613415f318dcc7c3421d7eb3823b7e2f35525a1f75201f31677646f71e3f6ce8`). -The package is for isolated qualification; the build script does not install it. +The locally built arm64 package from commit `df9379c` is saved at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-xwayland-release/pantheon-rust-compositor_0.1.0+gitdf9379c_arm64.deb` +(SHA-256 `f113fc8dfe04112eb11fbebd2b8d0b4b83e264afdea24b0c42ee15caab4efc75`). +It includes the XWayland runtime dependency and is for isolated qualification; +the build script does not install it. ## Dock HDMI boundary From 144b91fe88ee0ff4dd373bc8dffc4a23ac1214ed Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:23:18 -0400 Subject: [PATCH 119/798] Bridge X11 PRIMARY selection to Wayland --- .../EXPERIENCE-REQUIREMENTS.md | 2 +- mseries/pantheon-rust-compositor/README.md | 11 ++--- rust/pantheon-compositor/src/handlers/mod.rs | 12 +++++- rust/pantheon-compositor/src/state.rs | 5 ++- rust/pantheon-compositor/src/xwayland.rs | 42 +++++++++++++------ 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index ffbae5727..87bee7695 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -16,7 +16,7 @@ it does not imply that the current prototype supports it. | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | -| Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus and clipboard exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard and clipboard paths compile but still need nested and on-device runtime qualification | +| Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 0c15bc2b3..af238bfb3 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -41,11 +41,12 @@ rather than a first-session requirement. The prototype now starts Smithay's rootless XWayland window manager, maps X11 windows into the shared desktop space, routes move/resize, maximize, fullscreen, minimize, restore, and activation requests, sends keyboard input to -focused X11 clients, and bridges clipboard ownership and transfers. Native -startup exports the selected `DISPLAY` through D-Bus and systemd activation environments; the -`--run` client waits for XWayland readiness. XWayland keyboard, clipboard, -session activation, and X11 window behavior compile but still need runtime -qualification. The package depends on the `xwayland` executable. +focused X11 clients, and bridges clipboard and PRIMARY selection ownership and +transfers. Native startup exports the selected `DISPLAY` through D-Bus and +systemd activation environments; the `--run` client waits for XWayland +readiness. XWayland keyboard, clipboard, PRIMARY selection, session activation, +and X11 window behavior compile but still need runtime qualification. The +package depends on the `xwayland` executable. Panel reservations and managed maximize/tile layouts are tracked per output connector. Maximized/fullscreen windows and popup constraints use the window's diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs index 2310aa0dc..bd257bcda 100644 --- a/rust/pantheon-compositor/src/handlers/mod.rs +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -18,6 +18,9 @@ use smithay::wayland::pointer_constraints::PointerConstraintsHandler; use smithay::wayland::selection::data_device::{ DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, }; +use smithay::wayland::selection::primary_selection::{ + PrimarySelectionHandler, PrimarySelectionState, set_primary_focus, +}; use smithay::wayland::selection::{SelectionHandler, SelectionSource, SelectionTarget}; use smithay::wayland::session_lock::{ LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker, @@ -45,7 +48,8 @@ impl SeatHandler for PantheonCompositor { fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { let dh = &self.display_handle; let client = focused.and_then(|s| dh.get_client(s.id()).ok()); - set_data_device_focus(dh, seat, client); + set_data_device_focus(dh, seat, client.clone()); + set_primary_focus(dh, seat, client); } } @@ -98,6 +102,12 @@ impl DataDeviceHandler for PantheonCompositor { } } +impl PrimarySelectionHandler for PantheonCompositor { + fn primary_selection_state(&mut self) -> &mut PrimarySelectionState { + &mut self.primary_selection_state + } +} + impl DndGrabHandler for PantheonCompositor {} impl WaylandDndGrabHandler for PantheonCompositor { fn dnd_requested( diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 2de741293..9be1d7219 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -24,7 +24,7 @@ use smithay::{ FractionalScaleHandler, FractionalScaleManagerState, with_fractional_scale, }, output::OutputManagerState, - selection::data_device::DataDeviceState, + selection::{data_device::DataDeviceState, primary_selection::PrimarySelectionState}, session_lock::{LockSurface, SessionLockManagerState, SessionLocker}, shell::xdg::XdgShellState, shm::ShmState, @@ -83,6 +83,7 @@ pub struct PantheonCompositor { pub seat_state: SeatState, pub(crate) keyboard_config: crate::input::StartupKeyboardConfig, pub data_device_state: DataDeviceState, + pub primary_selection_state: PrimarySelectionState, pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, pub shell_compat: ShellCompatHandle, @@ -169,6 +170,7 @@ impl PantheonCompositor { // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); + let primary_selection_state = PrimarySelectionState::new::(&dh); let pantheon_shell_state = PantheonShellState::new::(&dh); let (display_config, apply_requests) = DisplayConfigHandle::start(); event_loop.handle().insert_source( @@ -299,6 +301,7 @@ impl PantheonCompositor { seat_state, keyboard_config, data_device_state, + primary_selection_state, pantheon_shell_state, display_config, shell_compat, diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs index 685b4867c..3be5c7782 100644 --- a/rust/pantheon-compositor/src/xwayland.rs +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -13,6 +13,10 @@ use smithay::{ clear_data_device_selection, current_data_device_selection_userdata, request_data_device_client_selection, set_data_device_selection, }, + primary_selection::{ + clear_primary_selection, current_primary_selection_userdata, + request_primary_client_selection, set_primary_selection, + }, }, xwayland_shell::XWaylandShellHandler, }, @@ -311,12 +315,10 @@ impl XwmHandler for PantheonCompositor { self.begin_x11_move(&window); } - fn allow_selection_access(&mut self, xwm: XwmId, selection: SelectionTarget) -> bool { - selection == SelectionTarget::Clipboard - && self - .x11_keyboard_focus - .as_ref() - .is_some_and(|surface| surface.xwm_id() == Some(xwm)) + fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool { + self.x11_keyboard_focus + .as_ref() + .is_some_and(|surface| surface.xwm_id() == Some(xwm)) } fn send_selection( @@ -326,19 +328,29 @@ impl XwmHandler for PantheonCompositor { mime_type: String, fd: std::os::fd::OwnedFd, ) { - if selection == SelectionTarget::Clipboard - && let Err(error) = request_data_device_client_selection(&self.seat, mime_type, fd) - { - tracing::warn!( - ?error, - "could not transfer the Wayland clipboard to XWayland" - ); + match selection { + SelectionTarget::Clipboard => { + if let Err(error) = request_data_device_client_selection(&self.seat, mime_type, fd) + { + tracing::warn!(?error, "could not transfer Wayland clipboard to XWayland"); + } + } + SelectionTarget::Primary => { + if let Err(error) = request_primary_client_selection(&self.seat, mime_type, fd) { + tracing::warn!( + ?error, + "could not transfer Wayland PRIMARY selection to XWayland" + ); + } + } } } fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec) { if selection == SelectionTarget::Clipboard { set_data_device_selection(&self.display_handle, &self.seat, mime_types, ()); + } else { + set_primary_selection(&self.display_handle, &self.seat, mime_types, ()); } } @@ -347,6 +359,10 @@ impl XwmHandler for PantheonCompositor { && current_data_device_selection_userdata(&self.seat).is_some() { clear_data_device_selection(&self.display_handle, &self.seat); + } else if selection == SelectionTarget::Primary + && current_primary_selection_userdata(&self.seat).is_some() + { + clear_primary_selection(&self.display_handle, &self.seat); } } From 307c9af9c1de85d92e3a4516e08e6e19233e4994 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:23:31 -0400 Subject: [PATCH 120/798] mseries-vm: GPU=ramfb key (fixes frozen Windows setup display); handoff: Windows installer findings Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 15 +++++++++++++++ mseries/vms/mseries-vm | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 359f98991..e5a367342 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6212,3 +6212,18 @@ wizard (language, keyboard, account), the greeter and the elementary desktop (We **boots and works on QEMU/KVM when it runs the generic kernel**; the Fairydust kernel still cannot boot on QEMU virt (see above). Test account in the VM: `~/vms/elementary/credentials.txt` (owner-only, VM only). Not verified: the Fairydust kernel on QEMU, audio, networking inside the guest, suspend, or any Apple hardware path. + +## 2026-10-19: Windows 11 ARM64 installer on this Mac (QEMU/KVM) + +Booted the locally built `26100.1_PROFESSIONAL_ARM64_EN-US.ISO` in the `mseries-vm` Windows guest (TPM on, Secure Boot +firmware, NVMe disk). Findings: + +- **Display:** with the default `virtio-gpu-pci` the screen froze on the firmware logo for minutes (looked like a hang; it + is a stale framebuffer). New `GPU=ramfb` key (`mseries-vm set windows GPU ramfb`) fixes it: the boot manager prompt + ("Press any key to boot from CD or DVD") and Windows Setup now render. The prompt needs a key within a few seconds, + about 30 s after start; spamming Return once a second works. +- **Storage:** Windows Setup then stops at "Install driver to display hardware / A media driver your computer needs is + missing": WinPE has no virtio-scsi driver, so it cannot read the installer ISO attached as `scsi-cd`. Tried `usb-storage` + (boot manager hangs after reading 3 MiB) and AHCI `ide-cd` (the firmware does not list it as a boot device). Needs the + virtio-win drivers: attach `virtio-win.iso` (`create ... --virtio-media`, or `EXTRA_MEDIA`) and load `vioscsi` from + Setup's "Browse" dialog. Not done: that ISO is not downloaded yet. diff --git a/mseries/vms/mseries-vm b/mseries/vms/mseries-vm index 8f9690f5c..7c55be14c 100755 --- a/mseries/vms/mseries-vm +++ b/mseries/vms/mseries-vm @@ -386,7 +386,11 @@ cmd_start() { -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis-device,tpmdev=tpm0) fi - if [[ $GL == on ]]; then + if [[ ${GPU:-virtio} == ramfb ]]; then + # Windows setup draws through the firmware framebuffer; with virtio-gpu + # the screen stays stale or black until a guest driver is installed. + cmd+=(-device ramfb) + elif [[ $GL == on ]]; then cmd+=(-device virtio-gpu-gl-pci) else cmd+=(-device virtio-gpu-pci) @@ -616,6 +620,7 @@ cmd_set() { HOSTFWD) [[ $value != *[[:space:]]* ]] || die "HOSTFWD must be a hostfwd rule, e.g. tcp::2222-:22" ;; TPM) [[ $value == on || $value == off ]] || die "$key must be on or off" ;; AUDIO|GL) [[ $value == on || $value == off ]] || die "$key must be on or off" ;; + GPU) [[ $value == virtio || $value == ramfb ]] || die "GPU must be virtio or ramfb" ;; CPUS|MEMORY_MB) [[ $value =~ ^[1-9][0-9]*$ ]] || die "$key must be a positive integer" ;; DISK_BUS) [[ $value == virtio || $value == nvme ]] || die "DISK_BUS must be virtio or nvme" ;; FIRMWARE) [[ -f $AAVMF_DIR/$value ]] || die "firmware not found: $AAVMF_DIR/$value" ;; From 394728eb8ba05216df1730fbf7df1788ebd155bc Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:24:35 -0400 Subject: [PATCH 121/798] Record ARM64 compositor qualification package --- mseries/pantheon-rust-compositor/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index af238bfb3..e3e93b7cd 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -91,11 +91,12 @@ exercised in an isolated login. with the separate experimental session files. It derives shared-library dependencies and requires `gnome-session` and `gtklock`. Build it locally with `OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The -The locally built arm64 package from commit `df9379c` is saved at -`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-xwayland-release/pantheon-rust-compositor_0.1.0+gitdf9379c_arm64.deb` -(SHA-256 `f113fc8dfe04112eb11fbebd2b8d0b4b83e264afdea24b0c42ee15caab4efc75`). +The locally built arm64 package from commit `144b91f` is saved at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-primary-selection-release/pantheon-rust-compositor_0.1.0+git144b91f_arm64.deb` +(SHA-256 `fa140652fcd8daf2ba2254062436c17c9ab581e5a2c9606760f1b7859e492513`). It includes the XWayland runtime dependency and is for isolated qualification; -the build script does not install it. +the build script does not install it. The package contents and dependency +metadata were inspected after building. No runtime or display takeover was run. ## Dock HDMI boundary From d15feadaca9872b27247904e6b62bf94eae01d6f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:25:52 -0400 Subject: [PATCH 122/798] Align compositor status with current implementation --- rust/pantheon-compositor/README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 1558dd94c..369f80dee 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -87,17 +87,24 @@ pointer motion at the panel's output edge reveals it. Hiding releases the panel's work area; revealing it restores that area and reconfigures managed windows. The active host Dock setting is `overlapping-window`. The transition is immediate; animated hiding and runtime Dock interaction remain unqualified. -Blur, overview behavior and multi-output Dock behavior also remain open. The compositor now implements -Smithay's `ext-session-lock-v1`: it blanks each output until its lock surface is -ready, confines input to the locker, and restores the prior keyboard focus on -unlock. The compositor exports the observed `org.gnome.ScreenSaver` API and -launches `gtklock` on `Lock`/`SetActive(true)`; set `PANTHEON_LOCK_COMMAND` to -select another executable. The locker is not installed by this source tree, and -the D-Bus/session path has not yet been runtime-qualified. Widget, -extended-behavior and greeter protocol requests are accepted but currently have -no behavior. Touch input, multi-monitor configuration, accessibility, IBus, -Xwayland, screen-sharing portals and production crash recovery also remain -unimplemented. +Blur, overview behavior and multi-output Dock behavior also remain open. The +compositor implements Smithay's `ext-session-lock-v1`: it blanks each output +until its lock surface is ready, confines input to the locker, and restores the +prior keyboard focus on unlock. It exports the observed +`org.gnome.ScreenSaver` API and launches `gtklock` on `Lock`/`SetActive(true)`; +set `PANTHEON_LOCK_COMMAND` to select another executable. The locker and +D-Bus/session path still need runtime qualification. Widget, extended-behavior +and greeter protocol requests are accepted but currently have no behavior. + +Rootless XWayland startup, X11 window management, keyboard routing, session +`DISPLAY` propagation, clipboard and PRIMARY selection bridging are implemented +and compile-checked, but still need runtime qualification. Native multi-output +enumeration and hotplug reconciliation exist; configuration rejects output +disabling, mirroring, unsupported monitor properties and nested mode changes. +Monitor modeset/rollback and hotplug remain unqualified on hardware. IBus text +input, accessibility protocol support, screen-sharing portals, touch input and +production crash recovery remain unimplemented or unqualified; add them only +when an observed Pantheon workflow requires them. `native-drm` enables Smithay's DRM, GBM, libinput, udev, libseat, EGL, and multi-GPU dependencies. The executable defaults to nested Winit; only From 7b1b46bd6bf9959507fe53b35787fb23ed6173da Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:29:32 -0400 Subject: [PATCH 123/798] Add trusted IBus text input support --- .../EXPERIENCE-REQUIREMENTS.md | 11 +++-- mseries/pantheon-rust-compositor/README.md | 6 ++- rust/pantheon-compositor/README.md | 11 +++-- .../src/handlers/input_method.rs | 46 +++++++++++++++++++ rust/pantheon-compositor/src/handlers/mod.rs | 1 + rust/pantheon-compositor/src/state.rs | 33 ++++++++++++- 6 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 rust/pantheon-compositor/src/handlers/input_method.rs diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 87bee7695..9fd0ca697 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -16,6 +16,7 @@ it does not imply that the current prototype supports it. | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | +| Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | @@ -24,8 +25,8 @@ it does not imply that the current prototype supports it. ## Conditional work, driven by an observed need Add these only when an M-series workflow depends on them, and keep each as a -separate acceptance item: IBus/input-method support for the installed input method; accessibility protocols/actions used by -the owner's setup; screencast/remote-desktop portal support for screen sharing; +separate acceptance item: accessibility protocols/actions used by the owner's +setup; screencast/remote-desktop portal support for screen sharing; touch/gestures if the selected hardware and workflow use them. The measured M1 Pro topology already requires cross-device buffer sharing for native display output; additional GPU topologies remain conditional. @@ -34,9 +35,9 @@ presence in Mutter is not by itself a reason to reproduce them in the first cut. On the current host, the configured sources are US XKB and IBus `mozc-jp`, with -US selected. `ibus` and `ibus-mozc` are installed. This confirms the input -method is available but does not establish it as an active daily workflow; keep -the IBus bridge conditional until that workflow is confirmed. +US selected. The active F5 dictation workflow uses focus-bound IBus output; +`mozc-jp` composition itself is not currently selected and still needs its own +acceptance check. Keep notification UI, app launchers, panel/dock rendering, settings surfaces, wallpaper and other shell UI in their existing Pantheon components unless a diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index e3e93b7cd..842d46c8e 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -35,8 +35,10 @@ options, and repeat settings from GNOME GSettings. This applies the M-series `ctrl:swap_lwin_lctl` Command/Ctrl mapping. The compositor polls settings once per second and applies layout, option, and repeat changes live; this path is build-checked but not runtime-qualified. The current source is US; -`mozc-jp` is installed but inactive, so IBus remains a conditional workflow -rather than a first-session requirement. +`mozc-jp` is installed but inactive. F5 dictation is an active IBus workflow +and inserts committed Unicode into focused fields; the Rust compositor now +advertises Smithay text-input-v3/input-method-v2 for that path, but still needs +qualification with the running IBus daemon and a native Wayland field. The prototype now starts Smithay's rootless XWayland window manager, maps X11 windows into the shared desktop space, routes move/resize, maximize, diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 369f80dee..4aeb5629c 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -101,10 +101,13 @@ Rootless XWayland startup, X11 window management, keyboard routing, session and compile-checked, but still need runtime qualification. Native multi-output enumeration and hotplug reconciliation exist; configuration rejects output disabling, mirroring, unsupported monitor properties and nested mode changes. -Monitor modeset/rollback and hotplug remain unqualified on hardware. IBus text -input, accessibility protocol support, screen-sharing portals, touch input and -production crash recovery remain unimplemented or unqualified; add them only -when an observed Pantheon workflow requires them. +Monitor modeset/rollback and hotplug remain unqualified on hardware. Smithay's +text-input-v3 and input-method-v2 globals are now wired for the trusted IBus +daemon, including input-method popup tracking; this path is compile-checked but +still needs a real IBus-to-Wayland field qualification. Accessibility protocol +support, screen-sharing portals, touch input and production crash recovery +remain unimplemented or unqualified; add them only when an observed Pantheon +workflow requires them. `native-drm` enables Smithay's DRM, GBM, libinput, udev, libseat, EGL, and multi-GPU dependencies. The executable defaults to nested Winit; only diff --git a/rust/pantheon-compositor/src/handlers/input_method.rs b/rust/pantheon-compositor/src/handlers/input_method.rs new file mode 100644 index 000000000..408bdd639 --- /dev/null +++ b/rust/pantheon-compositor/src/handlers/input_method.rs @@ -0,0 +1,46 @@ +use smithay::{ + desktop::{PopupKind, PopupManager}, + utils::{Logical, Rectangle}, + wayland::{ + input_method::{InputMethodHandler, PopupSurface}, + text_input::TextInputActivation, + }, +}; + +use crate::PantheonCompositor; + +impl TextInputActivation for PantheonCompositor {} + +impl InputMethodHandler for PantheonCompositor { + fn new_popup(&mut self, surface: PopupSurface) { + if let Err(error) = self.popups.track_popup(PopupKind::from(surface)) { + tracing::warn!(?error, "could not track IBus input-method popup"); + } + } + + fn dismiss_popup(&mut self, surface: PopupSurface) { + if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) { + let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface)); + } + } + + fn popup_repositioned(&mut self, _surface: PopupSurface) { + #[cfg(feature = "native-drm")] + self.request_native_redraw(); + } + + fn parent_geometry( + &self, + parent: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, + ) -> Rectangle { + self.space + .elements() + .find_map(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == parent) + .then(|| window.geometry()) + }) + .unwrap_or_default() + } +} diff --git a/rust/pantheon-compositor/src/handlers/mod.rs b/rust/pantheon-compositor/src/handlers/mod.rs index bd257bcda..06ce8a03a 100644 --- a/rust/pantheon-compositor/src/handlers/mod.rs +++ b/rust/pantheon-compositor/src/handlers/mod.rs @@ -1,4 +1,5 @@ mod compositor; +mod input_method; mod xdg_shell; use crate::PantheonCompositor; diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 9be1d7219..c0ca9b844 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -12,7 +12,7 @@ use smithay::{ timer::{TimeoutAction, Timer}, }, wayland_server::{ - Display, DisplayHandle, Resource, + Client, Display, DisplayHandle, Resource, backend::{ClientData, ClientId, DisconnectReason}, protocol::wl_surface::WlSurface, }, @@ -23,12 +23,14 @@ use smithay::{ fractional_scale::{ FractionalScaleHandler, FractionalScaleManagerState, with_fractional_scale, }, + input_method::InputMethodManagerState, output::OutputManagerState, selection::{data_device::DataDeviceState, primary_selection::PrimarySelectionState}, session_lock::{LockSurface, SessionLockManagerState, SessionLocker}, shell::xdg::XdgShellState, shm::ShmState, socket::ListeningSocketSource, + text_input::TextInputManagerState, viewporter::ViewporterState, xdg_activation::{ XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, @@ -50,6 +52,25 @@ use crate::tiling::{CornerSnapPreview, TilingBindings}; #[cfg(feature = "native-drm")] use smithay::reexports::calloop::channel::Sender; +fn is_ibus_input_method_client(client: &Client, display: &DisplayHandle) -> bool { + use std::os::unix::fs::MetadataExt; + + let Ok(credentials) = client.get_credentials(display) else { + return false; + }; + let Ok(current_user) = std::fs::metadata("/proc/self") else { + return false; + }; + if credentials.uid != current_user.uid() { + return false; + } + + std::fs::read_link(format!("/proc/{}/exe", credentials.pid)) + .ok() + .and_then(|path| path.file_name().map(|name| name.to_owned())) + .is_some_and(|name| name == "ibus-daemon") +} + pub struct PantheonCompositor { pub start_time: std::time::Instant, pub socket_name: OsString, @@ -84,6 +105,8 @@ pub struct PantheonCompositor { pub(crate) keyboard_config: crate::input::StartupKeyboardConfig, pub data_device_state: DataDeviceState, pub primary_selection_state: PrimarySelectionState, + pub text_input_manager_state: TextInputManagerState, + pub input_method_manager_state: InputMethodManagerState, pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, pub shell_compat: ShellCompatHandle, @@ -171,6 +194,12 @@ impl PantheonCompositor { // Data device is responsible for clipboard and drag-and-drop let data_device_state = DataDeviceState::new::(&dh); let primary_selection_state = PrimarySelectionState::new::(&dh); + let text_input_manager_state = TextInputManagerState::new::(&dh); + let input_method_display_handle = dh.clone(); + let input_method_manager_state = + InputMethodManagerState::new::(&dh, move |client| { + is_ibus_input_method_client(client, &input_method_display_handle) + }); let pantheon_shell_state = PantheonShellState::new::(&dh); let (display_config, apply_requests) = DisplayConfigHandle::start(); event_loop.handle().insert_source( @@ -302,6 +331,8 @@ impl PantheonCompositor { keyboard_config, data_device_state, primary_selection_state, + text_input_manager_state, + input_method_manager_state, pantheon_shell_state, display_config, shell_compat, From e2dd80f80f98ce31f682faadf9c9d92363b21e44 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:30:16 -0400 Subject: [PATCH 124/798] Record IBus-enabled ARM64 candidate --- mseries/pantheon-rust-compositor/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 842d46c8e..84fdfc667 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -93,12 +93,13 @@ exercised in an isolated login. with the separate experimental session files. It derives shared-library dependencies and requires `gnome-session` and `gtklock`. Build it locally with `OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The -The locally built arm64 package from commit `144b91f` is saved at -`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-primary-selection-release/pantheon-rust-compositor_0.1.0+git144b91f_arm64.deb` -(SHA-256 `fa140652fcd8daf2ba2254062436c17c9ab581e5a2c9606760f1b7859e492513`). +The locally built arm64 package from commit `7b1b46b` is saved at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-ibus-release/pantheon-rust-compositor_0.1.0+git7b1b46b_arm64.deb` +(SHA-256 `031b409e6c367487bc97adba375a865c79cb691bd48ea8f8b7e9b4255d3dd885`). It includes the XWayland runtime dependency and is for isolated qualification; the build script does not install it. The package contents and dependency -metadata were inspected after building. No runtime or display takeover was run. +metadata were inspected after building. The IBus Wayland path has not yet been +runtime-qualified, and no display takeover was run. ## Dock HDMI boundary From 8a2f2a82619774c3a77425415dd49b4ed93f52e2 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:30:57 -0400 Subject: [PATCH 125/798] Record active input and accessibility requirements --- mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 9fd0ca697..fc219775f 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -39,6 +39,12 @@ US selected. The active F5 dictation workflow uses focus-bound IBus output; `mozc-jp` composition itself is not currently selected and still needs its own acceptance check. +The 2026-10-19 user-settings snapshot has the screen reader, screen magnifier, +and accessibility keyboard features disabled. Toolkit accessibility is enabled +for app-field metadata used by the F5 workflow; that app-side AT-SPI use does +not itself require compositor accessibility protocols. GNOME Remote Desktop +RDP and VNC are both disabled, so remote screen sharing remains conditional. + Keep notification UI, app launchers, panel/dock rendering, settings surfaces, wallpaper and other shell UI in their existing Pantheon components unless a documented requirement shows they cannot work through the compositor contract. From 0305ea104c30e36f6a9fcc2b794029de3010d379 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 07:58:39 -0400 Subject: [PATCH 126/798] handoff: record 2026-10-19 apt upgrade and re-applied panel patches Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 5ac3de5cf..bfb328af8 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,16 @@ # OS 9 implementation / Claude Code handoff +## apt upgrade of 25 elementary daily packages — 2026-10-19 (Claude Code) + +`sudo apt upgrade` (gala r3203->r3204, panel power/sound/bluetooth, granite, settings-daemon, pantheon-files, +claude-desktop 2.42304.2) finished cleanly; `dpkg --audit` empty. The upgrade overwrote our patched +`libpower.so`, `libsound.so` and `libbluetooth.so` with stock builds. Re-ran `install-battery-eta.sh` +(Wingpanel restarted by it, still running) and `install-sound-indicator-fix.sh` (needs a Wingpanel reload to +show). `check-codexbar-port.sh` passed. **Open:** the patched Bluetooth indicator has no installer or patch in +the repo and is stock again (nothing saved before the upgrade); the running session still uses the old Gala until +re-login; snapping/PiP gala plugins are not checked against the new `libgala0`. Lesson: re-run the +indicator installers after every `apt upgrade` that touches `io.elementary.panel.*`. + ## Dock icons halved — 2026-10-18 (Claude Code) Owner asked for the applications tray icons at half size. Ran From d967d6953c05569b9af0e88fca199f1b1ba86fa8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:01:06 -0400 Subject: [PATCH 127/798] Clarify Rust Pantheon session readiness --- mseries/OS9-HANDOFF.md | 10 ++++++++++ mseries/pantheon-rust-compositor/README.md | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 020233699..c3f2824aa 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5762,3 +5762,13 @@ does not prove DRM framebuffer creation or physical scanout. The probe opens the devices but does not acquire DRM master, create framebuffers, or modeset. The next hardware gate remains a controlled native compositor start in an isolated session. + +### Rust Pantheon branch readiness decision — 2026-10-19 + +The branch is useful for nested development and has a locally built ARM64 +native candidate, but it is **not yet an experimental Pantheon swap-in**. No +isolated native login or real Wayland app/IBus/panel/dock/lock workflow has been +qualified. Gala remains selected. The next gate is nested runtime qualification +with GTK/Qt plus the existing shell clients, then native login only with a +separate tested recovery path. See +`mseries/pantheon-rust-compositor/README.md` for the readiness criteria. diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 84fdfc667..cbf341994 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -83,6 +83,23 @@ session, input and recovery behavior still need on-device qualification. The staged login-session files are separate from the active Gala session and are not installed or selected by default. +### Readiness decision (2026-10-19) + +This branch is **not yet ready to use as an experimental Pantheon session**. +The nested backend is suitable for development and controlled protocol +qualification, and the ARM64 `native-drm` package builds on the M-series host. +Neither result demonstrates that the session can replace Gala. No isolated +native login has been run, so actual scanout, keyboard/pointer input, app +launch, IBus text commits, panel/dock behavior, lock/unlock and crash recovery +are still unknown. Keep Gala selected and use the Rust compositor only in an +explicitly isolated qualification session after its prerequisites are checked. + +The next gate is staged nested qualification with real GTK/Qt clients and the +existing Pantheon shell clients, including IBus focus/commit behavior. After +that passes, run the package in a separate native login with a tested recovery +path. Only record it as an experimental session that works after those runtime +gates pass; a successful compile or package build is not that evidence. + The staged systemd service now retries an unexpected compositor failure with a two-second delay, capped at three starts per minute. After that limit, its `OnFailure` action requests the GNOME session shutdown target rather than From eb7a32b7b5c1c21f22bec03f07d8709034150d5f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:01:36 -0400 Subject: [PATCH 128/798] local-ai: plan isolated omarchy-mlx model A/B --- mseries/OS9-HANDOFF.md | 21 +++++++++ .../OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md | 46 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index bfb328af8..31d970fce 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,26 @@ # OS 9 implementation / Claude Code handoff +## omarchy-mlx main candidate and planned model A/B — 2026-10-19 + +Candidate `61495cb` built in `~/.cache/omarchy-mlx-candidate-61495cb`, isolated +beside installed v0.7.33. No runtime was installed or replaced. Four selected +C++ test binaries passed; `omarchy_matmul_family_tests` passed 41/43, with two +MoE gather_qmm tile tests throwing on empty CPU reference data in the CPU-off +build (cause suspected, unconfirmed). Receipt: +`mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-build.md`. + +Model A/B has not run. Readiness check at 08:00 EDT found +`mseries-ai-everyday.service` failed because its configured 4B target was +incomplete and offline mode refused a download; no MLX worker was running. +The current candidate is C++-only, with no Python bindings/wheel. A detailed +plan now records candidate preparation, 4B row-reduce and optional 9B native +GQA gates, required service handling, and a proposed after-10 PM Eastern quiet +window: `mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md`. +Next: build a Python-capable candidate in a separate venv/prefix and confirm +the same pinned model is complete and the installed baseline is healthy before +any model launch. Owner's desktop is not to be interrupted outside an agreed +quiet window. + ## apt upgrade of 25 elementary daily packages — 2026-10-19 (Claude Code) `sudo apt upgrade` (gala r3203->r3204, panel power/sound/bluetooth, granite, settings-daemon, pantheon-files, diff --git a/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md b/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md new file mode 100644 index 000000000..089721e00 --- /dev/null +++ b/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md @@ -0,0 +1,46 @@ +# omarchy-mlx candidate model A/B plan — October 19, 2026 + +Planning only. No models were launched or stopped for this plan. + +## Current readiness + +- Candidate source `61495cb` builds at `~/.cache/omarchy-mlx-candidate-61495cb`; selected C++ results are recorded in [the candidate build receipt](receipts/2026-10-19-omarchy-mlx-candidate-build.md). It has no Python bindings or wheel yet, so it cannot currently run the model A/B. +- The installed runtime remains v0.7.33. Keep it intact as the baseline. +- At 08:00 EDT on October 19, `mseries-ai-everyday.service` was failed, not active. Its log says the explicitly named `qwen3.5-4b-mseries-4bit` target is incomplete locally and offline mode refused to download it. No MLX model worker process was present. `mseries-ai-quality.service` was inactive. Confirm model availability and restore a healthy baseline before any comparison; do not count this failure as an A/B result. +- The 9B profile is configured as `qwen3.5-9b-mlx-4bit`; model files were not confirmed in this readiness check. + +## Candidate preparation gate + +1. Build the Python-enabled candidate runtime from the frozen `61495cb` source in a separate prefix/venv. Do not run the repository installer or write into `/usr/lib/omarchy-mlx` or `~/.local/share/mlx-omarchy`. +2. Keep mlx-lm pinned to the version/API the candidate supports. Do not apply the upstream 0.32-only server patches to our installed 0.31.3 environment. Use an isolated direct generation harness unless the private server patches are reviewed and applied to the candidate venv. +3. Verify candidate and baseline library provenance and hashes, model revision and file completeness, Vulkan device/ICD, and effective environment flags before the first model load. Verify that logs/counters show the selected kernel route is actually reached. +4. Use fixed local prompts and greedy decoding. Save token IDs, timing samples and telemetry to a new receipt; omit personal prompts or model output containing private data. + +## 4B row-reduce comparison + +Target: the exact 4B model revision used by the everyday profile, once its complete local files and healthy baseline are confirmed. + +1. Compare candidate `MLX_OMARCHY_ROW_REDUCE=1` with candidate `MLX_OMARCHY_ROW_REDUCE=0`; also record installed v0.7.33 as a baseline if it can load the same exact model files. The primary feature comparison is candidate on/off, since v0.7.33 predates this route. +2. First do a short route-reachability/profile check. If this model does not dispatch a qualifying long-row Sum, stop: record “route not reached,” with no performance claim. +3. If reached, run deterministic greedy generations with identical prompt, context and max tokens. Compare token IDs, then compare at least three alternating on/off rounds after equivalent warm-up and cooling. Record load time, first-token latency, decode time, peak/available memory, temperature and fan samples. +4. Accept only exact token identity for the numerics gate. Treat any speed difference as local to this model, prompt, machine and run conditions. + +## Optional 9B GDN experiment + +Target: `qwen3.5-9b-mlx-4bit`, using the existing `quality` profile's 8K context and 9 GiB admission budget. Do not use the 64K `personal` profile for this kernel comparison. + +1. Confirm the exact 9B files are complete and that 9B admission leaves the documented additional 6 GiB desktop reserve. If admission refuses or the model is absent, skip this leg; do not download weights during the quiet window. +2. Compare candidate `MLX_OMARCHY_GDN_PF_GQA=0` and `=1`, with identical greedy prompts, context and token limit. Because the GPU identifies as `Apple M1 Pro (G13S C0)`, do not assume the automatic G13C/G14C route applies. The opt-in must be proven by dispatch evidence before measuring it. +3. Check token identity and route reachability first, then use at least three alternating rounds with the same warm-up/cooling treatment. Capture decode time, memory, temperature and fan. If the flag does not select the native-GQA route on this GPU, record no result rather than treating fallback timing as a kernel comparison. + +## Worker control and quiet window + +Do not run either leg until the everyday service is healthy and a quiet window is agreed. Suggested window: after 10:00 PM Eastern, with 60 minutes available and the machine plugged in, only if the owner expects no interactive desktop work during that period. + +At the start of each leg, stop the managed model-worker services listed in `mseries/local-ai/README.md`, including everyday, quality, coder, compact and optional model units, plus any legacy `mlx-omarchy-chat.service` that is actually running. Verify there are no remaining owned MLX inference workers and that the shared worker lock is free. Do not stop dictation/audio services or other unrelated desktop services. Run only one model at a time. Restore the healthy everyday service after the leg and verify its readiness before ending the window. + +Honor the existing policy: 6 GiB minimum desktop reserve; pause at 2700 RPM / 70 C and resume at 2600 RPM / 65 C; stop on the existing hard limits. Missing required telemetry or an unavailable reserve means skip the run. No governor, kernel, system Mesa, boot or audio changes. + +## Decision + +Do not bump the installed runtime from C++ unit tests alone. A bump candidate needs a Python-capable isolated build, a healthy baseline, route reachability, exact token identity for the 4B comparison, and no reserve/thermal regression. Treat 9B GDN as optional evidence, not a release prerequisite if the route cannot be selected on G13S. From 96d052a591baf8a2cd37816523e9319960c09374 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:04:04 -0400 Subject: [PATCH 129/798] handoff: boot menu cleanup (stable .20 + dev enh1) and upstream parity check Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 31d970fce..ec4a01177 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -4562,3 +4562,22 @@ free-space warning. Verified: builds, Broadway renders, pause/resume against a h Omarchy nightly. Not verified: a complete guest install/boot (~6 GiB download; host had ~19 GiB free) and suspend/resume of the app itself. Next: run the Omarchy automatic install once space is freed, and lift `~/Documents/Codex/2026-10-18/f-2/work/youtube-iso-test` (42 GB, owner decides) if space is needed. + +## Boot menu cleanup and upstream parity check — 2026-10-19 (Claude Code) + +**Boot menu (owner-approved).** Running kernel `7.1.12-elementary-aurora20-enh1` was booted from the manual +*diagnostic* entry (`initcall_debug`, `ignore_loglevel`, `log_buf_len=8M`, `plymouth.enable=0`); the default was `.20`. +`/boot/grub/custom.cfg` is now two entries: `elementary-aurora20` (STABLE, default, `vmlinuz-aurora-sep-20261017.20`, notch=1) +and `elementary-aurora20-enh1` (DEV, `vmlinuz-aurora20-enh1`, clean cmdline; one-shot via `grub-reboot` still works). Stock Arch +`7.1.13-3-2-ARCH` recovery stays in `grub.cfg`. Removed: nosep, hdmiaudio, full-height, diagnostic entries, the diagnostic initrd, +9 old `custom.cfg.*`, `grubenv.pre-e4-*`. Backups: `~/boot-backups-20261019/` (local, not in Git). `grub-script-check` passes; +no reboot was done after the change, so the new menu is unbooted. `/boot` 215 MiB free. Not touched: grubenv, m1n1, modules. +Note: this boot logged "FAT-fs nvme0n1p4: Volume was not properly unmounted" and a replaced journal (previous shutdown was unclean); consider `fsck.vfat` on `/boot` from a safe state. + +**Upstream parity (read-only, 2026-10-19 ~09:30Z).** Aurora `integration/aurora` is ahead of `.20` by #147 (M4 wait state) and #149 +(macaudio selects MAX98357A); no release after `aurora-2026.10.17.20`. Neither looks J314S-relevant; not in `.20-enh1`. Draft/open PR #139 +(Asahi 7.2.10 + aurora + aurora-wip, now "ready for review", qualified on J700/J316s/J413, not J314S) remains the WIP track; stay build-only. +PR #122 (HDMI-hold follow-ups) is **T6030 (M3 Pro)**, not the J314S/T6000, so it is NOT a dock fix for us; deprioritised. New upstream +open PRs of note: #146/#148/#151/#152 are M3/G17 work, not J314S. Dock was unplugged this boot (all DRM external connectors disconnected, +bolt dock `disconnected`), so no dock retrain log could be examined; re-check with the dock attached. Next: attach dock and capture +DCP/dptx log over 10 min on enh1; test SEP probe on enh1; compare #139 `100-sep`/`200-dcp` topics against `.20-enh1` for J314S gaps. From f29b55a7a8cfedda1d9bd7051fe8b675600129b1 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:05:34 -0400 Subject: [PATCH 130/798] docs: assess Phosphene on M-Series Linux --- mseries/OS9-HANDOFF.md | 14 ++++ mseries/ROADMAP.md | 2 + .../PHOSPHENE-FEASIBILITY-2026-10-19.md | 78 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 mseries/local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index ec4a01177..879ed51cc 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,19 @@ # OS 9 implementation / Claude Code handoff +## Phosphene local media feasibility — 2026-10-19 + +Reviewed Phosphene `v4.19.5` at `287a964` against the 32 GB M1 Pro/T6000 and +our elementary OS ARM64 + omarchy-mlx Vulkan stack. The project now describes +Linux on Apple Silicon as community-maintained/best-effort, though the checked +Pinokio installer still gates on Darwin/ARM64. The video stack is MLX-specific; +compatibility with our Vulkan fork is unproven. Phosphene lists 48 GB for +advanced features and 36 GB+ for compact-Q8 Hailuo H3, exceeding this host's +memory. Its 27.5 GB LTX base is model download/storage size, not asserted runtime +RAM. No software or weights were installed and no renders were run. Decision: +do not prioritize a port; reconsider with a specific media need or backend +support. Full evidence and privacy/license caveats: +[`local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md`](local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md). + ## omarchy-mlx main candidate and planned model A/B — 2026-10-19 Candidate `61495cb` built in `~/.cache/omarchy-mlx-candidate-61495cb`, isolated diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 787d6aaad..236a3e92b 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1069,6 +1069,8 @@ browser integration and remaining Files/Gala patches remain open. - Agent-first Pantheon feature proposal (October 18): shared broker/permissions and durable alerts before UI, native app adapters, speech-to-text/speech-to-speech, and explicitly paired WebRTC interaction. See [design and acceptance plan](local-ai/PANTHEON-MCP-DESIGN.md). Proposal only; no new alert broker, voice conversation or WebRTC service installed. - Upstream performance review (October 18): MLX main d8ac5fe is 28 commits ahead of tested 33630f9. Native-GQA is a candidate; numerically rejected gather routes remain excluded. Mac/MX notification-agent/voice patterns and oMLX options inform the agent-first design. [Pinned comparison and experiment plan](local-ai/UPSTREAM-FEATURE-COMPARISON.md); no runtime/governor changes or M1 Pro speedup claimed. + +- Phosphene feasibility (October 19): reviewed v4.19.5 / `287a964` for the 32 GB M1 Pro and elementary OS Vulkan MLX stack. Linux Apple Silicon is community/best-effort, but the checked installer still gates on Darwin/ARM64; MLX video operations are not qualified on our Vulkan fork. Advanced modes need 48 GB and compact-Q8 Hailuo is listed at 36 GB+, so a port is not a priority. No install or render test. [Assessment](local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md). ## CodexBar native Claude account switching — 2026-10-18 Implemented app session capture, one-time native sign-in setup, and **CLI only / Switch both** choices. diff --git a/mseries/local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md b/mseries/local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md new file mode 100644 index 000000000..51659aca6 --- /dev/null +++ b/mseries/local-ai/PHOSPHENE-FEASIBILITY-2026-10-19.md @@ -0,0 +1,78 @@ +# Phosphene feasibility on elementary OS M-Series — 2026-10-19 + +## Decision + +Do not port Phosphene's video-generation stack to this machine as a project priority. +The application is aimed at local generative video, image editing, audio/video and +character LoRA workflows; those features are outside the dock HDMI primary goal. +Revisit only if local video creation becomes a concrete owner priority or the +Linux/Vulkan backend situation changes substantially. + +This is a source and hardware assessment, not an installation or runtime test. +No Phosphene package or model was installed, no render was attempted, and no +performance result is claimed. + +## What was assessed + +- Project: [Phosphene](https://github.com/mrbizarro/Phosphene), the repository + linked by the supplied `joshuaswarren/Phosphene` URL. +- Upstream state reviewed: release `v4.19.5`, commit + [`287a964`](https://github.com/mrbizarro/Phosphene/commit/287a96403f178030eba96eb3f2a341092c469b91). +- Machine: Apple M1 Pro (T6000/G13S), 32 GB unified memory, elementary OS M-Series + Linux ARM64, with the project’s existing `omarchy-mlx` Vulkan fork rather than + Apple's Metal MLX runtime. +- Files reviewed: upstream README and `install.js`; local hardware/stack details + were read from the project handoff and Omarchy MLX planning notes. + +## Fit and blockers + +Phosphene's README describes LTX-Video 2.5 and Hailuo H3 video engines, Qwen Image +Edit, and optional YuE2 audio generation. Its panel now calls Linux on Apple +Silicon a community-maintained, best-effort target, but the checked Pinokio +installer explicitly skips unless the platform is Darwin on ARM64. This means +the standard install path is not enabled for elementary OS; a Linux community +path does not establish compatibility with this machine's Vulkan MLX fork. + +The engines are built around Apple's MLX and MLX-specific companion projects. +Our installed MLX is a Linux Vulkan port, with a pinned release and changes +focused on language-model inference. Compatibility of the video diffusion, +audio, VAE and image-edit operations with that fork is unknown. A functional +port would need backend support and output-quality validation across those +operations, plus a Linux-native install/update and UI integration path. This is +substantial engineering with no demonstrated fit to the primary project goal. + +The memory tiers also rule out key features: Phosphene says character mode, +advanced keyframing/extension and Q8 HQ pipelines require 48 GB or more. Hailuo +H3's compact Q8 engine is listed at 36 GB or more of unified memory, above this +machine's 32 GB. The listed LTX base install is about 27.5 GB of model files +(storage, not a claim of 27.5 GB runtime memory); its README says the basic +surface adapts to lower-memory Macs, but does not establish an acceptable Linux +Vulkan footprint or render speed here. The README's example of a 7-second +character clip taking about 6 minutes is on an M4 Max with 64 GB and is not a +performance estimate for this M1 Pro. + +## Practical follow-up + +If the need is local still-image generation/editing, evaluate a smaller +Linux/Vulkan-native candidate independently against the resident assistant and +the existing desktop-memory reserve. Do not assume it can reuse Phosphene's MLX +pipeline. If the need is video, the practical current option is a supported Mac +with sufficient unified memory; do not spend porting effort on this host unless +the owner reprioritizes the use case. + +## Privacy and licensing notes + +Phosphene says prompts and media stay on-device, while its default install sends +anonymous usage counts (including hardware class and render statistics); the +README documents a Settings control to disable analytics. Model downloads still +contact their respective hosts. The panel code is MIT-licensed, but model and +dependency terms vary: the README identifies separate LTX, MiniMax, YuE2 and +other terms. Review the exact model/dependency licenses before redistribution +or commercial use; this assessment does not qualify those rights. + +## Sources + +- [Phosphene README at reviewed release](https://github.com/mrbizarro/Phosphene/blob/v4.19.5/README.md) +- [Phosphene install.js at reviewed release](https://github.com/mrbizarro/Phosphene/blob/v4.19.5/install.js) +- [M-Series Omarchy MLX bump plan](OMARCHY-MLX-BUMP-PLAN-2026-10-19.md) +- [Project port boundary](../PORT-MAP.md) From 44583549ef274971d2c0f6a05ba77178155555b8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:07:27 -0400 Subject: [PATCH 131/798] Support native monitor disable and re-enable --- mseries/OS9-HANDOFF.md | 19 ++ mseries/ROADMAP.md | 10 +- .../EXPERIENCE-REQUIREMENTS.md | 21 +- rust/pantheon-compositor/README.md | 18 +- .../pantheon-compositor/src/display_config.rs | 48 ++-- rust/pantheon-compositor/src/native.rs | 187 +++++++++++++++- rust/pantheon-compositor/src/state.rs | 209 ++++++++++++++---- rust/pantheon-compositor/src/winit.rs | 2 + 8 files changed, 437 insertions(+), 77 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c3f2824aa..b65662281 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5772,3 +5772,22 @@ qualified. Gala remains selected. The next gate is nested runtime qualification with GTK/Qt plus the existing shell clients, then native login only with a separate tested recovery path. See `mseries/pantheon-rust-compositor/README.md` for the readiness criteria. + +### Add native monitor disable/re-enable — 2026-10-19 + +`ApplyMonitorsConfig` now accepts an active logical-monitor list that omits +connected connectors. Disabled monitors remain in `GetCurrentState`'s monitor +inventory but have no logical placement. Native DRM drops their Smithay output +and recreates it when re-enabled. Monitor profiles now store enabled state; +existing seven-field profiles load as enabled, and native startup honors saved +disabled connectors. Disabling a monitor moves windows whose centers would be +off-screen onto the primary output. To avoid an apply leaving no scanout if a +replacement fails to initialize, each request must preserve one current output; +enable a replacement in one apply before disabling the old output in another. +Mirroring and nested Winit monitor-set changes remain unsupported. + +Default and `native-drm` `cargo check` pass, and the compositor source is +formatted. No test or display session was run. Live disable/re-enable, +disconnect/reconnect, modeset failure recovery, Wingpanel Settings behavior, +and native output-set changes remain unqualified. The existing Gala session was +not changed. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index e954f28f2..9318ec978 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -38,9 +38,13 @@ missing-service error. `ApplyMonitorsConfig` validates connector assignments, mode IDs, scale and transform, applies native DRM mode changes through Smithay's shared output manager with rollback, and applies temporary layout changes. Method 2 saves a per-user monitor profile and native startup restores it. -Disabling outputs, mirroring, unsupported properties and nested Winit mode -changes are rejected. Both default and native DRM builds compile; live native -mode changes remain unqualified. The compositor advertises +Native output disable/re-enable is implemented, including persistence of the +enabled state and keeping disabled connectors in the monitor inventory. +Each apply must retain one currently active output; enable a replacement in +one apply before disabling the old output in a second apply. +Mirroring, unsupported properties and nested Winit monitor-set changes remain +rejected. Both default and native DRM builds compile; live native mode and +output-set changes remain unqualified. The compositor advertises `wp_fractional_scale_v1` and `wp_viewporter`, updating window surface scale preferences from mapped outputs. Moving surfaces across differently scaled outputs still needs runtime verification. The compositor also serves Wingpanel's `org.gnome.Shell` diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index fc219775f..30289ea3d 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -106,13 +106,20 @@ Wayland session and rendered SHM buffers. The prototype serves the `org.gnome.Mutter.DisplayConfig.GetCurrentState` query Wingpanel uses, and Wingpanel completes that call. Native `GetCurrentState` lists connector modes reported by DRM and marks current/preferred modes. `ApplyMonitorsConfig` now -validates serials, output assignments, mode IDs, scale and transform. Native -mode changes use Smithay's shared output manager with rollback; temporary -layout, scale and transform changes update Wayland outputs. Method 2 saves a -per-user profile and native startup restores saved modes and layout. Disabling -outputs, mirroring, unsupported properties and nested Winit mode changes are -rejected. Both default and native DRM builds compile; runtime changes on the -active M-series display remain unqualified. The compositor advertises +validates serials, connector assignments, mode IDs, scale and transform. It +supports disabling and re-enabling native outputs: disabled connectors remain +in the monitor inventory without a logical placement, the KMS output is +released/recreated, and method 2 persists enabled state alongside each +monitor's mode and layout. Existing seven-field profiles default to enabled. +For safety, a single apply request must retain at least one currently active +output; a disjoint output-set transition must enable the replacement first and +disable the old output in a subsequent apply. +Native mode changes use Smithay's shared output manager with rollback; +temporary layout, scale and transform changes update Wayland outputs. Mirroring, +unsupported properties and nested Winit monitor-set changes remain rejected. +Both default and native DRM builds compile; disable/re-enable, reconnect and +mode-rollback behavior remain unqualified at runtime on the active M-series +display. The compositor advertises `wp_fractional_scale_v1` and `wp_viewporter`, setting each window surface's preferred scale from its first mapped output. Runtime checks for moving a surface between differently scaled outputs remain open. The compositor now diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 4aeb5629c..77d035374 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -184,11 +184,19 @@ roll back earlier changes if a later mode fails. Temporary layout, scale and transform changes update the Wayland outputs. Method 2 saves a per-user profile at `$XDG_CONFIG_HOME/pantheon-rust-compositor/monitors.conf` (or `~/.config/pantheon-rust-compositor/monitors.conf`); native startup restores -saved modes and layout. Output disabling, mirroring, unsupported properties and -nested Winit mode changes are rejected. Output and configuration updates -increment the serial and emit `MonitorsChanged`. Default and native DRM builds -compile, but no native session or live monitor change has been run from this -development session. The compositor also advertises `wp_fractional_scale_v1` +saved modes and layout. Native output disable/re-enable is implemented by +removing/recreating the KMS output, and disabled connectors stay in the monitor +inventory without a logical placement. Existing seven-field profiles remain +enabled by default; new profiles store an enabled flag. A single apply request +must retain one currently active output so a failed enable cannot leave the +session without scanout; enable a replacement first before disabling the old +output. Mirroring, unsupported properties and nested Winit monitor-set changes +are rejected. Output and +configuration updates increment the serial and emit `MonitorsChanged`. Default +and native DRM builds compile, but no native session or live monitor change has +been run from this development session. Disable/re-enable, reconnect and +combined mode/layout rollback remain runtime-unqualified. The compositor also +advertises `wp_fractional_scale_v1` and `wp_viewporter`, setting window surfaces' preferred scale from their first mapped output and refreshing preferences during rendering and after monitor configuration changes. Cross-output moves and fractional rendering quality diff --git a/rust/pantheon-compositor/src/display_config.rs b/rust/pantheon-compositor/src/display_config.rs index 8bb726ea2..732548429 100644 --- a/rust/pantheon-compositor/src/display_config.rs +++ b/rust/pantheon-compositor/src/display_config.rs @@ -28,6 +28,7 @@ pub struct OutputSnapshot { pub x: i32, pub y: i32, pub primary: bool, + pub enabled: bool, } #[derive(Clone, Debug)] @@ -60,6 +61,7 @@ pub struct SavedMonitorConfiguration { pub scale: f64, pub transform: u32, pub primary: bool, + pub enabled: bool, } fn monitor_configuration_path() -> Option { @@ -100,6 +102,14 @@ pub fn load_saved_monitor_configurations() -> HashMap true, + Some("0") => false, + Some(_) => return None, + }; + if fields.next().is_some() { + return None; + } let scale = scale.parse::().ok()?; let transform = transform.parse::().ok()?; if !scale.is_finite() || !(0.5..=4.0).contains(&scale) || transform > 7 { @@ -113,13 +123,14 @@ pub fn load_saved_monitor_configurations() -> HashMap>(); if configurations .values() - .filter(|configuration| configuration.primary) + .filter(|configuration| configuration.primary && configuration.enabled) .count() == 1 { @@ -153,7 +164,7 @@ pub fn save_monitor_configurations(configurations: &[SavedMonitorConfiguration]) )); } contents.push_str(&format!( - "{}\t{}\t{}\t{}\t{}\t{}\t{}\n", + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n", configuration.connector, configuration.mode, configuration.x, @@ -161,6 +172,7 @@ pub fn save_monitor_configurations(configurations: &[SavedMonitorConfiguration]) configuration.scale, configuration.transform, u8::from(configuration.primary), + u8::from(configuration.enabled), )); } fs::write(&temporary_path, contents)?; @@ -187,6 +199,7 @@ pub struct ApplyRequest { pub serial: u32, pub method: u32, pub logical_monitors: Vec, + pub current_outputs: Vec, pub response: mpsc::SyncSender>, } @@ -246,8 +259,10 @@ impl DisplayConfigServer { let (serial, outputs) = self.0.lock().expect("display config lock poisoned").clone(); let mut outputs = outputs.into_values().collect::>(); outputs.sort_by(|a, b| (a.x, a.y, &a.connector).cmp(&(b.x, b.y, &b.connector))); - if !outputs.iter().any(|output| output.primary) - && let Some(first) = outputs.first_mut() + if !outputs + .iter() + .any(|output| output.primary && output.enabled) + && let Some(first) = outputs.iter_mut().find(|output| output.enabled) { first.primary = true; } @@ -301,15 +316,17 @@ impl DisplayConfigServer { }) .collect(); monitors.push((monitor_info.clone(), modes, Properties::new())); - logical_monitors.push(( - output.x, - output.y, - output.scale, - output.transform, - output.primary, - vec![monitor_info], - Properties::new(), - )); + if output.enabled { + logical_monitors.push(( + output.x, + output.y, + output.scale, + output.transform, + output.primary, + vec![monitor_info], + Properties::new(), + )); + } } let mut properties = Properties::new(); properties.insert("layout-mode".into(), OwnedValue::from(1_u32)); @@ -323,13 +340,13 @@ impl DisplayConfigServer { logical_monitors: Vec, _properties: Properties, ) -> zbus::fdo::Result<()> { - let current_serial = self + let (current_serial, current_outputs) = self .0 .lock() .map_err(|_| { zbus::fdo::Error::Failed("display configuration state is unavailable".into()) })? - .0; + .clone(); if serial != current_serial { return Err(zbus::fdo::Error::Failed(format!( "stale display configuration serial {serial}; current serial is {current_serial}" @@ -347,6 +364,7 @@ impl DisplayConfigServer { serial, method, logical_monitors, + current_outputs: current_outputs.into_values().collect(), response, }) .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into()))?; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 8f70e0b4e..2428a6024 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -112,6 +112,8 @@ pub struct NativeKmsDevice { pub node: DrmNode, pub manager: NativeOutputManager, pub outputs: Vec, + disabled_connectors: HashSet, + disconnected_disabled_connectors: HashSet, pub notifier: Option, pending_global_removals: Vec<( smithay::reexports::wayland_server::backend::GlobalId, @@ -133,6 +135,93 @@ struct ConnectedOutput { } impl NativeKmsDevice { + pub fn set_enabled_outputs( + &mut self, + renderer: &mut NativeRenderer, + state: &mut PantheonCompositor, + enabled: &HashSet, + ) -> Result<(), String> { + if enabled.is_empty() { + return Err("at least one output must remain enabled".into()); + } + let resources = self + .manager + .device_mut() + .resource_handles() + .map_err(|error| error.to_string())?; + let mut name_to_connector = HashMap::new(); + for handle in resources.connectors() { + let info = self + .manager + .device_mut() + .get_connector(*handle, false) + .map_err(|error| error.to_string())?; + if info.state() == connector::State::Connected { + let name = format!("{}-{}", info.interface().as_str(), info.interface_id()); + name_to_connector.insert(name, info.handle()); + } + } + for name in enabled { + if !name_to_connector.contains_key(name) { + return Err(format!("monitor {name} is no longer connected")); + } + } + let current = self + .outputs + .iter() + .map(|output| output.output.name()) + .collect::>(); + if !current.iter().any(|name| enabled.contains(name)) { + return Err( + "monitor change must keep one current output active while enabling the new layout" + .into(), + ); + } + let disabling = self + .outputs + .iter() + .filter(|output| !enabled.contains(&output.output.name())) + .map(|output| output.connector) + .collect::>(); + for connector in disabling { + if let Some(index) = self + .outputs + .iter() + .position(|output| output.connector == connector) + { + let output = self.outputs.remove(index); + state + .display_handle + .disable_global::(output.global_id.clone()); + self.pending_global_removals + .push((output.global_id.clone(), Instant::now())); + state.space.unmap_output(&output.output); + state.forget_output_during_lock(&output.output.name()); + self.disabled_connectors.insert(connector); + tracing::info!(output = %output.output.name(), "disabled DRM output by monitor configuration"); + drop(output); + } + } + + for name in enabled { + let connector = name_to_connector + .get(name) + .ok_or_else(|| format!("monitor {name} is no longer connected"))?; + self.disabled_connectors.remove(connector); + } + self.refresh_connectors(renderer, state) + .map_err(|error| format!("could not enable requested monitor: {error}"))?; + let active = self + .outputs + .iter() + .map(|output| output.output.name()) + .collect::>(); + if &active != enabled { + return Err("native backend could not activate the requested output set".into()); + } + Ok(()) + } + /// Apply output modes as one transaction. Smithay coordinates the shared /// DRM device while retaining rendered content for every CRTC; if a mode /// fails, previously switched outputs are restored before returning. @@ -401,6 +490,15 @@ impl NativeKmsDevice { .filter(|output| !connected_handles.contains(&output.connector)) .map(|output| output.connector) .collect::>(); + for connector in self.disabled_connectors.iter().copied() { + if !connected_handles.contains(&connector) + && let Ok(info) = self.manager.device_mut().get_connector(connector, false) + { + let name = format!("{}-{}", info.interface().as_str(), info.interface_id()); + state.display_config.remove(name); + self.disconnected_disabled_connectors.insert(connector); + } + } let mut changed = !disconnected.is_empty(); if changed { for output in self @@ -428,6 +526,36 @@ impl NativeKmsDevice { .map(|output| output.crtc) .collect::>(); for info in connected { + if self.disabled_connectors.contains(&info.handle()) { + if self.disconnected_disabled_connectors.remove(&info.handle()) + && let Some(mode) = info + .modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .or_else(|| info.modes().first().copied()) + { + let mode = Mode::from(mode); + let name = format!("{}-{}", info.interface().as_str(), info.interface_id()); + state.display_config.update(OutputSnapshot { + connector: name.clone(), + vendor: "Apple".into(), + product: name, + serial: "Unknown".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + modes: output_mode_snapshots(info.modes()), + scale: 1.0, + transform: 0, + x: 0, + y: 0, + primary: false, + enabled: false, + }); + } + continue; + } if self .outputs .iter() @@ -593,6 +721,7 @@ impl NativeKmsDevice { y: location.y, primary: primary_output_name.as_deref() == Some(output.output.name().as_str()), + enabled: true, }); } } @@ -704,8 +833,58 @@ impl NativeKmsDevice { }); let mut connected = Vec::new(); + let mut disabled_connectors = HashSet::new(); let mut claimed_crtcs = HashSet::new(); for connector in discovered { + let name = format!( + "{}-{}", + connector.interface().as_str(), + connector.interface_id() + ); + if let Some(saved) = saved_configurations + .get(&name) + .filter(|configuration| !configuration.enabled) + { + let preferred = connector + .modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .unwrap_or(connector.modes()[0]); + let selected = connector + .modes() + .iter() + .find(|mode| { + let mode = Mode::from(**mode); + format!( + "{}x{}@{:.3}", + mode.size.w, + mode.size.h, + f64::from(mode.refresh) / 1000.0 + ) == saved.mode + }) + .copied() + .unwrap_or(preferred); + let mode = Mode::from(selected); + state.display_config.update(OutputSnapshot { + connector: name.clone(), + vendor: "Apple".into(), + product: name, + serial: "Unknown".into(), + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + modes: output_mode_snapshots(connector.modes()), + scale: saved.scale, + transform: saved.transform, + x: saved.x, + y: saved.y, + primary: false, + enabled: false, + }); + disabled_connectors.insert(connector.handle()); + continue; + } let encoder = connector .current_encoder() .and_then(|handle| drm.get_encoder(handle).ok()) @@ -730,11 +909,6 @@ impl NativeKmsDevice { .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) .copied() .unwrap_or(connector.modes()[0]); - let name = format!( - "{}-{}", - connector.interface().as_str(), - connector.interface_id() - ); let mode = saved_configurations .get(&name) .and_then(|saved| { @@ -877,6 +1051,7 @@ impl NativeKmsDevice { x, y, primary, + enabled: true, }); next_x = next_x.max(x + (mode.size.w as f64 / scale).round() as i32); outputs.push(NativeKmsOutput { @@ -900,6 +1075,8 @@ impl NativeKmsDevice { node, manager, outputs, + disabled_connectors, + disconnected_disabled_connectors: HashSet::new(), notifier: Some(notifier), pending_global_removals: Vec::new(), }) diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index c0ca9b844..ce873b69b 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -383,13 +383,14 @@ impl PantheonCompositor { return Err("monitor configuration cannot change while the session is locked".into()); } + #[cfg(not(feature = "native-drm"))] let outputs = self.space.outputs().cloned().collect::>(); - if request.logical_monitors.len() != outputs.len() { - return Err("disabling outputs and monitor mirroring are not supported yet".into()); - } - + let known_outputs = request + .current_outputs + .iter() + .map(|output| (output.connector.clone(), output.clone())) + .collect::>(); let mut configurations = HashMap::new(); - let mut saved_configurations = Vec::new(); let mut primary_count = 0; for (x, y, scale, transform, primary, monitors, properties) in &request.logical_monitors { if !scale.is_finite() || !(0.5..=4.0).contains(scale) { @@ -417,23 +418,31 @@ impl PantheonCompositor { if !monitor_properties.is_empty() { return Err("monitor configuration properties are not supported".into()); } - let Some(output) = outputs.iter().find(|output| output.name() == *connector) else { + let Some(snapshot) = known_outputs.get(connector) else { return Err(format!("monitor {connector} is no longer connected")); }; - let Some(mode) = output.modes().into_iter().find(|mode| { + if !snapshot.modes.iter().any(|mode| { format!( "{}x{}@{:.3}", - mode.size.w, - mode.size.h, - f64::from(mode.refresh) / 1000.0 + mode.width, + mode.height, + f64::from(mode.refresh_millihz) / 1000.0 ) == *mode_id - }) else { + }) { return Err(format!("mode {mode_id} is unavailable on {connector}")); - }; + } if configurations .insert( connector.clone(), - (*x, *y, *scale, transform, *primary, mode), + ( + *x, + *y, + *scale, + transform, + *primary, + mode_id.clone(), + transform_value, + ), ) .is_some() { @@ -442,36 +451,109 @@ impl PantheonCompositor { if *primary { primary_count += 1; } - saved_configurations.push(SavedMonitorConfiguration { - connector: connector.clone(), - mode: mode_id.clone(), - x: *x, - y: *y, - scale: *scale, - transform: transform_value, - primary: *primary, - }); - } - if configurations.len() != outputs.len() { - return Err("every connected output must appear exactly once".into()); } - if primary_count != 1 { + if configurations.is_empty() || primary_count != 1 { return Err("exactly one primary monitor is required".into()); } + if configurations + .values() + .filter(|configuration| configuration.4) + .count() + != 1 + { + return Err("at least one enabled output and exactly one primary are required".into()); + } + + let mut saved_configurations = known_outputs + .values() + .map(|snapshot| { + if let Some((x, y, scale, _transform, primary, mode, transform_value)) = + configurations.get(&snapshot.connector) + { + SavedMonitorConfiguration { + connector: snapshot.connector.clone(), + mode: mode.clone(), + x: *x, + y: *y, + scale: *scale, + transform: *transform_value, + primary: *primary, + enabled: true, + } + } else { + SavedMonitorConfiguration { + connector: snapshot.connector.clone(), + mode: format!( + "{}x{}@{:.3}", + snapshot.width, + snapshot.height, + f64::from(snapshot.refresh_millihz) / 1000.0 + ), + x: snapshot.x, + y: snapshot.y, + scale: snapshot.scale, + transform: snapshot.transform, + primary: false, + enabled: false, + } + } + }) + .collect::>(); + saved_configurations.sort_by(|a, b| a.connector.cmp(&b.connector)); if request.method == 0 { return Ok(()); } - let primary_output_name = outputs + let primary_output_name = configurations .iter() - .find(|output| configurations[&output.name()].4) - .map(|output| output.name()); + .find_map(|(name, configuration)| configuration.4.then(|| name.clone())); - let requested_modes = configurations + let enabled_connectors = configurations + .keys() + .cloned() + .collect::>(); + #[cfg(feature = "native-drm")] + { + let Some(mut backend) = self.native_backend.take() else { + return Err("native DRM backend is unavailable".into()); + }; + let result = + backend + .kms + .set_enabled_outputs(&mut backend.renderer, self, &enabled_connectors); + self.native_backend = Some(backend); + result?; + } + #[cfg(not(feature = "native-drm"))] + if outputs .iter() - .map(|(name, (_, _, _, _, _, mode))| (name.clone(), *mode)) - .collect::>(); + .map(|output| output.name()) + .collect::>() + != enabled_connectors + { + return Err("the nested backend cannot enable or disable monitors".into()); + } + + let outputs = self.space.outputs().cloned().collect::>(); + + let mut requested_modes = HashMap::new(); + for output in &outputs { + let (_, _, _, _, _, mode_id, _) = &configurations[&output.name()]; + let mode = output + .modes() + .into_iter() + .find(|mode| { + format!( + "{}x{}@{:.3}", + mode.size.w, + mode.size.h, + f64::from(mode.refresh) / 1000.0 + ) == *mode_id + }) + .ok_or_else(|| format!("mode {mode_id} is unavailable on {}", output.name()))?; + requested_modes.insert(output.name(), mode); + } if outputs .iter() .any(|output| output.current_mode() != requested_modes.get(&output.name()).copied()) @@ -493,14 +575,15 @@ impl PantheonCompositor { let mut snapshots = Vec::with_capacity(outputs.len()); for output in outputs { - let (x, y, scale, transform, primary, mode) = configurations[&output.name()]; + let (x, y, scale, transform, primary, _mode_id, _) = &configurations[&output.name()]; + let mode = requested_modes[&output.name()]; output.change_current_state( Some(mode), - Some(transform), - Some(smithay::output::Scale::Fractional(scale)), - Some((x, y).into()), + Some(*transform), + Some(smithay::output::Scale::Fractional(*scale)), + Some((*x, *y).into()), ); - self.space.map_output(&output, (x, y)); + self.space.map_output(&output, (*x, *y)); let physical = output.physical_properties(); snapshots.push(OutputSnapshot { connector: output.name(), @@ -520,16 +603,58 @@ impl PantheonCompositor { preferred: output.preferred_mode() == Some(mode), }) .collect(), - scale, - transform: transform_to_dbus(transform), - x, - y, - primary, + scale: *scale, + transform: transform_to_dbus(*transform), + x: *x, + y: *y, + primary: *primary, + enabled: true, }); } + for output in &request.current_outputs { + if !enabled_connectors.contains(&output.connector) { + let mut disabled = output.clone(); + disabled.enabled = false; + disabled.primary = false; + snapshots.push(disabled); + } + } self.primary_output_name = primary_output_name; self.display_config.update_many(snapshots); + let output_geometries = self + .space + .outputs() + .filter_map(|output| self.space.output_geometry(output)) + .collect::>(); + if let Some(fallback) = self + .primary_output() + .and_then(|output| self.space.output_geometry(&output)) + { + let windows = self.space.elements().cloned().collect::>(); + for window in windows { + let Some(location) = self.space.element_location(&window) else { + continue; + }; + let size = window.geometry().size; + let center_x = location.x + size.w / 2; + let center_y = location.y + size.h / 2; + let visible = output_geometries.iter().any(|geometry| { + center_x >= geometry.loc.x + && center_x < geometry.loc.x + geometry.size.w + && center_y >= geometry.loc.y + && center_y < geometry.loc.y + geometry.size.h + }); + if !visible { + self.space.map_element( + window, + (fallback.loc.x + 32, fallback.loc.y + 32), + false, + ); + } + } + } + self.last_work_areas.clear(); let panels = self .space diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index cc1272d49..8c11b9628 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -81,6 +81,7 @@ pub fn init_winit( x: output.current_location().x, y: output.current_location().y, primary: true, + enabled: true, }); let mut damage_tracker = OutputDamageTracker::from_output(&output); @@ -121,6 +122,7 @@ pub fn init_winit( x: output.current_location().x, y: output.current_location().y, primary: true, + enabled: true, }); } WinitEvent::Input(event) => state.process_input_event(event), From 3719a9930731362785d79f020c20ff0be16100d7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:08:11 -0400 Subject: [PATCH 132/798] Record ARM64 monitor toggle candidate --- mseries/OS9-HANDOFF.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index b65662281..68de43c33 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5790,4 +5790,7 @@ Default and `native-drm` `cargo check` pass, and the compositor source is formatted. No test or display session was run. Live disable/re-enable, disconnect/reconnect, modeset failure recovery, Wingpanel Settings behavior, and native output-set changes remain unqualified. The existing Gala session was -not changed. +not changed. The native ARM64 release build from `4458354` is packaged at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-monitor-toggle-release/pantheon-rust-compositor_0.1.0+git4458354_arm64.deb` +(SHA-256 `c2371d1b8810de75e137a61f05f7b6ecacb8e066ebc001239a6a3dee3191638f`). +Package metadata and contents were inspected. It was not installed. From 6ec311391cebee7a020e4f740d52ba415409b0b6 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:11:22 -0400 Subject: [PATCH 133/798] Stage disjoint native monitor changes --- mseries/OS9-HANDOFF.md | 23 +- mseries/ROADMAP.md | 6 +- .../EXPERIENCE-REQUIREMENTS.md | 9 +- rust/pantheon-compositor/README.md | 13 +- rust/pantheon-compositor/src/native.rs | 56 ++++- rust/pantheon-compositor/src/state.rs | 200 +++++++++++++++++- 6 files changed, 285 insertions(+), 22 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 68de43c33..83bacbdbe 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5781,9 +5781,9 @@ inventory but have no logical placement. Native DRM drops their Smithay output and recreates it when re-enabled. Monitor profiles now store enabled state; existing seven-field profiles load as enabled, and native startup honors saved disabled connectors. Disabling a monitor moves windows whose centers would be -off-screen onto the primary output. To avoid an apply leaving no scanout if a -replacement fails to initialize, each request must preserve one current output; -enable a replacement in one apply before disabling the old output in another. +off-screen onto the primary output. The first implementation required overlap +between old and new output sets; the follow-up below stages disjoint sets while +retaining one live output. Mirroring and nested Winit monitor-set changes remain unsupported. Default and `native-drm` `cargo check` pass, and the compositor source is @@ -5794,3 +5794,20 @@ not changed. The native ARM64 release build from `4458354` is packaged at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-monitor-toggle-release/pantheon-rust-compositor_0.1.0+git4458354_arm64.deb` (SHA-256 `c2371d1b8810de75e137a61f05f7b6ecacb8e066ebc001239a6a3dee3191638f`). Package metadata and contents were inspected. It was not installed. + +### Stage disjoint native output-set changes with a live anchor — 2026-10-19 + +Removed the old/new set overlap requirement. The native backend retains one +current output while it releases other CRTCs and initializes the requested +connectors, then removes the anchor after output initialization succeeds. When +initialization or modesetting fails, the compositor attempts to restore the +previous enabled set, modes, layout, work areas and window placement, and +reconciles `GetCurrentState` if rollback itself fails. This supports disjoint +transitions when the DRM device has enough compatible CRTCs. The current +Smithay output-manager path still cannot reassign a single CRTC atomically, so +a one-CRTC disjoint switch returns an error and retains the old output. + +Default and `native-drm` `cargo check` pass. No tests or display session were +run; failure injection, page-flip timing, disjoint transitions on hardware and +rollback remain runtime-unqualified. Build a new candidate from this commit +before any monitor qualification; the `4458354` package predates this change. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 9318ec978..7c1b48d16 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -40,8 +40,10 @@ shared output manager with rollback, and applies temporary layout changes. Method 2 saves a per-user monitor profile and native startup restores it. Native output disable/re-enable is implemented, including persistence of the enabled state and keeping disabled connectors in the monitor inventory. -Each apply must retain one currently active output; enable a replacement in -one apply before disabling the old output in a second apply. +Native transitions keep an output live while staging replacements, then retire +it after initialization succeeds; disjoint sets can be staged when the DRM +device has enough compatible CRTCs. Failures attempt to restore the prior set, +modes and layout. Atomic reassignment of a single CRTC remains a gap. Mirroring, unsupported properties and nested Winit monitor-set changes remain rejected. Both default and native DRM builds compile; live native mode and output-set changes remain unqualified. The compositor advertises diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 30289ea3d..a14da9912 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -111,9 +111,12 @@ supports disabling and re-enabling native outputs: disabled connectors remain in the monitor inventory without a logical placement, the KMS output is released/recreated, and method 2 persists enabled state alongside each monitor's mode and layout. Existing seven-field profiles default to enabled. -For safety, a single apply request must retain at least one currently active -output; a disjoint output-set transition must enable the replacement first and -disable the old output in a subsequent apply. +The native backend keeps one output live while it releases CRTCs and +initializes the requested set, then retires that anchor after initialization +succeeds. If an output cannot be initialized or modesetting fails, it attempts +to restore the previous enabled set, modes and layout. Disjoint output-set +changes can be staged when the device has enough compatible CRTCs; a single +CRTC cannot be reassigned atomically by this Smithay output-manager path yet. Native mode changes use Smithay's shared output manager with rollback; temporary layout, scale and transform changes update Wayland outputs. Mirroring, unsupported properties and nested Winit monitor-set changes remain rejected. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 77d035374..619d67a73 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -187,11 +187,14 @@ at `$XDG_CONFIG_HOME/pantheon-rust-compositor/monitors.conf` (or saved modes and layout. Native output disable/re-enable is implemented by removing/recreating the KMS output, and disabled connectors stay in the monitor inventory without a logical placement. Existing seven-field profiles remain -enabled by default; new profiles store an enabled flag. A single apply request -must retain one currently active output so a failed enable cannot leave the -session without scanout; enable a replacement first before disabling the old -output. Mirroring, unsupported properties and nested Winit monitor-set changes -are rejected. Output and +enabled by default; new profiles store an enabled flag. During a native +transition the backend keeps one output live while freeing CRTCs and +initializing the requested set, then retires that output when initialization +succeeds. It attempts to restore the prior set, modes and layout if +initialization or modesetting fails. Disjoint transitions work when the device +has enough compatible CRTCs; this Smithay output-manager path cannot yet +reassign a single CRTC atomically. Mirroring, unsupported properties and nested +Winit monitor-set changes are rejected. Output and configuration updates increment the serial and emit `MonitorsChanged`. Default and native DRM builds compile, but no native session or live monitor change has been run from this development session. Disable/re-enable, reconnect and diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 2428a6024..e27ce64f9 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -171,16 +171,32 @@ impl NativeKmsDevice { .iter() .map(|output| output.output.name()) .collect::>(); - if !current.iter().any(|name| enabled.contains(name)) { - return Err( - "monitor change must keep one current output active while enabling the new layout" - .into(), - ); + let anchor = current + .iter() + .find(|name| enabled.contains(*name)) + .cloned() + .or_else(|| { + current + .iter() + .find(|name| name.starts_with("eDP-")) + .cloned() + }) + .or_else(|| current.iter().next().cloned()) + .ok_or_else(|| { + "native backend has no active output to anchor the transition".to_string() + })?; + + for (name, connector) in &name_to_connector { + if !enabled.contains(name) && name != &anchor { + self.disabled_connectors.insert(*connector); + } } let disabling = self .outputs .iter() - .filter(|output| !enabled.contains(&output.output.name())) + .filter(|output| { + output.output.name() != anchor && !enabled.contains(&output.output.name()) + }) .map(|output| output.connector) .collect::>(); for connector in disabling { @@ -211,11 +227,37 @@ impl NativeKmsDevice { } self.refresh_connectors(renderer, state) .map_err(|error| format!("could not enable requested monitor: {error}"))?; - let active = self + let mut active = self .outputs .iter() .map(|output| output.output.name()) .collect::>(); + if !enabled.is_subset(&active) { + for name in enabled.difference(&active) { + if let Some(connector) = name_to_connector.get(name) { + self.disabled_connectors.insert(*connector); + } + } + return Err("native backend could not activate every requested output; the prior output remains active".into()); + } + if !enabled.contains(&anchor) + && let Some(index) = self + .outputs + .iter() + .position(|output| output.output.name() == anchor) + { + let output = self.outputs.remove(index); + state + .display_handle + .disable_global::(output.global_id.clone()); + self.pending_global_removals + .push((output.global_id.clone(), Instant::now())); + state.space.unmap_output(&output.output); + state.forget_output_during_lock(&output.output.name()); + self.disabled_connectors.insert(output.connector); + drop(output); + active.remove(&anchor); + } if &active != enabled { return Err("native backend could not activate the requested output set".into()); } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index ce873b69b..a2961141c 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -375,6 +375,182 @@ impl PantheonCompositor { .or_else(|| self.space.outputs().next().cloned()) } + #[cfg(feature = "native-drm")] + fn restore_native_outputs_from_snapshots( + &mut self, + snapshots: &[OutputSnapshot], + ) -> Result<(), String> { + let enabled = snapshots + .iter() + .filter(|snapshot| snapshot.enabled) + .map(|snapshot| snapshot.connector.clone()) + .collect::>(); + let Some(mut backend) = self.native_backend.take() else { + return Err("native DRM backend is unavailable during monitor rollback".into()); + }; + let result = (|| { + let mut restored = false; + let mut last_error = String::new(); + for _ in 0..=snapshots.len() { + match backend + .kms + .set_enabled_outputs(&mut backend.renderer, self, &enabled) + { + Ok(()) => { + restored = true; + break; + } + Err(error) => last_error = error, + } + } + if !restored { + return Err(format!( + "could not restore the previous output set: {last_error}" + )); + } + let outputs = self.space.outputs().cloned().collect::>(); + let mut requested_modes = HashMap::new(); + for output in &outputs { + let snapshot = snapshots + .iter() + .find(|snapshot| snapshot.connector == output.name() && snapshot.enabled) + .ok_or_else(|| format!("missing rollback state for {}", output.name()))?; + let mode = output + .modes() + .into_iter() + .find(|mode| { + mode.size.w == snapshot.width + && mode.size.h == snapshot.height + && mode.refresh == snapshot.refresh_millihz + }) + .ok_or_else(|| { + format!("rollback mode is no longer available on {}", output.name()) + })?; + requested_modes.insert(output.name(), mode); + } + backend + .kms + .apply_modes(&mut backend.renderer, self, &requested_modes)?; + + let primary = snapshots + .iter() + .find(|snapshot| snapshot.enabled && snapshot.primary) + .map(|snapshot| snapshot.connector.clone()); + for output in outputs { + let snapshot = snapshots + .iter() + .find(|snapshot| snapshot.connector == output.name() && snapshot.enabled) + .expect("rollback state was validated for every active output"); + let transform = match snapshot.transform { + 0 => smithay::utils::Transform::Normal, + 1 => smithay::utils::Transform::_90, + 2 => smithay::utils::Transform::_180, + 3 => smithay::utils::Transform::_270, + 4 => smithay::utils::Transform::Flipped, + 5 => smithay::utils::Transform::Flipped90, + 6 => smithay::utils::Transform::Flipped180, + _ => smithay::utils::Transform::Flipped270, + }; + let mode = requested_modes[&output.name()]; + output.change_current_state( + Some(mode), + Some(transform), + Some(smithay::output::Scale::Fractional(snapshot.scale)), + Some((snapshot.x, snapshot.y).into()), + ); + self.space.map_output(&output, (snapshot.x, snapshot.y)); + } + self.primary_output_name = primary; + self.display_config.update_many(snapshots.to_vec()); + self.last_work_areas.clear(); + let panels = self + .space + .elements() + .filter_map(|window| { + let surface = window.toplevel()?.wl_surface(); + self.is_panel_surface(surface).then(|| surface.clone()) + }) + .collect::>(); + for panel in panels { + self.update_panel(&panel, |_| {}); + } + self.reconfigure_managed_windows(); + self.space.refresh(); + self.update_fractional_scale_preferences(); + self.request_native_redraw(); + let _ = self.display_handle.flush_clients(); + Ok(()) + })(); + self.native_backend = Some(backend); + result + } + + #[cfg(feature = "native-drm")] + fn reconcile_display_config_after_failed_apply(&mut self, previous: &[OutputSnapshot]) { + let outputs = self.space.outputs().cloned().collect::>(); + let active_names = outputs + .iter() + .map(|output| output.name()) + .collect::>(); + let primary = self + .primary_output_name + .as_ref() + .filter(|name| active_names.contains(*name)) + .cloned() + .or_else(|| { + outputs + .iter() + .find(|output| output.name().starts_with("eDP-")) + .map(|output| output.name()) + }) + .or_else(|| outputs.first().map(|output| output.name())); + self.primary_output_name = primary.clone(); + + let mut snapshots = outputs + .iter() + .filter_map(|output| { + let mode = output.current_mode()?; + let physical = output.physical_properties(); + let location = output.current_location(); + Some(OutputSnapshot { + connector: output.name(), + vendor: physical.make, + product: physical.model, + serial: physical.serial_number, + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + modes: output + .modes() + .into_iter() + .map(|mode| OutputModeSnapshot { + width: mode.size.w, + height: mode.size.h, + refresh_millihz: mode.refresh, + preferred: output.preferred_mode() == Some(mode), + }) + .collect(), + scale: output.current_scale().fractional_scale(), + transform: transform_to_dbus(output.current_transform()), + x: location.x, + y: location.y, + primary: primary.as_deref() == Some(output.name().as_str()), + enabled: true, + }) + }) + .collect::>(); + snapshots.extend(previous.iter().filter_map(|snapshot| { + if active_names.contains(&snapshot.connector) { + return None; + } + let mut snapshot = snapshot.clone(); + snapshot.enabled = false; + snapshot.primary = false; + Some(snapshot) + })); + self.display_config.update_many(snapshots); + } + fn apply_display_configuration(&mut self, request: &ApplyRequest) -> Result<(), String> { if request.logical_monitors.is_empty() { return Err("at least one logical monitor is required".into()); @@ -523,7 +699,17 @@ impl PantheonCompositor { .kms .set_enabled_outputs(&mut backend.renderer, self, &enabled_connectors); self.native_backend = Some(backend); - result?; + if let Err(error) = result { + if let Err(rollback_error) = + self.restore_native_outputs_from_snapshots(&request.current_outputs) + { + self.reconcile_display_config_after_failed_apply(&request.current_outputs); + return Err(format!( + "{error}; monitor rollback failed: {rollback_error}" + )); + } + return Err(error); + } } #[cfg(not(feature = "native-drm"))] if outputs @@ -567,7 +753,17 @@ impl PantheonCompositor { .kms .apply_modes(&mut backend.renderer, self, &requested_modes); self.native_backend = Some(backend); - result?; + if let Err(error) = result { + if let Err(rollback_error) = + self.restore_native_outputs_from_snapshots(&request.current_outputs) + { + self.reconcile_display_config_after_failed_apply(&request.current_outputs); + return Err(format!( + "{error}; monitor rollback failed: {rollback_error}" + )); + } + return Err(error); + } } #[cfg(not(feature = "native-drm"))] return Err("this build cannot change native display modes".into()); From ec065f9aa61c98c50e3d80d86524cef1f0ebda02 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:12:09 -0400 Subject: [PATCH 134/798] Record staged monitor transition package --- mseries/OS9-HANDOFF.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 83bacbdbe..8a0ab357a 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5811,3 +5811,7 @@ Default and `native-drm` `cargo check` pass. No tests or display session were run; failure injection, page-flip timing, disjoint transitions on hardware and rollback remain runtime-unqualified. Build a new candidate from this commit before any monitor qualification; the `4458354` package predates this change. +The ARM64 package built from `6ec3113` is at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-monitor-toggle-stage-release/pantheon-rust-compositor_0.1.0+git6ec3113_arm64.deb` +(SHA-256 `457a2847d8588cc108f44b726909a42eeb2b804bb38e71fa74034de537f11a88`). +Its package metadata and contents were inspected; it was not installed. From 6eec9a5a7ea25505200e97648a91f769e7363ca8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:16:49 -0400 Subject: [PATCH 135/798] Support USB driver media for Windows VMs --- mseries/OS9-HANDOFF.md | 8 ++++++++ mseries/vms/mseries-vm | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e5a367342..05a2928b5 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6227,3 +6227,11 @@ firmware, NVMe disk). Findings: (boot manager hangs after reading 3 MiB) and AHCI `ide-cd` (the firmware does not list it as a boot device). Needs the virtio-win drivers: attach `virtio-win.iso` (`create ... --virtio-media`, or `EXTRA_MEDIA`) and load `vioscsi` from Setup's "Browse" dialog. Not done: that ISO is not downloaded yet. + +## 2026-10-19: Windows ARM64 install attempt and VirtIO media + +Downloaded Fedora's stable `virtio-win-0.1.302` ISO to `~/vms/downloads/virtio-win/virtio-win.iso` (837 MiB). Verification: the official Fedora stable RPM's published MD5 (`66f65c16ab3e8dfe12c2855a0d1ac303`) matched the downloaded RPM; the ISO extracted from that RPM had SHA-256 `303f7ae40dad495d6ae474fdc571df58958a4dbc5c37a522d80f9a203867949d`, matching the downloaded ISO. The ISO contains ARM64 `vioscsi`, `viostor` and `NetKVM` drivers. It is attached as `EXTRA_MEDIA` to `~/vms/windows/vm.conf` and to the isolated `windows-install-test4` guest. + +Install remains blocked before Windows Setup. Test4's virtual disk is blank (196 KiB allocated). Its ARM64 Windows ISO contains `EFI/BOOT/BOOTAA64.EFI`, `sources/boot.wim` and `sources/install.wim`, but AAVMF times out starting the installer from both SCSI CD-ROM and USB storage. Repeated with Secure Boot firmware and without it, with 4 GiB and 8 GiB RAM, and with a VGA device alongside ramfb; the same timeout recurred. No Windows files were installed. The test VM was stopped cleanly and retained for further diagnosis. PR #260 is merged to `dev` at `493fa2ba88b3f4ce651e29cfa13d7f4f7410f62f`. + +The companion `mseries-vm` change adds `EXTRA_BUS=usb` for presenting the extra driver ISO through USB storage. The USB path was exercised in test4, but the installer boot timeout means driver loading and installation remain unverified. Next: diagnose why this AAVMF build cannot complete the ARM64 Windows boot path, then continue Setup and load the storage driver if prompted. diff --git a/mseries/vms/mseries-vm b/mseries/vms/mseries-vm index 7c55be14c..c6d7800c5 100755 --- a/mseries/vms/mseries-vm +++ b/mseries/vms/mseries-vm @@ -421,8 +421,14 @@ cmd_start() { if (( index == 0 )) && [[ $BOOT == iso ]] && [[ -n $ISO ]]; then boot_arg=",bootindex=1" fi + # Extra media can ride on USB: Windows PE reads USB storage with its + # in-box driver but has no virtio-scsi driver until you load one from it. + local cd_device=scsi-cd + if (( index > 0 )) && [[ ${EXTRA_BUS:-scsi} == usb ]]; then + cd_device=usb-storage + fi cmd+=(-drive "if=none,id=cdrom$index,format=raw,media=cdrom,readonly=on,file=$medium" - -device "scsi-cd,drive=cdrom$index$boot_arg") + -device "$cd_device,drive=cdrom$index$boot_arg") index=$((index + 1)) done if [[ $NET == none ]]; then @@ -621,6 +627,7 @@ cmd_set() { TPM) [[ $value == on || $value == off ]] || die "$key must be on or off" ;; AUDIO|GL) [[ $value == on || $value == off ]] || die "$key must be on or off" ;; GPU) [[ $value == virtio || $value == ramfb ]] || die "GPU must be virtio or ramfb" ;; + EXTRA_BUS) [[ $value == scsi || $value == usb ]] || die "EXTRA_BUS must be scsi or usb" ;; CPUS|MEMORY_MB) [[ $value =~ ^[1-9][0-9]*$ ]] || die "$key must be a positive integer" ;; DISK_BUS) [[ $value == virtio || $value == nvme ]] || die "DISK_BUS must be virtio or nvme" ;; FIRMWARE) [[ -f $AAVMF_DIR/$value ]] || die "firmware not found: $AAVMF_DIR/$value" ;; From c91ec2d7be881e36d7f7d810ef640d123b5ccf59 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:17:03 -0400 Subject: [PATCH 136/798] Reassign a native CRTC between connectors --- mseries/OS9-HANDOFF.md | 31 +- mseries/ROADMAP.md | 9 +- .../EXPERIENCE-REQUIREMENTS.md | 13 +- rust/pantheon-compositor/README.md | 15 +- rust/pantheon-compositor/src/native.rs | 332 +++++++++++++++--- rust/pantheon-compositor/src/state.rs | 40 ++- 6 files changed, 362 insertions(+), 78 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 8a0ab357a..1ae786a95 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5797,15 +5797,15 @@ Package metadata and contents were inspected. It was not installed. ### Stage disjoint native output-set changes with a live anchor — 2026-10-19 -Removed the old/new set overlap requirement. The native backend retains one -current output while it releases other CRTCs and initializes the requested -connectors, then removes the anchor after output initialization succeeds. When +Removed the old/new set overlap requirement. The native backend retains +existing outputs while it initializes requested connectors, then removes old +outputs after output initialization succeeds. When initialization or modesetting fails, the compositor attempts to restore the previous enabled set, modes, layout, work areas and window placement, and reconciles `GetCurrentState` if rollback itself fails. This supports disjoint -transitions when the DRM device has enough compatible CRTCs. The current -Smithay output-manager path still cannot reassign a single CRTC atomically, so -a one-CRTC disjoint switch returns an error and retains the old output. +transitions when the DRM device has enough compatible CRTCs. At that point, +single-CRTC reassignment was not yet supported; the follow-up below adds it via +the pinned Smithay surface connector/mode API. Default and `native-drm` `cargo check` pass. No tests or display session were run; failure injection, page-flip timing, disjoint transitions on hardware and @@ -5815,3 +5815,22 @@ The ARM64 package built from `6ec3113` is at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-monitor-toggle-stage-release/pantheon-rust-compositor_0.1.0+git6ec3113_arm64.deb` (SHA-256 `457a2847d8588cc108f44b726909a42eeb2b804bb38e71fa74034de537f11a88`). Its package metadata and contents were inspected; it was not installed. + +### Reassign a single DRM CRTC to a new connector — 2026-10-19 + +The pinned Smithay API supports double-buffered connector changes on a DRM +surface. `NativeKmsDevice` now uses that surface's connector and mode update to +route an existing CRTC to a newly requested connector, preserving the scanout +pipeline when no spare CRTC is available. The new Wayland output replaces the +old connector's global after the mode commit. If connector activation fails, +the old connector and mode are restored; the outer ApplyMonitorsConfig rollback +restores the previous output set, modes, layout and work areas. Targets that +cannot use the existing CRTC still require another compatible CRTC and return +an error without intentionally dropping all active outputs. + +Both default and `native-drm` `cargo check` pass. Smithay source inspection +confirms the connector set is double-buffered and `use_mode` submits the pending +mode with the composited frame; this is source/API evidence, not runtime proof. +No monitor switch, page-flip check or failure injection has been run on the +M-series device. The ARM64 package at `6ec3113` predates this change and must +not be used to qualify it. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 7c1b48d16..e4e360ce5 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -40,10 +40,11 @@ shared output manager with rollback, and applies temporary layout changes. Method 2 saves a per-user monitor profile and native startup restores it. Native output disable/re-enable is implemented, including persistence of the enabled state and keeping disabled connectors in the monitor inventory. -Native transitions keep an output live while staging replacements, then retire -it after initialization succeeds; disjoint sets can be staged when the DRM -device has enough compatible CRTCs. Failures attempt to restore the prior set, -modes and layout. Atomic reassignment of a single CRTC remains a gap. +Native transitions keep existing outputs live while staging replacements, and +can atomically reassign one CRTC to a different connector when the encoder +supports it. Outputs outside the requested set are retired after all requested +outputs are active; failures attempt to restore the prior set, modes and layout. +The path builds but still needs hardware and failure qualification. Mirroring, unsupported properties and nested Winit monitor-set changes remain rejected. Both default and native DRM builds compile; live native mode and output-set changes remain unqualified. The compositor advertises diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index a14da9912..2ba9d50f1 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -111,12 +111,13 @@ supports disabling and re-enabling native outputs: disabled connectors remain in the monitor inventory without a logical placement, the KMS output is released/recreated, and method 2 persists enabled state alongside each monitor's mode and layout. Existing seven-field profiles default to enabled. -The native backend keeps one output live while it releases CRTCs and -initializes the requested set, then retires that anchor after initialization -succeeds. If an output cannot be initialized or modesetting fails, it attempts -to restore the previous enabled set, modes and layout. Disjoint output-set -changes can be staged when the device has enough compatible CRTCs; a single -CRTC cannot be reassigned atomically by this Smithay output-manager path yet. +The native backend keeps existing outputs live while it stages the requested +set. It can reassign a CRTC to a new connector through a DRM surface connector +and mode commit, including on a single-CRTC device, when the encoder supports +that route. It retires outputs outside the requested set only after all +requested outputs are active. If connector activation or modesetting fails, it +attempts to restore the previous enabled set, modes and layout. This path +builds but still needs failure-injection and hardware qualification. Native mode changes use Smithay's shared output manager with rollback; temporary layout, scale and transform changes update Wayland outputs. Mirroring, unsupported properties and nested Winit monitor-set changes remain rejected. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 619d67a73..0bb503553 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -188,13 +188,14 @@ saved modes and layout. Native output disable/re-enable is implemented by removing/recreating the KMS output, and disabled connectors stay in the monitor inventory without a logical placement. Existing seven-field profiles remain enabled by default; new profiles store an enabled flag. During a native -transition the backend keeps one output live while freeing CRTCs and -initializing the requested set, then retires that output when initialization -succeeds. It attempts to restore the prior set, modes and layout if -initialization or modesetting fails. Disjoint transitions work when the device -has enough compatible CRTCs; this Smithay output-manager path cannot yet -reassign a single CRTC atomically. Mirroring, unsupported properties and nested -Winit monitor-set changes are rejected. Output and +transition the backend keeps existing outputs live while staging the requested +set. It can reassign a CRTC to a different connector and mode in one DRM surface +commit, including on a single-CRTC device when that encoder route is supported. +It retires outputs outside the requested set after all requested outputs are +active, and attempts to restore the prior set, modes and layout if activation +or modesetting fails. This path compiles but still needs hardware and failure +qualification. Mirroring, unsupported properties and nested Winit monitor-set +changes are rejected. Output and configuration updates increment the serial and emit `MonitorsChanged`. Default and native DRM builds compile, but no native session or live monitor change has been run from this development session. Disable/re-enable, reconnect and diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index e27ce64f9..e3dd80582 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -135,11 +135,212 @@ struct ConnectedOutput { } impl NativeKmsDevice { + fn render_elements_for_output<'a>( + &self, + gpu_renderer: &mut NativeMultiRenderer<'a>, + state: &PantheonCompositor, + replacement: Option<(smithay::reexports::drm::control::crtc::Handle, &Output)>, + ) -> Result< + DrmOutputRenderElements< + NativeMultiRenderer<'a>, + CursorRenderElement>, + >, + String, + > { + let mut render_elements = DrmOutputRenderElements::new(); + for output in &self.outputs { + let render_output = replacement + .filter(|(crtc, _)| *crtc == output.crtc) + .map(|(_, output)| output) + .unwrap_or(&output.output); + let elements = if state.session_locked { + render_lock_surface_elements(gpu_renderer, state, render_output) + } else { + let space_elements = smithay::desktop::space::space_render_elements( + gpu_renderer, + [&state.space], + render_output, + render_output.current_scale().fractional_scale() as f32, + ) + .map_err(|error| format!("could not prepare output frame: {error:?}"))?; + let cursor_elements = render_cursor_elements(gpu_renderer, state, render_output); + let mut elements = space_elements + .into_iter() + .map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + }) + .collect::>(); + elements.extend(cursor_elements); + elements + }; + render_elements.add_output(&output.crtc, [0.1, 0.1, 0.1, 1.0].into(), elements); + } + Ok(render_elements) + } + + fn reassign_crtc_connector( + &mut self, + renderer: &mut NativeRenderer, + state: &mut PantheonCompositor, + old_name: &str, + connector_info: &connector::Info, + mode: smithay::reexports::drm::control::Mode, + ) -> Result<(), String> { + let Some(index) = self + .outputs + .iter() + .position(|output| output.output.name() == old_name) + else { + return Err(format!("output {old_name} is no longer active")); + }; + let old_output = self.outputs[index].output.clone(); + let old_connector = self.outputs[index].connector; + let old_drm_mode = old_output + .current_mode() + .and_then(|current| { + self.outputs[index] + .modes + .iter() + .find(|mode| Mode::from(**mode) == current) + .copied() + }) + .ok_or_else(|| format!("{old_name} has no current DRM mode to restore"))?; + let crtc = self.outputs[index].crtc; + let location = old_output.current_location(); + let scale = old_output.current_scale(); + let transform = old_output.current_transform(); + let name = format!( + "{}-{}", + connector_info.interface().as_str(), + connector_info.interface_id() + ); + let (physical_width, physical_height) = connector_info.size().unwrap_or((0, 0)); + let output = Output::new( + name.clone(), + PhysicalProperties { + size: (physical_width as i32, physical_height as i32).into(), + subpixel: connector_info.subpixel().into(), + make: "Apple".into(), + model: name.clone(), + serial_number: "Unknown".into(), + }, + ); + let available_modes = connector_info.modes().to_vec(); + let preferred = available_modes + .iter() + .find(|candidate| candidate.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .unwrap_or(mode); + output.set_preferred(Mode::from(preferred)); + for available_mode in &available_modes { + output.add_mode(Mode::from(*available_mode)); + } + output.change_current_state( + Some(Mode::from(mode)), + Some(transform), + Some(scale), + Some(location), + ); + + state.space.map_output(&output, location); + let connectors = [connector_info.handle()]; + let connector_result = self.outputs[index] + .drm_output + .with_compositor(|compositor| { + compositor.set_output_mode_source((&output).into()); + compositor.set_connectors(&connectors) + }); + if let Err(error) = connector_result { + self.outputs[index] + .drm_output + .with_compositor(|compositor| { + compositor.set_output_mode_source((&old_output).into()); + }); + state.space.unmap_output(&output); + return Err(format!("DRM rejected connector reassignment: {error:?}")); + } + + let result = (|| { + let mut gpu_renderer = renderer + .gpus + .single_renderer(&renderer.render_node) + .map_err(|error| format!("could not activate display renderer: {error:?}"))?; + let render_elements = + self.render_elements_for_output(&mut gpu_renderer, state, Some((crtc, &output)))?; + self.outputs[index] + .drm_output + .use_mode(mode, &mut gpu_renderer, &render_elements) + .map_err(|error| format!("could not commit connector reassignment: {error:?}")) + })(); + + if let Err(error) = result { + let connectors = [old_connector]; + self.outputs[index] + .drm_output + .with_compositor(|compositor| { + compositor.set_output_mode_source((&old_output).into()); + compositor.set_connectors(&connectors) + }) + .map_err(|rollback| { + state.space.unmap_output(&output); + format!("{error}; could not restore previous connector: {rollback:?}") + })?; + let mut gpu_renderer = renderer + .gpus + .single_renderer(&renderer.render_node) + .map_err(|rollback| { + state.space.unmap_output(&output); + format!("{error}; could not activate renderer for rollback: {rollback:?}") + })?; + let old_elements = self.render_elements_for_output( + &mut gpu_renderer, + state, + Some((crtc, &old_output)), + )?; + self.outputs[index] + .drm_output + .use_mode(old_drm_mode, &mut gpu_renderer, &old_elements) + .map_err(|rollback| { + state.space.unmap_output(&output); + format!("{error}; previous output rollback failed: {rollback:?}") + })?; + state.space.unmap_output(&output); + return Err(error); + } + + state + .display_handle + .disable_global::(self.outputs[index].global_id.clone()); + self.pending_global_removals + .push((self.outputs[index].global_id.clone(), Instant::now())); + state.space.unmap_output(&old_output); + state.forget_output_during_lock(old_name); + state.track_output_for_lock(&output); + let global_id = output.create_global::(&state.display_handle); + self.outputs[index].connector = connector_info.handle(); + self.outputs[index].modes = available_modes; + self.outputs[index].output = output; + self.outputs[index].global_id = global_id; + self.outputs[index].frame_pending = false; + self.disabled_connectors.insert(old_connector); + self.disabled_connectors.remove(&connector_info.handle()); + Ok(()) + } + pub fn set_enabled_outputs( &mut self, renderer: &mut NativeRenderer, state: &mut PantheonCompositor, enabled: &HashSet, + requested_mode_ids: &HashMap, ) -> Result<(), String> { if enabled.is_empty() { return Err("at least one output must remain enabled".into()); @@ -171,53 +372,15 @@ impl NativeKmsDevice { .iter() .map(|output| output.output.name()) .collect::>(); - let anchor = current - .iter() - .find(|name| enabled.contains(*name)) - .cloned() - .or_else(|| { - current - .iter() - .find(|name| name.starts_with("eDP-")) - .cloned() - }) - .or_else(|| current.iter().next().cloned()) - .ok_or_else(|| { - "native backend has no active output to anchor the transition".to_string() - })?; + if current.is_empty() { + return Err("native backend has no active output to anchor the transition".into()); + } for (name, connector) in &name_to_connector { - if !enabled.contains(name) && name != &anchor { + if !enabled.contains(name) && !current.contains(name) { self.disabled_connectors.insert(*connector); } } - let disabling = self - .outputs - .iter() - .filter(|output| { - output.output.name() != anchor && !enabled.contains(&output.output.name()) - }) - .map(|output| output.connector) - .collect::>(); - for connector in disabling { - if let Some(index) = self - .outputs - .iter() - .position(|output| output.connector == connector) - { - let output = self.outputs.remove(index); - state - .display_handle - .disable_global::(output.global_id.clone()); - self.pending_global_removals - .push((output.global_id.clone(), Instant::now())); - state.space.unmap_output(&output.output); - state.forget_output_during_lock(&output.output.name()); - self.disabled_connectors.insert(connector); - tracing::info!(output = %output.output.name(), "disabled DRM output by monitor configuration"); - drop(output); - } - } for name in enabled { let connector = name_to_connector @@ -227,6 +390,68 @@ impl NativeKmsDevice { } self.refresh_connectors(renderer, state) .map_err(|error| format!("could not enable requested monitor: {error}"))?; + let active_names = self + .outputs + .iter() + .map(|output| output.output.name()) + .collect::>(); + let mut missing = enabled + .difference(&active_names) + .cloned() + .collect::>(); + missing.sort(); + for target_name in missing { + let target_handle = name_to_connector[&target_name]; + let info = self + .manager + .device_mut() + .get_connector(target_handle, false) + .map_err(|error| error.to_string())?; + let encoder = info + .current_encoder() + .and_then(|handle| self.manager.device_mut().get_encoder(handle).ok()) + .or_else(|| { + info.encoders() + .iter() + .find_map(|handle| self.manager.device_mut().get_encoder(*handle).ok()) + }) + .ok_or_else(|| format!("monitor {target_name} has no usable encoder"))?; + let compatible_crtcs = resources.filter_crtcs(encoder.possible_crtcs()); + let old_name = self + .outputs + .iter() + .find(|output| { + !enabled.contains(&output.output.name()) + && compatible_crtcs.contains(&output.crtc) + }) + .map(|output| output.output.name()) + .ok_or_else(|| { + format!("no compatible active CRTC can be reassigned to {target_name}") + })?; + let mode = info + .modes() + .iter() + .find(|mode| { + requested_mode_ids.get(&target_name).is_some_and(|mode_id| { + let mode = Mode::from(**mode); + format!( + "{}x{}@{:.3}", + mode.size.w, + mode.size.h, + f64::from(mode.refresh) / 1000.0 + ) == *mode_id + }) + }) + .or_else(|| { + info.modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + }) + .copied() + .or_else(|| info.modes().first().copied()) + .ok_or_else(|| format!("monitor {target_name} has no modes"))?; + self.reassign_crtc_connector(renderer, state, &old_name, &info, mode)?; + } let mut active = self .outputs .iter() @@ -240,12 +465,20 @@ impl NativeKmsDevice { } return Err("native backend could not activate every requested output; the prior output remains active".into()); } - if !enabled.contains(&anchor) - && let Some(index) = self + let disabling = self + .outputs + .iter() + .filter(|output| !enabled.contains(&output.output.name())) + .map(|output| output.connector) + .collect::>(); + for connector in disabling { + let Some(index) = self .outputs .iter() - .position(|output| output.output.name() == anchor) - { + .position(|output| output.connector == connector) + else { + continue; + }; let output = self.outputs.remove(index); state .display_handle @@ -255,9 +488,14 @@ impl NativeKmsDevice { state.space.unmap_output(&output.output); state.forget_output_during_lock(&output.output.name()); self.disabled_connectors.insert(output.connector); + tracing::info!(output = %output.output.name(), "disabled DRM output by monitor configuration"); drop(output); - active.remove(&anchor); } + active = self + .outputs + .iter() + .map(|output| output.output.name()) + .collect(); if &active != enabled { return Err("native backend could not activate the requested output set".into()); } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index a2961141c..38e610b82 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -385,6 +385,21 @@ impl PantheonCompositor { .filter(|snapshot| snapshot.enabled) .map(|snapshot| snapshot.connector.clone()) .collect::>(); + let requested_mode_ids = snapshots + .iter() + .filter(|snapshot| snapshot.enabled) + .map(|snapshot| { + ( + snapshot.connector.clone(), + format!( + "{}x{}@{:.3}", + snapshot.width, + snapshot.height, + f64::from(snapshot.refresh_millihz) / 1000.0 + ), + ) + }) + .collect::>(); let Some(mut backend) = self.native_backend.take() else { return Err("native DRM backend is unavailable during monitor rollback".into()); }; @@ -392,10 +407,12 @@ impl PantheonCompositor { let mut restored = false; let mut last_error = String::new(); for _ in 0..=snapshots.len() { - match backend - .kms - .set_enabled_outputs(&mut backend.renderer, self, &enabled) - { + match backend.kms.set_enabled_outputs( + &mut backend.renderer, + self, + &enabled, + &requested_mode_ids, + ) { Ok(()) => { restored = true; break; @@ -690,14 +707,21 @@ impl PantheonCompositor { .cloned() .collect::>(); #[cfg(feature = "native-drm")] + let requested_mode_ids = configurations + .iter() + .map(|(name, configuration)| (name.clone(), configuration.5.clone())) + .collect::>(); + #[cfg(feature = "native-drm")] { let Some(mut backend) = self.native_backend.take() else { return Err("native DRM backend is unavailable".into()); }; - let result = - backend - .kms - .set_enabled_outputs(&mut backend.renderer, self, &enabled_connectors); + let result = backend.kms.set_enabled_outputs( + &mut backend.renderer, + self, + &enabled_connectors, + &requested_mode_ids, + ); self.native_backend = Some(backend); if let Err(error) = result { if let Err(rollback_error) = From 95ccc822575921617eb72fc8e145f003cc34820a Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:17:44 -0400 Subject: [PATCH 137/798] Record single-CRTC ARM64 candidate --- mseries/OS9-HANDOFF.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 1ae786a95..4f6604036 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5833,4 +5833,7 @@ confirms the connector set is double-buffered and `use_mode` submits the pending mode with the composited frame; this is source/API evidence, not runtime proof. No monitor switch, page-flip check or failure injection has been run on the M-series device. The ARM64 package at `6ec3113` predates this change and must -not be used to qualify it. +not be used to qualify it. The ARM64 release candidate built from `c91ec2d` is +at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-crtc-reassign-release/pantheon-rust-compositor_0.1.0+gitc91ec2d_arm64.deb` +(SHA-256 `46baf4fc264cb23b8e784bc57a2b23d7f34b5a8f0930391a34f9d341f327303c`). +Its package metadata and contents were inspected; it was not installed. From 3c087ab6fd04c211f89ab4bd879cb551a3fdb376 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:17:52 -0400 Subject: [PATCH 138/798] mseries-vm: EXTRA_BUS=usb so Windows PE can read virtio-win media; handoff: Windows installer reaches license terms Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 05a2928b5..8cd56de49 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6235,3 +6235,15 @@ Downloaded Fedora's stable `virtio-win-0.1.302` ISO to `~/vms/downloads/virtio-w Install remains blocked before Windows Setup. Test4's virtual disk is blank (196 KiB allocated). Its ARM64 Windows ISO contains `EFI/BOOT/BOOTAA64.EFI`, `sources/boot.wim` and `sources/install.wim`, but AAVMF times out starting the installer from both SCSI CD-ROM and USB storage. Repeated with Secure Boot firmware and without it, with 4 GiB and 8 GiB RAM, and with a VGA device alongside ramfb; the same timeout recurred. No Windows files were installed. The test VM was stopped cleanly and retained for further diagnosis. PR #260 is merged to `dev` at `493fa2ba88b3f4ce651e29cfa13d7f4f7410f62f`. The companion `mseries-vm` change adds `EXTRA_BUS=usb` for presenting the extra driver ISO through USB storage. The USB path was exercised in test4, but the installer boot timeout means driver loading and installation remain unverified. Next: diagnose why this AAVMF build cannot complete the ARM64 Windows boot path, then continue Setup and load the storage driver if prompted. + +### Windows installer: storage fixed (2026-10-19, later) + +`virtio-win.iso` 0.1.302 (stable, from fedorapeople.org, 877 MB, SHA-256 `303f7ae4…867949d`, kept in +`~/vms/downloads/virtio-win/`) attached as extra media on USB (`mseries-vm set NAME EXTRA_BUS usb`; new key) is readable by +Windows PE. In Setup's "Install driver" page, browse to `D:\vioscsi\w11\ARM64`, pick "Red Hat VirtIO SCSI pass-through +controller" and Install: Setup then reads the installer ISO, and reaches language, setup option, product key +("I don't have a product key") and the license terms page. Tested in a separate VM `windows-lab` (other agents were +using `windows` and `windows-install-test*`). Stopped at "Applicable notices and license terms": accepting Microsoft's +license is the owner's call. The Setup UI is driven with absolute-pointer clicks over QMP; typing into WinPE text +fields drops characters, so use the Browse tree. Not verified: the install finishing, first boot, or the virtio-win +guest drivers after install. From 1be0f2762399ecba99fc0bdce66ab24cee8b13f7 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:19:44 -0400 Subject: [PATCH 139/798] Clean up compositor session environment --- mseries/pantheon-rust-compositor/session/README.md | 9 +++++---- .../lib/systemd/user/pantheon-rust-compositor@.service | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 0dd9d2031..fcf799e84 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -13,10 +13,11 @@ The session is named **Pantheon (Rust compositor, experimental)** and uses the separate `pantheon-rust-wayland` session ID. The existing `pantheon-wayland` entry and Gala units are not modified. The service binds `wayland-0`, publishes that value to the systemd user manager after readiness, -and clears the manager value when it stops. `PANTHEON_WAYLAND_DISPLAY` selects -the explicit socket; without that variable the nested development backend -continues to allocate an automatic socket, and the native backend can also use -automatic allocation for manual development launches. +and clears the display variables and fixed socket when it stops. +`PANTHEON_WAYLAND_DISPLAY` selects the explicit socket; without that variable +the nested development backend continues to allocate an automatic socket, and +the native backend can also use automatic allocation for manual development +launches. The experimental service retries an unexpected compositor failure, with a two-second delay and a limit of three starts within one minute. Once the limit diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service index 5e66bc081..f4865e9ba 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service @@ -22,7 +22,9 @@ Environment=WAYLAND_DISPLAY=wayland-0 Environment=PANTHEON_WAYLAND_DISPLAY=wayland-0 ExecStart=/usr/bin/pantheon-compositor --backend native --notify-systemd ExecStartPost=/usr/bin/systemctl --user import-environment WAYLAND_DISPLAY -ExecStopPost=-/usr/bin/systemctl --user unset-environment WAYLAND_DISPLAY +# Clear compositor-owned display variables and remove the fixed socket after +# the session ends, including after an unexpected compositor failure. +ExecStopPost=-/bin/sh -c 'test "$SERVICE_RESULT" != "exec-condition" && systemctl --user unset-environment GNOME_SETUP_DISPLAY WAYLAND_DISPLAY DISPLAY XAUTHORITY && rm -f "$XDG_RUNTIME_DIR/wayland-0" "$XDG_RUNTIME_DIR/wayland-0.lock"' Restart=on-failure RestartSec=2s TimeoutStopSec=5 From 86e2520b9fb4b5cfc9770271155bec867ee16715 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:22:16 -0400 Subject: [PATCH 140/798] local-ai: record isolated omarchy-mlx Python candidate --- mseries/OS9-HANDOFF.md | 23 ++++++++++++------- .../OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md | 7 ++++-- ...19-omarchy-mlx-candidate-python-runtime.md | 22 ++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-python-runtime.md diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 879ed51cc..f6fa8dfa0 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -26,14 +26,21 @@ build (cause suspected, unconfirmed). Receipt: Model A/B has not run. Readiness check at 08:00 EDT found `mseries-ai-everyday.service` failed because its configured 4B target was incomplete and offline mode refused a download; no MLX worker was running. -The current candidate is C++-only, with no Python bindings/wheel. A detailed -plan now records candidate preparation, 4B row-reduce and optional 9B native -GQA gates, required service handling, and a proposed after-10 PM Eastern quiet -window: `mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md`. -Next: build a Python-capable candidate in a separate venv/prefix and confirm -the same pinned model is complete and the installed baseline is healthy before -any model launch. Owner's desktop is not to be interrupted outside an agreed -quiet window. +Since then, a Python wheel for `61495cb` was built and installed in an isolated +cache-local venv. It imports mlx-lm 0.31.3 and completes a tiny GPU sum on +Honeykrisp. Build used CPU=OFF because LAPACKE development files are absent; +Python 3.14 headers were unpacked to the candidate cache without system package +installation. The wheel is `0.32.4.dev202610191211+61495cb`, while omarchy-mlx's +lock labels its pinned MLX archive 0.32.3; the hash-verified archive's +`mlx/version.h` says 0.32.4. Resolve/document this mismatch before any bump. + +The pinned 4B model download is now underway in the normal HF cache, still +incomplete at the last check. No model generation has run and no service was +started or stopped. The detailed plan records the A/B gates and quiet window: +`mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md`. Next: finish the +download and verify pinned files, then check baseline service readiness and +wait for the agreed quiet window before generation. Do not interrupt the +owner's desktop outside that window. ## apt upgrade of 25 elementary daily packages — 2026-10-19 (Claude Code) diff --git a/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md b/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md index 089721e00..d57c6787a 100644 --- a/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md +++ b/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md @@ -4,14 +4,17 @@ Planning only. No models were launched or stopped for this plan. ## Current readiness -- Candidate source `61495cb` builds at `~/.cache/omarchy-mlx-candidate-61495cb`; selected C++ results are recorded in [the candidate build receipt](receipts/2026-10-19-omarchy-mlx-candidate-build.md). It has no Python bindings or wheel yet, so it cannot currently run the model A/B. +- Candidate source `61495cb` builds at `~/.cache/omarchy-mlx-candidate-61495cb`; selected C++ results are recorded in [the candidate build receipt](receipts/2026-10-19-omarchy-mlx-candidate-build.md). A Python-enabled candidate wheel has since been built and installed in that cache's `runtime-venv`; details are in [the Python runtime receipt](receipts/2026-10-19-omarchy-mlx-candidate-python-runtime.md). +- The host lacks LAPACKE development files and Python 3.14 C headers. To keep the build isolated, I unpacked the matching Ubuntu Python development packages into the cache (no system packages installed) and built with `MLX_BUILD_CPU=OFF`. The candidate imports alongside mlx-lm 0.31.3 and completed a small GPU sum on Honeykrisp. It has no CPU backend, so do not use it for tests or code paths that require CPU execution. +- Source version discrepancy to resolve before considering a bump: omarchy-mlx `mlx.lock` labels pinned MLX commit `9c3d355` as 0.32.3, while the downloaded, checksum-verified archive's `mlx/version.h` and the built wheel both identify 0.32.4. Keep the immutable commit as provenance; do not describe this build simply as MLX 0.32.3. - The installed runtime remains v0.7.33. Keep it intact as the baseline. - At 08:00 EDT on October 19, `mseries-ai-everyday.service` was failed, not active. Its log says the explicitly named `qwen3.5-4b-mseries-4bit` target is incomplete locally and offline mode refused to download it. No MLX model worker process was present. `mseries-ai-quality.service` was inactive. Confirm model availability and restore a healthy baseline before any comparison; do not count this failure as an A/B result. +- The pinned 4B model download (`mlx-community/Qwen3.5-4B-MLX-4bit`, revision `32f3e8ecf65426fc3306969496342d504bfa13f3`) is now underway in the standard Hugging Face cache so both runtimes can use identical weights. It was still in progress at the last check. Do not start generation until it completes and its catalog file hashes are verified. - The 9B profile is configured as `qwen3.5-9b-mlx-4bit`; model files were not confirmed in this readiness check. ## Candidate preparation gate -1. Build the Python-enabled candidate runtime from the frozen `61495cb` source in a separate prefix/venv. Do not run the repository installer or write into `/usr/lib/omarchy-mlx` or `~/.local/share/mlx-omarchy`. +1. ~~Build Python-enabled candidate from frozen `61495cb` in a separate prefix/venv~~ Done in `~/.cache/omarchy-mlx-candidate-61495cb/runtime-venv`. Do not run the repository installer or write into `/usr/lib/omarchy-mlx` or `~/.local/share/mlx-omarchy`. 2. Keep mlx-lm pinned to the version/API the candidate supports. Do not apply the upstream 0.32-only server patches to our installed 0.31.3 environment. Use an isolated direct generation harness unless the private server patches are reviewed and applied to the candidate venv. 3. Verify candidate and baseline library provenance and hashes, model revision and file completeness, Vulkan device/ICD, and effective environment flags before the first model load. Verify that logs/counters show the selected kernel route is actually reached. 4. Use fixed local prompts and greedy decoding. Save token IDs, timing samples and telemetry to a new receipt; omit personal prompts or model output containing private data. diff --git a/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-python-runtime.md b/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-python-runtime.md new file mode 100644 index 000000000..ef085243a --- /dev/null +++ b/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-candidate-python-runtime.md @@ -0,0 +1,22 @@ +# omarchy-mlx candidate Python runtime — 2026-10-19 (M1 Pro, G13S C0) + +Candidate source: `61495cb66cb9a5fb8e127001c7c1eb959a9d6c22` (public main). Installed v0.7.33 was untouched. Build and venv are under `~/.cache/omarchy-mlx-candidate-61495cb` and `~/.cache/omarchy-mlx-wheel-candidate-61495cb`; no system runtime or package was installed. + +## Build + +- Build source archive: MLX commit `9c3d35571ac450a8ecf5c17b4d0e3fac52c08bc8`, archive SHA-256 `3e564bf7a0d5e5bac3a56fc26668cdcedd4a9db9fbfbf2bb6d762632ceb6f65a`. +- `omarchy-mlx/mlx.lock` says version 0.32.3, but this checksum-verified archive has `mlx/version.h` values 0.32.4. The produced wheel metadata is `0.32.4.dev202610191211+61495cb`; investigate this upstream lock/version mismatch before a runtime bump. +- Host did not have `lapacke.h` or Python 3.14 C headers. Python development packages `python3.14-dev` and `libpython3.14-dev` version `3.14.4-1ubuntu0.2` were downloaded and unpacked under the candidate cache only. No apt install or system file change. +- Built with `MLX_BUILD_CPU=OFF`, `MLX_BUILD_OMARCHY=ON`, `MLX_BUILD_PYTHON_BINDINGS=ON`, `MLX_OMARCHY_ANE_DEVICE=ON`, two build jobs, low priority. The build helper in the cached source clone was locally adjusted to skip its CPU/LAPACKE guard and point CMake at the cached Python headers. Do not treat this as the upstream default release build. +- The 458 MB pinned encoder bundle staged from the installed private runtime and passed `verify_runtime_assets.py`. `stamp_worker_pin.py` stamped the wheel; `openblas_isolation.py check` passed (the CPU-off library has no bare OpenBLAS dependency). + +## Artifact and smoke + +- Wheel: `mlx_omarchy-0.32.4.dev202610191211+61495cb-cp314-cp314-linux_aarch64.whl` +- SHA-256 after worker-pin stamping: `cf7cfab014b067dd0d43328f0630294e176d32ac70cf99233f580a1905f528d5`. +- Installed with `pip --no-deps` into the separate candidate venv. The candidate `mlx.core` resolves from that venv; mlx-lm 0.31.3 and its Python dependencies resolve through a read-only path entry to the existing runtime's site-packages. +- With `OMARCHY_MLX_SYSTEM_PREFIX` and `VK_DRIVER_FILES` pointed at the existing private runtime, `mx.set_default_device(mx.gpu); mx.array([1,2,3]).sum().item()` returned 6. This is a tiny GPU smoke, not a model result or model qualification. Mesa emitted its existing 26.0.8 warning; no Mesa was changed. + +## Model readiness + +The everyday service was failed because the pinned 4B weights were missing. A direct `snapshot_download` of `mlx-community/Qwen3.5-4B-MLX-4bit` revision `32f3e8ecf65426fc3306969496342d504bfa13f3` is in progress in `~/.cache/huggingface/hub`, unauthenticated and low priority. At the last observation it was still downloading; verify `config.json` and `model.safetensors` against `qwen35-4b-catalog-entry.json` when complete. No model generation ran and no services were started or stopped. From 25a3aba6f5e0b833f3ba44f4d563b9d26fafd8ae Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:23:10 -0400 Subject: [PATCH 141/798] Fail native session on persistent display errors --- .../EXPERIENCE-REQUIREMENTS.md | 2 +- mseries/pantheon-rust-compositor/README.md | 8 ++++-- rust/pantheon-compositor/README.md | 27 +++++++++++-------- rust/pantheon-compositor/src/main.rs | 3 +++ rust/pantheon-compositor/src/native.rs | 26 +++++++++++++++--- rust/pantheon-compositor/src/state.rs | 8 ++++++ 6 files changed, 57 insertions(+), 17 deletions(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 2ba9d50f1..ee1c933aa 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -18,7 +18,7 @@ it does not imply that the current prototype supports it. | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | -| M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns connected `eDP-1` and `DP-2` and has no EGL device | For this measured topology, import/share rendered buffers across the two DRM devices, allocate scanout buffers on the display controller, and handle import/presentation failure without losing the session | +| M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index cbf341994..957786b63 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -103,8 +103,12 @@ gates pass; a successful compile or package build is not that evidence. The staged systemd service now retries an unexpected compositor failure with a two-second delay, capped at three starts per minute. After that limit, its `OnFailure` action requests the GNOME session shutdown target rather than -looping indefinitely. This is configuration only; a crash/restart has not been -exercised in an isolated login. +looping indefinitely. Native DRM event-source and frame-completion errors now +stop the compositor with a failure status; three consecutive render failures +do the same. This connects unrecoverable display failures to the service's +restart path instead of leaving a running but unusable session. The restart, +shutdown and hardware-failure paths have not been exercised in an isolated +login. `build-deb.sh` builds an arm64 release binary with `native-drm` and packages it with the separate experimental session files. It derives shared-library diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 0bb503553..fe78c43f5 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -30,6 +30,11 @@ cargo run -p pantheon-compositor --features native-drm -- --backend native Native startup acquires the active libseat session and initializes a DRM/KMS output. It takes over the display device, so do not launch it inside the currently active desktop. It is not yet runtime-qualified on M-series hardware. +Fatal DRM event/frame-completion errors end the process with a failure status; +three consecutive native render failures do likewise. In the experimental +session service this allows systemd's bounded restart and shutdown policy to +run. These failure paths have only been compile-checked, not induced on the +active display. The experimental systemd user service uses `--notify-systemd` to send `READY=1` to `NOTIFY_SOCKET` after the Wayland listener and selected backend initialize. On @@ -126,17 +131,17 @@ feature-enabled `cargo check --bins` and `cargo build --bins` pass locally; builds do not initialize or take over a native session. With `native-drm` enabled, `pantheon-drm-probe` safely enumerates EGL devices -without acquiring DRM master or changing display modes. On the current M-series -host it finds EGL rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, Asahi) -but KMS resource queries there return `Operation not supported`. The Apple -display-controller `/dev/dri/card2` owns KMS: `eDP-1` is connected at -3024×1964@120 Hz and `DP-2` is connected with 3840×2160@30 Hz and 1920×1080@60 -Hz modes; `HDMI-A-1` is disconnected. Card2 has no EGL device. The probe now -allocates 64×64 render/scanout GBM buffers on Asahi and attempts GBM DMA-BUF -imports on Apple KMS. On this host, implicit-modifier imports succeeded for -AB30, AR30, AB24 and AR24. This supports the chosen cross-device path, but does -not prove DRM framebuffer creation, atomic modesetting, or display scanout. The -probe does not acquire DRM master, force a connector reprobe, or change modes. +without acquiring DRM master or changing display modes. In the 2026-10-19 +snapshot it finds EGL rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, +Asahi) but KMS resource queries there return `Operation not supported`. The +Apple display-controller `/dev/dri/card2` owns KMS and the connected internal +`eDP-1` at 3024×1964@120 Hz; external connectors are currently disconnected. +Card2 has no EGL device. The probe allocates 64×64 render/scanout GBM buffers on +Asahi and attempts GBM DMA-BUF imports on Apple KMS. Implicit-modifier imports +succeeded for AB30, AR30, AB24 and AR24. This supports the chosen cross-device +path, but does not prove DRM framebuffer creation, atomic modesetting, or +display scanout. The probe does not acquire DRM master, force a connector +reprobe, or change modes. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index fe6e6b612..0ba7a34f6 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -74,6 +74,9 @@ fn main() -> Result<(), Box> { unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) }; event_loop.run(None, &mut state, move |_| {})?; + if let Some(error) = state.fatal_error.take() { + return Err(io::Error::other(error).into()); + } Ok(()) } diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index e3dd80582..5e50cfcb4 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -1434,6 +1434,9 @@ impl NativeBackend { Ok(None) => return, Err(error) => { tracing::error!(?error, "failed to complete native DRM frame"); + state.stop_with_error(format!( + "native DRM frame completion failed: {error:?}" + )); return; } }; @@ -1453,6 +1456,7 @@ impl NativeBackend { } smithay::backend::drm::DrmEvent::Error(error) => { tracing::error!(?error, "native DRM event source failed"); + state.stop_with_error(format!("native DRM event source failed: {error:?}")); } })?; @@ -1464,17 +1468,33 @@ impl NativeBackend { kms, session_paused: false, }); + let mut consecutive_render_failures = 0; event_loop .handle() - .insert_source(redraw_receiver, |event, _, state| { + .insert_source(redraw_receiver, move |event, _, state| { if !matches!(event, ChannelEvent::Msg(())) { return; } let Some(mut backend) = state.native_backend.take() else { return; }; - if let Err(error) = backend.kms.render(&mut backend.renderer, state) { - tracing::error!(?error, "native output render failed"); + match backend.kms.render(&mut backend.renderer, state) { + Ok(_) => consecutive_render_failures = 0, + Err(error) => { + consecutive_render_failures += 1; + tracing::error!( + ?error, + consecutive_render_failures, + "native output render failed" + ); + if consecutive_render_failures >= 3 { + state.stop_with_error(format!( + "native output rendering failed {consecutive_render_failures} consecutive times: {error}" + )); + } else { + state.request_native_redraw(); + } + } } state.native_backend = Some(backend); })?; diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 38e610b82..d60ae0736 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -101,6 +101,7 @@ pub struct PantheonCompositor { pub xdisplay: Option, pub pending_command: Option<(String, Vec)>, pub notify_systemd_when_ready: bool, + pub fatal_error: Option, pub seat_state: SeatState, pub(crate) keyboard_config: crate::input::StartupKeyboardConfig, pub data_device_state: DataDeviceState, @@ -327,6 +328,7 @@ impl PantheonCompositor { xdisplay: None, pending_command: None, notify_systemd_when_ready: false, + fatal_error: None, seat_state, keyboard_config, data_device_state, @@ -367,6 +369,12 @@ impl PantheonCompositor { } } + #[cfg(feature = "native-drm")] + pub fn stop_with_error(&mut self, error: impl Into) { + self.fatal_error = Some(error.into()); + self.loop_signal.stop(); + } + pub fn primary_output(&self) -> Option { self.primary_output_name .as_deref() From 67a248b626db65f3b5ee6a340d55afbe2793bf89 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:30:50 -0400 Subject: [PATCH 142/798] Set compositor as the default Cargo binary --- rust/pantheon-compositor/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index 2f240b928..e1bf56f31 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "pantheon-compositor" +default-run = "pantheon-compositor" version = "0.1.0" edition = "2024" license = "GPL-3.0-only" From d3c12d184a888a563e8ea971ab8dbe19a8178574 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:33:03 -0400 Subject: [PATCH 143/798] local-ai: record 4B model readiness and admission gate --- mseries/OS9-HANDOFF.md | 19 ++++++++------ .../OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md | 5 ++-- .../2026-10-19-omarchy-mlx-4b-readiness.md | 25 +++++++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 mseries/local-ai/receipts/2026-10-19-omarchy-mlx-4b-readiness.md diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index f6fa8dfa0..b7602b6d1 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -34,13 +34,18 @@ installation. The wheel is `0.32.4.dev202610191211+61495cb`, while omarchy-mlx's lock labels its pinned MLX archive 0.32.3; the hash-verified archive's `mlx/version.h` says 0.32.4. Resolve/document this mismatch before any bump. -The pinned 4B model download is now underway in the normal HF cache, still -incomplete at the last check. No model generation has run and no service was -started or stopped. The detailed plan records the A/B gates and quiet window: -`mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md`. Next: finish the -download and verify pinned files, then check baseline service readiness and -wait for the agreed quiet window before generation. Do not interrupt the -owner's desktop outside that window. +The pinned 4B model download is complete in the normal HF cache and verified +against the catalog hashes. A separate diagnostics +wheel proves a synthetic 8192-element float32 Sum dispatches `ReduceRowF32` +on G13S; this does not prove the 4B model reaches it. At the latest machine +sample, 12.26 GiB was available, below the 12.5 GiB admission needed for the +6.5 GiB model plus 6 GiB desktop reserve. No model generation has run and no +service was started or stopped; everyday remains failed from its earlier +missing-weights launch. Detailed A/B gates are in +`mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md`. Next: wait until +memory admission passes, verify baseline service readiness, and run the model +comparison only in the planned quiet window. Do not force admission or +interrupt the owner's desktop. ## apt upgrade of 25 elementary daily packages — 2026-10-19 (Claude Code) diff --git a/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md b/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md index d57c6787a..ecdb1af23 100644 --- a/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md +++ b/mseries/local-ai/OMARCHY-MLX-MODEL-AB-PLAN-2026-10-19.md @@ -6,15 +6,16 @@ Planning only. No models were launched or stopped for this plan. - Candidate source `61495cb` builds at `~/.cache/omarchy-mlx-candidate-61495cb`; selected C++ results are recorded in [the candidate build receipt](receipts/2026-10-19-omarchy-mlx-candidate-build.md). A Python-enabled candidate wheel has since been built and installed in that cache's `runtime-venv`; details are in [the Python runtime receipt](receipts/2026-10-19-omarchy-mlx-candidate-python-runtime.md). - The host lacks LAPACKE development files and Python 3.14 C headers. To keep the build isolated, I unpacked the matching Ubuntu Python development packages into the cache (no system packages installed) and built with `MLX_BUILD_CPU=OFF`. The candidate imports alongside mlx-lm 0.31.3 and completed a small GPU sum on Honeykrisp. It has no CPU backend, so do not use it for tests or code paths that require CPU execution. +- A separate diagnostics wheel was built with the GPU profiler compiled in. A synthetic float32 sum of shape `[1,8192]` on `Apple M1 Pro (G13S C0)` profiled as `ReduceRowF32` (one dispatch). This confirms the profiler and route switch work on this device; it does not show that Qwen3.5-4B reaches that route. - Source version discrepancy to resolve before considering a bump: omarchy-mlx `mlx.lock` labels pinned MLX commit `9c3d355` as 0.32.3, while the downloaded, checksum-verified archive's `mlx/version.h` and the built wheel both identify 0.32.4. Keep the immutable commit as provenance; do not describe this build simply as MLX 0.32.3. - The installed runtime remains v0.7.33. Keep it intact as the baseline. - At 08:00 EDT on October 19, `mseries-ai-everyday.service` was failed, not active. Its log says the explicitly named `qwen3.5-4b-mseries-4bit` target is incomplete locally and offline mode refused to download it. No MLX model worker process was present. `mseries-ai-quality.service` was inactive. Confirm model availability and restore a healthy baseline before any comparison; do not count this failure as an A/B result. -- The pinned 4B model download (`mlx-community/Qwen3.5-4B-MLX-4bit`, revision `32f3e8ecf65426fc3306969496342d504bfa13f3`) is now underway in the standard Hugging Face cache so both runtimes can use identical weights. It was still in progress at the last check. Do not start generation until it completes and its catalog file hashes are verified. +- The pinned 4B model (`mlx-community/Qwen3.5-4B-MLX-4bit`, revision `32f3e8ecf65426fc3306969496342d504bfa13f3`) is downloaded in the standard Hugging Face cache. `config.json` and the 3,034,300,695-byte `model.safetensors` both match the catalog SHA-256 hashes. At the latest admission sample, 12.26 GiB was available; 6.5 GiB model admission plus the 6 GiB desktop reserve requires 12.5 GiB, so policy correctly refuses a model load. The everyday unit remains failed and was not restarted. - The 9B profile is configured as `qwen3.5-9b-mlx-4bit`; model files were not confirmed in this readiness check. ## Candidate preparation gate -1. ~~Build Python-enabled candidate from frozen `61495cb` in a separate prefix/venv~~ Done in `~/.cache/omarchy-mlx-candidate-61495cb/runtime-venv`. Do not run the repository installer or write into `/usr/lib/omarchy-mlx` or `~/.local/share/mlx-omarchy`. +1. ~~Build Python-enabled candidate from frozen `61495cb` in a separate prefix/venv~~ Done in `~/.cache/omarchy-mlx-candidate-61495cb/runtime-venv`; separate diagnostics venv also exists for route proof. Do not run the repository installer or write into `/usr/lib/omarchy-mlx` or `~/.local/share/mlx-omarchy`. 2. Keep mlx-lm pinned to the version/API the candidate supports. Do not apply the upstream 0.32-only server patches to our installed 0.31.3 environment. Use an isolated direct generation harness unless the private server patches are reviewed and applied to the candidate venv. 3. Verify candidate and baseline library provenance and hashes, model revision and file completeness, Vulkan device/ICD, and effective environment flags before the first model load. Verify that logs/counters show the selected kernel route is actually reached. 4. Use fixed local prompts and greedy decoding. Save token IDs, timing samples and telemetry to a new receipt; omit personal prompts or model output containing private data. diff --git a/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-4b-readiness.md b/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-4b-readiness.md new file mode 100644 index 000000000..bf1c25df7 --- /dev/null +++ b/mseries/local-ai/receipts/2026-10-19-omarchy-mlx-4b-readiness.md @@ -0,0 +1,25 @@ +# omarchy-mlx Qwen3.5-4B A/B readiness — 2026-10-19 (M1 Pro, G13S C0) + +No model inference ran. The candidate diagnostics wheel and exact model files are prepared; admission currently refuses the model load. + +## Diagnostics route proof + +- Candidate: omarchy-mlx `61495cb`, diagnostics wheel `mlx_omarchy-0.32.4.dev202610191224+diag.61495cb-cp314-cp314-linux_aarch64.whl`. +- Final wheel SHA-256 after worker-pin stamping: `f9e469265b770acfaba8ab62d2e1785c886c497fc7f5c6930b2aebf9c92c8047`. +- GPU profiler literal and `mlx/bin/mlx-omarchy-info` were present; runtime assets and wheel worker pin were verified. The diagnostics wheel is in a separate cache-local venv from the non-diagnostics candidate. +- With the private Honeykrisp ICD, `mx.sum(mx.ones((1,8192), dtype=mx.float32))` returned `8192.0`. The GPU profile identified `Apple M1 Pro (G13S C0)` and one `ReduceRowF32` dispatch (79.8 μs). This verifies a synthetic route only; Qwen3.5-4B route reachability is still unknown. + +## Pinned model files + +Downloaded `mlx-community/Qwen3.5-4B-MLX-4bit`, revision `32f3e8ecf65426fc3306969496342d504bfa13f3`, to the normal Hugging Face cache (unauthenticated download). Catalog verification passed: + +| File | Bytes | SHA-256 | +|---|---:|---| +| `config.json` | 3,366 | `f3efc81b2ea8d96a45301037d3ccccbcccdef44a961845c87f286aaddbc6eaaa` | +| `model.safetensors` | 3,034,300,695 | `5fb9acd0246866381cf8c5c354c6db1019f6498eec4ccb4f5edcc71ffeacb2db` | + +## Admission and service state + +Latest read-only sample: 12.26 GiB available, fan telemetry present (0 RPM reported), max temperature 51.2 C. The 4B profile's 6.5 GiB admission plus 6 GiB desktop reserve requires at least 12.5 GiB available, so `machine.violations` correctly refused admission. The everyday service remains failed from its earlier offline/missing-weights attempt; it has not been restarted. No user services were stopped or changed, and no LLM model was loaded. + +Next gate: wait until the normal reserve policy allows admission; then confirm the installed baseline service can load these exact files before running the 4B A/B in the quiet-window plan. From 78c5d0f3d88880984c9b12e41cdb9998f2ed589c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:37:40 -0400 Subject: [PATCH 144/798] Flush Wayland replies after client dispatch --- rust/pantheon-compositor/src/state.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index d60ae0736..8d7134d47 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -943,6 +943,7 @@ impl PantheonCompositor { let loop_handle = event_loop.handle(); loop_handle.insert_source(listening_socket, move |client_stream, _, state| { + tracing::debug!("accepted Wayland client connection"); // Inside the callback, you should insert the client into the display. // // You may also associate some data with the client when inserting the client. @@ -959,7 +960,9 @@ impl PantheonCompositor { |_, display, state| { // Safety: we don't drop the display unsafe { - display.get_mut().dispatch_clients(state).unwrap(); + let display = display.get_mut(); + display.dispatch_clients(state).unwrap(); + display.flush_clients().unwrap(); } Ok(PostAction::Continue) }, From e1eb710888db88bea379f836f468508737f30cf1 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:42:24 -0400 Subject: [PATCH 145/798] Handoff: Windows 11 ARM64 installs and first-boots to OOBE in QEMU Co-Authored-By: Claude Sonnet 5.5 --- .../windows11-arm64-qemu-first-boot-oobe.png | Bin 0 -> 87507 bytes mseries/OS9-HANDOFF.md | 11 +++++++++++ 2 files changed, 11 insertions(+) create mode 100644 docs/images/windows11-arm64-qemu-first-boot-oobe.png diff --git a/docs/images/windows11-arm64-qemu-first-boot-oobe.png b/docs/images/windows11-arm64-qemu-first-boot-oobe.png new file mode 100644 index 0000000000000000000000000000000000000000..4f7da1e1eaa5fb5515bcf2aeb7c3c9876fa987ad GIT binary patch literal 87507 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 e*+(xnOaBKu@4e0z+k-~{0000 Date: Mon, 19 Oct 2026 08:46:29 -0400 Subject: [PATCH 146/798] Schedule nested redraws on demand --- rust/pantheon-compositor/README.md | 11 ++++--- .../src/handlers/compositor.rs | 1 + rust/pantheon-compositor/src/state.rs | 9 ++++++ rust/pantheon-compositor/src/winit.rs | 32 ++++++++++++++----- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index fe78c43f5..044354d94 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -56,10 +56,13 @@ cargo run -p pantheon-compositor -- --run target/debug/pantheon-shell-probe ``` The probe binds the shell and panel v2 interfaces, creates a Wayland surface, -and sends `get_panel`, anchor, size, hide-mode, and offset requests. It has -passed under Xvfb with Mesa llvmpipe. It does not attach a buffer or xdg role, so -it validates protocol dispatch rather than panel rendering or Wingpanel/Dock -integration. +and sends `get_panel`, anchor, size, hide-mode, and offset requests. An earlier +run passed under Xvfb with Mesa llvmpipe, but the latest isolated rerun hangs +waiting for the initial registry roundtrip: the client connects, while the +compositor does not accept the queued socket connection. Treat nested protocol +dispatch as unqualified until this is resolved. Even a passing probe does not +attach a buffer or xdg role, so it validates protocol dispatch rather than +panel rendering or Wingpanel/Dock integration. ## Current limits diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index 807437c5d..b91e4c495 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -45,6 +45,7 @@ impl CompositorHandler for PantheonCompositor { xdg_shell::handle_commit(self, surface); pantheon_shell::handle_surface_commit(self, surface); resize_grab::handle_commit(&mut self.space, surface); + self.request_winit_redraw(); #[cfg(feature = "native-drm")] self.request_native_redraw(); } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 8d7134d47..c58cb5dbc 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -133,6 +133,7 @@ pub struct PantheonCompositor { pub default_cursor: MemoryRenderBuffer, pub seat: Seat, + pub winit_redraw_sender: Option>, #[cfg(feature = "native-drm")] pub native_backend: Option, #[cfg(feature = "native-drm")] @@ -290,6 +291,7 @@ impl PantheonCompositor { .handle() .insert_source(osd_redraw_receiver, |event, _, _state| { if matches!(event, channel::Event::Msg(())) { + _state.request_winit_redraw(); #[cfg(feature = "native-drm")] _state.request_native_redraw(); } @@ -355,6 +357,7 @@ impl PantheonCompositor { cursor_status: CursorImageStatus::default_named(), default_cursor: default_cursor_buffer(), seat, + winit_redraw_sender: None, #[cfg(feature = "native-drm")] native_backend: None, #[cfg(feature = "native-drm")] @@ -369,6 +372,12 @@ impl PantheonCompositor { } } + pub fn request_winit_redraw(&self) { + if let Some(sender) = &self.winit_redraw_sender { + let _ = sender.send(()); + } + } + #[cfg(feature = "native-drm")] pub fn stop_with_error(&mut self, error: impl Into) { self.fatal_error = Some(error.into()); diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 8c11b9628..12e79b76d 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{cell::RefCell, rc::Rc, time::Duration}; use smithay::{ backend::{ @@ -6,7 +6,7 @@ use smithay::{ winit::{self, WinitEvent}, }, output::{Mode, Output, PhysicalProperties, Subpixel}, - reexports::calloop::EventLoop, + reexports::calloop::{EventLoop, channel}, utils::{Rectangle, Transform}, }; @@ -20,10 +20,11 @@ pub fn init_winit( event_loop: &mut EventLoop, state: &mut PantheonCompositor, ) -> Result<(), Box> { - let (mut backend, winit) = winit::init()?; + let (backend, winit) = winit::init()?; + let backend = Rc::new(RefCell::new(backend)); let mode = Mode { - size: backend.window_size(), + size: backend.borrow().window_size(), refresh: 60_000, }; @@ -86,9 +87,11 @@ pub fn init_winit( let mut damage_tracker = OutputDamageTracker::from_output(&output); + let event_backend = backend.clone(); event_loop .handle() .insert_source(winit, move |event, _, state| { + let mut backend = event_backend.borrow_mut(); match event { WinitEvent::Resized { size, .. } => { let mode = Mode { @@ -125,8 +128,12 @@ pub fn init_winit( enabled: true, }); } - WinitEvent::Input(event) => state.process_input_event(event), + WinitEvent::Input(event) => { + state.process_input_event(event); + backend.window().request_redraw(); + } WinitEvent::Redraw => { + tracing::trace!("rendering nested compositor frame"); state.update_fractional_scale_preferences(); let size = backend.window_size(); let damage = Rectangle::from_size(size); @@ -184,9 +191,6 @@ pub fn init_winit( state.space.refresh(); state.popups.cleanup(); let _ = state.display_handle.flush_clients(); - - // Ask for redraw to schedule new frame. - backend.window().request_redraw(); } WinitEvent::CloseRequested => { state.loop_signal.stop(); @@ -195,5 +199,17 @@ pub fn init_winit( }; })?; + let (redraw_sender, redraw_receiver) = channel::channel(); + state.winit_redraw_sender = Some(redraw_sender); + let redraw_backend = backend.clone(); + event_loop + .handle() + .insert_source(redraw_receiver, move |event, _, _state| { + if matches!(event, channel::Event::Msg(())) { + redraw_backend.borrow().window().request_redraw(); + } + })?; + backend.borrow().window().request_redraw(); + Ok(()) } From 002b1ea10a208abba0597b5cac1ff19a05073452 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:54:12 -0400 Subject: [PATCH 147/798] Fix Wayland registry startup for IBus and XWayland --- rust/pantheon-compositor/README.md | 15 +++++---- .../src/handlers/compositor.rs | 11 +++++-- rust/pantheon-compositor/src/state.rs | 33 +++++++++++-------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 044354d94..314ea74f2 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -56,13 +56,14 @@ cargo run -p pantheon-compositor -- --run target/debug/pantheon-shell-probe ``` The probe binds the shell and panel v2 interfaces, creates a Wayland surface, -and sends `get_panel`, anchor, size, hide-mode, and offset requests. An earlier -run passed under Xvfb with Mesa llvmpipe, but the latest isolated rerun hangs -waiting for the initial registry roundtrip: the client connects, while the -compositor does not accept the queued socket connection. Treat nested protocol -dispatch as unqualified until this is resolved. Even a passing probe does not -attach a buffer or xdg role, so it validates protocol dispatch rather than -panel rendering or Wingpanel/Dock integration. +and sends `get_panel`, anchor, size, hide-mode, and offset requests. The probe +passes under Xvfb with Mesa llvmpipe and a private runtime directory. The +compositor caches the connecting process identity before global advertisement +so the IBus-only input-method filter does not re-enter the Wayland backend while +it holds its dispatch lock. It also uses Smithay's `XWaylandClientData` for the +XWayland client's compositor state. The probe does not attach a buffer or xdg +role, so it validates protocol dispatch rather than panel rendering or +Wingpanel/Dock integration. ## Current limits diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index b91e4c495..1f3aae7e7 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -13,6 +13,7 @@ use smithay::{ }, shm::{ShmHandler, ShmState}, }, + xwayland::XWaylandClientData, }; use super::xdg_shell; @@ -23,7 +24,13 @@ impl CompositorHandler for PantheonCompositor { } fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { - &client.get_data::().unwrap().compositor_state + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + panic!("unknown Wayland client data type") } fn commit(&mut self, surface: &WlSurface) { @@ -45,8 +52,6 @@ impl CompositorHandler for PantheonCompositor { xdg_shell::handle_commit(self, surface); pantheon_shell::handle_surface_commit(self, surface); resize_grab::handle_commit(&mut self.space, surface); - self.request_winit_redraw(); - #[cfg(feature = "native-drm")] self.request_native_redraw(); } } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index c58cb5dbc..a2b7a4c6c 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -12,7 +12,7 @@ use smithay::{ timer::{TimeoutAction, Timer}, }, wayland_server::{ - Client, Display, DisplayHandle, Resource, + Display, DisplayHandle, Resource, backend::{ClientData, ClientId, DisconnectReason}, protocol::wl_surface::WlSurface, }, @@ -52,20 +52,20 @@ use crate::tiling::{CornerSnapPreview, TilingBindings}; #[cfg(feature = "native-drm")] use smithay::reexports::calloop::channel::Sender; -fn is_ibus_input_method_client(client: &Client, display: &DisplayHandle) -> bool { +fn is_ibus_input_method_stream(stream: &std::os::unix::net::UnixStream) -> bool { use std::os::unix::fs::MetadataExt; - let Ok(credentials) = client.get_credentials(display) else { + let Ok(credentials) = smithay::reexports::rustix::net::sockopt::socket_peercred(stream) else { return false; }; let Ok(current_user) = std::fs::metadata("/proc/self") else { return false; }; - if credentials.uid != current_user.uid() { + if credentials.uid.as_raw() != current_user.uid() { return false; } - std::fs::read_link(format!("/proc/{}/exe", credentials.pid)) + std::fs::read_link(format!("/proc/{}/exe", credentials.pid.as_raw_pid())) .ok() .and_then(|path| path.file_name().map(|name| name.to_owned())) .is_some_and(|name| name == "ibus-daemon") @@ -197,11 +197,11 @@ impl PantheonCompositor { let data_device_state = DataDeviceState::new::(&dh); let primary_selection_state = PrimarySelectionState::new::(&dh); let text_input_manager_state = TextInputManagerState::new::(&dh); - let input_method_display_handle = dh.clone(); - let input_method_manager_state = - InputMethodManagerState::new::(&dh, move |client| { - is_ibus_input_method_client(client, &input_method_display_handle) - }); + let input_method_manager_state = InputMethodManagerState::new::(&dh, |client| { + client + .get_data::() + .is_some_and(|data| data.is_ibus_input_method_client) + }); let pantheon_shell_state = PantheonShellState::new::(&dh); let (display_config, apply_requests) = DisplayConfigHandle::start(); event_loop.handle().insert_source( @@ -291,8 +291,6 @@ impl PantheonCompositor { .handle() .insert_source(osd_redraw_receiver, |event, _, _state| { if matches!(event, channel::Event::Msg(())) { - _state.request_winit_redraw(); - #[cfg(feature = "native-drm")] _state.request_native_redraw(); } })?; @@ -366,6 +364,7 @@ impl PantheonCompositor { } pub fn request_native_redraw(&self) { + self.request_winit_redraw(); #[cfg(feature = "native-drm")] if let Some(sender) = &self.native_redraw_sender { let _ = sender.send(()); @@ -956,9 +955,16 @@ impl PantheonCompositor { // Inside the callback, you should insert the client into the display. // // You may also associate some data with the client when inserting the client. + let is_ibus_input_method_client = is_ibus_input_method_stream(&client_stream); state .display_handle - .insert_client(client_stream, Arc::new(ClientState::default())) + .insert_client( + client_stream, + Arc::new(ClientState { + is_ibus_input_method_client, + ..ClientState::default() + }), + ) .unwrap(); })?; @@ -1140,6 +1146,7 @@ impl XdgActivationHandler for PantheonCompositor { #[derive(Default)] pub struct ClientState { pub compositor_state: CompositorClientState, + pub is_ibus_input_method_client: bool, } impl ClientData for ClientState { From b261cc2acbcdbb7a1a2ad64dfab0a3ebac83f9e2 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 08:57:28 -0400 Subject: [PATCH 148/798] Record nested Wingpanel qualification --- mseries/pantheon-rust-compositor/README.md | 9 ++++++--- rust/pantheon-compositor/README.md | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 957786b63..11c325089 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -89,9 +89,12 @@ This branch is **not yet ready to use as an experimental Pantheon session**. The nested backend is suitable for development and controlled protocol qualification, and the ARM64 `native-drm` package builds on the M-series host. Neither result demonstrates that the session can replace Gala. No isolated -native login has been run, so actual scanout, keyboard/pointer input, app -launch, IBus text commits, panel/dock behavior, lock/unlock and crash recovery -are still unknown. Keep Gala selected and use the Rust compositor only in an +native login has been run, so actual scanout, native keyboard/pointer input, +application launch, IBus text commits, Dock behavior, lock/unlock and crash +recovery are still unknown. Installed Wingpanel now binds the shell protocol +and attaches shared-memory panel buffers in an isolated Xvfb run, but its +keyboard module emits a critical assertion there; that does not qualify full +panel behavior. Keep Gala selected and use the Rust compositor only in an explicitly isolated qualification session after its prerequisites are checked. The next gate is staged nested qualification with real GTK/Qt clients and the diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 314ea74f2..a5fcf8e1a 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -185,8 +185,13 @@ sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated and attached shared-memory buffers. Wingpanel requires `org.gnome.Mutter.DisplayConfig`; the compositor serves its `GetCurrentState` query with every tracked output's current mode, logical position, scale, and -primary status. Native outputs list connector modes reported by DRM and mark -active/preferred entries; the nested backend reports its actual Winit size. +primary status. A 2026-10-19 rerun with `GSK_RENDERER=cairo` again showed +Wingpanel binding the shell global, sending `get_panel`, anchor and hide-mode +requests, and attaching shared-memory buffers to its panel surface. It emitted +an `io.elementary.panel.keyboard` critical assertion under Xvfb, so panel +keyboard interaction remains unqualified. Native outputs list connector modes +reported by DRM and mark active/preferred entries; the nested backend reports +its actual Winit size. `ApplyMonitorsConfig` validates serials, connector assignments, mode IDs, scale and transform. Native DRM mode changes use Smithay's shared output manager and roll back earlier changes if a later mode fails. Temporary layout, scale and From af90f6aa286229b06e3a9865b5d768f1df9add07 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:00:47 -0400 Subject: [PATCH 149/798] Clarify Wingpanel keyboard fixture qualification --- mseries/pantheon-rust-compositor/README.md | 8 +++++--- rust/pantheon-compositor/README.md | 13 ++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 11c325089..044666f39 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -92,9 +92,11 @@ Neither result demonstrates that the session can replace Gala. No isolated native login has been run, so actual scanout, native keyboard/pointer input, application launch, IBus text commits, Dock behavior, lock/unlock and crash recovery are still unknown. Installed Wingpanel now binds the shell protocol -and attaches shared-memory panel buffers in an isolated Xvfb run, but its -keyboard module emits a critical assertion there; that does not qualify full -panel behavior. Keep Gala selected and use the Rust compositor only in an +and attaches shared-memory panel buffers in an isolated Xvfb run. The panel +keyboard assertion seen with empty settings disappeared after the test used a +private GSettings fixture containing the host's input-source values; this +confirms a fixture issue, while direct keyboard interaction remains untested. +Keep Gala selected and use the Rust compositor only in an explicitly isolated qualification session after its prerequisites are checked. The next gate is staged nested qualification with real GTK/Qt clients and the diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index a5fcf8e1a..304e3e5e2 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -185,11 +185,14 @@ sent `get_panel`, `set_anchor(top)`, and `set_hide_mode(never)`, then allocated and attached shared-memory buffers. Wingpanel requires `org.gnome.Mutter.DisplayConfig`; the compositor serves its `GetCurrentState` query with every tracked output's current mode, logical position, scale, and -primary status. A 2026-10-19 rerun with `GSK_RENDERER=cairo` again showed -Wingpanel binding the shell global, sending `get_panel`, anchor and hide-mode -requests, and attaching shared-memory buffers to its panel surface. It emitted -an `io.elementary.panel.keyboard` critical assertion under Xvfb, so panel -keyboard interaction remains unqualified. Native outputs list connector modes +primary status. A 2026-10-19 rerun with `GSK_RENDERER=cairo` and the host's +input-source settings copied into a private GSettings keyfile showed Wingpanel +binding the shell global, sending `get_panel`, anchor and hide-mode requests, +receiving the compositor keymap and keyboard focus, and attaching 69 shared- +memory buffers without a critical assertion. A run with empty input-source +settings had emitted an `io.elementary.panel.keyboard` assertion; that result +was caused by the incomplete test fixture. Panel keyboard interaction still +needs direct input qualification. Native outputs list connector modes reported by DRM and mark active/preferred entries; the nested backend reports its actual Winit size. `ApplyMonitorsConfig` validates serials, connector assignments, mode IDs, scale From 02628643cc1d6fa82ad4464cc603c35945f2bc67 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:10:17 -0400 Subject: [PATCH 150/798] Expose Pantheon window data to the Dock --- mseries/pantheon-rust-compositor/README.md | 14 +- rust/pantheon-compositor/README.md | 14 +- rust/pantheon-compositor/src/gala_compat.rs | 297 ++++++++++++++++++++ rust/pantheon-compositor/src/main.rs | 1 + rust/pantheon-compositor/src/state.rs | 146 ++++++++++ 5 files changed, 467 insertions(+), 5 deletions(-) create mode 100644 rust/pantheon-compositor/src/gala_compat.rs diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 044666f39..b4af40390 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -99,9 +99,19 @@ confirms a fixture issue, while direct keyboard interaction remains untested. Keep Gala selected and use the Rust compositor only in an explicitly isolated qualification session after its prerequisites are checked. +The compositor now exposes Dock's `org.pantheon.gala.DesktopIntegration` +D-Bus interface. Window enumeration, app IDs, focus state and focusing a +window are implemented. A temporary Wayland window was returned with its app +ID and focus state; a direct `FocusWindow` call focused it. The current model +exposes one workspace and does not implement overview or cross-workspace +movement. +The nested stock Dock 8.3.3 smoke bound the panel protocol and attached buffers, +but also logged its known asynchronous workspace-initialization critical, so +Dock interaction is still not qualified end to end. + The next gate is staged nested qualification with real GTK/Qt clients and the -existing Pantheon shell clients, including IBus focus/commit behavior. After -that passes, run the package in a separate native login with a tested recovery +existing Pantheon shell clients, including Dock launcher/window switching and +IBus focus/commit behavior. After that passes, run the package in a separate native login with a tested recovery path. Only record it as an experimental session that works after those runtime gates pass; a successful compile or package build is not that evidence. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 304e3e5e2..b7cc88209 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -94,9 +94,17 @@ The Dock's existing hide modes now drive compositor visibility: maximize and overlap modes hide while their condition is true, `always` starts hidden, and pointer motion at the panel's output edge reveals it. Hiding releases the panel's work area; revealing it restores that area and reconfigures managed -windows. The active host Dock setting is `overlapping-window`. The transition -is immediate; animated hiding and runtime Dock interaction remain unqualified. -Blur, overview behavior and multi-output Dock behavior also remain open. The +windows. The active host Dock setting is `overlapping-window`. A nested Xvfb +run of Dock 8.3.3 bound the shell protocol, requested anchor and hide mode, and +attached shared-memory buffers. The compositor now exposes the Gala +`DesktopIntegration` D-Bus contract used by Dock, including a live window list +and focus requests; a temporary Wayland window appeared in `GetWindows`, and +`FocusWindow` routed back to the compositor. The current workspace model is +single-workspace: moving/reordering windows across workspaces and Gala's +multitasking view are not implemented. Stock Dock also logged its existing +workspace-startup race during the nested smoke, so full Dock behavior remains +unqualified. Hiding is immediate; blur and multi-output Dock behavior remain +open. The compositor implements Smithay's `ext-session-lock-v1`: it blanks each output until its lock surface is ready, confines input to the locker, and restores the prior keyboard focus on unlock. It exports the observed diff --git a/rust/pantheon-compositor/src/gala_compat.rs b/rust/pantheon-compositor/src/gala_compat.rs new file mode 100644 index 000000000..ddd81aced --- /dev/null +++ b/rust/pantheon-compositor/src/gala_compat.rs @@ -0,0 +1,297 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex, mpsc}, + thread, + time::Duration, +}; + +use zbus::{blocking::Connection, interface, object_server::SignalEmitter, zvariant::OwnedValue}; + +const BUS_NAME: &str = "org.pantheon.gala"; +const ROOT_PATH: &str = "/org/pantheon/gala"; +const DESKTOP_PATH: &str = "/org/pantheon/gala/DesktopInterface"; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct GalaWindow { + pub uid: u64, + pub app_id: String, + pub has_focus: bool, + pub workspace_index: i32, + pub time_appeared_on_workspace: i64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct DesktopSnapshot { + pub windows: Vec, + pub active_workspace: i32, + pub workspace_count: i32, +} + +#[derive(Clone, Debug)] +pub(crate) enum GalaAction { + PerformAction(i32), + ShowWindowsFor(String), + FocusWindow(u64), + MoveWindowToWorkspace(u64, i32), + ActivateWorkspace(i32), + ReorderWorkspace(i32, i32), +} + +enum GalaSignal { + RunningApplicationsChanged, + WindowsChanged, + ActiveWorkspaceChanged, +} + +#[derive(Clone)] +pub(crate) struct GalaCompatHandle { + snapshot: Arc>, + signals: mpsc::Sender, +} + +impl GalaCompatHandle { + pub fn start() -> (Self, mpsc::Receiver) { + let snapshot = Arc::new(Mutex::new(DesktopSnapshot::default())); + let (signals, signal_receiver) = mpsc::channel(); + let (actions, action_receiver) = mpsc::channel(); + let service_snapshot = snapshot.clone(); + thread::Builder::new() + .name("pantheon-gala-compat-dbus".into()) + .spawn(move || serve_gala_compat(service_snapshot, actions, signal_receiver)) + .expect("failed to start Pantheon Gala compatibility D-Bus thread"); + (Self { snapshot, signals }, action_receiver) + } + + pub fn update(&self, next: DesktopSnapshot) { + let (windows_changed, apps_changed, active_changed) = { + let mut snapshot = self + .snapshot + .lock() + .expect("Pantheon D-Bus snapshot lock poisoned"); + let previous_apps = snapshot + .windows + .iter() + .map(|window| window.app_id.as_str()) + .collect::>(); + let next_apps = next + .windows + .iter() + .map(|window| window.app_id.as_str()) + .collect::>(); + let windows_changed = snapshot.windows != next.windows; + let apps_changed = previous_apps != next_apps; + let active_changed = snapshot.active_workspace != next.active_workspace; + if windows_changed || active_changed || snapshot.workspace_count != next.workspace_count + { + *snapshot = next; + } + (windows_changed, apps_changed, active_changed) + }; + + if apps_changed { + let _ = self.signals.send(GalaSignal::RunningApplicationsChanged); + } + if windows_changed { + let _ = self.signals.send(GalaSignal::WindowsChanged); + } + if active_changed { + let _ = self.signals.send(GalaSignal::ActiveWorkspaceChanged); + } + } +} + +struct GalaRootServer(mpsc::Sender); + +#[interface(name = "org.pantheon.gala")] +impl GalaRootServer { + fn perform_action(&self, action: i32) -> zbus::fdo::Result<()> { + self.0 + .send(GalaAction::PerformAction(action)) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + } +} + +struct DesktopIntegrationServer(Arc>, mpsc::Sender); + +type WindowProperties = (u64, HashMap); +type RunningApplication = (String, HashMap); + +#[interface(name = "org.pantheon.gala.DesktopIntegration")] +impl DesktopIntegrationServer { + #[zbus(property)] + fn version(&self) -> u32 { + 1 + } + + fn get_running_applications(&self) -> zbus::fdo::Result> { + let snapshot = self + .0 + .lock() + .map_err(|_| zbus::fdo::Error::Failed("window snapshot is unavailable".into()))?; + let mut seen = HashSet::new(); + Ok(snapshot + .windows + .iter() + .filter(|window| !window.app_id.is_empty() && seen.insert(window.app_id.clone())) + .map(|window| (window.app_id.clone(), HashMap::new())) + .collect()) + } + + fn get_windows(&self) -> zbus::fdo::Result> { + let snapshot = self + .0 + .lock() + .map_err(|_| zbus::fdo::Error::Failed("window snapshot is unavailable".into()))?; + snapshot + .windows + .iter() + .map(|window| { + let mut properties = HashMap::new(); + properties.insert("app-id".into(), string_value(&window.app_id)?); + properties.insert("has-focus".into(), OwnedValue::from(window.has_focus)); + properties.insert( + "workspace-index".into(), + OwnedValue::from(window.workspace_index), + ); + properties.insert( + "time-appeared-on-workspace".into(), + OwnedValue::from(window.time_appeared_on_workspace), + ); + Ok((window.uid, properties)) + }) + .collect() + } + + fn focus_window(&self, uid: u64) -> zbus::fdo::Result<()> { + self.1 + .send(GalaAction::FocusWindow(uid)) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + } + + fn move_window_to_workspace(&self, uid: u64, index: i32) -> zbus::fdo::Result<()> { + self.1 + .send(GalaAction::MoveWindowToWorkspace(uid, index)) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + } + + fn activate_workspace(&self, index: i32) -> zbus::fdo::Result<()> { + self.1 + .send(GalaAction::ActivateWorkspace(index)) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + } + + fn get_n_workspaces(&self) -> zbus::fdo::Result { + Ok(self + .0 + .lock() + .map_err(|_| zbus::fdo::Error::Failed("workspace state is unavailable".into()))? + .workspace_count) + } + + fn get_active_workspace(&self) -> zbus::fdo::Result { + Ok(self + .0 + .lock() + .map_err(|_| zbus::fdo::Error::Failed("workspace state is unavailable".into()))? + .active_workspace) + } + + fn show_windows_for(&self, app_id: String) -> zbus::fdo::Result<()> { + self.1 + .send(GalaAction::ShowWindowsFor(app_id)) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + } + + fn reorder_workspace(&self, index: i32, new_index: i32) -> zbus::fdo::Result<()> { + self.1 + .send(GalaAction::ReorderWorkspace(index, new_index)) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + } + + #[zbus(signal)] + async fn running_applications_changed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; + + #[zbus(signal)] + async fn windows_changed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; + + #[zbus(signal)] + async fn active_workspace_changed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>; + + #[zbus(signal)] + async fn workspace_removed(emitter: &SignalEmitter<'_>, index: i32) -> zbus::Result<()>; +} + +fn string_value(value: &str) -> zbus::fdo::Result { + OwnedValue::try_from(zbus::zvariant::Value::from(zbus::zvariant::Str::from( + value, + ))) + .map_err(|error| zbus::fdo::Error::Failed(error.to_string())) +} + +fn serve_gala_compat( + snapshot: Arc>, + actions: mpsc::Sender, + signal_receiver: mpsc::Receiver, +) { + let connection = match Connection::session() { + Ok(connection) => connection, + Err(error) => { + tracing::warn!(%error, "cannot connect to the session bus; Pantheon Dock integration is disabled"); + return; + } + }; + + if let Err(error) = connection + .object_server() + .at(ROOT_PATH, GalaRootServer(actions.clone())) + { + tracing::warn!(%error, "cannot register the Pantheon Gala action interface"); + return; + } + if let Err(error) = connection + .object_server() + .at(DESKTOP_PATH, DesktopIntegrationServer(snapshot, actions)) + { + tracing::warn!(%error, "cannot register the Pantheon desktop integration interface"); + return; + } + if let Err(error) = connection.request_name(BUS_NAME) { + tracing::warn!(%error, "cannot own the Pantheon Gala compatibility name"); + return; + } + + tracing::info!("serving Pantheon Dock window and workspace integration"); + loop { + let signal = match signal_receiver.recv_timeout(Duration::from_millis(100)) { + Ok(signal) => signal, + Err(mpsc::RecvTimeoutError::Timeout) => continue, + Err(mpsc::RecvTimeoutError::Disconnected) => return, + }; + let result = match signal { + GalaSignal::RunningApplicationsChanged => connection.emit_signal( + None::<&str>, + DESKTOP_PATH, + "org.pantheon.gala.DesktopIntegration", + "RunningApplicationsChanged", + &(), + ), + GalaSignal::WindowsChanged => connection.emit_signal( + None::<&str>, + DESKTOP_PATH, + "org.pantheon.gala.DesktopIntegration", + "WindowsChanged", + &(), + ), + GalaSignal::ActiveWorkspaceChanged => connection.emit_signal( + None::<&str>, + DESKTOP_PATH, + "org.pantheon.gala.DesktopIntegration", + "ActiveWorkspaceChanged", + &(), + ), + }; + if let Err(error) = result { + tracing::warn!(%error, "could not emit a Pantheon desktop integration signal"); + } + } +} diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 0ba7a34f6..66e194b5f 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -2,6 +2,7 @@ mod cursor; mod display_config; +mod gala_compat; mod grabs; mod handlers; mod input; diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index a2b7a4c6c..8c79abb63 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -44,6 +44,7 @@ use crate::display_config::{ ApplyRequest, DisplayConfigHandle, OutputModeSnapshot, OutputSnapshot, SavedMonitorConfiguration, save_monitor_configurations, transform_to_dbus, }; +use crate::gala_compat::{DesktopSnapshot, GalaAction, GalaCompatHandle, GalaWindow}; #[cfg(feature = "native-drm")] use crate::native::NativeBackend; use crate::pantheon_shell::{PanelState, PantheonShellState}; @@ -110,6 +111,7 @@ pub struct PantheonCompositor { pub input_method_manager_state: InputMethodManagerState, pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, + pub(crate) gala_compat: GalaCompatHandle, pub shell_compat: ShellCompatHandle, pub primary_output_name: Option, pub panels: HashMap, @@ -295,6 +297,17 @@ impl PantheonCompositor { } })?; let shell_compat = ShellCompatHandle::start(socket_name.clone(), osd_redraw_sender); + let (gala_compat, gala_actions) = GalaCompatHandle::start(); + event_loop.handle().insert_source( + Timer::from_duration(Duration::from_millis(100)), + move |_, _, state| { + while let Ok(action) = gala_actions.try_recv() { + state.handle_gala_action(action); + } + state.publish_gala_snapshot(); + TimeoutAction::ToDuration(Duration::from_millis(100)) + }, + )?; // Get the loop signal, used to stop the event loop let loop_signal = event_loop.get_signal(); @@ -337,6 +350,7 @@ impl PantheonCompositor { input_method_manager_state, pantheon_shell_state, display_config, + gala_compat, shell_compat, primary_output_name: None, panels: HashMap::new(), @@ -377,6 +391,138 @@ impl PantheonCompositor { } } + fn publish_gala_snapshot(&self) { + let focus = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()); + let windows = self + .space + .elements() + .map(|window| { + let app_id = if let Some(toplevel) = window.toplevel() { + smithay::wayland::compositor::with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .and_then(|attributes| attributes.lock().ok()) + .and_then(|attributes| attributes.app_id.clone()) + .unwrap_or_default() + }) + } else if let Some(surface) = window.x11_surface() { + let class = surface.class(); + if class.is_empty() { + surface.title() + } else { + class + } + } else { + String::new() + }; + let has_focus = window + .toplevel() + .is_some_and(|toplevel| focus.as_ref() == Some(toplevel.wl_surface())) + || window + .x11_surface() + .is_some_and(|surface| self.x11_keyboard_focus.as_ref() == Some(surface)); + GalaWindow { + uid: window.id() as u64, + app_id, + has_focus, + workspace_index: 0, + time_appeared_on_workspace: window.id() as i64, + } + }) + .collect(); + self.gala_compat.update(DesktopSnapshot { + windows, + active_workspace: 0, + workspace_count: 1, + }); + } + + fn handle_gala_action(&mut self, action: GalaAction) { + match action { + GalaAction::FocusWindow(uid) => self.focus_window_by_uid(uid), + GalaAction::ShowWindowsFor(app_id) => { + let uid = self + .space + .elements() + .find(|window| self.window_app_id(window).as_deref() == Some(app_id.as_str())) + .map(|window| window.id() as u64); + if let Some(uid) = uid { + self.focus_window_by_uid(uid); + } + } + GalaAction::MoveWindowToWorkspace(uid, index) => { + if index != 0 { + tracing::warn!(uid, index, "Dock requested an unavailable workspace"); + } + } + GalaAction::ActivateWorkspace(index) => { + if index != 0 { + tracing::warn!(index, "Dock requested an unavailable workspace"); + } + } + GalaAction::ReorderWorkspace(index, new_index) => { + if index != new_index { + tracing::warn!(index, new_index, "Dock requested workspace reordering"); + } + } + GalaAction::PerformAction(action) => { + tracing::warn!( + action, + "Dock requested a Gala action that is not implemented" + ); + } + } + } + + fn window_app_id(&self, window: &Window) -> Option { + if let Some(toplevel) = window.toplevel() { + smithay::wayland::compositor::with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .and_then(|attributes| attributes.lock().ok()) + .and_then(|attributes| attributes.app_id.clone()) + }) + } else { + window.x11_surface().map(|surface| { + let class = surface.class(); + if class.is_empty() { + surface.title() + } else { + class + } + }) + } + } + + fn focus_window_by_uid(&mut self, uid: u64) { + let Some(window) = self + .space + .elements() + .find(|window| window.id() as u64 == uid) + .cloned() + else { + return; + }; + self.space.raise_element(&window, true); + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + if let Some(toplevel) = window.toplevel() { + if !self.session_locked + && let Some(keyboard) = self.seat.get_keyboard() + { + keyboard.set_focus(self, Some(toplevel.wl_surface().clone()), serial); + } + } else if let Some(surface) = window.x11_surface().cloned() { + self.set_x11_keyboard_focus(Some(surface), serial); + } + self.refresh_panel_visibility(); + self.request_native_redraw(); + } + #[cfg(feature = "native-drm")] pub fn stop_with_error(&mut self, error: impl Into) { self.fatal_error = Some(error.into()); From 619ca5cf9ef43e62910fec1358ef9b4b1c589fc6 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:18:46 -0400 Subject: [PATCH 151/798] Implement dynamic workspace management --- mseries/pantheon-rust-compositor/README.md | 10 +- rust/pantheon-compositor/README.md | 9 +- rust/pantheon-compositor/src/gala_compat.rs | 73 ++-- .../src/handlers/xdg_shell.rs | 21 +- rust/pantheon-compositor/src/main.rs | 1 + .../pantheon-compositor/src/pantheon_shell.rs | 13 + rust/pantheon-compositor/src/state.rs | 374 +++++++++++++++--- rust/pantheon-compositor/src/workspace.rs | 175 ++++++++ rust/pantheon-compositor/src/xwayland.rs | 26 +- 9 files changed, 616 insertions(+), 86 deletions(-) create mode 100644 rust/pantheon-compositor/src/workspace.rs diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index b4af40390..5ecd366c6 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -100,11 +100,11 @@ Keep Gala selected and use the Rust compositor only in an explicitly isolated qualification session after its prerequisites are checked. The compositor now exposes Dock's `org.pantheon.gala.DesktopIntegration` -D-Bus interface. Window enumeration, app IDs, focus state and focusing a -window are implemented. A temporary Wayland window was returned with its app -ID and focus state; a direct `FocusWindow` call focused it. The current model -exposes one workspace and does not implement overview or cross-workspace -movement. +D-Bus interface. Window enumeration, app IDs, focus state, workspace listing, +activation and reorder, dynamic workspace creation, window movement, and +cross-workspace focus are implemented. An isolated Xvfb run with a temporary +Wayland client exercised workspace creation, movement, switching and focus. +Overview remains unsupported. The nested stock Dock 8.3.3 smoke bound the panel protocol and attached buffers, but also logged its known asynchronous workspace-initialization critical, so Dock interaction is still not qualified end to end. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index b7cc88209..6a8966d4c 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -99,9 +99,12 @@ run of Dock 8.3.3 bound the shell protocol, requested anchor and hide mode, and attached shared-memory buffers. The compositor now exposes the Gala `DesktopIntegration` D-Bus contract used by Dock, including a live window list and focus requests; a temporary Wayland window appeared in `GetWindows`, and -`FocusWindow` routed back to the compositor. The current workspace model is -single-workspace: moving/reordering windows across workspaces and Gala's -multitasking view are not implemented. Stock Dock also logged its existing +`FocusWindow` routed back to the compositor. The compositor tracks windows +across workspaces, switches visible windows, and supports Dock requests to +create the dynamic workspace, move windows, reorder workspaces and focus a +window across workspaces. An isolated Xvfb run with a temporary Wayland client +exercised creation, movement, switching and focus. Gala's multitasking view is +not implemented. Stock Dock also logged its existing workspace-startup race during the nested smoke, so full Dock behavior remains unqualified. Hiding is immediate; blur and multi-output Dock behavior remain open. The diff --git a/rust/pantheon-compositor/src/gala_compat.rs b/rust/pantheon-compositor/src/gala_compat.rs index ddd81aced..c691b041a 100644 --- a/rust/pantheon-compositor/src/gala_compat.rs +++ b/rust/pantheon-compositor/src/gala_compat.rs @@ -37,6 +37,11 @@ pub(crate) enum GalaAction { ReorderWorkspace(i32, i32), } +pub(crate) struct GalaActionRequest { + pub action: GalaAction, + pub response: mpsc::SyncSender>, +} + enum GalaSignal { RunningApplicationsChanged, WindowsChanged, @@ -50,8 +55,11 @@ pub(crate) struct GalaCompatHandle { } impl GalaCompatHandle { - pub fn start() -> (Self, mpsc::Receiver) { - let snapshot = Arc::new(Mutex::new(DesktopSnapshot::default())); + pub fn start() -> (Self, mpsc::Receiver) { + let snapshot = Arc::new(Mutex::new(DesktopSnapshot { + workspace_count: 2, + ..DesktopSnapshot::default() + })); let (signals, signal_receiver) = mpsc::channel(); let (actions, action_receiver) = mpsc::channel(); let service_snapshot = snapshot.clone(); @@ -63,7 +71,7 @@ impl GalaCompatHandle { } pub fn update(&self, next: DesktopSnapshot) { - let (windows_changed, apps_changed, active_changed) = { + let (windows_changed, apps_changed, active_changed, count_changed) = { let mut snapshot = self .snapshot .lock() @@ -81,37 +89,40 @@ impl GalaCompatHandle { let windows_changed = snapshot.windows != next.windows; let apps_changed = previous_apps != next_apps; let active_changed = snapshot.active_workspace != next.active_workspace; + let count_changed = snapshot.workspace_count != next.workspace_count; if windows_changed || active_changed || snapshot.workspace_count != next.workspace_count { *snapshot = next; } - (windows_changed, apps_changed, active_changed) + (windows_changed, apps_changed, active_changed, count_changed) }; if apps_changed { let _ = self.signals.send(GalaSignal::RunningApplicationsChanged); } - if windows_changed { + if windows_changed || count_changed { let _ = self.signals.send(GalaSignal::WindowsChanged); } if active_changed { let _ = self.signals.send(GalaSignal::ActiveWorkspaceChanged); } } + + pub fn request_windows_changed(&self) { + let _ = self.signals.send(GalaSignal::WindowsChanged); + } } -struct GalaRootServer(mpsc::Sender); +struct GalaRootServer(mpsc::Sender); #[interface(name = "org.pantheon.gala")] impl GalaRootServer { fn perform_action(&self, action: i32) -> zbus::fdo::Result<()> { - self.0 - .send(GalaAction::PerformAction(action)) - .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + dispatch_action(&self.0, GalaAction::PerformAction(action)) } } -struct DesktopIntegrationServer(Arc>, mpsc::Sender); +struct DesktopIntegrationServer(Arc>, mpsc::Sender); type WindowProperties = (u64, HashMap); type RunningApplication = (String, HashMap); @@ -163,21 +174,15 @@ impl DesktopIntegrationServer { } fn focus_window(&self, uid: u64) -> zbus::fdo::Result<()> { - self.1 - .send(GalaAction::FocusWindow(uid)) - .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + dispatch_action(&self.1, GalaAction::FocusWindow(uid)) } fn move_window_to_workspace(&self, uid: u64, index: i32) -> zbus::fdo::Result<()> { - self.1 - .send(GalaAction::MoveWindowToWorkspace(uid, index)) - .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + dispatch_action(&self.1, GalaAction::MoveWindowToWorkspace(uid, index)) } fn activate_workspace(&self, index: i32) -> zbus::fdo::Result<()> { - self.1 - .send(GalaAction::ActivateWorkspace(index)) - .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + dispatch_action(&self.1, GalaAction::ActivateWorkspace(index)) } fn get_n_workspaces(&self) -> zbus::fdo::Result { @@ -197,15 +202,11 @@ impl DesktopIntegrationServer { } fn show_windows_for(&self, app_id: String) -> zbus::fdo::Result<()> { - self.1 - .send(GalaAction::ShowWindowsFor(app_id)) - .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + dispatch_action(&self.1, GalaAction::ShowWindowsFor(app_id)) } fn reorder_workspace(&self, index: i32, new_index: i32) -> zbus::fdo::Result<()> { - self.1 - .send(GalaAction::ReorderWorkspace(index, new_index)) - .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into())) + dispatch_action(&self.1, GalaAction::ReorderWorkspace(index, new_index)) } #[zbus(signal)] @@ -228,9 +229,29 @@ fn string_value(value: &str) -> zbus::fdo::Result { .map_err(|error| zbus::fdo::Error::Failed(error.to_string())) } +fn dispatch_action( + sender: &mpsc::Sender, + action: GalaAction, +) -> zbus::fdo::Result<()> { + let (response, result) = mpsc::sync_channel(1); + sender + .send(GalaActionRequest { action, response }) + .map_err(|_| zbus::fdo::Error::Failed("compositor event loop is unavailable".into()))?; + match result.recv_timeout(Duration::from_secs(5)) { + Ok(Ok(())) => Ok(()), + Ok(Err(error)) => Err(zbus::fdo::Error::Failed(error)), + Err(mpsc::RecvTimeoutError::Timeout) => Err(zbus::fdo::Error::Timeout( + "compositor did not complete the requested action in time".into(), + )), + Err(mpsc::RecvTimeoutError::Disconnected) => Err(zbus::fdo::Error::Failed( + "compositor stopped before completing the requested action".into(), + )), + } +} + fn serve_gala_compat( snapshot: Arc>, - actions: mpsc::Sender, + actions: mpsc::Sender, signal_receiver: mpsc::Receiver, ) { let connection = match Connection::session() { diff --git a/rust/pantheon-compositor/src/handlers/xdg_shell.rs b/rust/pantheon-compositor/src/handlers/xdg_shell.rs index 914fc3e7f..8bbbb2308 100644 --- a/rust/pantheon-compositor/src/handlers/xdg_shell.rs +++ b/rust/pantheon-compositor/src/handlers/xdg_shell.rs @@ -14,7 +14,7 @@ use smithay::{ protocol::{wl_seat, wl_surface::WlSurface}, }, }, - utils::{Rectangle, Serial}, + utils::{Point, Rectangle, Serial}, wayland::{ compositor::with_states, shell::xdg::{ @@ -36,7 +36,9 @@ impl XdgShellHandler for PantheonCompositor { fn new_toplevel(&mut self, surface: ToplevelSurface) { let window = Window::new_wayland_window(surface); - self.space.map_element(window, (0, 0), false); + self.space.map_element(window.clone(), (0, 0), false); + self.workspace_manager + .add_window(window, Point::from((0, 0))); } fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { @@ -185,6 +187,7 @@ impl XdgShellHandler for PantheonCompositor { .get_keyboard() .and_then(|keyboard| keyboard.current_focus()) .is_some_and(|focused| surface_has_root(&focused, wl_surface)); + self.workspace_manager.set_location(window.id(), location); self.space.unmap_elem(&window); self.minimized_windows.insert(id, (window, location)); if was_focused { @@ -216,9 +219,21 @@ impl XdgShellHandler for PantheonCompositor { .toplevel() .is_some_and(|top| top.wl_surface() == wl_surface) }) - .cloned(); + .cloned() + .or_else(|| { + self.workspace_manager + .windows() + .into_iter() + .find(|(window, _, _)| { + window + .toplevel() + .is_some_and(|top| top.wl_surface() == wl_surface) + }) + .map(|(window, _, _)| window) + }); if let Some(window) = window { self.space.unmap_elem(&window); + self.workspace_manager.remove_window(window.id()); } if was_focused { diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 66e194b5f..46e7dd5a2 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -14,6 +14,7 @@ mod shell_compat; mod state; mod tiling; mod winit; +mod workspace; mod xwayland; use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index a04e4dc71..986050492 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -229,6 +229,19 @@ impl impl PantheonCompositor { pub fn add_panel(&mut self, surface: WlSurface) { self.panels.insert(surface.id(), PanelState::default()); + let window_id = self + .workspace_manager + .windows() + .into_iter() + .find(|(window, _, _)| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == &surface) + }) + .map(|(window, _, _)| window.id()); + if let Some(window_id) = window_id { + self.workspace_manager.remove_window(window_id); + } self.position_panel(&surface); } diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 8c79abb63..576d40e7d 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -50,6 +50,7 @@ use crate::native::NativeBackend; use crate::pantheon_shell::{PanelState, PantheonShellState}; use crate::shell_compat::ShellCompatHandle; use crate::tiling::{CornerSnapPreview, TilingBindings}; +use crate::workspace::WorkspaceManager; #[cfg(feature = "native-drm")] use smithay::reexports::calloop::channel::Sender; @@ -112,6 +113,7 @@ pub struct PantheonCompositor { pub pantheon_shell_state: PantheonShellState, pub display_config: DisplayConfigHandle, pub(crate) gala_compat: GalaCompatHandle, + pub(crate) workspace_manager: WorkspaceManager, pub shell_compat: ShellCompatHandle, pub primary_output_name: Option, pub panels: HashMap, @@ -301,8 +303,10 @@ impl PantheonCompositor { event_loop.handle().insert_source( Timer::from_duration(Duration::from_millis(100)), move |_, _, state| { - while let Ok(action) = gala_actions.try_recv() { - state.handle_gala_action(action); + while let Ok(request) = gala_actions.try_recv() { + let result = state.handle_gala_action(request.action); + state.publish_gala_snapshot(); + let _ = request.response.send(result); } state.publish_gala_snapshot(); TimeoutAction::ToDuration(Duration::from_millis(100)) @@ -351,6 +355,7 @@ impl PantheonCompositor { pantheon_shell_state, display_config, gala_compat, + workspace_manager: WorkspaceManager::new(), shell_compat, primary_output_name: None, panels: HashMap::new(), @@ -397,9 +402,15 @@ impl PantheonCompositor { .get_keyboard() .and_then(|keyboard| keyboard.current_focus()); let windows = self - .space - .elements() - .map(|window| { + .workspace_manager + .windows() + .into_iter() + .filter(|(window, _, _)| { + window + .toplevel() + .is_none_or(|toplevel| !self.is_panel_surface(toplevel.wl_surface())) + }) + .map(|(window, workspace_index, _)| { let app_id = if let Some(toplevel) = window.toplevel() { smithay::wayland::compositor::with_states(toplevel.wl_surface(), |states| { states @@ -429,52 +440,59 @@ impl PantheonCompositor { uid: window.id() as u64, app_id, has_focus, - workspace_index: 0, + workspace_index: workspace_index as i32, time_appeared_on_workspace: window.id() as i64, } }) .collect(); self.gala_compat.update(DesktopSnapshot { windows, - active_workspace: 0, - workspace_count: 1, + active_workspace: self.workspace_manager.active() as i32, + workspace_count: self.workspace_manager.workspace_count() as i32 + 1, }); } - fn handle_gala_action(&mut self, action: GalaAction) { + fn handle_gala_action(&mut self, action: GalaAction) -> Result<(), String> { match action { GalaAction::FocusWindow(uid) => self.focus_window_by_uid(uid), GalaAction::ShowWindowsFor(app_id) => { let uid = self - .space - .elements() - .find(|window| self.window_app_id(window).as_deref() == Some(app_id.as_str())) - .map(|window| window.id() as u64); + .workspace_manager + .windows() + .into_iter() + .find(|(window, _, _)| { + self.window_app_id(window).as_deref() == Some(app_id.as_str()) + }) + .map(|(window, _, _)| window.id() as u64); if let Some(uid) = uid { - self.focus_window_by_uid(uid); + self.focus_window_by_uid(uid) + } else { + Err(format!("no mapped windows found for app {app_id}")) } } GalaAction::MoveWindowToWorkspace(uid, index) => { - if index != 0 { - tracing::warn!(uid, index, "Dock requested an unavailable workspace"); - } - } - GalaAction::ActivateWorkspace(index) => { - if index != 0 { - tracing::warn!(index, "Dock requested an unavailable workspace"); - } + self.move_window_to_workspace(uid, index) } + GalaAction::ActivateWorkspace(index) => self.activate_workspace(index), GalaAction::ReorderWorkspace(index, new_index) => { - if index != new_index { - tracing::warn!(index, new_index, "Dock requested workspace reordering"); + if self.session_locked { + return Err("cannot reorder workspaces while the session is locked".into()); + } + let (Ok(index), Ok(new_index)) = + (usize::try_from(index), usize::try_from(new_index)) + else { + return Err("workspace indices must be non-negative".into()); + }; + if self.workspace_manager.reorder_workspace(index, new_index) { + self.gala_compat.request_windows_changed(); + Ok(()) + } else { + Err("workspace index is outside the current workspace list".into()) } } - GalaAction::PerformAction(action) => { - tracing::warn!( - action, - "Dock requested a Gala action that is not implemented" - ); - } + GalaAction::PerformAction(action) => Err(format!( + "Gala action {action} is not implemented by this compositor" + )), } } @@ -499,15 +517,28 @@ impl PantheonCompositor { } } - fn focus_window_by_uid(&mut self, uid: u64) { - let Some(window) = self - .space - .elements() - .find(|window| window.id() as u64 == uid) - .cloned() - else { - return; + fn focus_window_by_uid(&mut self, uid: u64) -> Result<(), String> { + if self.session_locked { + return Err("cannot focus a window while the session is locked".into()); + } + let Ok(id) = usize::try_from(uid) else { + return Err("window ID is outside the supported range".into()); }; + let Some(workspace) = self.workspace_manager.workspace_for(id) else { + return Err(format!("window {uid} does not exist")); + }; + if workspace != self.workspace_manager.active() { + self.switch_workspace(workspace)?; + } + let Some(window) = self.workspace_manager.window(id).cloned() else { + return Err(format!("window {uid} does not exist")); + }; + if !self.space.elements().any(|candidate| candidate == &window) { + self.restore_minimized_workspace_window(&window); + if !self.space.elements().any(|candidate| candidate == &window) { + return Err(format!("window {uid} could not be restored")); + } + } self.space.raise_element(&window, true); let serial = smithay::utils::SERIAL_COUNTER.next_serial(); if let Some(toplevel) = window.toplevel() { @@ -521,6 +552,214 @@ impl PantheonCompositor { } self.refresh_panel_visibility(); self.request_native_redraw(); + Ok(()) + } + + fn activate_workspace(&mut self, index: i32) -> Result<(), String> { + if self.session_locked { + return Err("cannot switch workspaces while the session is locked".into()); + } + let Ok(index) = usize::try_from(index) else { + return Err("workspace index must be non-negative".into()); + }; + let workspace = if index == self.workspace_manager.dynamic_workspace_index() { + self.workspace_manager.add_workspace() + } else { + index + }; + self.switch_workspace(workspace) + } + + fn switch_workspace(&mut self, workspace: usize) -> Result<(), String> { + let current = self.workspace_manager.active(); + if workspace >= self.workspace_manager.workspace_count() { + return Err("workspace index is outside the current workspace list".into()); + } + if current == workspace { + return Ok(()); + } + + let visible = self + .space + .elements() + .filter(|window| self.workspace_manager.workspace_for(window.id()) == Some(current)) + .cloned() + .collect::>(); + let ordered_ids = visible.iter().map(Window::id).collect::>(); + for window in &visible { + if let Some(location) = self.space.element_location(window) { + self.workspace_manager.set_location(window.id(), location); + } + } + self.workspace_manager + .set_order_from_visible(&ordered_ids, current); + + let focused = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()); + if focused.as_ref().is_some_and(|surface| { + visible.iter().any(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == surface) + }) + }) && let Some(keyboard) = self.seat.get_keyboard() + { + keyboard.set_focus(self, None, smithay::utils::SERIAL_COUNTER.next_serial()); + } + if self.x11_keyboard_focus.as_ref().is_some_and(|focused| { + visible + .iter() + .any(|window| window.x11_surface() == Some(focused)) + }) { + self.set_x11_keyboard_focus(None, smithay::utils::SERIAL_COUNTER.next_serial()); + } + + for window in &visible { + window.set_activated(false); + self.space.unmap_elem(window); + } + self.workspace_manager.set_active(workspace); + + let minimized_wayland = self + .minimized_windows + .values() + .map(|(window, _)| window.id()) + .collect::>(); + let minimized_x11 = self + .x11_minimized_windows + .values() + .map(|(window, _)| window.id()) + .collect::>(); + let target_windows = self + .workspace_manager + .windows_in(workspace) + .into_iter() + .filter(|(window, _)| { + !minimized_wayland.contains(&window.id()) && !minimized_x11.contains(&window.id()) + }) + .collect::>(); + for (window, location) in &target_windows { + self.space.map_element(window.clone(), *location, false); + } + if let Some((window, _)) = target_windows.last() { + self.space.raise_element(window, true); + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + if let Some(toplevel) = window.toplevel() { + if let Some(keyboard) = self.seat.get_keyboard() { + keyboard.set_focus(self, Some(toplevel.wl_surface().clone()), serial); + } + } else if let Some(surface) = window.x11_surface().cloned() { + self.set_x11_keyboard_focus(Some(surface), serial); + } + } + self.refresh_panel_visibility(); + self.reconfigure_managed_windows(); + self.update_fractional_scale_preferences(); + self.gala_compat.request_windows_changed(); + self.request_native_redraw(); + Ok(()) + } + + fn move_window_to_workspace(&mut self, uid: u64, index: i32) -> Result<(), String> { + if self.session_locked { + return Err("cannot move windows while the session is locked".into()); + } + let Ok(id) = usize::try_from(uid) else { + return Err("window ID is outside the supported range".into()); + }; + let Ok(index) = usize::try_from(index) else { + return Err("workspace index must be non-negative".into()); + }; + if self.workspace_manager.workspace_for(id).is_none() { + return Err(format!("window {uid} does not exist")); + } + let target = if index == self.workspace_manager.dynamic_workspace_index() { + self.workspace_manager.add_workspace() + } else { + index + }; + if target >= self.workspace_manager.workspace_count() { + return Err("workspace index is outside the current workspace list".into()); + } + let source = self.workspace_manager.workspace_for(id).unwrap(); + if source == target { + return Ok(()); + } + let Some(window) = self.workspace_manager.window(id).cloned() else { + return Err(format!("window {uid} does not exist")); + }; + if source == self.workspace_manager.active() { + if let Some(location) = self.space.element_location(&window) { + self.workspace_manager.set_location(id, location); + } + let was_x11_focused = window + .x11_surface() + .is_some_and(|surface| self.x11_keyboard_focus.as_ref() == Some(surface)); + window.set_activated(false); + self.space.unmap_elem(&window); + if was_x11_focused { + self.set_x11_keyboard_focus(None, smithay::utils::SERIAL_COUNTER.next_serial()); + self.focus_next_window(); + } + if window.toplevel().is_some_and(|toplevel| { + self.seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .is_some_and(|focused| focused == *toplevel.wl_surface()) + }) { + self.focus_next_window(); + } + } + self.workspace_manager.assign_window(id, target); + if target == self.workspace_manager.active() && !self.is_workspace_window_minimized(&window) + { + if let Some(location) = self.workspace_manager.location(id) { + self.space.map_element(window.clone(), location, true); + } + self.focus_window_by_uid(uid)?; + } + self.refresh_panel_visibility(); + self.request_native_redraw(); + self.gala_compat.request_windows_changed(); + Ok(()) + } + + fn is_workspace_window_minimized(&self, window: &Window) -> bool { + self.minimized_windows + .values() + .any(|(minimized, _)| minimized == window) + || self + .x11_minimized_windows + .values() + .any(|(minimized, _)| minimized == window) + } + + fn restore_minimized_workspace_window(&mut self, window: &Window) { + if let Some((_, location)) = window + .toplevel() + .and_then(|toplevel| self.minimized_windows.remove(&toplevel.wl_surface().id())) + { + let location = self + .workspace_manager + .location(window.id()) + .unwrap_or(location); + self.space.map_element(window.clone(), location, true); + return; + } + if let Some(surface) = window.x11_surface() + && let Some((_, location)) = self.x11_minimized_windows.remove(&surface.window_id()) + { + let location = self + .workspace_manager + .location(window.id()) + .unwrap_or(location); + if surface.set_mapped(true).is_ok() { + self.space.map_element(window.clone(), location, true); + window.set_activated(true); + } + } } #[cfg(feature = "native-drm")] @@ -1028,10 +1267,32 @@ impl PantheonCompositor { && center_y < geometry.loc.y + geometry.size.h }); if !visible { - self.space.map_element( - window, - (fallback.loc.x + 32, fallback.loc.y + 32), - false, + let target = Point::from((fallback.loc.x + 32, fallback.loc.y + 32)); + self.space.map_element(window.clone(), target, false); + self.workspace_manager.set_location(window.id(), target); + } + } + + let hidden_windows = self + .workspace_manager + .windows() + .into_iter() + .filter(|(_, workspace, _)| *workspace != self.workspace_manager.active()) + .collect::>(); + for (window, _, location) in hidden_windows { + let size = window.geometry().size; + let center_x = location.x + size.w / 2; + let center_y = location.y + size.h / 2; + let visible = output_geometries.iter().any(|geometry| { + center_x >= geometry.loc.x + && center_x < geometry.loc.x + geometry.size.w + && center_y >= geometry.loc.y + && center_y < geometry.loc.y + geometry.size.h + }); + if !visible { + self.workspace_manager.set_location( + window.id(), + Point::from((fallback.loc.x + 32, fallback.loc.y + 32)), ); } } @@ -1257,15 +1518,32 @@ impl XdgActivationHandler for PantheonCompositor { while let Some(parent) = smithay::wayland::compositor::get_parent(&root) { root = parent; } - let root_id = root.id(); - let window = self.find_window_for_surface(&root).or_else(|| { - let (window, location) = self.minimized_windows.remove(&root_id)?; - self.space.map_element(window.clone(), location, false); - Some(window) - }); + let managed = self + .workspace_manager + .windows() + .into_iter() + .find(|(window, _, _)| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == &root) + }); + if let Some((_, workspace, _)) = &managed + && *workspace != self.workspace_manager.active() + { + let _ = self.switch_workspace(*workspace); + } + let window = self + .find_window_for_surface(&root) + .or_else(|| managed.map(|(window, _, _)| window)); let Some(window) = window else { return; }; + if !self.space.elements().any(|candidate| candidate == &window) { + self.restore_minimized_workspace_window(&window); + if !self.space.elements().any(|candidate| candidate == &window) { + return; + } + } self.space.raise_element(&window, true); if let Some(surface) = window .toplevel() diff --git a/rust/pantheon-compositor/src/workspace.rs b/rust/pantheon-compositor/src/workspace.rs new file mode 100644 index 000000000..6001d6d4f --- /dev/null +++ b/rust/pantheon-compositor/src/workspace.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; + +use smithay::{ + desktop::Window, + utils::{Logical, Point}, +}; + +#[derive(Default)] +pub(crate) struct WorkspaceManager { + active: usize, + workspaces: Vec>, + windows: HashMap, + assignments: HashMap, + locations: HashMap>, +} + +impl WorkspaceManager { + pub fn new() -> Self { + Self { + active: 0, + workspaces: vec![Vec::new()], + ..Self::default() + } + } + + pub fn active(&self) -> usize { + self.active + } + + pub fn workspace_count(&self) -> usize { + self.workspaces.len() + } + + pub fn dynamic_workspace_index(&self) -> usize { + self.workspaces.len() + } + + pub fn add_window(&mut self, window: Window, location: Point) { + let id = window.id(); + if let Some(previous) = self.assignments.remove(&id) { + if let Some(windows) = self.workspaces.get_mut(previous) { + windows.retain(|existing| *existing != id); + } + } + self.windows.insert(id, window); + self.assignments.insert(id, self.active); + self.locations.insert(id, location); + self.workspaces[self.active].push(id); + } + + pub fn remove_window(&mut self, id: usize) { + if let Some(workspace) = self.assignments.remove(&id) + && let Some(windows) = self.workspaces.get_mut(workspace) + { + windows.retain(|existing| *existing != id); + } + self.windows.remove(&id); + self.locations.remove(&id); + } + + pub fn assign_window(&mut self, id: usize, workspace: usize) -> bool { + if workspace >= self.workspaces.len() || !self.windows.contains_key(&id) { + return false; + } + let Some(previous) = self.assignments.get(&id).copied() else { + return false; + }; + if previous == workspace { + return true; + } + if let Some(windows) = self.workspaces.get_mut(previous) { + windows.retain(|existing| *existing != id); + } + self.assignments.insert(id, workspace); + self.workspaces[workspace].push(id); + true + } + + pub fn add_workspace(&mut self) -> usize { + self.workspaces.push(Vec::new()); + self.workspaces.len() - 1 + } + + pub fn set_active(&mut self, workspace: usize) -> bool { + if workspace >= self.workspaces.len() { + return false; + } + self.active = workspace; + true + } + + pub fn reorder_workspace(&mut self, index: usize, new_index: usize) -> bool { + if index >= self.workspaces.len() || new_index >= self.workspaces.len() { + return false; + } + if index == new_index { + return true; + } + let workspace = self.workspaces.remove(index); + self.workspaces.insert(new_index, workspace); + for (workspace_index, windows) in self.workspaces.iter().enumerate() { + for id in windows { + self.assignments.insert(*id, workspace_index); + } + } + if self.active == index { + self.active = new_index; + } else if index < self.active && new_index >= self.active { + self.active -= 1; + } else if index > self.active && new_index <= self.active { + self.active += 1; + } + true + } + + pub fn workspace_for(&self, id: usize) -> Option { + self.assignments.get(&id).copied() + } + + pub fn window(&self, id: usize) -> Option<&Window> { + self.windows.get(&id) + } + + pub fn location(&self, id: usize) -> Option> { + self.locations.get(&id).copied() + } + + pub fn set_location(&mut self, id: usize, location: Point) { + if self.windows.contains_key(&id) { + self.locations.insert(id, location); + } + } + + pub fn windows_in(&self, workspace: usize) -> Vec<(Window, Point)> { + self.workspaces + .get(workspace) + .into_iter() + .flatten() + .filter_map(|id| Some((self.windows.get(id)?.clone(), self.location(*id)?))) + .collect() + } + + pub fn windows(&self) -> Vec<(Window, usize, Point)> { + self.workspaces + .iter() + .enumerate() + .flat_map(|(workspace, windows)| { + windows.iter().filter_map(move |id| { + Some(( + self.windows.get(id)?.clone(), + workspace, + self.location(*id)?, + )) + }) + }) + .collect() + } + + pub fn set_order_from_visible(&mut self, ordered_ids: &[usize], workspace: usize) { + let Some(windows) = self.workspaces.get_mut(workspace) else { + return; + }; + let mut visible = ordered_ids + .iter() + .copied() + .filter(|id| self.assignments.get(id) == Some(&workspace)) + .collect::>(); + for id in windows.iter().copied() { + if !visible.contains(&id) { + visible.push(id); + } + } + *windows = visible; + } +} diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs index 3be5c7782..ffde41744 100644 --- a/rust/pantheon-compositor/src/xwayland.rs +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -157,6 +157,7 @@ impl XwmHandler for PantheonCompositor { .unwrap_or(Point::from((0, 0))); let window = Window::new_x11_window(surface.clone()); self.space.map_element(window.clone(), location, true); + self.workspace_manager.add_window(window.clone(), location); let configured = Rectangle::new(location, geometry.size); if let Err(error) = surface.configure(Some(configured)) { tracing::warn!(?error, "could not configure mapped X11 window"); @@ -183,9 +184,22 @@ impl XwmHandler for PantheonCompositor { .is_some_and(|candidate| candidate == &surface) }) .cloned() - }; + } + .or_else(|| { + self.workspace_manager + .windows() + .into_iter() + .find(|(window, _, _)| window.x11_surface() == Some(&surface)) + .map(|(window, _, _)| window) + }); if let Some(window) = window { self.space.unmap_elem(&window); + if !self + .x11_minimized_windows + .contains_key(&surface.window_id()) + { + self.workspace_manager.remove_window(window.id()); + } } if !surface.is_override_redirect() && let Err(error) = surface.set_mapped(false) @@ -201,6 +215,15 @@ impl XwmHandler for PantheonCompositor { fn destroyed_window(&mut self, _xwm: XwmId, surface: X11Surface) { self.x11_managed_windows.remove(&surface.window_id()); self.x11_minimized_windows.remove(&surface.window_id()); + let id = self + .workspace_manager + .windows() + .into_iter() + .find(|(window, _, _)| window.x11_surface() == Some(&surface)) + .map(|(window, _, _)| window.id()); + if let Some(id) = id { + self.workspace_manager.remove_window(id); + } } fn maximize_request(&mut self, _xwm: XwmId, surface: X11Surface) { @@ -228,6 +251,7 @@ impl XwmHandler for PantheonCompositor { }; self.x11_minimized_windows .insert(surface.window_id(), (window.clone(), location)); + self.workspace_manager.set_location(window.id(), location); window.set_activated(false); self.space.unmap_elem(&window); if let Err(error) = surface.set_mapped(false) { From 47dac24c9ca591435942e546cd738a52092e660f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:22:03 -0400 Subject: [PATCH 152/798] Record Rust Pantheon release smoke results --- mseries/OS9-HANDOFF.md | 16 ++++++++++++++++ mseries/ROADMAP.md | 7 +++++++ mseries/pantheon-rust-compositor/README.md | 21 ++++++++++++++------- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4f6604036..8ff75270c 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5837,3 +5837,19 @@ not be used to qualify it. The ARM64 release candidate built from `c91ec2d` is at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-crtc-reassign-release/pantheon-rust-compositor_0.1.0+gitc91ec2d_arm64.deb` (SHA-256 `46baf4fc264cb23b8e784bc57a2b23d7f34b5a8f0930391a34f9d341f327303c`). Its package metadata and contents were inspected; it was not installed. + +### Build current Rust Pantheon workspace release candidate — 2026-10-19 + +Built the current `619ca5c` ARM64 `native-drm` release package locally at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git619ca5c_arm64.deb` +(SHA-256 `aee4c9af0d12c1a2bddbfd45dcd5a79e71afe8c8adc28d9ff46e7cf204d0ea13`). +Package contents, Debian dependency metadata and `ldd` links were inspected; +the package was not installed. The release binary started under private D-Bus +and Xvfb, initialized Winit and XWayland, and launched a Zenity Wayland window. +`GetNWorkspaces` returned 2, and `GetWindows` returned the focused Zenity +window on workspace 0. This proves a useful nested release-build path and +means development qualification can proceed now. It does not prove the UI was +visually inspected, the Pantheon shell works end to end, or native scanout, +input, locking and recovery. Gala remains the selected session. Next: qualify +GTK/Qt apps, IBus commit behavior, and the installed shell clients in the +isolated nested session; do not install this package as the active session. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index e4e360ce5..868023aad 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -2,6 +2,13 @@ ## Mutter-minimal viable Rust for elementaryOS M-series — started 2026-10-18 +The `619ca5c` workspace slice is committed and pushed. A current ARM64 release +candidate was packaged locally and checked in private Xvfb: Winit and XWayland +started, and Zenity appeared in the compositor's live Gala-compatible window +snapshot with focus and the expected two-workspace count. This allows broader +nested development qualification to proceed. It is not evidence for native +display takeover or an experimental Pantheon login; those remain unqualified. + Working title: `Mutter-Minimal-Viable-Rust_elementaryos-mseries`. The owner clarified that the goal is to identify and implement the compositor features needed by the M-series/Pantheon experience, not port Mutter or reproduce all diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 5ecd366c6..840e99a3e 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -109,6 +109,17 @@ The nested stock Dock 8.3.3 smoke bound the panel protocol and attached buffers, but also logged its known asynchronous workspace-initialization critical, so Dock interaction is still not qualified end to end. +The current ARM64 release candidate from commit `619ca5c` is saved at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git619ca5c_arm64.deb` +(SHA-256 `aee4c9af0d12c1a2bddbfd45dcd5a79e71afe8c8adc28d9ff46e7cf204d0ea13`). +The package contents, control dependencies and shared-library links were +inspected; it was not installed. Its release binary started in private Xvfb, +reported XWayland ready, and served a Zenity window through `GetWindows` with +`app-id=zenity`, focus true and the initial two-workspace count. This qualifies +the nested release path for continued development testing only. It does not +qualify visible rendering, native DRM scanout, the full Pantheon shell or a +login session. + The next gate is staged nested qualification with real GTK/Qt clients and the existing Pantheon shell clients, including Dock launcher/window switching and IBus focus/commit behavior. After that passes, run the package in a separate native login with a tested recovery @@ -129,13 +140,9 @@ login. with the separate experimental session files. It derives shared-library dependencies and requires `gnome-session` and `gtklock`. Build it locally with `OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The -The locally built arm64 package from commit `7b1b46b` is saved at -`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-ibus-release/pantheon-rust-compositor_0.1.0+git7b1b46b_arm64.deb` -(SHA-256 `031b409e6c367487bc97adba375a865c79cb691bd48ea8f8b7e9b4255d3dd885`). -It includes the XWayland runtime dependency and is for isolated qualification; -the build script does not install it. The package contents and dependency -metadata were inspected after building. The IBus Wayland path has not yet been -runtime-qualified, and no display takeover was run. +current package above includes the XWayland runtime dependency and is for +isolated qualification; the build script does not install it. The IBus Wayland +path has not yet been runtime-qualified, and no display takeover was run. ## Dock HDMI boundary From db75b6e22562376f92aa991519406467f1841fb1 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:23:54 -0400 Subject: [PATCH 153/798] Record nested Wingpanel release qualification --- mseries/OS9-HANDOFF.md | 15 +++++++++++++++ mseries/ROADMAP.md | 7 +++++-- mseries/pantheon-rust-compositor/README.md | 8 +++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 8ff75270c..a83210758 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5853,3 +5853,18 @@ visually inspected, the Pantheon shell works end to end, or native scanout, input, locking and recovery. Gala remains the selected session. Next: qualify GTK/Qt apps, IBus commit behavior, and the installed shell clients in the isolated nested session; do not install this package as the active session. + +### Qualify release candidate with installed Wingpanel — 2026-10-19 + +Ran `/usr/bin/io.elementary.wingpanel` in a separate D-Bus/Xvfb session against +the release binary from `619ca5c`, with Wayland forced and a private GSettings +fixture populated from the host's input sources and keyboard settings. The +Wingpanel client trace shows shell v2 `get_panel`, top anchor, `never` hide +mode, and shared-memory pool allocation. A Mutter DisplayConfig +`GetCurrentState` call returned serial 2 and the nested Winit output at +1280×800@60. No panel keyboard assertion occurred. The user-owned Wingpanel +process remained untouched; no Dock binary was run. The package remains +uninstalled. This qualifies protocol connection and panel buffer submission, +not visual inspection or direct keyboard interaction. Next: qualify nested +keyboard delivery and actual IBus commits in a GTK field, then the Dock only +through the approved local GUI policy. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 868023aad..ced2cdcea 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -6,8 +6,11 @@ The `619ca5c` workspace slice is committed and pushed. A current ARM64 release candidate was packaged locally and checked in private Xvfb: Winit and XWayland started, and Zenity appeared in the compositor's live Gala-compatible window snapshot with focus and the expected two-workspace count. This allows broader -nested development qualification to proceed. It is not evidence for native -display takeover or an experimental Pantheon login; those remain unqualified. +nested development qualification to proceed. Installed Wingpanel also +connected in private Xvfb, sent shell v2 panel requests, allocated SHM buffers, +and received a valid nested `GetCurrentState` response. It is not evidence for +direct keyboard input, IBus commits, native display takeover, or an +experimental Pantheon login; those remain unqualified. Working title: `Mutter-Minimal-Viable-Rust_elementaryos-mseries`. The owner clarified that the goal is to identify and implement the compositor features diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 840e99a3e..3d606aa0e 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -118,7 +118,13 @@ reported XWayland ready, and served a Zenity window through `GetWindows` with `app-id=zenity`, focus true and the initial two-workspace count. This qualifies the nested release path for continued development testing only. It does not qualify visible rendering, native DRM scanout, the full Pantheon shell or a -login session. +login session. A second isolated run started Wingpanel against the same release +binary: it sent `get_panel`, top anchor and `never` hide-mode requests, and +allocated SHM buffers without a keyboard assertion. The compositor's +`org.gnome.Mutter.DisplayConfig.GetCurrentState` returned the nested 1280×800 +output. Both runs used private D-Bus/Xvfb and a private GSettings fixture; the +active Wingpanel session was not restarted. These checks do not qualify native +DRM scanout, physical keyboard input, the Dock, or a complete login session. The next gate is staged nested qualification with real GTK/Qt clients and the existing Pantheon shell clients, including Dock launcher/window switching and From a183bb5bf452f4376553d92aa8d844a80c301459 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:33:43 -0400 Subject: [PATCH 154/798] Record nested IBus and keyboard qualification --- mseries/OS9-HANDOFF.md | 25 ++++++++++-- mseries/ROADMAP.md | 7 +++- .../EXPERIENCE-REQUIREMENTS.md | 11 +++-- mseries/pantheon-rust-compositor/README.md | 40 ++++++++++--------- rust/pantheon-compositor/README.md | 7 +++- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index a83210758..507b35368 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -5865,6 +5865,25 @@ mode, and shared-memory pool allocation. A Mutter DisplayConfig 1280×800@60. No panel keyboard assertion occurred. The user-owned Wingpanel process remained untouched; no Dock binary was run. The package remains uninstalled. This qualifies protocol connection and panel buffer submission, -not visual inspection or direct keyboard interaction. Next: qualify nested -keyboard delivery and actual IBus commits in a GTK field, then the Dock only -through the approved local GUI policy. +not visual inspection or direct keyboard interaction. The follow-on input +qualification is recorded below. + +### Qualify nested keyboard and IBus/Mozc commit — 2026-10-19 + +Used the current `619ca5c` release binary under private D-Bus/Xvfb, with +private HOME, cache, GSettings and Mozc config. XTest events sent only to the +Xvfb Winit window reached a focused GTK Wayland Zenity entry, which returned +the exact typed text. With a private Mozc `ibus_config.textproto` selecting +`mozc-jp`, US layout and `active_on_launch: True`, romaji `konnichiha` plus +Space/Return committed exactly `こんにちは` into the GTK field. The +compositor's `FocusWindow` D-Bus action moved the mapped field from unfocused to +focused before entry; the active XKB/IBus source state on the host was not +changed. Mozc documents `active_on_launch` as false by default (see +https://github.com/google/mozc/blob/master/docs/configurations.md#activate-mozc-on-launch). +This qualifies nested keyboard delivery and IBus input-method-v2/text-input-v3 +commit behavior for that client and setting. F5 dictation, host source +switching, physical keyboard input, candidate-window visual behavior and +native session behavior remain open. All temporary compositor, Xvfb, Zenity, +IBus and Mozc processes were cleaned up; the host IBus engine remains +`xkb:us::eng`. Next: qualify more GTK/Qt clients and the F5 app workflow, then +the Dock only through the approved local GUI policy. diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ced2cdcea..6c235bee9 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -9,8 +9,11 @@ snapshot with focus and the expected two-workspace count. This allows broader nested development qualification to proceed. Installed Wingpanel also connected in private Xvfb, sent shell v2 panel requests, allocated SHM buffers, and received a valid nested `GetCurrentState` response. It is not evidence for -direct keyboard input, IBus commits, native display takeover, or an -experimental Pantheon login; those remain unqualified. +physical keyboard input or native display takeover. A private GTK/Wayland +Zenity field now also receives nested key events and commits Japanese text +through IBus/Mozc `mozc-jp` with a private Hiragana-on profile. F5 app behavior, +host source switching, native display/input, and an experimental Pantheon login +remain unqualified. Working title: `Mutter-Minimal-Viable-Rust_elementaryos-mseries`. The owner clarified that the goal is to identify and implement the compositor features diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index ee1c933aa..8920239af 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -35,9 +35,14 @@ presence in Mutter is not by itself a reason to reproduce them in the first cut. On the current host, the configured sources are US XKB and IBus `mozc-jp`, with -US selected. The active F5 dictation workflow uses focus-bound IBus output; -`mozc-jp` composition itself is not currently selected and still needs its own -acceptance check. +US selected. The nested release binary has now passed a GTK Wayland field +commit using `mozc-jp`: romaji `konnichiha` produced `こんにちは`. The test +used a private Mozc profile with `active_on_launch: True` because the documented +default is direct Latin input; it did not change host settings. This qualifies +the compositor's nested IBus/text-input path, not host source switching, +physical keyboard input or the F5 dictation workflow. See the +[Mozc IBus configuration](https://github.com/google/mozc/blob/master/docs/configurations.md#activate-mozc-on-launch) +for the input-mode setting. The 2026-10-19 user-settings snapshot has the screen reader, screen magnifier, and accessibility keyboard features disabled. Toolkit accessibility is enabled diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 3d606aa0e..56150599e 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -36,9 +36,9 @@ options, and repeat settings from GNOME GSettings. This applies the M-series once per second and applies layout, option, and repeat changes live; this path is build-checked but not runtime-qualified. The current source is US; `mozc-jp` is installed but inactive. F5 dictation is an active IBus workflow -and inserts committed Unicode into focused fields; the Rust compositor now -advertises Smithay text-input-v3/input-method-v2 for that path, but still needs -qualification with the running IBus daemon and a native Wayland field. +and inserts committed Unicode into focused fields. The Rust compositor's +nested GTK Wayland field now passed a Mozc `mozc-jp` commit using the private +test profile described below; the native field and F5 workflow remain open. The prototype now starts Smithay's rootless XWayland window manager, maps X11 windows into the shared desktop space, routes move/resize, maximize, @@ -90,14 +90,16 @@ The nested backend is suitable for development and controlled protocol qualification, and the ARM64 `native-drm` package builds on the M-series host. Neither result demonstrates that the session can replace Gala. No isolated native login has been run, so actual scanout, native keyboard/pointer input, -application launch, IBus text commits, Dock behavior, lock/unlock and crash -recovery are still unknown. Installed Wingpanel now binds the shell protocol -and attaches shared-memory panel buffers in an isolated Xvfb run. The panel -keyboard assertion seen with empty settings disappeared after the test used a -private GSettings fixture containing the host's input-source values; this -confirms a fixture issue, while direct keyboard interaction remains untested. -Keep Gala selected and use the Rust compositor only in an -explicitly isolated qualification session after its prerequisites are checked. +Dock behavior, lock/unlock and crash recovery are still unknown. In private +Xvfb, the release binary launched a GTK Wayland Zenity field; synthetic key +events reached it, and IBus/Mozc committed `konnichiha` as `こんにちは` using +the configured `mozc-jp` engine with Hiragana enabled in a private profile. +The same session verified the compositor's focus action was needed before the +field accepted input. This qualifies nested input-method-v2/text-input-v3 +delivery, not the host's source switching, F5 dictation, physical keyboard or +native session. Installed Wingpanel also binds the shell protocol and submits +shared-memory panel buffers in private Xvfb. Keep Gala selected and continue +only in isolated qualification sessions. The compositor now exposes Dock's `org.pantheon.gala.DesktopIntegration` D-Bus interface. Window enumeration, app IDs, focus state, workspace listing, @@ -126,11 +128,12 @@ output. Both runs used private D-Bus/Xvfb and a private GSettings fixture; the active Wingpanel session was not restarted. These checks do not qualify native DRM scanout, physical keyboard input, the Dock, or a complete login session. -The next gate is staged nested qualification with real GTK/Qt clients and the -existing Pantheon shell clients, including Dock launcher/window switching and -IBus focus/commit behavior. After that passes, run the package in a separate native login with a tested recovery -path. Only record it as an experimental session that works after those runtime -gates pass; a successful compile or package build is not that evidence. +The next gate is staged nested qualification with additional GTK/Qt clients +and existing Pantheon shell clients, including Dock launcher/window switching +and the real F5 dictation path. After that passes, run the package in a +separate native login with a tested recovery path. Only record it as an +experimental session that works after those runtime gates pass; a successful +compile or package build is not that evidence. The staged systemd service now retries an unexpected compositor failure with a two-second delay, capped at three starts per minute. After that limit, its @@ -147,8 +150,9 @@ with the separate experimental session files. It derives shared-library dependencies and requires `gnome-session` and `gtklock`. Build it locally with `OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The current package above includes the XWayland runtime dependency and is for -isolated qualification; the build script does not install it. The IBus Wayland -path has not yet been runtime-qualified, and no display takeover was run. +isolated qualification; the build script does not install it. The nested IBus +path is qualified for one GTK/Mozc configuration; native IBus behavior and any +display takeover remain unqualified. ## Dock HDMI boundary diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 6a8966d4c..45d2984fc 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -123,8 +123,11 @@ enumeration and hotplug reconciliation exist; configuration rejects output disabling, mirroring, unsupported monitor properties and nested mode changes. Monitor modeset/rollback and hotplug remain unqualified on hardware. Smithay's text-input-v3 and input-method-v2 globals are now wired for the trusted IBus -daemon, including input-method popup tracking; this path is compile-checked but -still needs a real IBus-to-Wayland field qualification. Accessibility protocol +daemon, including input-method popup tracking. A private Xvfb test with the +release binary and a GTK Wayland Zenity field committed `konnichiha` as +`こんにちは` through IBus/Mozc `mozc-jp` after enabling Hiragana mode in the +private profile. Host source switching, the F5 workflow and native keyboard +input remain unqualified. Accessibility protocol support, screen-sharing portals, touch input and production crash recovery remain unimplemented or unqualified; add them only when an observed Pantheon workflow requires them. From fd04a9107d37de44622a66c19700b8f465f636f5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:45:02 -0400 Subject: [PATCH 155/798] Record M-series cross-GPU import preflight --- mseries/OS9-HANDOFF.md | 19 +++++++++++++++++++ mseries/ROADMAP.md | 20 ++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 507b35368..ff2cd707e 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,24 @@ # OS 9 implementation / Claude Code handoff +## Mutter-minimal viable Rust compositor: cross-GPU import preflight — 2026-10-19 + +Ran the existing release-mode `pantheon-drm-probe` on the M1 Pro with +`native-drm`. It found EGL rendering on Asahi `/dev/dri/card1` and +`/dev/dri/renderD128`; Apple `/dev/dri/card2` owns the connected internal +`eDP-1` at 3024×1964@120 Hz and currently reports the external connectors as +disconnected. The probe allocated 64×64 scanout-capable DMA-BUFs on Asahi and +successfully imported AB30, AR30, AB24 and AR24 into Apple GBM using the +implicit modifier. It did not acquire DRM master, create DRM framebuffers, +modeset, or change output state. This removes the basic GBM-import concern, +but does not prove Apple `ADDFB2` acceptance, scanout, or an interactive native +session. Gala stayed active. Next display gate: establish whether the Apple KMS +device accepts an imported buffer as a framebuffer without committing a mode; +only then plan the separately isolated native-session takeover. + +The release probe rebuilt successfully in 0.82 seconds locally. Working tree +was clean before this handoff update; branch remains +`codex/rust-mutter-pantheon`. + ## Mutter-minimal viable Rust compositor progress — 2026-10-18 On branch `codex/rust-mutter-pantheon`, recent changes reconfigure already diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 6c235bee9..8a883afa2 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -104,14 +104,18 @@ host found the Asahi GPU at `/dev/dri/card1`, Apple display controller at not evidence of compositor modesetting. The opt-in native DRM path is implemented while keeping nested Winit as the default. The `native-drm` Smithay dependency feature now compiles locally after installing -`libseat-dev`. The new non-invasive `pantheon-drm-probe` reports the Asahi -renderer on card1/renderD128, with KMS resource queries unsupported there. Card2 -owns `eDP-1` and `DP-2`, while `HDMI-A-1` is disconnected; card2 has no EGL -device. Therefore cross-device buffer import, display-controller scanout -allocation and failure handling are first-session requirements for this M1 Pro, -not optional general multi-GPU support. The read-only probe did not change -modes. The executable now keeps nested Winit as its default and provides an -explicit `--backend native` path behind `native-drm`; this wires libseat, +`libseat-dev`. The non-invasive `pantheon-drm-probe` reports the Asahi renderer +on card1/renderD128, with KMS resource queries unsupported there. Card2 owns +`eDP-1` and currently reports external connectors disconnected; card2 has no +EGL device. A 2026-10-19 release-mode preflight allocated 64×64 scanout-capable +DMA-BUFs on Asahi and successfully imported AB30, AR30, AB24 and AR24 into +Apple GBM using the implicit modifier. The probe did not acquire DRM master, +create DRM framebuffers, modeset, or change output state. This establishes GBM +import compatibility only; KMS framebuffer creation, actual scanout and +failure handling remain first-session requirements for this M1 Pro, not +optional general multi-GPU support. The executable now keeps nested Winit as +its default and provides an explicit `--backend native` path behind +`native-drm`; this wires libseat, libinput, connected outputs on the first usable KMS device, calloop rendering and vblank completion. It prioritizes eDP and places additional outputs to its right. DisplayConfig no longer advertises a fabricated Winit output in native From 83a663c51dd0b5a162685fc54ad2d947628dcae9 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:48:02 -0400 Subject: [PATCH 156/798] Probe Apple KMS framebuffer import formats --- mseries/OS9-HANDOFF.md | 26 +++--- mseries/ROADMAP.md | 17 ++-- rust/pantheon-compositor/README.md | 17 ++-- .../src/bin/pantheon-drm-probe.rs | 92 ++++++++++++++----- rust/pantheon-compositor/src/native.rs | 1 - 5 files changed, 105 insertions(+), 48 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index ff2cd707e..41e13a627 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,22 +1,24 @@ # OS 9 implementation / Claude Code handoff -## Mutter-minimal viable Rust compositor: cross-GPU import preflight — 2026-10-19 +## Mutter-minimal viable Rust compositor: KMS framebuffer preflight — 2026-10-19 Ran the existing release-mode `pantheon-drm-probe` on the M1 Pro with `native-drm`. It found EGL rendering on Asahi `/dev/dri/card1` and `/dev/dri/renderD128`; Apple `/dev/dri/card2` owns the connected internal `eDP-1` at 3024×1964@120 Hz and currently reports the external connectors as -disconnected. The probe allocated 64×64 scanout-capable DMA-BUFs on Asahi and -successfully imported AB30, AR30, AB24 and AR24 into Apple GBM using the -implicit modifier. It did not acquire DRM master, create DRM framebuffers, -modeset, or change output state. This removes the basic GBM-import concern, -but does not prove Apple `ADDFB2` acceptance, scanout, or an interactive native -session. Gala stayed active. Next display gate: establish whether the Apple KMS -device accepts an imported buffer as a framebuffer without committing a mode; -only then plan the separately isolated native-session takeover. - -The release probe rebuilt successfully in 0.82 seconds locally. Working tree -was clean before this handoff update; branch remains +disconnected. The probe allocated scanout-capable DMA-BUFs on Asahi at the +preferred panel size and imported AB30, AR30, AB24 and AR24 into Apple GBM using +the implicit modifier. Apple KMS rejected AB30 with `EINVAL`, but accepted +AR30, AB24 and AR24 as `ADDFB2` framebuffers; each was immediately removed +without plane attachment. Native output format selection now omits AB30 and +prefers the three formats that passed. The probe did not acquire DRM master, +modeset, or change output state. Gala stayed active. This qualifies buffer +import and framebuffer registration, not plane acceptance, visible scanout or +an interactive native session. The next display gate requires an isolated +session to attempt its first real modeset, with a tested return path. + +The release probe rebuilt successfully in 0.85 seconds locally, and the +native compositor release binary built successfully. Branch: `codex/rust-mutter-pantheon`. ## Mutter-minimal viable Rust compositor progress — 2026-10-18 diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 8a883afa2..defaf023e 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -107,13 +107,16 @@ Smithay dependency feature now compiles locally after installing `libseat-dev`. The non-invasive `pantheon-drm-probe` reports the Asahi renderer on card1/renderD128, with KMS resource queries unsupported there. Card2 owns `eDP-1` and currently reports external connectors disconnected; card2 has no -EGL device. A 2026-10-19 release-mode preflight allocated 64×64 scanout-capable -DMA-BUFs on Asahi and successfully imported AB30, AR30, AB24 and AR24 into -Apple GBM using the implicit modifier. The probe did not acquire DRM master, -create DRM framebuffers, modeset, or change output state. This establishes GBM -import compatibility only; KMS framebuffer creation, actual scanout and -failure handling remain first-session requirements for this M1 Pro, not -optional general multi-GPU support. The executable now keeps nested Winit as +EGL device. A 2026-10-19 release-mode preflight allocated scanout-capable +DMA-BUFs at the preferred 3024×1964 size on Asahi and imported AB30, AR30, AB24 +and AR24 into Apple GBM using the implicit modifier. Apple `ADDFB2` rejected +AB30 with `EINVAL`; it accepted AR30, AB24 and AR24. Each unassigned test +framebuffer was immediately removed. Native output formats now omit AB30 and +prefer the three accepted formats. The probe did not acquire DRM master, attach +a framebuffer to a plane, modeset, or change output state. This establishes +cross-device import and KMS framebuffer registration only; atomic modesetting, +visible scanout and failure handling remain first-session requirements for +this M1 Pro, not optional general multi-GPU support. The executable now keeps nested Winit as its default and provides an explicit `--backend native` path behind `native-drm`; this wires libseat, libinput, connected outputs on the first usable KMS device, calloop rendering diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 45d2984fc..ed32e2754 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -154,12 +154,17 @@ snapshot it finds EGL rendering on `/dev/dri/card1` (`/dev/dri/renderD128`, Asahi) but KMS resource queries there return `Operation not supported`. The Apple display-controller `/dev/dri/card2` owns KMS and the connected internal `eDP-1` at 3024×1964@120 Hz; external connectors are currently disconnected. -Card2 has no EGL device. The probe allocates 64×64 render/scanout GBM buffers on -Asahi and attempts GBM DMA-BUF imports on Apple KMS. Implicit-modifier imports -succeeded for AB30, AR30, AB24 and AR24. This supports the chosen cross-device -path, but does not prove DRM framebuffer creation, atomic modesetting, or -display scanout. The probe does not acquire DRM master, force a connector -reprobe, or change modes. +Card2 has no EGL device. The probe allocates render/scanout GBM buffers on +Asahi at the internal panel's preferred 3024×1964 size and imports them into +Apple GBM. It registers each imported buffer with `ADDFB2` and immediately +removes the unassigned framebuffer. Imports succeeded for AB30, AR30, AB24 and +AR24; framebuffer creation succeeded for AR30, AB24 and AR24, while AB30 was +rejected with `EINVAL`. Native output formats now prefer the three formats +accepted by Apple KMS and omit AB30. This qualifies cross-device import and +framebuffer registration at the preferred panel size. It does not prove plane +acceptance, atomic modesetting, or visible scanout. The probe does not acquire +DRM master, attach a framebuffer to a plane, force a connector reprobe, or +change modes. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs index 44de200ee..d786da2db 100644 --- a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -12,17 +12,24 @@ use smithay::backend::{ }, egl::EGLDevice, }; +use smithay::reexports::drm::control::{Device as _, FbCmd2Flags}; -struct ReadOnlyDrmDevice(File); +struct KmsTarget { + path: PathBuf, + width: u32, + height: u32, +} + +struct DrmDeviceFile(File); -impl AsFd for ReadOnlyDrmDevice { +impl AsFd for DrmDeviceFile { fn as_fd(&self) -> BorrowedFd<'_> { self.0.as_fd() } } -impl smithay::reexports::drm::Device for ReadOnlyDrmDevice {} -impl smithay::reexports::drm::control::Device for ReadOnlyDrmDevice {} +impl smithay::reexports::drm::Device for DrmDeviceFile {} +impl smithay::reexports::drm::control::Device for DrmDeviceFile {} fn main() -> Result<(), Box> { let devices = EGLDevice::enumerate()?; @@ -61,9 +68,9 @@ fn main() -> Result<(), Box> { println!("No EGL devices found."); } - let kms_path = inspect_kms_connectors(hardware_render_path.as_deref())?; - match (hardware_render_path, kms_path) { - (Some(render_path), Some(kms_path)) => probe_cross_gpu_import(&render_path, &kms_path), + let kms_target = inspect_kms_connectors(hardware_render_path.as_deref())?; + match (hardware_render_path, kms_target) { + (Some(render_path), Some(kms_target)) => probe_cross_gpu_import(&render_path, &kms_target), _ => println!( "Cross-GPU GBM import: skipped (could not identify a hardware renderer and connected KMS device)." ), @@ -74,7 +81,7 @@ fn main() -> Result<(), Box> { fn inspect_kms_connectors( render_path: Option<&std::path::Path>, -) -> Result, Box> { +) -> Result, Box> { use smithay::reexports::drm::control::Device as _; let mut cards = std::fs::read_dir("/dev/dri")? @@ -95,7 +102,7 @@ fn inspect_kms_connectors( for path in cards { println!("KMS device {}:", path.display()); let device = match File::open(&path) { - Ok(file) => ReadOnlyDrmDevice(file), + Ok(file) => DrmDeviceFile(file), Err(error) => { println!(" cannot open read-only: {error}"); continue; @@ -123,7 +130,23 @@ fn inspect_kms_connectors( && connector.state() == smithay::reexports::drm::control::connector::State::Connected { - connected_kms.get_or_insert_with(|| path.clone()); + let selected_mode = connector + .modes() + .iter() + .find(|mode| { + mode.mode_type().contains( + smithay::reexports::drm::control::ModeTypeFlags::PREFERRED, + ) + }) + .or_else(|| connector.modes().first()); + if let Some(mode) = selected_mode { + let (width, height) = mode.size(); + connected_kms.get_or_insert_with(|| KmsTarget { + path: path.clone(), + width: u32::from(width), + height: u32::from(height), + }); + } } for mode in connector.modes().iter().take(8) { let (width, height) = mode.size(); @@ -142,12 +165,14 @@ fn inspect_kms_connectors( } /// Check whether the render GPU can allocate a scanout-capable DMA-BUF that -/// the connected KMS device's GBM implementation can import. This does not -/// create a DRM framebuffer, acquire DRM master, or change an output mode. -fn probe_cross_gpu_import(render_path: &PathBuf, kms_path: &PathBuf) { +/// the connected KMS device can import and register as a framebuffer. Created +/// framebuffers are immediately removed; no plane or mode is changed and DRM +/// master is not acquired. +fn probe_cross_gpu_import(render_path: &PathBuf, kms_target: &KmsTarget) { println!("Cross-GPU GBM import preflight:"); println!(" render node: {}", render_path.display()); - println!(" KMS device: {}", kms_path.display()); + println!(" KMS device: {}", kms_target.path.display()); + println!(" preferred mode size: {}x{}", kms_target.width, kms_target.height); let render_file = match OpenOptions::new().read(true).write(true).open(render_path) { Ok(file) => file, @@ -163,14 +188,18 @@ fn probe_cross_gpu_import(render_path: &PathBuf, kms_path: &PathBuf) { return; } }; - let kms_file = match OpenOptions::new().read(true).write(true).open(kms_path) { + let kms_file = match OpenOptions::new() + .read(true) + .write(true) + .open(&kms_target.path) + { Ok(file) => file, Err(error) => { println!(" cannot open KMS device read/write: {error}"); return; } }; - let kms_gbm = match GbmDevice::new(kms_file) { + let kms_gbm = match GbmDevice::new(DrmDeviceFile(kms_file)) { Ok(gbm) => gbm, Err(error) => { println!(" cannot create KMS GBM device: {error}"); @@ -189,7 +218,12 @@ fn probe_cross_gpu_import(render_path: &PathBuf, kms_path: &PathBuf) { Fourcc::Argb8888, ]; for format in formats { - let buffer = match allocator.create_buffer(64, 64, format, &[Modifier::Invalid]) { + let buffer = match allocator.create_buffer( + kms_target.width, + kms_target.height, + format, + &[Modifier::Invalid], + ) { Ok(buffer) => buffer, Err(error) => { println!(" {format:?}: render allocation failed: {error}"); @@ -204,15 +238,29 @@ fn probe_cross_gpu_import(render_path: &PathBuf, kms_path: &PathBuf) { } }; match dmabuf.import_to(&kms_gbm, GbmBufferFlags::SCANOUT) { - Ok(_) => println!( - " {format:?} modifier {:?}: KMS GBM import succeeded", - dmabuf.format().modifier - ), + Ok(kms_buffer) => { + println!( + " {format:?} modifier {:?}: KMS GBM import succeeded", + dmabuf.format().modifier + ); + match kms_gbm.add_planar_framebuffer(&kms_buffer, FbCmd2Flags::empty()) { + Ok(framebuffer) => { + println!(" ADDFB2 registration succeeded"); + match kms_gbm.destroy_framebuffer(framebuffer) { + Ok(()) => println!(" unassigned framebuffer removed"), + Err(error) => println!( + " framebuffer removal failed; it will be released when the KMS fd closes: {error}" + ), + } + } + Err(error) => println!(" ADDFB2 registration failed: {error}"), + } + } Err(error) => println!( " {format:?} modifier {:?}: KMS GBM import failed: {error}", dmabuf.format().modifier ), } } - println!(" framebuffer creation and modesetting were not attempted."); + println!(" No framebuffer was attached to a plane; modesetting was not attempted."); } diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 5e50cfcb4..02f0099c7 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -63,7 +63,6 @@ pub type NativeDrmOutput = DrmOutput = MultiRenderer<'a, 'a, GbmRendererBackend, GbmRendererBackend>; const OUTPUT_FORMATS: &[smithay::backend::allocator::Fourcc] = &[ - smithay::backend::allocator::Fourcc::Abgr2101010, smithay::backend::allocator::Fourcc::Argb2101010, smithay::backend::allocator::Fourcc::Abgr8888, smithay::backend::allocator::Fourcc::Argb8888, From d14595e9a74ba4bcd1ec6be81ae059bb7b63985f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:48:44 -0400 Subject: [PATCH 157/798] Record KMS framebuffer qualification and package --- mseries/OS9-HANDOFF.md | 8 ++++++-- mseries/pantheon-rust-compositor/README.md | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 41e13a627..37d339b06 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -18,8 +18,12 @@ an interactive native session. The next display gate requires an isolated session to attempt its first real modeset, with a tested return path. The release probe rebuilt successfully in 0.85 seconds locally, and the -native compositor release binary built successfully. Branch: -`codex/rust-mutter-pantheon`. +native compositor release binary built successfully. The ARM64 package built +from this source is +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git83a663c_arm64.deb` +(SHA-256 `f34220be364417e92c4dab2aa8b9c3c29b5b1cfc9427c710318b42634684634a`). +Its package metadata, payload paths and dependencies were inspected; it was +not installed. Branch: `codex/rust-mutter-pantheon`. ## Mutter-minimal viable Rust compositor progress — 2026-10-18 diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 56150599e..bc69f6cc9 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -111,11 +111,16 @@ The nested stock Dock 8.3.3 smoke bound the panel protocol and attached buffers, but also logged its known asynchronous workspace-initialization critical, so Dock interaction is still not qualified end to end. -The current ARM64 release candidate from commit `619ca5c` is saved at +The current ARM64 package candidate from commit `83a663c` is saved at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git83a663c_arm64.deb` +(SHA-256 `f34220be364417e92c4dab2aa8b9c3c29b5b1cfc9427c710318b42634684634a`). +Its package contents, dependencies and shared-library links were inspected; +it was not installed. The preflight and native DRM release build match this +candidate's source. The nested runtime evidence below used the earlier package +from commit `619ca5c`, saved at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git619ca5c_arm64.deb` (SHA-256 `aee4c9af0d12c1a2bddbfd45dcd5a79e71afe8c8adc28d9ff46e7cf204d0ea13`). -The package contents, control dependencies and shared-library links were -inspected; it was not installed. Its release binary started in private Xvfb, +The earlier package was not installed. Its release binary started in private Xvfb, reported XWayland ready, and served a Zenity window through `GetWindows` with `app-id=zenity`, focus true and the initial two-workspace count. This qualifies the nested release path for continued development testing only. It does not From e2dc935fe8dd2f8309904c620f0ca0bc2d34cc24 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 09:50:21 -0400 Subject: [PATCH 158/798] Inspect Apple KMS plane format support --- mseries/OS9-HANDOFF.md | 11 ++-- mseries/ROADMAP.md | 4 +- rust/pantheon-compositor/README.md | 5 +- .../src/bin/pantheon-drm-probe.rs | 56 +++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 37d339b06..9c1db216d 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -11,10 +11,13 @@ preferred panel size and imported AB30, AR30, AB24 and AR24 into Apple GBM using the implicit modifier. Apple KMS rejected AB30 with `EINVAL`, but accepted AR30, AB24 and AR24 as `ADDFB2` framebuffers; each was immediately removed without plane attachment. Native output format selection now omits AB30 and -prefers the three formats that passed. The probe did not acquire DRM master, -modeset, or change output state. Gala stayed active. This qualifies buffer -import and framebuffer registration, not plane acceptance, visible scanout or -an interactive native session. The next display gate requires an isolated +prefers the three formats that passed. A read-only query also found AR30, +AB24 and AR24 in each of the three Apple plane format lists; none reported a +current CRTC or framebuffer assignment to this non-master client. The probe +did not acquire DRM master, attach a framebuffer to a plane, modeset, or change +output state. Gala stayed active. This qualifies buffer import, framebuffer +registration and advertised plane formats, not plane routing, visible scanout +or an interactive native session. The next display gate requires an isolated session to attempt its first real modeset, with a tested return path. The release probe rebuilt successfully in 0.85 seconds locally, and the diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index defaf023e..6fa186638 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -114,7 +114,9 @@ AB30 with `EINVAL`; it accepted AR30, AB24 and AR24. Each unassigned test framebuffer was immediately removed. Native output formats now omit AB30 and prefer the three accepted formats. The probe did not acquire DRM master, attach a framebuffer to a plane, modeset, or change output state. This establishes -cross-device import and KMS framebuffer registration only; atomic modesetting, +cross-device import and KMS framebuffer registration. All three DRM plane +objects also advertise AR30, AB24 and AR24, though the non-master query reports +no current CRTC/framebuffer assignment. Plane routing, atomic modesetting, visible scanout and failure handling remain first-session requirements for this M1 Pro, not optional general multi-GPU support. The executable now keeps nested Winit as its default and provides an explicit `--backend native` path behind diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index ed32e2754..220a0a229 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -164,7 +164,10 @@ accepted by Apple KMS and omit AB30. This qualifies cross-device import and framebuffer registration at the preferred panel size. It does not prove plane acceptance, atomic modesetting, or visible scanout. The probe does not acquire DRM master, attach a framebuffer to a plane, force a connector reprobe, or -change modes. +change modes. A follow-up read-only plane query found AR30, AB24 and AR24 in +the advertised format lists of all three Apple KMS planes; the query returned +no current CRTC or framebuffer assignment. This confirms format advertisement +only, not plane routing or acceptance by a real modeset. Relative pointer motion now updates Wayland pointer focus and dispatches motion through active compositor grabs; coordinates are clamped to the mapped output diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs index d786da2db..71c6edd38 100644 --- a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -159,11 +159,67 @@ fn inspect_kms_connectors( Err(error) => println!(" connector {:?}: {error}", handle), } } + + let is_render_device = render_path.is_some_and(|render_path| { + smithay::backend::drm::DrmNode::from_path(render_path) + .ok() + .zip(smithay::backend::drm::DrmNode::from_path(&path).ok()) + .is_some_and(|(render, kms)| render.dev_id() == kms.dev_id()) + }); + if !is_render_device { + inspect_kms_planes(&device); + } } Ok(connected_kms) } +/// Read advertised plane formats from the KMS device. This does not change +/// plane state or attach a framebuffer. +fn inspect_kms_planes(device: &DrmDeviceFile) { + use smithay::reexports::drm::control::Device as _; + + let plane_handles = match device.plane_handles() { + Ok(planes) => planes, + Err(error) => { + println!(" cannot query KMS planes: {error}"); + return; + } + }; + + println!(" KMS planes:"); + for handle in plane_handles { + let plane = match device.get_plane(handle) { + Ok(plane) => plane, + Err(error) => { + println!(" {handle:?}: {error}"); + continue; + } + }; + let supported = [ + (Fourcc::Argb2101010, "AR30"), + (Fourcc::Abgr8888, "AB24"), + (Fourcc::Argb8888, "AR24"), + (Fourcc::Abgr2101010, "AB30"), + ] + .into_iter() + .filter_map(|(format, name)| { + plane + .formats() + .contains(&(format as u32)) + .then_some(name) + }) + .collect::>(); + println!( + " {handle:?}: crtc={:?} framebuffer={:?}, tested formats advertised={:?} ({} total formats)", + plane.crtc(), + plane.framebuffer(), + supported, + plane.formats().len() + ); + } +} + /// Check whether the render GPU can allocate a scanout-capable DMA-BUF that /// the connected KMS device can import and register as a framebuffer. Created /// framebuffers are immediately removed; no plane or mode is changed and DRM From 28fa25a74ee4356c8583950ade849ea0b5affcd0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:00:31 -0400 Subject: [PATCH 159/798] Record first native Rust compositor scanout --- mseries/OS9-HANDOFF.md | 31 +++++++++++++++++++ mseries/ROADMAP.md | 9 ++++-- .../EXPERIENCE-REQUIREMENTS.md | 15 +++++++++ mseries/pantheon-rust-compositor/README.md | 8 +++-- .../session/README.md | 11 ++++--- rust/pantheon-compositor/README.md | 19 +++++++++--- 6 files changed, 79 insertions(+), 14 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 9c1db216d..10847ad53 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,36 @@ # OS 9 implementation / Claude Code handoff +## First native Rust compositor login and scanout — 2026-10-19 + +Installed the ARM64 opt-in session package +`pantheon-rust-compositor_0.1.0+git83a663c_arm64.deb` on the M1 Pro, along +with its missing `gnome-session` and `gtklock` dependencies. Gala's existing +LightDM session entry remains intact. For one isolated native qualification, +LightDM autologin was temporarily pointed at `pantheon-rust-wayland` and a +five-minute systemd rollback timer was armed to remove the override and restart +LightDM back into the original Gala session. + +The experimental GNOME session reached `READY=1`, opened `/dev/dri/card1` and +Apple `/dev/dri/card2`, started XWayland and the installed Pantheon settings +services, and launched the user's existing desktop apps. Kernel DRM state at +`/sys/kernel/debug/dri/2/state` confirms plane 0 on `crtc-0` is scanning out +framebuffer 87, allocated by `pantheon-compositor`, in AR30 at 3024x1964. The +owner reports the desktop appears to work and is usable, with slower-feeling +startup. The top menu/Wingpanel and bottom dock are both missing; neither +process is running, and the experimental session's `RequiredComponents` list +does not include either shell client. This proves first native login and +visible internal-panel scanout, but not full Pantheon readiness, external +display use, input coverage, lock/logout/recovery, performance improvement or +crash-rate improvement. Gala remains the rollback path. + +Also passed an isolated XWayland smoke before native login: an X11 Zenity +window mapped under Xvfb, accepted keyboard input, and appeared in the live +window-list D-Bus query. Clipboard/PRIMARY transfers and broader X11 workflows +remain open. The quality gates now explicitly call for matched Gala/Rust +startup, resource, latency, power and crash/recovery measurements before +claiming the migration is faster, lighter or more reliable. See +[`pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). + ## Mutter-minimal viable Rust compositor: KMS framebuffer preflight — 2026-10-19 Ran the existing release-mode `pantheon-drm-probe` on the M1 Pro with diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 6fa186638..1ed1872f1 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -5,8 +5,13 @@ The `619ca5c` workspace slice is committed and pushed. A current ARM64 release candidate was packaged locally and checked in private Xvfb: Winit and XWayland started, and Zenity appeared in the compositor's live Gala-compatible window -snapshot with focus and the expected two-workspace count. This allows broader -nested development qualification to proceed. Installed Wingpanel also +snapshot with focus and the expected two-workspace count. On 2026-10-19 the +opt-in LightDM session also completed its first native M1 Pro login and scanout: +kernel DRM state showed the compositor's AR30 framebuffer active at 3024x1964, +and the owner confirmed the desktop was usable. Startup felt slow, and Wingpanel +and the bottom dock were absent because the experimental session does not start +them yet. This proves the first native display path, not full Pantheon readiness +or daily-use stability. Installed Wingpanel also connected in private Xvfb, sent shell v2 panel requests, allocated SHM buffers, and received a valid nested `GetCurrentState` response. It is not evidence for physical keyboard input or native display takeover. A private GTK/Wayland diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 8920239af..6071cb357 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -96,6 +96,21 @@ planning against a release. 4. Add conditional capabilities only against named apps/workflows and explicit acceptance checks. Keep Gala available as rollback until owner acceptance. +## Reliability and performance gates + +The migration target includes a measurable quality improvement: faster startup +and interaction, lower steady-state CPU/memory/GPU and power use, and fewer +crashes than Gala on the same M-series hardware. Do not claim those gains from +feature coverage or a single successful login. Capture a Gala baseline and a +Rust result under the same display mode and workloads: cold login-to-usable +time; idle and representative-workload CPU, resident memory, GPU activity and +battery/power draw; frame/input latency where measurable; and crash-free +session duration plus restart/recovery outcomes. Keep dated results and the +workload/environment with each run. Set numerical pass thresholds from the +baseline before calling the migration better or ready for daily use. Track +startup and runtime faults with journal excerpts and crash signatures so +regressions can be tied to a specific branch revision. + The nested prototype serves the Pantheon shell protocol v2, places a panel from its anchor/size/offset, and honors hide modes by tracking maximized/focused or overlapping windows. Pointer motion at the output edge reveals hidden panels; diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index bc69f6cc9..97c52efdc 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -46,9 +46,11 @@ fullscreen, minimize, restore, and activation requests, sends keyboard input to focused X11 clients, and bridges clipboard and PRIMARY selection ownership and transfers. Native startup exports the selected `DISPLAY` through D-Bus and systemd activation environments; the `--run` client waits for XWayland -readiness. XWayland keyboard, clipboard, PRIMARY selection, session activation, -and X11 window behavior compile but still need runtime qualification. The -package depends on the `xwayland` executable. +readiness. An isolated Xvfb smoke mapped an X11 Zenity window, accepted +keyboard input, and exposed the focused window (`zenity`, workspace 0) through +the live window-list D-Bus method. Clipboard/PRIMARY transfers, session +activation, and broader X11 window behavior remain unqualified. The package +depends on the `xwayland` executable. Panel reservations and managed maximize/tile layouts are tracked per output connector. Maximized/fullscreen windows and popup constraints use the window's diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index fcf799e84..1f2cc5215 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -29,11 +29,12 @@ have not been exercised in a live GNOME session. The directory layout mirrors install paths below `usr/`. A distro package can stage the payload under its package root and install it with the compositor binary at `/usr/bin/pantheon-compositor`; the repository's -`../build-deb.sh` creates that arm64 package. Do not add these files to -the active host session until the native backend, login, app startup, lock, -logout and recovery have been qualified on a separate session. Native DRM -startup takes over the display device and has not been runtime-qualified on -this machine. +`../build-deb.sh` creates that arm64 package. On 2026-10-19 the installed opt-in +session completed a native M1 Pro login and first scanout at 3024x1964 using an +AR30 framebuffer. The owner confirmed the desktop became usable. Wingpanel and +the bottom dock were missing, startup felt slow, and app behavior, lock/logout, +recovery, performance and external-monitor use remain unqualified. Keep Gala as +the rollback session while these gates are addressed. The session carries forward the installed Pantheon settings-daemon, keyring, Orca and elementary settings-daemon targets. It intentionally does not promise diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 220a0a229..159c2a9ae 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -29,7 +29,12 @@ cargo run -p pantheon-compositor --features native-drm -- --backend native Native startup acquires the active libseat session and initializes a DRM/KMS output. It takes over the display device, so do not launch it inside the -currently active desktop. It is not yet runtime-qualified on M-series hardware. +currently active desktop. On 2026-10-19 the opt-in LightDM session reached a +real M1 Pro scanout: Apple DRM plane 0 had an active 3024x1964 AR30 framebuffer +allocated by `pantheon-compositor`, and the owner confirmed the desktop was +visible and usable. This is first-login evidence, not daily-use qualification; +the top panel and bottom dock were absent, startup felt slow, and performance, +crash recovery and external-monitor behavior remain open. Fatal DRM event/frame-completion errors end the process with a failure status; three consecutive native render failures do likewise. In the experimental session service this allows systemd's bounded restart and shutdown policy to @@ -45,7 +50,10 @@ The separate Pantheon login-session payload is maintained in [`../../mseries/pantheon-rust-compositor/session/`](../../mseries/pantheon-rust-compositor/session/README.md). Its `PANTHEON_WAYLAND_DISPLAY` environment override selects a fixed socket for the session service; without it the compositor chooses an available socket. -The payload has not been installed or runtime-qualified. +The ARM64 package has been installed on the M1 Pro, and LightDM lists the +separate **Pantheon (Rust compositor, experimental)** session. It reached +native scanout on the M1 Pro, but still lacks a complete shell: Wingpanel and +the bottom dock did not start. Gala remains the rollback session. To exercise the Pantheon protocol against the nested socket, build both binaries and start the compositor with its probe client: @@ -117,8 +125,11 @@ D-Bus/session path still need runtime qualification. Widget, extended-behavior and greeter protocol requests are accepted but currently have no behavior. Rootless XWayland startup, X11 window management, keyboard routing, session -`DISPLAY` propagation, clipboard and PRIMARY selection bridging are implemented -and compile-checked, but still need runtime qualification. Native multi-output +`DISPLAY` propagation, clipboard and PRIMARY selection bridging are implemented. +A private Xvfb smoke mapped an X11 Zenity window, typed `wayland input 42` into +it, and observed its focused `zenity` entry on workspace 0 through the live +window-list D-Bus method. Clipboard/PRIMARY transfer, activation from the +desktop session, and other X11 workflows remain unqualified. Native multi-output enumeration and hotplug reconciliation exist; configuration rejects output disabling, mirroring, unsupported monitor properties and nested mode changes. Monitor modeset/rollback and hotplug remain unqualified on hardware. Smithay's From b413d3360a96ba546eabf910318346a7efc913b2 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:04:49 -0400 Subject: [PATCH 160/798] Document native session rollback and shell gaps --- mseries/OS9-HANDOFF.md | 17 +++++++++++++++++ mseries/ROADMAP.md | 16 +++++++++------- mseries/pantheon-rust-compositor/README.md | 7 +++++++ .../pantheon-rust-compositor/session/README.md | 9 ++++++--- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 10847ad53..c15aea7c6 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -23,6 +23,23 @@ visible internal-panel scanout, but not full Pantheon readiness, external display use, input coverage, lock/logout/recovery, performance improvement or crash-rate improvement. Gala remains the rollback path. +The installed desktop shell contract is `/etc/xdg/io.elementary.desktop.wm.shell`: +desktop mode names `io.elementary.wingpanel` and `io.elementary.dock`, with +`wait-for-n-panels=1`. The experimental session launched neither client, and +its required-components list contains neither. The Rust session needs an +equivalent managed shell-launch path plus a desktop background before it can +replace Gala for normal Pantheon use. + +The five-minute rollback fired at 10:00:55 EDT. LightDM removed the temporary +override and auto-logged into the original `pantheon-wayland` session; current +logind state identifies that session, with Gala and Wingpanel running and the +Rust compositor stopped. The owner reports the wallpaper/background is present +again in Gala, while the Rust session had no colored background. This completes +the bounded native-login trial and verifies the automatic return path. The +experimental session still needs its wallpaper/background, Wingpanel and +Dock startup integration investigated and wired in before another full Pantheon +login attempt. + Also passed an isolated XWayland smoke before native login: an X11 Zenity window mapped under Xvfb, accepted keyboard input, and appeared in the live window-list D-Bus query. Clipboard/PRIMARY transfers and broader X11 workflows diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 1ed1872f1..72f6c9460 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -8,17 +8,19 @@ started, and Zenity appeared in the compositor's live Gala-compatible window snapshot with focus and the expected two-workspace count. On 2026-10-19 the opt-in LightDM session also completed its first native M1 Pro login and scanout: kernel DRM state showed the compositor's AR30 framebuffer active at 3024x1964, -and the owner confirmed the desktop was usable. Startup felt slow, and Wingpanel -and the bottom dock were absent because the experimental session does not start -them yet. This proves the first native display path, not full Pantheon readiness -or daily-use stability. Installed Wingpanel also +and the owner confirmed the desktop was usable. Startup felt slow; Wingpanel, +the bottom dock and a colored wallpaper/background were absent in the Rust +session. A five-minute rollback returned LightDM to Gala, where the owner +reported the background had returned. The native display path and automatic +rollback are proven, but full Pantheon readiness and daily-use stability are +not. Installed Wingpanel also connected in private Xvfb, sent shell v2 panel requests, allocated SHM buffers, and received a valid nested `GetCurrentState` response. It is not evidence for -physical keyboard input or native display takeover. A private GTK/Wayland +physical keyboard input or full shell integration. A private GTK/Wayland Zenity field now also receives nested key events and commits Japanese text through IBus/Mozc `mozc-jp` with a private Hiragana-on profile. F5 app behavior, -host source switching, native display/input, and an experimental Pantheon login -remain unqualified. +host source switching, native input and full experimental Pantheon shell +integration remain unqualified. Working title: `Mutter-Minimal-Viable-Rust_elementaryos-mseries`. The owner clarified that the goal is to identify and implement the compositor features diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 97c52efdc..8facf8b8f 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -14,6 +14,13 @@ Anything beyond those needs is conditional on a named app or workflow. The prioritized list and upstream feature radar are in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md). +On 2026-10-19 the installed experimental session completed its first native +M1 Pro login and scanned out an AR30 3024x1964 framebuffer. The owner reported +that startup felt slow; the Rust session had no Wingpanel, bottom dock, or +colored wallpaper/background. A timed LightDM rollback returned to Gala and +restored the background. This proves the native display path and rollback, +not a complete Pantheon session or a performance/stability improvement. + The nested prototype now handles the existing half-snap chords (`Super+Left` and `Super+Right`) and quarter-snap chords (`Super+Alt+U/I/J/K` plus the existing Super+keypad aliases). It computes rectangles from the focused diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 1f2cc5215..85ec4c7dc 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -32,9 +32,12 @@ binary at `/usr/bin/pantheon-compositor`; the repository's `../build-deb.sh` creates that arm64 package. On 2026-10-19 the installed opt-in session completed a native M1 Pro login and first scanout at 3024x1964 using an AR30 framebuffer. The owner confirmed the desktop became usable. Wingpanel and -the bottom dock were missing, startup felt slow, and app behavior, lock/logout, -recovery, performance and external-monitor use remain unqualified. Keep Gala as -the rollback session while these gates are addressed. +the bottom dock and colored wallpaper/background were missing, and startup felt +slow. The five-minute LightDM rollback was exercised successfully and returned +to Gala; the owner reports its background was visible again. App behavior, +lock/logout, recovery under compositor failure, performance and external-monitor +use remain unqualified. Keep Gala as the rollback session while these gates are +addressed. The session carries forward the installed Pantheon settings-daemon, keyring, Orca and elementary settings-daemon targets. It intentionally does not promise From 8bfb038bd088331339d0229d75bf47f3a4276ecf Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:10:36 -0400 Subject: [PATCH 161/798] Document compositor stack and session readiness --- mseries/ROADMAP.md | 5 +- .../ARCHITECTURE-AND-STATUS.md | 147 ++++++++++++++++++ .../EXPERIENCE-REQUIREMENTS.md | 16 +- mseries/pantheon-rust-compositor/README.md | 75 +++++---- mseries/pantheon-rust-compositor/build-deb.sh | 2 +- .../session/README.md | 10 ++ .../session.conf | 2 + .../lib/systemd/user/pantheon-dock.service | 15 ++ .../systemd/user/pantheon-wingpanel.service | 15 ++ 9 files changed, 246 insertions(+), 41 deletions(-) create mode 100644 mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md create mode 100644 mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service create mode 100644 mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 72f6c9460..ec13a1fd3 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -101,8 +101,9 @@ updates pointer focus and drives the compositor's existing move/resize grabs; xdg popup requests now install Smithay keyboard/pointer grabs for managed toplevel roots. Both features build locally; popup behavior has not been runtime-qualified. Autohide, blur, multi-output reflow, monitor mode -application, Dock behavior, full session integration and M-series hardware -remain unqualified. A public search found elementary's +application, Dock behavior and full session integration remain unqualified. +The first internal-panel native scanout is confirmed; external output, input, +stability and comparative performance work remains. A public search found elementary's [Gala](https://github.com/elementary/gala) still based on libmutter, with no first-party Rust compositor to reuse. The WD22TB4 HDMI G3 DCP/kernel blocker remains a separate workstream. Read-only enumeration on the current M-series diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md new file mode 100644 index 000000000..bf6420316 --- /dev/null +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -0,0 +1,147 @@ +# GUI stack comparison and development status + +This page shows where the compositor sits in the desktop stack, how the +M-series prototype differs from the current Pantheon stack, and what the +native development session has proved so far. It is a working architecture +map, not a claim of feature parity. + +## Existing elementary OS / Pantheon on M-series + +Gala supplies Pantheon's window manager and compositor integration on top of +libmutter. Mutter provides the Wayland display-server work and delegates +device-level display support to Linux KMS/input drivers. Applications and +shell surfaces are Wayland clients; X11 applications use XWayland. On this +M1 Pro, rendering and display scanout use different DRM devices. + +```mermaid +flowchart TD + Apps[GTK / Qt / other Wayland apps] + XApps[X11 apps] + Shell[Pantheon shell clients\nWingpanel + Dock + background] + Xwayland[XWayland] + Gala[Gala + libmutter\nPantheon window manager, shell integration\n+ Wayland compositor] + KMS[Linux DRM / KMS + libinput] + Asahi[Asahi GPU\ncard1 / renderD128\nrendering] + Apple[Apple display controller\ncard2 / KMS] + DCP[DCP firmware / kernel driver] + Panel[Built-in display / external display path] + + Apps -->|Wayland| Gala + XApps --> Xwayland --> Gala + Shell -->|Wayland + Pantheon shell protocols| Gala + Gala -->|input + display requests| KMS + Gala -->|render buffers / DMA-BUF| Asahi + Asahi -->|imported scanout buffer| Apple + KMS --> Apple --> DCP --> Panel +``` + +The diagram is simplified: Gala is built against libmutter and uses its +compositor APIs, so replacing Mutter under Gala is not a binary-compatible +swap. Our Rust prototype keeps the existing shell applications as clients and +implements the small set of compositor and shell-facing contracts it needs. +The M-series graphics path is also layered: the compositor cannot replace +missing kernel or firmware display support. The Asahi M1 feature matrix lists +both GPU and DCP support in `linux-asahi`. + +## Omarchy running on Asahi + +Omarchy is an Arch-based desktop setup built around Hyprland and Quickshell. +Its manual describes running it on Asahi Alarm on M1/M2 as a community +workaround that takes effort; this is a different shell and workflow from +Pantheon, not a Pantheon compositor port. + +```mermaid +flowchart TD + Apps[Wayland apps] + XApps[X11 apps] + Shell[Omarchy shell / widgets\nQuickshell] + Xwayland[XWayland] + Hyprland[Hyprland\nWayland compositor + tiling WM] + Arch[Arch userspace\nAsahi Alarm base] + Kernel[Asahi Linux kernel\nDRM / input drivers] + GPU[Asahi GPU] + DCP[DCP firmware / kernel driver] + Displays[Built-in / supported external displays] + + Apps -->|Wayland| Hyprland + XApps --> Xwayland --> Hyprland + Shell -->|Wayland clients| Hyprland + Hyprland --> Arch --> Kernel + Kernel --> GPU + Kernel --> DCP --> Displays +``` + +## Proposed minimal viable Pantheon compositor for M-series + +Keep the Pantheon apps, shell clients, and settings services. Replace the +Gala/libmutter compositor layer with a Rust/Smithay process that speaks +Wayland, implements the Pantheon shell protocol and required GNOME session +bridges, and uses the existing Asahi rendering and Apple KMS path. Session +management starts the compositor before Wingpanel and Dock. Wallpaper, +lock-screen UI, accessibility, sharing, and other services remain separate +desktop components; the compositor must provide the protocols and hooks their +required workflows need. + +```mermaid +flowchart TD + Apps[Existing Pantheon / GTK / Qt apps] + XApps[X11 apps] + Shell[Existing Pantheon shell clients\nWingpanel + Dock + wallpaper] + Services[GNOME / Pantheon session services\nsettings, IBus, accessibility, sharing, lock UI] + Xwayland[XWayland] + Session[GNOME session + systemd user units] + Rust[Minimal viable Rust compositor\nSmithay + Wayland + Pantheon shell / Mutter-compatible bridges] + Render[Asahi GPU renderer\ncard1 / renderD128] + Scanout[Apple DRM / KMS\ncard2] + DCP[DCP firmware / kernel driver] + Displays[Built-in / external displays] + + Apps -->|Wayland| Rust + XApps --> Xwayland --> Rust + Shell -->|Wayland + Pantheon shell protocol| Rust + Services <-->|required compositor bridges| Rust + Session -->|start compositor, then shell clients| Rust + Session --> Shell + Rust -->|render buffers / DMA-BUF| Render + Render -->|imported scanout buffer| Scanout + Rust -->|modesetting / input via kernel| Scanout + Scanout --> DCP --> Displays +``` + +**Current implementation detail:** the session file starts the compositor; +new user units are staged to start Wingpanel and Dock after it. `After=` orders +process startup but does not yet wait for the panel to map or prove the +`wait-for-n-panels=1` shell-launcher contract. The units are included in a local +ARM64 package build but are not installed or runtime-qualified. The Rust +session also does not yet provide the +wallpaper/background path. + +## What is complete and what has been tested + +| Area | Evidence | State | +| --- | --- | --- | +| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; local ARM64 package build includes the staged shell startup units; compositor remains separate from the default Gala session | Builds; shell service runtime not yet qualified | +| Native display path | Experimental LightDM login reached the M1 Pro internal display; kernel DRM state showed compositor-owned AR30 framebuffer at 3024×1964 | Native internal-panel scanout passed once | +| Session recovery | Five-minute LightDM rollback returned to Gala; Wingpanel and background came back | Automatic rollback passed once | +| XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | +| Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | +| Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, and attached shared-memory buffers | Protocol smoke passed; panel absent from native session | +| Native full desktop | Owner observed slow startup, no top panel, no bottom dock, and no colored background during the Rust login | Incomplete; shell launch and wallpaper path remain open | +| Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | +| Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | + +The current login observation is useful development evidence, not a daily-use +readiness result. Continue testing in the isolated experimental session while +Gala remains the working fallback. The next development gate is to qualify +Wingpanel/Dock startup in a nested session and implement the background path, +then qualify shell startup and basic interaction before another native login. +After that, measure the same startup, idle and representative workloads under +Gala and Rust, and record crashes and recovery behavior. + +## References + +- [Gala upstream README](https://github.com/elementary/gala/blob/main/README.md) +- [Mutter upstream README](https://github.com/GNOME/mutter/blob/main/README.md) +- [Omarchy manual](https://omarchy.org/manual/) +- [Omarchy on Asahi manual](https://omarchy.org/manual/omarchy-on/) +- [Asahi Linux M1 feature support](https://github.com/AsahiLinux/docs/blob/main/docs/platform/feature-support/m1.md) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 6071cb357..0a9639d07 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -154,15 +154,17 @@ retains a window, and a later valid activation request restores it. Minimize / restore behavior remains runtime-unverified. It also implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered -`Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration, -physical media-key activation and M-series hardware remain unqualified; this -is not full session or hardware qualification. +`Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration +and physical media-key activation remain unqualified. The first M1 Pro scanout +passed in an isolated native login, but one scanout is not full session or +hardware qualification. The compositor now implements Smithay's `ext-session-lock-v1` protocol and the observed `org.gnome.ScreenSaver` action interface. Lock requests suppress application rendering, clear active grabs, route input to the lock surface, and confirm the lock only after each mapped output presents a cleared frame. -`Lock` starts a configured locker (default `gtklock`), but that package is not -installed by this source tree and no locker has been connected. Authentication, -runtime input/visual checks, native login and crash recovery remain open -acceptance items. +`Lock` starts a configured locker (default `gtklock`). The source tree does not +install the locker; the experimental ARM64 package includes the Ubuntu +`gtklock` dependency, but lock/unlock has not been exercised. Authentication, +runtime input/visual checks, full native session behavior and crash recovery +remain open acceptance items. diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 8facf8b8f..771ceec29 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -13,13 +13,19 @@ keep providing panel, dock and settings UI through a narrow shell contract. Anything beyond those needs is conditional on a named app or workflow. The prioritized list and upstream feature radar are in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md). +For a stack drawing comparing existing elementary OS, Omarchy on Asahi, and +the proposed Pantheon MVP—along with a tested-versus-open status table—see +[`ARCHITECTURE-AND-STATUS.md`](ARCHITECTURE-AND-STATUS.md). On 2026-10-19 the installed experimental session completed its first native M1 Pro login and scanned out an AR30 3024x1964 framebuffer. The owner reported that startup felt slow; the Rust session had no Wingpanel, bottom dock, or colored wallpaper/background. A timed LightDM rollback returned to Gala and -restored the background. This proves the native display path and rollback, -not a complete Pantheon session or a performance/stability improvement. +restored the background. Separate systemd user services have since been staged +to launch Wingpanel and Dock after the compositor. A local ARM64 package build +now includes those units; they are not installed or runtime-qualified. This +proves the native display path and rollback, not a +complete Pantheon session or a performance/stability improvement. The nested prototype now handles the existing half-snap chords (`Super+Left` and `Super+Right`) and quarter-snap chords (`Super+Alt+U/I/J/K` plus the @@ -86,29 +92,33 @@ required by the M-series/Pantheon session, in independently qualified slices. [`rust/pantheon-compositor`](../../rust/pantheon-compositor/README.md) has an opt-in nested Smithay/Winit backend for development and an explicit native DRM -backend for M-series qualification. It supports the Pantheon shell contract, -window basics, monitor configuration and lock bridge, but native display, -session, input and recovery behavior still need on-device qualification. The -staged login-session files are separate from the active Gala session and are -not installed or selected by default. +backend for M-series qualification. It supports the Pantheon shell protocol, +window basics, monitor configuration and lock bridge. The installed opt-in +session completed a first native display scanout on the M1 Pro, while full +shell, input and recovery behavior still need qualification. The staged session +remains separate from Gala and is not selected by default. ### Readiness decision (2026-10-19) -This branch is **not yet ready to use as an experimental Pantheon session**. -The nested backend is suitable for development and controlled protocol -qualification, and the ARM64 `native-drm` package builds on the M-series host. -Neither result demonstrates that the session can replace Gala. No isolated -native login has been run, so actual scanout, native keyboard/pointer input, -Dock behavior, lock/unlock and crash recovery are still unknown. In private -Xvfb, the release binary launched a GTK Wayland Zenity field; synthetic key -events reached it, and IBus/Mozc committed `konnichiha` as `こんにちは` using -the configured `mozc-jp` engine with Hiragana enabled in a private profile. -The same session verified the compositor's focus action was needed before the -field accepted input. This qualifies nested input-method-v2/text-input-v3 -delivery, not the host's source switching, F5 dictation, physical keyboard or -native session. Installed Wingpanel also binds the shell protocol and submits -shared-memory panel buffers in private Xvfb. Keep Gala selected and continue -only in isolated qualification sessions. +This branch is **not ready to replace Gala for daily Pantheon use**. On +2026-10-19, an isolated five-minute LightDM login completed and produced the +first native scanout on the M1 Pro: plane 0 used the compositor's AR30 +framebuffer at 3024x1964. The owner confirmed the desktop was usable but slow +to appear. Wingpanel, Dock and a colored wallpaper/background were missing. +The timed rollback returned to Gala and restored the background. This qualifies +native startup, internal-panel scanout and automatic rollback only; native +keyboard/pointer workflows, complete shell integration, external displays, +lock/unlock and compositor-failure recovery remain open. + +In private Xvfb, the release binary launched a GTK Wayland Zenity field; +synthetic key events reached it, and IBus/Mozc committed `konnichiha` as +`こんにちは` using `mozc-jp` with Hiragana enabled in a private profile. This +qualifies one nested input-method-v2/text-input-v3 workflow, not host source +switching, F5 dictation or native input. XWayland also passed an isolated X11 +Zenity window and keyboard smoke; clipboard/PRIMARY remain unqualified. +Installed Wingpanel binds the shell protocol and submits SHM panel buffers in +private Xvfb, but the Rust session did not launch it. Keep Gala selected while +the shell launcher and background path are implemented and runtime-qualified. The compositor now exposes Dock's `org.pantheon.gala.DesktopIntegration` D-Bus interface. Window enumeration, app IDs, focus state, workspace listing, @@ -120,12 +130,14 @@ The nested stock Dock 8.3.3 smoke bound the panel protocol and attached buffers, but also logged its known asynchronous workspace-initialization critical, so Dock interaction is still not qualified end to end. -The current ARM64 package candidate from commit `83a663c` is saved at +The ARM64 package from commit `83a663c` is saved at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git83a663c_arm64.deb` (SHA-256 `f34220be364417e92c4dab2aa8b9c3c29b5b1cfc9427c710318b42634684634a`). Its package contents, dependencies and shared-library links were inspected; -it was not installed. The preflight and native DRM release build match this -candidate's source. The nested runtime evidence below used the earlier package +it was installed with `gnome-session` and `gtklock` on 2026-10-19 and selected +once through LightDM. The compositor source is unchanged from `83a663c`; the +later commits in this branch update the DRM probe and documentation. The nested +runtime evidence below used the earlier package from commit `619ca5c`, saved at `/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/pantheon-workspace-release/pantheon-rust-compositor_0.1.0+git619ca5c_arm64.deb` (SHA-256 `aee4c9af0d12c1a2bddbfd45dcd5a79e71afe8c8adc28d9ff46e7cf204d0ea13`). @@ -142,12 +154,13 @@ output. Both runs used private D-Bus/Xvfb and a private GSettings fixture; the active Wingpanel session was not restarted. These checks do not qualify native DRM scanout, physical keyboard input, the Dock, or a complete login session. -The next gate is staged nested qualification with additional GTK/Qt clients -and existing Pantheon shell clients, including Dock launcher/window switching -and the real F5 dictation path. After that passes, run the package in a -separate native login with a tested recovery path. Only record it as an -experimental session that works after those runtime gates pass; a successful -compile or package build is not that evidence. +The next gate is to nested-qualify the staged Wingpanel and Dock startup against +`/etc/xdg/io.elementary.desktop.wm.shell`, and implement a desktop background +path. Then qualify shell rendering, Dock launcher/window switching, +GTK/Qt clients and real F5 dictation. Package those changes before another +native login. Keep the measured startup, resource, latency, power and +crash/recovery comparison against Gala open; one successful login does not +establish the migration as faster, lighter or more reliable. The staged systemd service now retries an unexpected compositor failure with a two-second delay, capped at three starts per minute. After that limit, its diff --git a/mseries/pantheon-rust-compositor/build-deb.sh b/mseries/pantheon-rust-compositor/build-deb.sh index 7ec4b37ce..c806d562a 100755 --- a/mseries/pantheon-rust-compositor/build-deb.sh +++ b/mseries/pantheon-rust-compositor/build-deb.sh @@ -59,7 +59,7 @@ Package: pantheon-rust-compositor Version: $VERSION Architecture: arm64 Maintainer: techmore -Depends: $SHLIB_DEPENDS, gnome-session, gtklock, libegl1, libgles2, xwayland +Depends: $SHLIB_DEPENDS, gnome-session, gtklock, io.elementary.dock, io.elementary.wingpanel, libegl1, libgles2, xwayland Description: experimental Rust compositor for the Pantheon M-series session An opt-in Smithay Wayland compositor and a separate experimental GNOME session. EOF diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 85ec4c7dc..3fdae6b8a 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -39,6 +39,16 @@ lock/logout, recovery under compositor failure, performance and external-monitor use remain unqualified. Keep Gala as the rollback session while these gates are addressed. +The default desktop shell entries in `/etc/xdg/io.elementary.desktop.wm.shell` +name `io.elementary.wingpanel` and `io.elementary.dock`. The experimental +session now stages separate systemd user services for these clients, ordered +after compositor readiness and Wingpanel-before-Dock. A local ARM64 package +build includes the new units, but they have not been installed or run in a live +session. The +desktop background is also missing; its provider/rendering path remains to be +implemented. The installed five-minute rollback was exercised and returned +LightDM to Gala. + The session carries forward the installed Pantheon settings-daemon, keyring, Orca and elementary settings-daemon targets. It intentionally does not promise that those services or the compositor are fully compatible; this is an diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf index 8cf14faef..1dac19b4c 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/gnome-session@pantheon-rust-wayland.target.d/session.conf @@ -19,5 +19,7 @@ Wants=gnome-keyring-daemon.socket Wants=gcr-ssh-agent.socket Wants=orca.service Wants=io.elementary.settings-daemon.service +Wants=pantheon-wingpanel.service +Wants=pantheon-dock.service Requires=pantheon-rust-compositor.target diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service new file mode 100644 index 000000000..33bebc1a4 --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service @@ -0,0 +1,15 @@ +[Unit] +Description=Elementary Dock for the experimental Pantheon Rust session +BindsTo=pantheon-rust-compositor@wayland.service +After=pantheon-rust-compositor@wayland.service pantheon-wingpanel.service +PartOf=gnome-session-initialized.target +ConditionEnvironment=XDG_SESSION_TYPE=wayland +StartLimitIntervalSec=60s +StartLimitBurst=3 + +[Service] +Type=simple +ExecStart=/usr/bin/io.elementary.dock +Restart=on-failure +RestartSec=2s +TimeoutStopSec=5s diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service new file mode 100644 index 000000000..23b9eb0db --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service @@ -0,0 +1,15 @@ +[Unit] +Description=Wingpanel for the experimental Pantheon Rust session +BindsTo=pantheon-rust-compositor@wayland.service +After=pantheon-rust-compositor@wayland.service +PartOf=gnome-session-initialized.target +ConditionEnvironment=XDG_SESSION_TYPE=wayland +StartLimitIntervalSec=60s +StartLimitBurst=3 + +[Service] +Type=simple +ExecStart=/usr/bin/io.elementary.wingpanel +Restart=on-failure +RestartSec=2s +TimeoutStopSec=5s From fa563ce8cbe20475f77bfae873f980973489ac81 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:13:59 -0400 Subject: [PATCH 162/798] codexbar: shrink panel bar font (8.5->7.5pt, secondary 6.5pt) Co-Authored-By: Claude Sonnet 5.5 --- mseries/codexbar-wingpanel/Indicator.vala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 39070ea6a..2aa801dc7 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -206,10 +206,10 @@ namespace MSeries { ".mseries-codexbar button.compact { padding: 1px 8px; min-height: 0; min-width: 0; } " + ".mseries-codexbar button.flat.compact { padding: 0 4px; } " + ".mseries-codexbar .mseries-switcher button { padding: 2px 6px; } " + - ".mseries-codexbar-bar label { font-size: 8.5pt; font-feature-settings: \"tnum\"; } " + + ".mseries-codexbar-bar label { font-size: 7.5pt; font-feature-settings: \"tnum\"; } " + ".mseries-codexbar-bar .seg-value { font-weight: 700; } " + - ".mseries-codexbar-bar .seg-reset { font-size: 7.5pt; opacity: 0.7; } " + - ".mseries-codexbar-bar .seg-secondary { font-size: 7.5pt; } " + + ".mseries-codexbar-bar .seg-reset { font-size: 6.5pt; opacity: 0.7; } " + + ".mseries-codexbar-bar .seg-secondary { font-size: 6.5pt; } " + ".mseries-codexbar-bar .level-low { color: @warning_color; } " + ".mseries-codexbar-bar .level-critical { color: @error_color; } " + ".mseries-codexbar-bar .mark-claude { color: #d97757; } " + From a6177c4e0f0a75bacb0c2bb2e9891b8f2f68e632 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:14:22 -0400 Subject: [PATCH 163/798] docs: record CodexBar panel font shrink Co-Authored-By: Claude Sonnet 5.5 --- mseries/CODEXBAR-PORT.md | 7 +++++++ mseries/OS9-HANDOFF.md | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/mseries/CODEXBAR-PORT.md b/mseries/CODEXBAR-PORT.md index 7037fec97..e2aa4f14f 100644 --- a/mseries/CODEXBAR-PORT.md +++ b/mseries/CODEXBAR-PORT.md @@ -182,3 +182,10 @@ expected approval URL in a live begin/cancel smoke test with saved IDs preserved second-account authorization still requires owner browser approval. Account activation was checked with OpenCode v2.0.32, including a live same-account saved-session round trip with the original selection restored. [Official account commands](https://dev.opencode.ai/v2/docs/cli/commands/) document the activation API. + +## Panel bar font size + +The text shown in the Wingpanel bar is styled in `install_style` in `codexbar-wingpanel/Indicator.vala` +(`.mseries-codexbar-bar` rules). It was shrunk on 2026-10-19 because it rendered too large next to the other +indicators: main label 8.5pt -> 7.5pt, `.seg-reset` and `.seg-secondary` 7.5pt -> 6.5pt. The popover text +(`.mseries-codexbar`, 9pt) is unchanged. To resize again, edit those values and run `bash mseries/deploy-codexbar.sh`. diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index b7602b6d1..31d5651a4 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,13 @@ # OS 9 implementation / Claude Code handoff +## CodexBar panel font shrink — 2026-10-19 (Claude Code) + +Owner reported the CodexBar bar text was too large (it may also have made the Applications menu hard to +open). Changed `.mseries-codexbar-bar` sizes in `codexbar-wingpanel/Indicator.vala`: label 8.5->7.5pt, +reset/secondary 7.5->6.5pt. Tests pass, `deploy-codexbar.sh` ran (Wingpanel and CodexBar restarted), owner +confirmed it looks right; pushed as `fa563ce`. **Open:** whether the Applications menu not opening is fully +fixed is not confirmed; the dock (`io.elementary.dock`) is still stopped. Documented in `CODEXBAR-PORT.md`. + ## Phosphene local media feasibility — 2026-10-19 Reviewed Phosphene `v4.19.5` at `287a964` against the 32 GB M1 Pro/T6000 and From e0d63c2fb2b3f3bbabfadf071bf0a80d953d9c6e Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:20:24 -0400 Subject: [PATCH 164/798] Machine Lab: full screen with clear exit controls (F11, Ctrl+Alt+F, floating bar); macOS (experimental) attempt page; roadmap Co-Authored-By: Claude Sonnet 5.5 --- mseries/machine-lab/ROADMAP.md | 36 ++++ mseries/machine-lab/data/style.css | 13 ++ mseries/machine-lab/meson.build | 2 +- mseries/machine-lab/src/macexp.vala | 246 ++++++++++++++++++++++++++++ mseries/machine-lab/src/main.vala | 41 ++++- mseries/machine-lab/src/page.vala | 14 ++ mseries/machine-lab/src/viewer.vala | 80 ++++++++- 7 files changed, 424 insertions(+), 8 deletions(-) create mode 100644 mseries/machine-lab/ROADMAP.md create mode 100644 mseries/machine-lab/src/macexp.vala diff --git a/mseries/machine-lab/ROADMAP.md b/mseries/machine-lab/ROADMAP.md new file mode 100644 index 000000000..f0936a479 --- /dev/null +++ b/mseries/machine-lab/ROADMAP.md @@ -0,0 +1,36 @@ +# Machine Lab roadmap + +Tracked as GitHub issues in `techmore/ElementaryOS_MSeries`. Status as of 2026-10-19. + +| Area | Issue | Status | +| --- | --- | --- | +| Display tab (frames, mouse, keyboard) | merged (#258, #259) | done; Wayland keyboard path unverified | +| Full screen with clear exit controls | this PR | done: F11, Ctrl+Alt+F (works while the keyboard is captured), floating Exit Full Screen / Release Keyboard bar | +| Bidirectional clipboard (text) | [#263](https://github.com/techmore/ElementaryOS_MSeries/issues/263) | not started: vdagent channel + guest agent | +| Object copy/paste, drag-and-drop, file transfer | [#264](https://github.com/techmore/ElementaryOS_MSeries/issues/264) | not started: needs #263 | +| Windows: virtio-win drivers, unattended install | [#265](https://github.com/techmore/ElementaryOS_MSeries/issues/265) | manual procedure documented; network driver investigated | +| Display: resize to window, cursor, audio, faster stream, WebRTC | [#266](https://github.com/techmore/ElementaryOS_MSeries/issues/266) | not started | +| macOS (experimental) | [#267](https://github.com/techmore/ElementaryOS_MSeries/issues/267) | attempt page done and shows how it fails; emulation route not attempted | + +## Windows network driver: what the investigation found + +In the Windows 11 ARM64 test VM the OOBE page "Let's connect you to a network" reports "No network drivers found" even after +the NetKVM driver is installed from `D:\NetKVM\w11\ARM64` through its **Install driver** button. The driver does load: + +- `pnputil /add-driver D:\NetKVM\w11\ARM64\*.inf /install` reports `netkvm.inf` "already exists in the system" and + "up-to-date on device PCI\VEN_1AF4&DEV_1000" (the virtio NIC). +- `ipconfig` shows `10.0.2.15` with gateway `10.0.2.2` (QEMU user-mode NAT), and `ping 10.0.2.2` answers. +- Pressing **Next** continues OOBE to "Checking for updates", so the network is usable; the page text is stale. + +So the "network driver" problem is a stale OOBE page plus a manual driver step, not a missing driver. The virtio-win ISO also +has `viogpu`, `vioinput`, `Balloon` and the guest agent for ARM64, which are not installed yet (see #265). + +## macOS: what the experiment shows + +The macOS (experimental) page in the app runs real commands and records the output (this Mac, 2026-10-19): + +1. `ls /System/Library/Frameworks/Virtualization.framework`: no such file or directory (macOS hosts only). +2. `qemu-system-aarch64 -machine apple-silicon-vm`: `unsupported machine type: "apple-silicon-vm"`. +3. `qemu-system-aarch64 -machine virt,accel=kvm -cpu Haswell`: `unable to find CPU model 'Haswell'`. +4. `qemu-system-x86_64`: not installed (and it could only emulate, never use KVM, on an ARM host). +5. Emulating an Intel Mac in software: not attempted (Apple recovery image, OpenCore, tens of times slower). diff --git a/mseries/machine-lab/data/style.css b/mseries/machine-lab/data/style.css index 472644101..995199546 100644 --- a/mseries/machine-lab/data/style.css +++ b/mseries/machine-lab/data/style.css @@ -163,3 +163,16 @@ textview.ml-console, textview.ml-console text { box-shadow: 0 0 0 1px alpha(@fg_color, 0.2), 0 2px 8px alpha(black, 0.25); } .ml-screen-frame label, .ml-screen-frame image { color: #d8dde6; } + +.ml-fs-bar { + padding: 6px 8px; + border-radius: 99px; + background: alpha(black, 0.72); + box-shadow: 0 4px 16px alpha(black, 0.4); + transition: opacity 250ms ease; +} + +.ml-fs-bar button.suggested-action { background: #3689e6; color: #fff; } +.ml-step-ok { color: #3a9104; font-weight: 700; } +.ml-step-fail { color: #c6262e; font-weight: 700; } +.ml-step-skip { opacity: 0.55; font-weight: 700; } diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index 94eaa35c1..986258326 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -16,7 +16,7 @@ resources = gnome.compile_resources('machine-lab-resources', add_project_arguments(['--pkg', 'posix'], language: 'vala') executable('machine-lab', - 'src/main.vala', 'src/machine.vala', 'src/page.vala', 'src/viewer.vala', + 'src/main.vala', 'src/machine.vala', 'src/page.vala', 'src/viewer.vala', 'src/macexp.vala', resources, dependencies: [gtk, granite], install: true diff --git a/mseries/machine-lab/src/macexp.vala b/mseries/machine-lab/src/macexp.vala new file mode 100644 index 000000000..4d81adf98 --- /dev/null +++ b/mseries/machine-lab/src/macexp.vala @@ -0,0 +1,246 @@ +/* "macOS (experimental)": runs real probes against this Mac and shows, + * step by step, why a macOS guest cannot run here and how each route fails. */ +using Gtk; + +public class MacExperiment : Gtk.Box { + private Gtk.Box steps_box; + private Gtk.TextView log_view; + private Gtk.Button run_button; + private Gtk.Label verdict_title; + private Gtk.Label verdict_text; + private Gtk.Box verdict_card; + private bool running = false; + + private struct Step { + public string title; + public string detail; + public string[] argv; + public string pass_when; // "ok" = command exits 0, "fail" = failing is the expected finding, "info" = always shown + } + + public MacExperiment () { + Object (orientation: Gtk.Orientation.VERTICAL, spacing: 0); + } + + construct { + hexpand = true; + var header = new Gtk.HeaderBar (); + header.add_css_class ("flat"); + header.decoration_layout = ":maximize"; + header.title_widget = new Gtk.Label (""); + var handle = new Gtk.WindowHandle (); + handle.child = header; + append (handle); + + var scroller = new Gtk.ScrolledWindow (); + scroller.vexpand = true; + scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; + append (scroller); + + var clamp = new Gtk.Box (Gtk.Orientation.VERTICAL, 18); + clamp.margin_start = 32; + clamp.margin_end = 32; + clamp.margin_bottom = 28; + clamp.width_request = 480; + scroller.child = clamp; + + var hero = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18); + var icon = new Gtk.Image.from_icon_name ("computer"); + icon.pixel_size = 64; + icon.add_css_class ("ml-hero-icon"); + icon.add_css_class ("ml-c-6"); + icon.valign = Gtk.Align.START; + hero.append (icon); + var text = new Gtk.Box (Gtk.Orientation.VERTICAL, 4); + text.hexpand = true; + text.valign = Gtk.Align.CENTER; + var title = new Gtk.Label ("macOS (experimental)"); + title.add_css_class ("title-1"); + title.xalign = 0; + text.append (title); + var sub = new Gtk.Label ("An honest attempt: each step runs a real command on this Mac and shows what happens."); + sub.wrap = true; + sub.xalign = 0; + sub.add_css_class ("dim-label"); + text.append (sub); + hero.append (text); + clamp.append (hero); + + steps_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + steps_box.add_css_class ("ml-card"); + clamp.append (steps_box); + + run_button = new Gtk.Button.with_label ("Run the experiment"); + run_button.add_css_class ("suggested-action"); + run_button.halign = Gtk.Align.END; + run_button.clicked.connect (() => run.begin ()); + clamp.append (run_button); + + verdict_card = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + verdict_card.add_css_class ("ml-note"); + verdict_card.visible = false; + verdict_title = new Gtk.Label (""); + verdict_title.xalign = 0; + verdict_title.add_css_class ("title-4"); + verdict_text = new Gtk.Label (""); + verdict_text.xalign = 0; + verdict_text.wrap = true; + verdict_card.append (verdict_title); + verdict_card.append (verdict_text); + clamp.append (verdict_card); + + var out_label = new Gtk.Label ("Command output"); + out_label.xalign = 0; + out_label.add_css_class ("ml-sidebar-title"); + out_label.margin_start = 0; + clamp.append (out_label); + log_view = new Gtk.TextView (); + log_view.editable = false; + log_view.monospace = true; + log_view.wrap_mode = Gtk.WrapMode.WORD_CHAR; + log_view.top_margin = 12; + log_view.bottom_margin = 12; + log_view.left_margin = 14; + log_view.right_margin = 14; + log_view.add_css_class ("ml-console"); + var frame = new Gtk.ScrolledWindow (); + frame.add_css_class ("ml-console-frame"); + frame.min_content_height = 220; + frame.child = log_view; + clamp.append (frame); + + build_rows (); + } + + private Step[] steps () { + return { + Step () { title = "This Mac's processor", detail = "What the host is.", + argv = { "sh", "-c", "uname -m; tr '\\0' ' ' < /proc/device-tree/compatible; echo" }, pass_when = "info" }, + Step () { title = "Apple's Virtualization framework", detail = "How macOS guests run on Apple Silicon (macOS hosts only).", + argv = { "ls", "/System/Library/Frameworks/Virtualization.framework" }, pass_when = "fail" }, + Step () { title = "A QEMU machine type for Apple virtual hardware", detail = "Ask QEMU for an Apple VM machine.", + argv = { "qemu-system-aarch64", "-machine", "apple-silicon-vm", "-display", "none", "-S" }, pass_when = "fail" }, + Step () { title = "An x86-64 CPU under hardware acceleration", detail = "Intel macOS needs an x86 CPU; ask KVM for one.", + argv = { "qemu-system-aarch64", "-machine", "virt,accel=kvm", "-cpu", "Haswell", "-display", "none", "-S" }, pass_when = "fail" }, + Step () { title = "An x86-64 emulator installed", detail = "Software emulation is the only x86 route on an ARM Mac.", + argv = { "sh", "-c", "command -v qemu-system-x86_64 || { echo 'qemu-system-x86_64: not installed'; exit 1; }" }, pass_when = "fail" } + }; + } + + private Gtk.Label[] marks = {}; + private Gtk.Label[] details = {}; + + private void build_rows () { + var all = steps (); + for (int i = 0; i < all.length; i++) { + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + row.add_css_class ("ml-form-row"); + if (i < all.length) row.add_css_class ("ml-form-sep"); + var mark = new Gtk.Label ("•"); + mark.add_css_class ("ml-step-skip"); + mark.width_request = 22; + mark.valign = Gtk.Align.START; + marks += mark; + row.append (mark); + var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + t.hexpand = true; + var tl = new Gtk.Label (all[i].title); + tl.xalign = 0; + tl.add_css_class ("ml-spec-value"); + var dl = new Gtk.Label (all[i].detail); + dl.xalign = 0; + dl.wrap = true; + dl.add_css_class ("ml-spec-label"); + details += dl; + t.append (tl); + t.append (dl); + row.append (t); + steps_box.append (row); + } + // Last row: the emulation route, which needs downloads and is not attempted. + var row = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 14); + row.add_css_class ("ml-form-row"); + var mark = new Gtk.Label ("–"); + mark.add_css_class ("ml-step-skip"); + mark.width_request = 22; + mark.valign = Gtk.Align.START; + row.append (mark); + var t = new Gtk.Box (Gtk.Orientation.VERTICAL, 2); + var tl = new Gtk.Label ("Emulating an Intel Mac in software"); + tl.xalign = 0; + tl.add_css_class ("ml-spec-value"); + var dl = new Gtk.Label ("Not attempted: it needs Apple's recovery image and an OpenCore boot disk (downloads), and runs tens of times slower than native."); + dl.xalign = 0; + dl.wrap = true; + dl.add_css_class ("ml-spec-label"); + t.append (tl); + t.append (dl); + row.append (t); + steps_box.append (row); + } + + private void log (string text) { + Gtk.TextIter end; + log_view.buffer.get_end_iter (out end); + log_view.buffer.insert (ref end, text, -1); + } + + private async void run () { + if (running) { + return; + } + running = true; + run_button.sensitive = false; + log_view.buffer.text = ""; + verdict_card.visible = false; + var all = steps (); + int blocked = 0; + for (int i = 0; i < all.length; i++) { + marks[i].label = "…"; + marks[i].remove_css_class ("ml-step-ok"); + marks[i].remove_css_class ("ml-step-fail"); + log ("$ %s\n".printf (string.joinv (" ", all[i].argv))); + string output = ""; + int code = -1; + try { + var p = new Subprocess.newv (all[i].argv, SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_MERGE); + uint kill = Timeout.add_seconds (8, () => { p.force_exit (); return Source.REMOVE; }); + string? so; + string? se; + yield p.communicate_utf8_async (null, null, out so, out se); + Source.remove (kill); + output = so ?? ""; + code = p.get_if_exited () ? p.get_exit_status () : -1; + } catch (Error e) { + output = e.message; + } + log (output.strip () + "\n[exit %d]\n\n".printf (code)); + bool ok = code == 0; + if (all[i].pass_when == "info") { + marks[i].label = "i"; + marks[i].add_css_class ("ml-step-ok"); + } else if (!ok) { + marks[i].label = "✗"; + marks[i].add_css_class ("ml-step-fail"); + blocked++; + var first = output.strip ().split ("\n")[0]; + details[i].label = first.length > 0 ? first : details[i].label; + } else { + marks[i].label = "✓"; + marks[i].add_css_class ("ml-step-ok"); + } + yield sleep_ms (350); + } + verdict_title.label = blocked > 0 ? "macOS can't run here, and now you can see why" : "Every probe passed"; + verdict_text.label = "A native macOS guest needs Apple's Virtualization framework and virtual hardware, which exist only on macOS hosts, so QEMU on this Linux Mac has no machine type for it. An Intel macOS guest would need x86 emulation in software, because KVM cannot present an x86 CPU on ARM. What does work: your internal macOS at the boot picker (hold the power button), or a macOS VM on a Mac running macOS reached over the network."; + verdict_card.visible = true; + run_button.label = "Run again"; + run_button.sensitive = true; + running = false; + } + + private async void sleep_ms (uint ms) { + Timeout.add (ms, () => { sleep_ms.callback (); return Source.REMOVE; }); + yield; + } +} diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index 5a3853298..c3875d134 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -101,6 +101,9 @@ public class MachineLab : Gtk.Application { private Granite.Toast toast; private Gtk.ListBox list; private Gtk.Stack content; + private bool special_page = false; + private Gtk.Widget sidebar_box; + private Gtk.Widget sidebar_sep; private HashTable pages = new HashTable (str_hash, str_equal); public MachineLab () { @@ -142,6 +145,15 @@ public class MachineLab : Gtk.Application { }); add_action (refresh_action); set_accels_for_action ("app.refresh", { "F5", "r" }); + var fs_action = new SimpleAction ("fullscreen", null); + fs_action.activate.connect (() => { + var row = list.get_selected_row (); + if (row != null) { + pages[((MachineRow) row).machine.id].toggle_fullscreen (); + } + }); + add_action (fs_action); + set_accels_for_action ("app.fullscreen", { "F11" }); set_accels_for_action ("app.quit", { "q" }); var quit = new SimpleAction ("quit", null); quit.activate.connect (() => window.close ()); @@ -163,13 +175,16 @@ public class MachineLab : Gtk.Application { window.titlebar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) { visible = false }; var layout = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); - layout.append (build_sidebar ()); - layout.append (new Gtk.Separator (Gtk.Orientation.VERTICAL)); + sidebar_box = build_sidebar (); + layout.append (sidebar_box); + sidebar_sep = new Gtk.Separator (Gtk.Orientation.VERTICAL); + layout.append (sidebar_sep); content = new Gtk.Stack (); content.hexpand = true; content.transition_type = Gtk.StackTransitionType.CROSSFADE; content.add_named (build_empty_state (), "empty"); + content.add_named (new MacExperiment (), "mac"); layout.append (content); var overlay = new Gtk.Overlay (); @@ -239,9 +254,12 @@ public class MachineLab : Gtk.Application { }); list.row_selected.connect ((row) => { if (row == null) { - content.visible_child_name = "empty"; + if (!special_page) { + content.visible_child_name = "empty"; + } return; } + special_page = false; content.visible_child_name = ((MachineRow) row).machine.id; }); var scroll = new Gtk.ScrolledWindow (); @@ -295,7 +313,7 @@ public class MachineLab : Gtk.Application { } var mac_item = new Gtk.Button (); mac_item.add_css_class ("flat"); - var mac_lbl = new Gtk.Label ("macOS… (not available on Linux)"); + var mac_lbl = new Gtk.Label ("macOS (experimental)…"); mac_lbl.xalign = 0; mac_lbl.add_css_class ("ml-machine-sub"); mac_item.child = mac_lbl; @@ -384,10 +402,10 @@ public class MachineLab : Gtk.Application { var mn = new Gtk.Label ("macOS"); mn.xalign = 0; mn.add_css_class ("ml-spec-value"); - var mbl = new Gtk.Label ("Not possible on this Mac under Linux"); + var mbl = new Gtk.Label ("Experimental: see how it fails"); mbl.xalign = 0; mbl.add_css_class ("ml-spec-label"); - var mlearn = new Gtk.Label ("Tap to see what works instead"); + var mlearn = new Gtk.Label ("Tap to run the attempt"); mlearn.xalign = 0; mlearn.add_css_class ("ml-fineprint"); mt.append (mn); @@ -415,6 +433,11 @@ public class MachineLab : Gtk.Application { MachineStore.save (machines); send_toast ("%s is installed. It will boot from its disk now.".printf (m.name)); }); + page.fullscreen_toggled.connect ((on) => { + window.fullscreened = on; + sidebar_box.visible = !on; + sidebar_sep.visible = !on; + }); page.start_requested.connect (() => start_machine (m)); page.remove_requested.connect (() => confirm_remove.begin (m)); pages[m.id] = page; @@ -444,6 +467,12 @@ public class MachineLab : Gtk.Application { } private void show_macos_info () { + special_page = true; + list.unselect_all (); + content.visible_child_name = "mac"; + } + + private void show_macos_info_dialog () { var dialog = new Gtk.AlertDialog ("macOS can't run as a guest here"); dialog.detail = "A macOS guest on Apple Silicon needs Apple's Virtualization framework and virtual hardware, which only exist on macOS hosts. Linux KVM on this Mac can't provide them, and no open implementation boots macOS.\n\nWhat works:\n• Your internal macOS is still installed. Hold the power button at startup and pick it at the boot picker.\n• Run a macOS VM on a Mac that runs macOS (UTM or Tart) and reach it over the network.\n• Use the elementary OS, Omarchy and other Linux machines here for everything else."; dialog.buttons = { "OK" }; diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 67f319648..386b482fb 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -10,6 +10,8 @@ public class MachinePage : Gtk.Box { public signal void changed (); // a persisted setting changed public signal void remove_requested (); public signal void start_requested (); + public signal void fullscreen_toggled (bool on); + private Gtk.Widget header_handle; private Gtk.Label title; private Gtk.Label status_pill; @@ -76,6 +78,10 @@ public class MachinePage : Gtk.Box { stack.add_titled (build_overview (), "overview", "Overview"); viewer = new Viewer (machine); viewer.toast.connect ((m) => toast (m)); + viewer.fullscreen_toggled.connect ((on) => { + header_handle.visible = !on; + fullscreen_toggled (on); + }); stack.add_titled (viewer, "display", "Display"); stack.add_titled (build_console (), "console", "Console"); stack.add_titled (build_settings (), "settings", "Settings"); @@ -99,6 +105,7 @@ public class MachinePage : Gtk.Box { header.pack_end (more); var handle = new Gtk.WindowHandle (); handle.child = header; + header_handle = handle; append (handle); append (stack); @@ -151,6 +158,13 @@ public class MachinePage : Gtk.Box { toast (free >= 0 ? "Checked again: %d GiB free".printf ((int) free) : "Checked again"); } + public void toggle_fullscreen () { + if (stack.visible_child_name != "display") { + show_tab ("display"); + } + viewer.go_fullscreen (!viewer.fullscreen); + } + public void show_tab (string name) { stack.visible_child_name = name; } diff --git a/mseries/machine-lab/src/viewer.vala b/mseries/machine-lab/src/viewer.vala index ceedeef64..41b62f553 100644 --- a/mseries/machine-lab/src/viewer.vala +++ b/mseries/machine-lab/src/viewer.vala @@ -5,6 +5,11 @@ using Gtk; public class Viewer : Gtk.Box { public Machine machine { get; construct; } public signal void toast (string message); + public signal void fullscreen_toggled (bool on); + public bool fullscreen { get; private set; default = false; } + private Gtk.Box exit_bar; + private Gtk.Box toolbar; + private uint hide_timer = 0; private Gtk.Picture picture; private Gtk.Stack stack; @@ -63,9 +68,34 @@ public class Viewer : Gtk.Box { stack.add_named (off, "off"); stack.add_named (picture, "screen"); stack.add_css_class ("ml-screen-frame"); - append (stack); + + // Floating exit controls, shown in full screen. + exit_bar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + exit_bar.halign = Gtk.Align.CENTER; + exit_bar.valign = Gtk.Align.START; + exit_bar.margin_top = 64; + exit_bar.add_css_class ("ml-fs-bar"); + exit_bar.visible = false; + var exit_btn = new Gtk.Button.with_label ("Exit Full Screen"); + exit_btn.add_css_class ("suggested-action"); + exit_btn.clicked.connect (() => go_fullscreen (false)); + exit_bar.append (exit_btn); + var rel_btn = new Gtk.Button.with_label ("Release Keyboard"); + rel_btn.tooltip_text = "Give the keyboard and mouse back (Ctrl+Alt+G)"; + rel_btn.clicked.connect (() => { + release_modifiers (); + captured = false; + update_hint (); + toast ("Keyboard released. Ctrl+Alt+F or F11 also leaves full screen."); + }); + exit_bar.append (rel_btn); + var overlay = new Gtk.Overlay (); + overlay.child = stack; + overlay.add_overlay (exit_bar); + append (overlay); var bar = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); + toolbar = bar; bar.margin_top = 10; hint = new Gtk.Label ("Click the screen to use the keyboard and mouse in the machine. Ctrl+Alt+G gives them back."); hint.hexpand = true; @@ -76,6 +106,10 @@ public class Viewer : Gtk.Box { size_label = new Gtk.Label (""); size_label.add_css_class ("ml-fineprint"); bar.append (size_label); + var fs = new Gtk.Button.from_icon_name ("view-fullscreen-symbolic"); + fs.tooltip_text = "Full screen (F11)"; + fs.clicked.connect (() => go_fullscreen (!fullscreen)); + bar.append (fs); var cad = new Gtk.Button.with_label ("Ctrl+Alt+Del"); cad.clicked.connect (() => { foreach (var k in new string[] { "ctrl", "alt", "delete" }) machine.send_key (k, true); @@ -138,6 +172,38 @@ public class Viewer : Gtk.Box { } } + public void go_fullscreen (bool on) { + if (fullscreen == on) { + return; + } + fullscreen = on; + toolbar.visible = !on; + margin_start = on ? 0 : 24; + margin_end = on ? 0 : 24; + margin_bottom = on ? 0 : 16; + margin_top = on ? 0 : 4; + exit_bar.visible = on; + exit_bar.opacity = 1.0; + if (on) { + toast ("Full screen: move the pointer to the top for the exit button, or press F11 / Ctrl+Alt+F"); + arm_hide (); + } + fullscreen_toggled (on); + } + + private void arm_hide () { + if (hide_timer != 0) { + Source.remove (hide_timer); + } + hide_timer = Timeout.add (3000, () => { + hide_timer = 0; + if (fullscreen) { + exit_bar.opacity = 0.0; // fades; still clickable once shown again + } + return Source.REMOVE; + }); + } + /* Only fetch frames while this tab is on screen: ~30 fps while the screen * changes, ~7 fps once it has been still for a moment. */ public void set_active (bool on) { @@ -234,6 +300,10 @@ public class Viewer : Gtk.Box { // Pointer position inside the picture's displayed image, as 0..1. private void move_pointer (double x, double y, bool force) { + if (fullscreen && y < 140) { + exit_bar.opacity = 1.0; + arm_hide (); + } if (tex_w == 0 || tex_h == 0) { return; } @@ -320,6 +390,14 @@ public class Viewer : Gtk.Box { if (!captured) { return false; } + // Ctrl+Alt+F toggles full screen even while the keyboard is captured. + if (evdev == 33 && ctrl_down && alt_down) { + if (down) { + release_modifiers (); + go_fullscreen (!fullscreen); + } + return true; + } // Ctrl+Alt+G releases the keyboard back to Machine Lab. if (evdev == 34 && ctrl_down && alt_down) { if (down) { From b486d560a145c53fd0cb2684896e0569843614b3 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:20:34 -0400 Subject: [PATCH 165/798] Render configured desktop wallpaper in Rust session --- .../ARCHITECTURE-AND-STATUS.md | 19 ++- .../EXPERIENCE-REQUIREMENTS.md | 8 +- mseries/pantheon-rust-compositor/README.md | 15 +- .../session/README.md | 8 +- rust/Cargo.lock | 159 ++++++++++++++++++ rust/pantheon-compositor/Cargo.toml | 1 + rust/pantheon-compositor/README.md | 6 + rust/pantheon-compositor/src/cursor.rs | 45 +++++ rust/pantheon-compositor/src/main.rs | 1 + rust/pantheon-compositor/src/native.rs | 101 ++++++----- rust/pantheon-compositor/src/state.rs | 2 + rust/pantheon-compositor/src/wallpaper.rs | 82 +++++++++ rust/pantheon-compositor/src/winit.rs | 56 ++++-- 13 files changed, 423 insertions(+), 80 deletions(-) create mode 100644 rust/pantheon-compositor/src/wallpaper.rs diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index bf6420316..33fb0c2b0 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -86,7 +86,8 @@ required workflows need. flowchart TD Apps[Existing Pantheon / GTK / Qt apps] XApps[X11 apps] - Shell[Existing Pantheon shell clients\nWingpanel + Dock + wallpaper] + Shell[Existing Pantheon shell clients\nWingpanel + Dock] + Settings[Existing desktop background settings\nwallpaper URI + picture options] Services[GNOME / Pantheon session services\nsettings, IBus, accessibility, sharing, lock UI] Xwayland[XWayland] Session[GNOME session + systemd user units] @@ -99,6 +100,7 @@ flowchart TD Apps -->|Wayland| Rust XApps --> Xwayland --> Rust Shell -->|Wayland + Pantheon shell protocol| Rust + Settings -->|GSettings read at startup| Rust Services <-->|required compositor bridges| Rust Session -->|start compositor, then shell clients| Rust Session --> Shell @@ -112,9 +114,10 @@ flowchart TD new user units are staged to start Wingpanel and Dock after it. `After=` orders process startup but does not yet wait for the panel to map or prove the `wait-for-n-panels=1` shell-launcher contract. The units are included in a local -ARM64 package build but are not installed or runtime-qualified. The Rust -session also does not yet provide the -wallpaper/background path. +ARM64 package build but are not installed or runtime-qualified. The compositor +now loads the configured local wallpaper at startup and renders it behind +windows, honoring the `zoom` crop mode; live wallpaper changes still require a +session restart. ## What is complete and what has been tested @@ -126,15 +129,17 @@ wallpaper/background path. | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, and attached shared-memory buffers | Protocol smoke passed; panel absent from native session | -| Native full desktop | Owner observed slow startup, no top panel, no bottom dock, and no colored background during the Rust login | Incomplete; shell launch and wallpaper path remain open | +| Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb | Nested visual smoke passed; native output and live setting changes remain open | +| Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while Gala remains the working fallback. The next development gate is to qualify -Wingpanel/Dock startup in a nested session and implement the background path, -then qualify shell startup and basic interaction before another native login. +Wingpanel/Dock startup in a nested session and qualify the new wallpaper path +on native output, then qualify shell startup and basic interaction before +another native login. After that, measure the same startup, idle and representative workloads under Gala and Rust, and record crashes and recovery behavior. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 0a9639d07..b4b964335 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -21,6 +21,7 @@ it does not imply that the current prototype supports it. | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | +| Desktop background | Show the configured elementary wallpaper behind windows on every output | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads the configured local PNG/JPEG/WebP at startup and renders it behind app surfaces. Native output and live wallpaper-setting changes still need qualification | ## Conditional work, driven by an observed need @@ -51,8 +52,11 @@ not itself require compositor accessibility protocols. GNOME Remote Desktop RDP and VNC are both disabled, so remote screen sharing remains conditional. Keep notification UI, app launchers, panel/dock rendering, settings surfaces, -wallpaper and other shell UI in their existing Pantheon components unless a -documented requirement shows they cannot work through the compositor contract. +and other shell UI in their existing Pantheon components unless a documented +requirement shows they cannot work through the compositor contract. Gala +currently owns wallpaper rendering in its compositor scene, so the Rust +prototype needs its own background renderer while the existing settings UI +continues to select the image. ## Explicitly separate workstreams diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 771ceec29..fff5c738f 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -19,8 +19,8 @@ the proposed Pantheon MVP—along with a tested-versus-open status table—see On 2026-10-19 the installed experimental session completed its first native M1 Pro login and scanned out an AR30 3024x1964 framebuffer. The owner reported -that startup felt slow; the Rust session had no Wingpanel, bottom dock, or -colored wallpaper/background. A timed LightDM rollback returned to Gala and +that startup felt slow; the first Rust session had no Wingpanel, bottom dock, +or wallpaper/background. A timed LightDM rollback returned to Gala and restored the background. Separate systemd user services have since been staged to launch Wingpanel and Dock after the compositor. A local ARM64 package build now includes those units; they are not installed or runtime-qualified. This @@ -104,11 +104,11 @@ This branch is **not ready to replace Gala for daily Pantheon use**. On 2026-10-19, an isolated five-minute LightDM login completed and produced the first native scanout on the M1 Pro: plane 0 used the compositor's AR30 framebuffer at 3024x1964. The owner confirmed the desktop was usable but slow -to appear. Wingpanel, Dock and a colored wallpaper/background were missing. +to appear. Wingpanel, Dock and wallpaper were missing in that first run. The timed rollback returned to Gala and restored the background. This qualifies native startup, internal-panel scanout and automatic rollback only; native keyboard/pointer workflows, complete shell integration, external displays, -lock/unlock and compositor-failure recovery remain open. +wallpaper after the new renderer, lock/unlock and compositor-failure recovery remain open. In private Xvfb, the release binary launched a GTK Wayland Zenity field; synthetic key events reached it, and IBus/Mozc committed `konnichiha` as @@ -118,7 +118,8 @@ switching, F5 dictation or native input. XWayland also passed an isolated X11 Zenity window and keyboard smoke; clipboard/PRIMARY remain unqualified. Installed Wingpanel binds the shell protocol and submits SHM panel buffers in private Xvfb, but the Rust session did not launch it. Keep Gala selected while -the shell launcher and background path are implemented and runtime-qualified. +the shell launcher is qualified and the new wallpaper renderer is tested on +native output. The compositor now exposes Dock's `org.pantheon.gala.DesktopIntegration` D-Bus interface. Window enumeration, app IDs, focus state, workspace listing, @@ -155,8 +156,8 @@ active Wingpanel session was not restarted. These checks do not qualify native DRM scanout, physical keyboard input, the Dock, or a complete login session. The next gate is to nested-qualify the staged Wingpanel and Dock startup against -`/etc/xdg/io.elementary.desktop.wm.shell`, and implement a desktop background -path. Then qualify shell rendering, Dock launcher/window switching, +`/etc/xdg/io.elementary.desktop.wm.shell`, and qualify the new wallpaper renderer +on native output. Then qualify shell rendering, Dock launcher/window switching, GTK/Qt clients and real F5 dictation. Package those changes before another native login. Keep the measured startup, resource, latency, power and crash/recovery comparison against Gala open; one successful login does not diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 3fdae6b8a..f10ea01dc 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -44,10 +44,10 @@ name `io.elementary.wingpanel` and `io.elementary.dock`. The experimental session now stages separate systemd user services for these clients, ordered after compositor readiness and Wingpanel-before-Dock. A local ARM64 package build includes the new units, but they have not been installed or run in a live -session. The -desktop background is also missing; its provider/rendering path remains to be -implemented. The installed five-minute rollback was exercised and returned -LightDM to Gala. +session. The first native package had no wallpaper; compositor-side wallpaper +rendering has since been implemented and visually smoke-tested in nested Xvfb, +but not yet qualified on native output. The installed five-minute rollback was +exercised and returned LightDM to Gala. The session carries forward the installed Pantheon settings-daemon, keyring, Orca and elementary settings-daemon targets. It intentionally does not promise diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f016285bd..f0bf1e68c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.5" @@ -261,6 +267,12 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.12.1" @@ -370,6 +382,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -547,12 +568,32 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + [[package]] name = "foldhash" version = "0.2.0" @@ -923,6 +964,32 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -1184,6 +1251,36 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "mseries-power" version = "0.1.0" @@ -1233,6 +1330,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -1371,6 +1477,7 @@ version = "0.1.0" dependencies = [ "bitflags", "gio", + "image", "smithay", "tracing", "tracing-subscriber", @@ -1442,6 +1549,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "polling" version = "3.11.0" @@ -1508,6 +1628,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.41.0" @@ -1746,6 +1878,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -3073,6 +3211,27 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.15.1" diff --git a/rust/pantheon-compositor/Cargo.toml b/rust/pantheon-compositor/Cargo.toml index e1bf56f31..c5b5f8d1f 100644 --- a/rust/pantheon-compositor/Cargo.toml +++ b/rust/pantheon-compositor/Cargo.toml @@ -42,6 +42,7 @@ wayland-client = "0.31.11" wayland-server = "0.31.14" zbus = "=5.19.0" gio = "0.20" +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } [dependencies.smithay] git = "https://github.com/Smithay/smithay.git" diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 159c2a9ae..630fcd24c 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -79,6 +79,12 @@ This is an early compositor implementation, not a Mutter port or a Pantheon replacement yet. The target is only the M-series/Pantheon behavior documented in [`../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`](../../mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md). +The compositor reads `org.gnome.desktop.background` at startup, loads a local +PNG/JPEG/WebP wallpaper and renders it behind app surfaces; `picture-options=zoom` +uses a centered crop. A private Xvfb screenshot showed the configured +elementary wallpaper behind a GTK Zenity window. Live wallpaper changes and +native image rendering still need qualification; restart the session after +changing the wallpaper. It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize grabs, clipboard, drag-and-drop and a compositor-rendered pointer. Client-set diff --git a/rust/pantheon-compositor/src/cursor.rs b/rust/pantheon-compositor/src/cursor.rs index 31acd2741..ace859f55 100644 --- a/rust/pantheon-compositor/src/cursor.rs +++ b/rust/pantheon-compositor/src/cursor.rs @@ -141,6 +141,51 @@ where elements } +pub fn render_wallpaper_element( + renderer: &mut R, + state: &PantheonCompositor, + output: &smithay::output::Output, +) -> Option> +where + R: Renderer + ImportAll + ImportMem, + R::TextureId: Texture + Clone + Send + 'static, +{ + let wallpaper = state.wallpaper.as_ref()?; + let output_geometry = state.space.output_geometry(output)?; + let source: Size = (wallpaper.width as f64, wallpaper.height as f64).into(); + let target = output_geometry.size.to_f64(); + let source_region = if wallpaper.picture_options == "zoom" && target.w > 0.0 && target.h > 0.0 { + let source_aspect = source.w / source.h; + let target_aspect = target.w / target.h; + if source_aspect > target_aspect { + let width = source.h * target_aspect; + Some(Rectangle::::new( + ((source.w - width) / 2.0, 0.0).into(), + (width, source.h).into(), + )) + } else { + let height = source.w / target_aspect; + Some(Rectangle::::new( + (0.0, (source.h - height) / 2.0).into(), + (source.w, height).into(), + )) + } + } else { + None + }; + MemoryRenderBufferRenderElement::from_buffer( + renderer, + (0.0, 0.0), + &wallpaper.buffer, + None, + source_region, + Some(output_geometry.size), + Kind::Unspecified, + ) + .map(CursorRenderElement::from) + .ok() +} + fn render_shell_osd_elements( _renderer: &mut R, state: &PantheonCompositor, diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 46e7dd5a2..356982a37 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -13,6 +13,7 @@ mod protocols; mod shell_compat; mod state; mod tiling; +mod wallpaper; mod winit; mod workspace; mod xwayland; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 02f0099c7..36bff708b 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -48,7 +48,10 @@ use smithay::{ use crate::{ PantheonCompositor, - cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}, + cursor::{ + CursorRenderElement, render_cursor_elements, render_lock_surface_elements, + render_wallpaper_element, + }, display_config::{ OutputModeSnapshot, OutputSnapshot, load_saved_monitor_configurations, transform_to_dbus, }, @@ -163,21 +166,23 @@ impl NativeKmsDevice { ) .map_err(|error| format!("could not prepare output frame: {error:?}"))?; let cursor_elements = render_cursor_elements(gpu_renderer, state, render_output); - let mut elements = space_elements - .into_iter() - .map(|element| match element { - smithay::desktop::space::SpaceRenderElements::Surface(surface) => { - CursorRenderElement::from(surface) - } - smithay::desktop::space::SpaceRenderElements::Element(element) => { - CursorRenderElement::from(element) - } - smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { - unreachable!("SpaceRenderElements generic catcher is never constructed") - } - }) - .collect::>(); - elements.extend(cursor_elements); + let mut elements = cursor_elements; + elements.extend(space_elements.into_iter().map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + })); + if let Some(wallpaper) = + render_wallpaper_element(gpu_renderer, state, render_output) + { + elements.push(wallpaper); + } elements }; render_elements.add_output(&output.crtc, [0.1, 0.1, 0.1, 1.0].into(), elements); @@ -531,21 +536,23 @@ impl NativeKmsDevice { .map_err(|error| format!("could not prepare current output contents: {error:?}"))?; let cursor_elements = render_cursor_elements(&mut gpu_renderer, state, &output.output); - let mut elements = space_elements - .into_iter() - .map(|element| match element { - smithay::desktop::space::SpaceRenderElements::Surface(surface) => { - CursorRenderElement::from(surface) - } - smithay::desktop::space::SpaceRenderElements::Element(element) => { - CursorRenderElement::from(element) - } - smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { - unreachable!("SpaceRenderElements generic catcher is never constructed") - } - }) - .collect::>(); - elements.extend(cursor_elements); + let mut elements = cursor_elements; + elements.extend(space_elements.into_iter().map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + })); + if let Some(wallpaper) = + render_wallpaper_element(&mut gpu_renderer, state, &output.output) + { + elements.push(wallpaper); + } elements }; render_elements.add_output(&output.crtc, [0.1, 0.1, 0.1, 1.0].into(), elements); @@ -674,21 +681,23 @@ impl NativeKmsDevice { )?; let cursor_elements = render_cursor_elements(&mut gpu_renderer, state, &output.output); - let mut elements = space_elements - .into_iter() - .map(|element| match element { - smithay::desktop::space::SpaceRenderElements::Surface(surface) => { - CursorRenderElement::from(surface) - } - smithay::desktop::space::SpaceRenderElements::Element(element) => { - CursorRenderElement::from(element) - } - smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { - unreachable!("SpaceRenderElements generic catcher is never constructed") - } - }) - .collect::>(); - elements.extend(cursor_elements); + let mut elements = cursor_elements; + elements.extend(space_elements.into_iter().map(|element| match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher(_) => { + unreachable!("SpaceRenderElements generic catcher is never constructed") + } + })); + if let Some(wallpaper) = + render_wallpaper_element(&mut gpu_renderer, state, &output.output) + { + elements.push(wallpaper); + } elements }; let frame = output.drm_output.render_frame( diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 576d40e7d..6965f5c7a 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -135,6 +135,7 @@ pub struct PantheonCompositor { pub popups: PopupManager, pub cursor_status: CursorImageStatus, pub default_cursor: MemoryRenderBuffer, + pub(crate) wallpaper: Option, pub seat: Seat, pub winit_redraw_sender: Option>, @@ -373,6 +374,7 @@ impl PantheonCompositor { popups, cursor_status: CursorImageStatus::default_named(), default_cursor: default_cursor_buffer(), + wallpaper: crate::wallpaper::load_configured_wallpaper(), seat, winit_redraw_sender: None, #[cfg(feature = "native-drm")] diff --git a/rust/pantheon-compositor/src/wallpaper.rs b/rust/pantheon-compositor/src/wallpaper.rs new file mode 100644 index 000000000..756280c3a --- /dev/null +++ b/rust/pantheon-compositor/src/wallpaper.rs @@ -0,0 +1,82 @@ +use gio::{ + Settings, SettingsBackend, SettingsSchemaSource, + prelude::{FileExt, SettingsExt}, +}; +use smithay::{ + backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer}, + utils::Transform, +}; + +pub(crate) struct Wallpaper { + pub buffer: MemoryRenderBuffer, + pub picture_options: String, + pub width: u32, + pub height: u32, +} + +/// Load the desktop wallpaper selected by GNOME's shared background settings. +/// The decoded image is held as a CPU buffer and uploaded by Smithay's renderer +/// as needed on each output device. +pub(crate) fn load_configured_wallpaper() -> Option { + match try_load_configured_wallpaper() { + Ok((buffer, width, height, path, picture_options)) => { + tracing::info!(path = %path.display(), width, height, "loaded configured desktop wallpaper"); + Some(Wallpaper { + buffer, + picture_options, + width, + height, + }) + } + Err(error) => { + tracing::warn!(%error, "could not load configured desktop wallpaper; using solid background"); + None + } + } +} + +fn try_load_configured_wallpaper() +-> Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box> { + let source = SettingsSchemaSource::default().ok_or("GSettings schemas are unavailable")?; + let schema = source + .lookup("org.gnome.desktop.background", true) + .ok_or("desktop background settings schema is unavailable")?; + let settings = Settings::new_full(&schema, None::<&SettingsBackend>, None); + let uri = settings.string("picture-uri"); + let picture_options = settings.string("picture-options").to_string(); + if uri.is_empty() || uri == "file://" { + return Err("no desktop wallpaper URI is configured".into()); + } + let file = gio::File::for_uri(&uri); + let path = file + .path() + .ok_or("configured wallpaper is not a local file")?; + + let mut limits = image::Limits::default(); + limits.max_image_width = Some(16_384); + limits.max_image_height = Some(16_384); + limits.max_alloc = Some(256 * 1024 * 1024); + let reader = image::ImageReader::open(&path)?.with_guessed_format()?; + let mut reader = reader; + reader.limits(limits); + let decoded = reader.decode()?.into_rgba8(); + let (width, height) = decoded.dimensions(); + if width == 0 || height == 0 || u64::from(width) * u64::from(height) > 64 * 1024 * 1024 { + return Err("wallpaper dimensions exceed the 64-megapixel limit".into()); + } + + // Fourcc::Argb8888 uses BGRA byte order on little-endian machines. + let mut pixels = decoded.into_raw(); + for pixel in pixels.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + let buffer = MemoryRenderBuffer::from_slice( + &pixels, + Fourcc::Argb8888, + (width as i32, height as i32), + 1, + Transform::Normal, + None, + ); + Ok((buffer, width, height, path, picture_options)) +} diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 12e79b76d..66cdfceb0 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -11,7 +11,10 @@ use smithay::{ }; use crate::PantheonCompositor; -use crate::cursor::{CursorRenderElement, render_cursor_elements, render_lock_surface_elements}; +use crate::cursor::{ + CursorRenderElement, render_cursor_elements, render_lock_surface_elements, + render_wallpaper_element, +}; use crate::display_config::{ OutputModeSnapshot, OutputSnapshot, load_saved_monitor_configurations, transform_to_dbus, }; @@ -154,23 +157,48 @@ pub fn init_winit( .unwrap(); } else { let cursor_elements = render_cursor_elements(renderer, state, &output); - smithay::desktop::space::render_output::< - _, - CursorRenderElement, - _, - _, - >( - &output, + let space_elements = smithay::desktop::space::space_render_elements( renderer, - &mut framebuffer, - output.current_scale().fractional_scale() as f32, - 0, [&state.space], - &cursor_elements, - &mut damage_tracker, - [0.1, 0.1, 0.1, 1.0], + &output, + output.current_scale().fractional_scale() as f32, ) .unwrap(); + let mut elements: Vec> = + cursor_elements; + elements.extend(space_elements.into_iter().map( + |element| { + match element { + smithay::desktop::space::SpaceRenderElements::Surface(surface) => { + CursorRenderElement::from(surface) + } + smithay::desktop::space::SpaceRenderElements::Element(element) => { + CursorRenderElement::from(element) + } + smithay::desktop::space::SpaceRenderElements::_GenericCatcher( + _, + ) => { + unreachable!( + "SpaceRenderElements generic catcher is never constructed" + ) + } + } + }, + )); + if let Some(wallpaper) = + render_wallpaper_element(renderer, state, &output) + { + elements.push(wallpaper); + } + damage_tracker + .render_output( + renderer, + &mut framebuffer, + 0, + &elements, + [0.1, 0.1, 0.1, 1.0], + ) + .unwrap(); } } backend.submit(Some(&[damage])).unwrap(); From 404555de66751141a766ce57faadbf9c096a3943 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:21:34 -0400 Subject: [PATCH 166/798] Record wallpaper package qualification status --- mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md | 6 ++++-- mseries/pantheon-rust-compositor/README.md | 5 +++-- mseries/pantheon-rust-compositor/session/README.md | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 33fb0c2b0..885078bb6 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -117,13 +117,15 @@ process startup but does not yet wait for the panel to map or prove the ARM64 package build but are not installed or runtime-qualified. The compositor now loads the configured local wallpaper at startup and renders it behind windows, honoring the `zoom` crop mode; live wallpaper changes still require a -session restart. +session restart. Refreshed package `0.1.0+gitb486d56` includes the wallpaper +renderer and shell units; it has not been installed or qualified on native +output. ## What is complete and what has been tested | Area | Evidence | State | | --- | --- | --- | -| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; local ARM64 package build includes the staged shell startup units; compositor remains separate from the default Gala session | Builds; shell service runtime not yet qualified | +| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; local ARM64 package `0.1.0+gitb486d56` includes wallpaper and staged shell startup units; compositor remains separate from the default Gala session | Builds; not installed; shell service runtime not yet qualified | | Native display path | Experimental LightDM login reached the M1 Pro internal display; kernel DRM state showed compositor-owned AR30 framebuffer at 3024×1964 | Native internal-panel scanout passed once | | Session recovery | Five-minute LightDM rollback returned to Gala; Wingpanel and background came back | Automatic rollback passed once | | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index fff5c738f..3f08b3fba 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -23,8 +23,9 @@ that startup felt slow; the first Rust session had no Wingpanel, bottom dock, or wallpaper/background. A timed LightDM rollback returned to Gala and restored the background. Separate systemd user services have since been staged to launch Wingpanel and Dock after the compositor. A local ARM64 package build -now includes those units; they are not installed or runtime-qualified. This -proves the native display path and rollback, not a +now includes those units. Refreshed package `0.1.0+gitb486d56` also includes +the wallpaper renderer; it is built but not installed or runtime-qualified. +This proves the native display path and rollback, not a complete Pantheon session or a performance/stability improvement. The nested prototype now handles the existing half-snap chords (`Super+Left` diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index f10ea01dc..b26d69523 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -46,8 +46,9 @@ after compositor readiness and Wingpanel-before-Dock. A local ARM64 package build includes the new units, but they have not been installed or run in a live session. The first native package had no wallpaper; compositor-side wallpaper rendering has since been implemented and visually smoke-tested in nested Xvfb, -but not yet qualified on native output. The installed five-minute rollback was -exercised and returned LightDM to Gala. +and refreshed ARM64 package `0.1.0+gitb486d56` includes it. The package has not +been installed or qualified on native output. The installed five-minute +rollback was exercised and returned LightDM to Gala. The session carries forward the installed Pantheon settings-daemon, keyring, Orca and elementary settings-daemon targets. It intentionally does not promise From cc8854081d51d3c3bac8a9cf1df39a905a280260 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:27:39 -0400 Subject: [PATCH 167/798] Document nested Pantheon shell qualification --- .../ARCHITECTURE-AND-STATUS.md | 27 +++++++++++++------ rust/pantheon-compositor/src/winit.rs | 5 +++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 885078bb6..a57e3b193 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -119,7 +119,17 @@ now loads the configured local wallpaper at startup and renders it behind windows, honoring the `zoom` crop mode; live wallpaper changes still require a session restart. Refreshed package `0.1.0+gitb486d56` includes the wallpaper renderer and shell units; it has not been installed or qualified on native -output. +output. In the nested Winit backend, the default transform is set to compensate +for the framebuffer orientation observed under Xvfb; this produced an upright +wallpaper and top Wingpanel in a fresh nested run. It does not alter the native +DRM output transform. + +**Development shell smoke:** installed Wingpanel 8.0.4 and the local Dock +binary both stayed alive as Wayland clients in an isolated nested session. +Wingpanel appeared upright along the top edge after the Winit transform fix. +Dock remained invisible even though its process was alive, so Dock protocol, +reveal behavior and positioning are still open. The test used a private +Xvfb/D-Bus profile and did not replace the active Gala session. ## What is complete and what has been tested @@ -130,18 +140,19 @@ output. | Session recovery | Five-minute LightDM rollback returned to Gala; Wingpanel and background came back | Automatic rollback passed once | | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | -| Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, and attached shared-memory buffers | Protocol smoke passed; panel absent from native session | -| Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb | Nested visual smoke passed; native output and live setting changes remain open | -| Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | +| Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | +| Pantheon shell process startup | Installed Wingpanel and local Dock both remained alive in a private nested Wayland session | Process smoke passed; Dock was not visible, and map/reveal behavior remains open | +| Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; fresh nested shell run showed wallpaper upright | Nested visual smoke passed; native output and live setting changes remain open | +| Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background. After returning to the regular desktop, a background was visible, but the active session was uncertain | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while -Gala remains the working fallback. The next development gate is to qualify -Wingpanel/Dock startup in a nested session and qualify the new wallpaper path -on native output, then qualify shell startup and basic interaction before -another native login. +Gala remains the working fallback. The next development gate is to diagnose +why the live Dock process does not render or reveal, then qualify shell startup +and basic interaction and the wallpaper path on native output before another +native login. After that, measure the same startup, idle and representative workloads under Gala and Rust, and record crashes and recovery behavior. diff --git a/rust/pantheon-compositor/src/winit.rs b/rust/pantheon-compositor/src/winit.rs index 66cdfceb0..d4f930080 100644 --- a/rust/pantheon-compositor/src/winit.rs +++ b/rust/pantheon-compositor/src/winit.rs @@ -46,7 +46,10 @@ pub fn init_winit( let x = saved.as_ref().map_or(0, |saved| saved.x); let y = saved.as_ref().map_or(0, |saved| saved.y); let scale = saved.as_ref().map_or(1.0, |saved| saved.scale); - let transform = match saved.as_ref().map_or(7, |saved| saved.transform) { + // The nested Winit target's framebuffer is vertically flipped relative to + // Smithay's logical output coordinates. Cancel that here; native DRM uses + // the connector transform reported by the kernel instead. + let transform = match saved.as_ref().map_or(6, |saved| saved.transform) { 0 => Transform::Normal, 1 => Transform::_90, 2 => Transform::_180, From adb35916c29576c1d037b54c147e9d54c160aff2 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:30:48 -0400 Subject: [PATCH 168/798] Record nested Dock visual smoke --- .../ARCHITECTURE-AND-STATUS.md | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index a57e3b193..c09bedb48 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -124,12 +124,16 @@ for the framebuffer orientation observed under Xvfb; this produced an upright wallpaper and top Wingpanel in a fresh nested run. It does not alter the native DRM output transform. -**Development shell smoke:** installed Wingpanel 8.0.4 and the local Dock -binary both stayed alive as Wayland clients in an isolated nested session. -Wingpanel appeared upright along the top edge after the Winit transform fix. -Dock remained invisible even though its process was alive, so Dock protocol, -reveal behavior and positioning are still open. The test used a private -Xvfb/D-Bus profile and did not replace the active Gala session. +**Development shell smoke:** the current release build rendered installed +Wingpanel 8.0.4 across the top and the stock `/usr/bin/io.elementary.dock` +along the bottom, over the wallpaper, in an isolated Xvfb session. Its Wayland +trace shows the Dock binding the Pantheon panel protocol, requesting the bottom +anchor and submitting shared-memory buffers. The normal local Dock command is +a wrapper; its existing user-disabled marker makes it run `sleep infinity`, so +the earlier “Dock process alive” check was a false positive. The marker remains +untouched, and the visual test launched the stock executable directly only +inside the private session. Hide/reveal behavior and native-session appearance +remain unqualified. No Gala session was replaced. ## What is complete and what has been tested @@ -141,7 +145,7 @@ Xvfb/D-Bus profile and did not replace the active Gala session. | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | -| Pantheon shell process startup | Installed Wingpanel and local Dock both remained alive in a private nested Wayland session | Process smoke passed; Dock was not visible, and map/reveal behavior remains open | +| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb; Dock trace requested bottom anchoring and submitted shared-memory buffers | Nested visual smoke passed; native appearance and hide/reveal behavior remain open. The local Dock wrapper remains intentionally disabled by the existing user marker | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; fresh nested shell run showed wallpaper upright | Nested visual smoke passed; native output and live setting changes remain open | | Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background. After returning to the regular desktop, a background was visible, but the active session was uncertain | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | @@ -149,10 +153,11 @@ Xvfb/D-Bus profile and did not replace the active Gala session. The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while -Gala remains the working fallback. The next development gate is to diagnose -why the live Dock process does not render or reveal, then qualify shell startup -and basic interaction and the wallpaper path on native output before another -native login. +Gala remains the working fallback. The next development gate is to exercise +Dock hide/reveal behavior and basic shell interaction in the nested session, +then qualify startup and the wallpaper path on native output before another +native login. The existing user-disabled Dock marker should remain in effect +unless the user explicitly chooses to re-enable Dock in the desktop session. After that, measure the same startup, idle and representative workloads under Gala and Rust, and record crashes and recovery behavior. From d73b501ce14257664b6e62178a8dcefdd614dfb4 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:32:07 -0400 Subject: [PATCH 169/798] Document nested Dock hide and reveal smoke --- .../ARCHITECTURE-AND-STATUS.md | 15 ++++++++------ .../EXPERIENCE-REQUIREMENTS.md | 20 +++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index c09bedb48..82f7259ca 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -132,8 +132,11 @@ anchor and submitting shared-memory buffers. The normal local Dock command is a wrapper; its existing user-disabled marker makes it run `sleep infinity`, so the earlier “Dock process alive” check was a false positive. The marker remains untouched, and the visual test launched the stock executable directly only -inside the private session. Hide/reveal behavior and native-session appearance -remain unqualified. No Gala session was replaced. +inside the private session. In that isolated profile's `overlapping-focused` +mode, a maximized focused GTK window hid the Dock, bottom-edge pointer motion +revealed it over the window, and closing the window restored it. The user's +saved hide mode and native-session appearance remain unqualified. No Gala +session was replaced. ## What is complete and what has been tested @@ -145,7 +148,7 @@ remain unqualified. No Gala session was replaced. | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | -| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb; Dock trace requested bottom anchoring and submitted shared-memory buffers | Nested visual smoke passed; native appearance and hide/reveal behavior remain open. The local Dock wrapper remains intentionally disabled by the existing user marker | +| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb; with private `overlapping-focused` mode, a maximized GTK window hid Dock, bottom-edge motion revealed it, and closing restored it | Nested visual and hide/reveal smoke passed; host hide mode and native appearance remain open. The local Dock wrapper remains intentionally disabled by the existing user marker | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; fresh nested shell run showed wallpaper upright | Nested visual smoke passed; native output and live setting changes remain open | | Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background. After returning to the regular desktop, a background was visible, but the active session was uncertain | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | @@ -154,9 +157,9 @@ remain unqualified. No Gala session was replaced. The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while Gala remains the working fallback. The next development gate is to exercise -Dock hide/reveal behavior and basic shell interaction in the nested session, -then qualify startup and the wallpaper path on native output before another -native login. The existing user-disabled Dock marker should remain in effect +basic shell interaction and startup timing in the nested session, then qualify +startup and the wallpaper path on native output before another native login. +The existing user-disabled Dock marker should remain in effect unless the user explicitly chooses to re-enable Dock in the desktop session. After that, measure the same startup, idle and representative workloads under Gala and Rust, and record crashes and recovery behavior. diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index b4b964335..0af513c4a 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -121,8 +121,14 @@ overlapping windows. Pointer motion at the output edge reveals hidden panels; work-area reservations are released while hidden and restored while visible, with managed windows reconfigured on transitions. Hiding currently moves the panel immediately without an animation. The active host Dock setting is -`overlapping-window`; the code has compile checks but no Dock runtime -qualification yet. It updates already-maximized windows when work areas change. +`overlapping-window`. In an isolated Xvfb session, the stock Dock rendered at +the bottom; with its private-profile `overlapping-focused` mode, a maximized +focused GTK window hid it, bottom-edge pointer motion revealed it over that +window, and closing the window restored it. The host's saved hide mode and +native behavior remain unqualified. The existing local wrapper is held at +`sleep infinity` by the user's disabled marker; the test invoked +`/usr/bin/io.elementary.dock` directly without changing that marker. The +compositor updates already-maximized windows when work areas change. The last local `cargo check` and `cargo build` pass. Its protocol probe completed under Xvfb over a nested Wayland socket. Installed Wingpanel 8.0.4 also connected in an isolated nested @@ -158,10 +164,12 @@ retains a window, and a later valid activation request restores it. Minimize / restore behavior remains runtime-unverified. It also implements the `org.gnome.Shell` accelerator registration and activation path: pressing the registered -`Super+N` chord emits `AcceleratorActivated` in the nested run. Dock integration -and physical media-key activation remain unqualified. The first M1 Pro scanout -passed in an isolated native login, but one scanout is not full session or -hardware qualification. +`Super+N` chord emits `AcceleratorActivated` in the nested run. Wingpanel and +Dock have nested visual smoke coverage, including Dock hide/reveal in the +private `overlapping-focused` mode. The host's configured hide mode, native +Dock behavior and physical media-key activation remain unqualified. The first +M1 Pro scanout passed in an isolated native login, but one scanout is not full +session or hardware qualification. The compositor now implements Smithay's `ext-session-lock-v1` protocol and the observed `org.gnome.ScreenSaver` action interface. Lock requests suppress From 176192954a353ef8fb51221dea22199b10c680d0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 10:33:07 -0400 Subject: [PATCH 170/798] Verify Dock overlap mode in nested session --- .../ARCHITECTURE-AND-STATUS.md | 11 ++++++----- .../EXPERIENCE-REQUIREMENTS.md | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 82f7259ca..a5ef5d630 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -132,10 +132,11 @@ anchor and submitting shared-memory buffers. The normal local Dock command is a wrapper; its existing user-disabled marker makes it run `sleep infinity`, so the earlier “Dock process alive” check was a false positive. The marker remains untouched, and the visual test launched the stock executable directly only -inside the private session. In that isolated profile's `overlapping-focused` -mode, a maximized focused GTK window hid the Dock, bottom-edge pointer motion -revealed it over the window, and closing the window restored it. The user's -saved hide mode and native-session appearance remain unqualified. No Gala +inside the private session. Hide/reveal also passed in private profiles using +both `overlapping-focused` and `overlapping-window` (the user's saved value): a +maximized GTK window hid the Dock, bottom-edge pointer motion revealed it over +the window, and closing the window restored it. The user's live configuration +was not modified. Native-session appearance remains unqualified. No Gala session was replaced. ## What is complete and what has been tested @@ -148,7 +149,7 @@ session was replaced. | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | -| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb; with private `overlapping-focused` mode, a maximized GTK window hid Dock, bottom-edge motion revealed it, and closing restored it | Nested visual and hide/reveal smoke passed; host hide mode and native appearance remain open. The local Dock wrapper remains intentionally disabled by the existing user marker | +| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; fresh nested shell run showed wallpaper upright | Nested visual smoke passed; native output and live setting changes remain open | | Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background. After returning to the regular desktop, a background was visible, but the active session was uncertain | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 0af513c4a..87624bbee 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -122,11 +122,12 @@ work-area reservations are released while hidden and restored while visible, with managed windows reconfigured on transitions. Hiding currently moves the panel immediately without an animation. The active host Dock setting is `overlapping-window`. In an isolated Xvfb session, the stock Dock rendered at -the bottom; with its private-profile `overlapping-focused` mode, a maximized -focused GTK window hid it, bottom-edge pointer motion revealed it over that -window, and closing the window restored it. The host's saved hide mode and -native behavior remain unqualified. The existing local wrapper is held at -`sleep infinity` by the user's disabled marker; the test invoked +the bottom. Hide/reveal passed in two private profiles: `overlapping-focused` +and `overlapping-window` (matching the host's saved value). With a maximized +GTK window, the Dock hid; bottom-edge pointer motion revealed it over that +window; closing the window restored it. This validates the behavior in nested +Winit, not on native DRM. The existing local wrapper is held at +`sleep infinity` by the user's disabled marker; each test invoked `/usr/bin/io.elementary.dock` directly without changing that marker. The compositor updates already-maximized windows when work areas change. The last local `cargo check` From c24d53afb20b77be32e1f225194a82271df8e15c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:02:38 -0400 Subject: [PATCH 171/798] Honor Dock opt-out in Rust session --- .../ARCHITECTURE-AND-STATUS.md | 18 ++++++++++-------- .../usr/lib/systemd/user/pantheon-dock.service | 3 +++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index a5ef5d630..2eb43ef50 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -114,8 +114,10 @@ flowchart TD new user units are staged to start Wingpanel and Dock after it. `After=` orders process startup but does not yet wait for the panel to map or prove the `wait-for-n-panels=1` shell-launcher contract. The units are included in a local -ARM64 package build but are not installed or runtime-qualified. The compositor -now loads the configured local wallpaper at startup and renders it behind +ARM64 package build but are not installed or native-session-qualified. The Dock +unit now skips startup when the existing user-disabled marker is present, +honoring the user's opt-out even though it invokes the distro executable by +absolute path. The compositor now loads the configured local wallpaper at startup and renders it behind windows, honoring the `zoom` crop mode; live wallpaper changes still require a session restart. Refreshed package `0.1.0+gitb486d56` includes the wallpaper renderer and shell units; it has not been installed or qualified on native @@ -143,7 +145,7 @@ session was replaced. | Area | Evidence | State | | --- | --- | --- | -| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; local ARM64 package `0.1.0+gitb486d56` includes wallpaper and staged shell startup units; compositor remains separate from the default Gala session | Builds; not installed; shell service runtime not yet qualified | +| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; locally rebuilt ARM64 package includes wallpaper, staged shell units and Dock opt-out guard; compositor remains separate from the default Gala session | Builds; newer package not installed; native shell service runtime remains open | | Native display path | Experimental LightDM login reached the M1 Pro internal display; kernel DRM state showed compositor-owned AR30 framebuffer at 3024×1964 | Native internal-panel scanout passed once | | Session recovery | Five-minute LightDM rollback returned to Gala; Wingpanel and background came back | Automatic rollback passed once | | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | @@ -151,16 +153,16 @@ session was replaced. | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; fresh nested shell run showed wallpaper upright | Nested visual smoke passed; native output and live setting changes remain open | -| Native full desktop | In the first Rust login, the owner observed slow startup, no top panel, no bottom dock, and no colored background. After returning to the regular desktop, a background was visible, but the active session was uncertain | Incomplete; wallpaper rendering landed afterward and has not been qualified on native output | +| Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current staged package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while -Gala remains the working fallback. The next development gate is to exercise -basic shell interaction and startup timing in the nested session, then qualify -startup and the wallpaper path on native output before another native login. -The existing user-disabled Dock marker should remain in effect +Gala remains the working fallback. The next development gate is to qualify the +newly built package's shell startup and wallpaper on native output. The earlier +compositor startup timing does not explain the owner's slower overall login; +capture session and client timing separately. The existing user-disabled Dock marker should remain in effect unless the user explicitly chooses to re-enable Dock in the desktop session. After that, measure the same startup, idle and representative workloads under Gala and Rust, and record crashes and recovery behavior. diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service index 33bebc1a4..d97bc1996 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service @@ -9,6 +9,9 @@ StartLimitBurst=3 [Service] Type=simple +# Respect the local user's opt-out even though this unit launches the distro +# binary directly rather than resolving the per-user Dock wrapper through PATH. +ExecCondition=/usr/bin/test ! -e %h/.config/mseries/dock/user-disabled ExecStart=/usr/bin/io.elementary.dock Restart=on-failure RestartSec=2s From 5e48c77d76e95fe1bc98a8c4e10c89a1984d443c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:03:52 -0400 Subject: [PATCH 172/798] Document current Rust session package status --- .../ARCHITECTURE-AND-STATUS.md | 13 ++++++++----- mseries/pantheon-rust-compositor/README.md | 5 +++-- mseries/pantheon-rust-compositor/session/README.md | 5 +++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 2eb43ef50..bc8e1d972 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -119,9 +119,11 @@ unit now skips startup when the existing user-disabled marker is present, honoring the user's opt-out even though it invokes the distro executable by absolute path. The compositor now loads the configured local wallpaper at startup and renders it behind windows, honoring the `zoom` crop mode; live wallpaper changes still require a -session restart. Refreshed package `0.1.0+gitb486d56` includes the wallpaper -renderer and shell units; it has not been installed or qualified on native -output. In the nested Winit backend, the default transform is set to compensate +session restart. The staged ARM64 package `0.1.0+gitc24d53a` includes the +wallpaper renderer, shell units, and Dock opt-out guard; it has not been +installed or qualified on native output. Its SHA-256 is +`39b5ea3c57c6bed02584fd27bd1ecdec8a9da73ec9c535fc03eec0e6600672c1`. In the +nested Winit backend, the default transform is set to compensate for the framebuffer orientation observed under Xvfb; this produced an upright wallpaper and top Wingpanel in a fresh nested run. It does not alter the native DRM output transform. @@ -162,8 +164,9 @@ readiness result. Continue testing in the isolated experimental session while Gala remains the working fallback. The next development gate is to qualify the newly built package's shell startup and wallpaper on native output. The earlier compositor startup timing does not explain the owner's slower overall login; -capture session and client timing separately. The existing user-disabled Dock marker should remain in effect -unless the user explicitly chooses to re-enable Dock in the desktop session. +capture session, compositor, Wingpanel, Dock, and wallpaper timing separately. +The existing user-disabled Dock marker should remain in effect unless the user +explicitly chooses to re-enable Dock in the desktop session. After that, measure the same startup, idle and representative workloads under Gala and Rust, and record crashes and recovery behavior. diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 3f08b3fba..a5f0571fb 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -23,8 +23,9 @@ that startup felt slow; the first Rust session had no Wingpanel, bottom dock, or wallpaper/background. A timed LightDM rollback returned to Gala and restored the background. Separate systemd user services have since been staged to launch Wingpanel and Dock after the compositor. A local ARM64 package build -now includes those units. Refreshed package `0.1.0+gitb486d56` also includes -the wallpaper renderer; it is built but not installed or runtime-qualified. +now includes those units. The staged ARM64 package +`0.1.0+gitc24d53a` also includes the wallpaper renderer and Dock opt-out +guard; it is built but not installed or runtime-qualified. This proves the native display path and rollback, not a complete Pantheon session or a performance/stability improvement. diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index b26d69523..666b59c7a 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -46,8 +46,9 @@ after compositor readiness and Wingpanel-before-Dock. A local ARM64 package build includes the new units, but they have not been installed or run in a live session. The first native package had no wallpaper; compositor-side wallpaper rendering has since been implemented and visually smoke-tested in nested Xvfb, -and refreshed ARM64 package `0.1.0+gitb486d56` includes it. The package has not -been installed or qualified on native output. The installed five-minute +and staged ARM64 package `0.1.0+gitc24d53a` includes it, along with the Dock +opt-out guard. The package has not been installed or qualified on native +output. The installed five-minute rollback was exercised and returned LightDM to Gala. The session carries forward the installed Pantheon settings-daemon, keyring, From 8c784dcf85079d2211f2dd8a8f962602c65497b1 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:06:10 -0400 Subject: [PATCH 173/798] Reload desktop wallpaper when settings change --- .../ARCHITECTURE-AND-STATUS.md | 7 +-- .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/pantheon-compositor/README.md | 7 +-- rust/pantheon-compositor/src/state.rs | 34 +++++++++++- rust/pantheon-compositor/src/wallpaper.rs | 53 +++++++++++++------ 5 files changed, 79 insertions(+), 24 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index bc8e1d972..0c57f9746 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -117,9 +117,10 @@ process startup but does not yet wait for the panel to map or prove the ARM64 package build but are not installed or native-session-qualified. The Dock unit now skips startup when the existing user-disabled marker is present, honoring the user's opt-out even though it invokes the distro executable by -absolute path. The compositor now loads the configured local wallpaper at startup and renders it behind -windows, honoring the `zoom` crop mode; live wallpaper changes still require a -session restart. The staged ARM64 package `0.1.0+gitc24d53a` includes the +absolute path. The compositor loads the configured local wallpaper, renders it +behind windows, and honors the `zoom` crop mode. It polls for URI or crop-mode +changes every two seconds and requests a redraw; this path is build-checked but +not runtime-qualified. The staged ARM64 package `0.1.0+gitc24d53a` includes the wallpaper renderer, shell units, and Dock opt-out guard; it has not been installed or qualified on native output. Its SHA-256 is `39b5ea3c57c6bed02584fd27bd1ecdec8a9da73ec9c535fc03eec0e6600672c1`. In the diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 87624bbee..179bc8794 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -21,7 +21,7 @@ it does not imply that the current prototype supports it. | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | -| Desktop background | Show the configured elementary wallpaper behind windows on every output | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads the configured local PNG/JPEG/WebP at startup and renders it behind app surfaces. Native output and live wallpaper-setting changes still need qualification | +| Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Live update and native output behavior still need runtime qualification | ## Conditional work, driven by an observed need diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 630fcd24c..7e9e5a2ba 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -82,9 +82,10 @@ M-series/Pantheon behavior documented in The compositor reads `org.gnome.desktop.background` at startup, loads a local PNG/JPEG/WebP wallpaper and renders it behind app surfaces; `picture-options=zoom` uses a centered crop. A private Xvfb screenshot showed the configured -elementary wallpaper behind a GTK Zenity window. Live wallpaper changes and -native image rendering still need qualification; restart the session after -changing the wallpaper. +elementary wallpaper behind a GTK Zenity window. The compositor now checks the +shared settings every two seconds, reloads only when the wallpaper URI or crop +mode changes, and requests an output redraw. The update path is build-checked; +live changes and native image rendering still need runtime qualification. It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize grabs, clipboard, drag-and-drop and a compositor-rendered pointer. Client-set diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 6965f5c7a..8d5fca703 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -136,6 +136,7 @@ pub struct PantheonCompositor { pub cursor_status: CursorImageStatus, pub default_cursor: MemoryRenderBuffer, pub(crate) wallpaper: Option, + pub(crate) wallpaper_selection: Option, pub seat: Seat, pub winit_redraw_sender: Option>, @@ -278,6 +279,25 @@ impl PantheonCompositor { TimeoutAction::ToDuration(Duration::from_secs(1)) }, )?; + event_loop.handle().insert_source( + Timer::from_duration(Duration::from_secs(2)), + |_, _, state| { + match crate::wallpaper::configured_selection() { + Ok(selection) if selection != state.wallpaper_selection => { + state.wallpaper = selection + .as_ref() + .and_then(crate::wallpaper::load_wallpaper); + state.wallpaper_selection = selection; + state.request_native_redraw(); + } + Ok(_) => {} + Err(error) => { + tracing::debug!(%error, "could not check desktop wallpaper settings") + } + } + TimeoutAction::ToDuration(Duration::from_secs(2)) + }, + )?; // Notify clients that we have a pointer (mouse) // Here we assume that there is always pointer plugged in @@ -289,6 +309,17 @@ impl PantheonCompositor { // Outputs become views of a part of the Space and can be rendered via Space::render_output. let space = Space::default(); + let wallpaper_selection = match crate::wallpaper::configured_selection() { + Ok(selection) => selection, + Err(error) => { + tracing::warn!(%error, "could not read desktop wallpaper settings"); + None + } + }; + let wallpaper = wallpaper_selection + .as_ref() + .and_then(crate::wallpaper::load_wallpaper); + // Setup a wayland socket that will be used to accept clients let socket_name = Self::init_wayland_listener(display, event_loop)?; let (osd_redraw_sender, osd_redraw_receiver) = channel::channel(); @@ -374,7 +405,8 @@ impl PantheonCompositor { popups, cursor_status: CursorImageStatus::default_named(), default_cursor: default_cursor_buffer(), - wallpaper: crate::wallpaper::load_configured_wallpaper(), + wallpaper, + wallpaper_selection, seat, winit_redraw_sender: None, #[cfg(feature = "native-drm")] diff --git a/rust/pantheon-compositor/src/wallpaper.rs b/rust/pantheon-compositor/src/wallpaper.rs index 756280c3a..bf8a2f0d8 100644 --- a/rust/pantheon-compositor/src/wallpaper.rs +++ b/rust/pantheon-compositor/src/wallpaper.rs @@ -14,11 +14,34 @@ pub(crate) struct Wallpaper { pub height: u32, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct WallpaperSelection { + uri: String, + picture_options: String, +} + /// Load the desktop wallpaper selected by GNOME's shared background settings. /// The decoded image is held as a CPU buffer and uploaded by Smithay's renderer /// as needed on each output device. -pub(crate) fn load_configured_wallpaper() -> Option { - match try_load_configured_wallpaper() { +pub(crate) fn configured_selection() +-> Result, Box> { + let source = SettingsSchemaSource::default().ok_or("GSettings schemas are unavailable")?; + let schema = source + .lookup("org.gnome.desktop.background", true) + .ok_or("desktop background settings schema is unavailable")?; + let settings = Settings::new_full(&schema, None::<&SettingsBackend>, None); + let uri = settings.string("picture-uri"); + if uri.is_empty() || uri == "file://" { + return Ok(None); + } + Ok(Some(WallpaperSelection { + uri: uri.to_string(), + picture_options: settings.string("picture-options").to_string(), + })) +} + +pub(crate) fn load_wallpaper(selection: &WallpaperSelection) -> Option { + match try_load_wallpaper(selection) { Ok((buffer, width, height, path, picture_options)) => { tracing::info!(path = %path.display(), width, height, "loaded configured desktop wallpaper"); Some(Wallpaper { @@ -35,19 +58,11 @@ pub(crate) fn load_configured_wallpaper() -> Option { } } -fn try_load_configured_wallpaper() --> Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box> { - let source = SettingsSchemaSource::default().ok_or("GSettings schemas are unavailable")?; - let schema = source - .lookup("org.gnome.desktop.background", true) - .ok_or("desktop background settings schema is unavailable")?; - let settings = Settings::new_full(&schema, None::<&SettingsBackend>, None); - let uri = settings.string("picture-uri"); - let picture_options = settings.string("picture-options").to_string(); - if uri.is_empty() || uri == "file://" { - return Err("no desktop wallpaper URI is configured".into()); - } - let file = gio::File::for_uri(&uri); +fn try_load_wallpaper( + selection: &WallpaperSelection, +) -> Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box> +{ + let file = gio::File::for_uri(&selection.uri); let path = file .path() .ok_or("configured wallpaper is not a local file")?; @@ -78,5 +93,11 @@ fn try_load_configured_wallpaper() Transform::Normal, None, ); - Ok((buffer, width, height, path, picture_options)) + Ok(( + buffer, + width, + height, + path, + selection.picture_options.clone(), + )) } From eee507c51c736e6d00e472ceb02826846f688de6 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:06:54 -0400 Subject: [PATCH 174/798] Record live wallpaper package build --- .../pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md | 9 +++++---- mseries/pantheon-rust-compositor/README.md | 6 ++++-- mseries/pantheon-rust-compositor/session/README.md | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 0c57f9746..8d3ae07d6 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -120,10 +120,11 @@ honoring the user's opt-out even though it invokes the distro executable by absolute path. The compositor loads the configured local wallpaper, renders it behind windows, and honors the `zoom` crop mode. It polls for URI or crop-mode changes every two seconds and requests a redraw; this path is build-checked but -not runtime-qualified. The staged ARM64 package `0.1.0+gitc24d53a` includes the -wallpaper renderer, shell units, and Dock opt-out guard; it has not been -installed or qualified on native output. Its SHA-256 is -`39b5ea3c57c6bed02584fd27bd1ecdec8a9da73ec9c535fc03eec0e6600672c1`. In the +not runtime-qualified. The staged ARM64 package `0.1.0+git8c784dc` includes the +wallpaper renderer, live wallpaper settings watcher, shell units, and Dock +opt-out guard; it has not been installed or qualified on native output. Its +SHA-256 is +`a83d4709064bc447ecd24909a4d9a0830d5f168ee20b62e468e23557e5257329`. In the nested Winit backend, the default transform is set to compensate for the framebuffer orientation observed under Xvfb; this produced an upright wallpaper and top Wingpanel in a fresh nested run. It does not alter the native diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index a5f0571fb..25d58aaf9 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -24,8 +24,10 @@ or wallpaper/background. A timed LightDM rollback returned to Gala and restored the background. Separate systemd user services have since been staged to launch Wingpanel and Dock after the compositor. A local ARM64 package build now includes those units. The staged ARM64 package -`0.1.0+gitc24d53a` also includes the wallpaper renderer and Dock opt-out -guard; it is built but not installed or runtime-qualified. +`0.1.0+git8c784dc` also includes the wallpaper renderer, live wallpaper +settings watcher, and Dock opt-out guard; it is built but not installed or +runtime-qualified. Package SHA-256: +`a83d4709064bc447ecd24909a4d9a0830d5f168ee20b62e468e23557e5257329`. This proves the native display path and rollback, not a complete Pantheon session or a performance/stability improvement. diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 666b59c7a..e2d9481a0 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -46,9 +46,9 @@ after compositor readiness and Wingpanel-before-Dock. A local ARM64 package build includes the new units, but they have not been installed or run in a live session. The first native package had no wallpaper; compositor-side wallpaper rendering has since been implemented and visually smoke-tested in nested Xvfb, -and staged ARM64 package `0.1.0+gitc24d53a` includes it, along with the Dock -opt-out guard. The package has not been installed or qualified on native -output. The installed five-minute +and staged ARM64 package `0.1.0+git8c784dc` includes it, along with the live +wallpaper settings watcher and Dock opt-out guard. The package has not been +installed or qualified on native output. The installed five-minute rollback was exercised and returned LightDM to Gala. The session carries forward the installed Pantheon settings-daemon, keyring, From a951c46cdae71053f16b3ea35fc9b3c4669da368 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:33:08 -0400 Subject: [PATCH 175/798] Document Rust session qualification results --- .../ARCHITECTURE-AND-STATUS.md | 23 +++++++++++-------- .../EXPERIENCE-REQUIREMENTS.md | 2 +- mseries/pantheon-rust-compositor/README.md | 13 +++++++---- .../session/README.md | 14 +++++++---- rust/pantheon-compositor/README.md | 6 +++-- rust/pantheon-compositor/src/state.rs | 17 ++++++++------ 6 files changed, 45 insertions(+), 30 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 8d3ae07d6..78865ae24 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -113,17 +113,19 @@ flowchart TD **Current implementation detail:** the session file starts the compositor; new user units are staged to start Wingpanel and Dock after it. `After=` orders process startup but does not yet wait for the panel to map or prove the -`wait-for-n-panels=1` shell-launcher contract. The units are included in a local -ARM64 package build but are not installed or native-session-qualified. The Dock -unit now skips startup when the existing user-disabled marker is present, +`wait-for-n-panels=1` shell-launcher contract. Package `0.1.0+git8c784dc` was +installed for a second native qualification attempt, but its session handoff +did not reach the compositor or shell units. The Dock unit now skips startup +when the existing user-disabled marker is present, honoring the user's opt-out even though it invokes the distro executable by absolute path. The compositor loads the configured local wallpaper, renders it behind windows, and honors the `zoom` crop mode. It polls for URI or crop-mode changes every two seconds and requests a redraw; this path is build-checked but -not runtime-qualified. The staged ARM64 package `0.1.0+git8c784dc` includes the -wallpaper renderer, live wallpaper settings watcher, shell units, and Dock -opt-out guard; it has not been installed or qualified on native output. Its -SHA-256 is +not runtime-qualified on native output. A private D-Bus/dconf + Xvfb smoke +changed the wallpaper URI during compositor runtime; logs showed the changed +selection and new image loading. Installed package `0.1.0+git8c784dc` includes +the wallpaper renderer, live wallpaper settings watcher, shell units, and Dock +opt-out guard. Its SHA-256 is `a83d4709064bc447ecd24909a4d9a0830d5f168ee20b62e468e23557e5257329`. In the nested Winit backend, the default transform is set to compensate for the framebuffer orientation observed under Xvfb; this produced an upright @@ -149,15 +151,16 @@ session was replaced. | Area | Evidence | State | | --- | --- | --- | -| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; locally rebuilt ARM64 package includes wallpaper, staged shell units and Dock opt-out guard; compositor remains separate from the default Gala session | Builds; newer package not installed; native shell service runtime remains open | +| Rust compositor and opt-in session package | Smithay prototype; nested Winit and native DRM backends; installed ARM64 package includes wallpaper, shell units and Dock opt-out guard; compositor remains separate from the default Gala session | Builds; latest session handoff fell back before compositor start; native shell service runtime remains open | | Native display path | Experimental LightDM login reached the M1 Pro internal display; kernel DRM state showed compositor-owned AR30 framebuffer at 3024×1964 | Native internal-panel scanout passed once | | Session recovery | Five-minute LightDM rollback returned to Gala; Wingpanel and background came back | Automatic rollback passed once | | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | -| Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; fresh nested shell run showed wallpaper upright | Nested visual smoke passed; native output and live setting changes remain open | -| Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current staged package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | +| Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | +| Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active | This attempt produced no new native compositor evidence; resolve the session-manager handoff before retrying | +| Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 179bc8794..f6074c101 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -21,7 +21,7 @@ it does not imply that the current prototype supports it. | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | -| Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Live update and native output behavior still need runtime qualification | +| Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Private dconf/Xvfb reload smoke passed; native output behavior still needs qualification | ## Conditional work, driven by an observed need diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 25d58aaf9..083ef0b10 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -22,11 +22,14 @@ M1 Pro login and scanned out an AR30 3024x1964 framebuffer. The owner reported that startup felt slow; the first Rust session had no Wingpanel, bottom dock, or wallpaper/background. A timed LightDM rollback returned to Gala and restored the background. Separate systemd user services have since been staged -to launch Wingpanel and Dock after the compositor. A local ARM64 package build -now includes those units. The staged ARM64 package -`0.1.0+git8c784dc` also includes the wallpaper renderer, live wallpaper -settings watcher, and Dock opt-out guard; it is built but not installed or -runtime-qualified. Package SHA-256: +to launch Wingpanel and Dock after the compositor. Installed ARM64 package +`0.1.0+git8c784dc` includes those units. Its attempted second LightDM launch +did not reach the compositor: GNOME systemd reported a target transaction +conflict while the previous session was stopping, then LightDM fell back to +Ubuntu. The temporary LightDM, `.dmrc`, and AccountsService selections were +restored, and Gala is active. Package `0.1.0+git8c784dc` also includes the +wallpaper renderer, live wallpaper settings watcher, and Dock opt-out guard; it +is installed but not qualified on native output. Package SHA-256: `a83d4709064bc447ecd24909a4d9a0830d5f168ee20b62e468e23557e5257329`. This proves the native display path and rollback, not a complete Pantheon session or a performance/stability improvement. diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index e2d9481a0..2f13d7621 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -43,12 +43,16 @@ The default desktop shell entries in `/etc/xdg/io.elementary.desktop.wm.shell` name `io.elementary.wingpanel` and `io.elementary.dock`. The experimental session now stages separate systemd user services for these clients, ordered after compositor readiness and Wingpanel-before-Dock. A local ARM64 package -build includes the new units, but they have not been installed or run in a live -session. The first native package had no wallpaper; compositor-side wallpaper +build includes the new units, and package `0.1.0+git8c784dc` was installed for +a second native qualification attempt. LightDM requested the Rust session, +but GNOME systemd rejected its target while the prior session was stopping; +subsequent autologin fell back to Ubuntu without starting the Rust compositor. +The temporary session-selection settings were restored and Gala is active. +The first native package had no wallpaper; compositor-side wallpaper rendering has since been implemented and visually smoke-tested in nested Xvfb, -and staged ARM64 package `0.1.0+git8c784dc` includes it, along with the live -wallpaper settings watcher and Dock opt-out guard. The package has not been -installed or qualified on native output. The installed five-minute +and installed ARM64 package `0.1.0+git8c784dc` includes it, along with the live +wallpaper settings watcher and Dock opt-out guard. The package still has not +qualified its wallpaper or shell on native output. The installed five-minute rollback was exercised and returned LightDM to Gala. The session carries forward the installed Pantheon settings-daemon, keyring, diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 7e9e5a2ba..72ae40c91 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -84,8 +84,10 @@ PNG/JPEG/WebP wallpaper and renders it behind app surfaces; `picture-options=zoo uses a centered crop. A private Xvfb screenshot showed the configured elementary wallpaper behind a GTK Zenity window. The compositor now checks the shared settings every two seconds, reloads only when the wallpaper URI or crop -mode changes, and requests an output redraw. The update path is build-checked; -live changes and native image rendering still need runtime qualification. +mode changes, and requests an output redraw. A private D-Bus/dconf + Xvfb +smoke changed the configured URI while the compositor was running; its log +showed the new wallpaper being loaded. Native image rendering still needs +qualification. It currently has one nested output, basic keyboard/pointer handling, xdg-shell windows and popup grabs, move/resize grabs, clipboard, drag-and-drop and a compositor-rendered pointer. Client-set diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 8d5fca703..babc6a913 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -283,14 +283,17 @@ impl PantheonCompositor { Timer::from_duration(Duration::from_secs(2)), |_, _, state| { match crate::wallpaper::configured_selection() { - Ok(selection) if selection != state.wallpaper_selection => { - state.wallpaper = selection - .as_ref() - .and_then(crate::wallpaper::load_wallpaper); - state.wallpaper_selection = selection; - state.request_native_redraw(); + Ok(selection) => { + tracing::trace!(?selection, "polled desktop wallpaper selection"); + if selection != state.wallpaper_selection { + tracing::info!(?selection, "desktop wallpaper settings changed"); + state.wallpaper = selection + .as_ref() + .and_then(crate::wallpaper::load_wallpaper); + state.wallpaper_selection = selection; + state.request_native_redraw(); + } } - Ok(_) => {} Err(error) => { tracing::debug!(%error, "could not check desktop wallpaper settings") } From e920d2e310e552d12670aea7c2c69ba8db15c391 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:34:41 -0400 Subject: [PATCH 176/798] Wait for prior GNOME session shutdown before login --- .../ARCHITECTURE-AND-STATUS.md | 2 +- mseries/pantheon-rust-compositor/README.md | 4 +++ mseries/pantheon-rust-compositor/build-deb.sh | 1 + .../session/README.md | 7 +++++ .../session/usr/bin/pantheon-rust-session | 29 +++++++++++++++++++ .../pantheon-rust-wayland.desktop | 4 +-- 6 files changed, 44 insertions(+), 3 deletions(-) create mode 100755 mseries/pantheon-rust-compositor/session/usr/bin/pantheon-rust-session diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 78865ae24..156461c76 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -159,7 +159,7 @@ session was replaced. | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | -| Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active | This attempt produced no new native compositor evidence; resolve the session-manager handoff before retrying | +| Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is staged and build-check pending; native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 083ef0b10..f41b05554 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -31,6 +31,10 @@ restored, and Gala is active. Package `0.1.0+git8c784dc` also includes the wallpaper renderer, live wallpaper settings watcher, and Dock opt-out guard; it is installed but not qualified on native output. Package SHA-256: `a83d4709064bc447ecd24909a4d9a0830d5f168ee20b62e468e23557e5257329`. +The experimental LightDM entry now waits up to 30 seconds for the previous +`gnome-session-pre.target` stop job to clear before starting GNOME. This +addresses the observed transaction conflict in source; it still needs a new +package build and native qualification. This proves the native display path and rollback, not a complete Pantheon session or a performance/stability improvement. diff --git a/mseries/pantheon-rust-compositor/build-deb.sh b/mseries/pantheon-rust-compositor/build-deb.sh index c806d562a..3e9092eec 100755 --- a/mseries/pantheon-rust-compositor/build-deb.sh +++ b/mseries/pantheon-rust-compositor/build-deb.sh @@ -30,6 +30,7 @@ cp -a "$ROOT/mseries/pantheon-rust-compositor/session/usr/." "$PACKAGE_ROOT/usr/ find "$PACKAGE_ROOT" -type d -exec chmod 0755 {} + find "$PACKAGE_ROOT" -type f -exec chmod 0644 {} + chmod 0755 "$PACKAGE_ROOT/usr/bin/pantheon-compositor" +chmod 0755 "$PACKAGE_ROOT/usr/bin/pantheon-rust-session" cat > "$OUTDIR/debian/control" <<'EOF' Source: pantheon-rust-compositor diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 2f13d7621..0d6415f7e 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -9,6 +9,13 @@ notification before session initialization continues. This follows the [GNOME session systemd documentation](https://gnome.pages.gitlab.gnome.org/gnome-session/re01.html) and the matching Pantheon units installed on this machine. +The LightDM entry uses `pantheon-rust-session`, which waits for any queued stop +job on `gnome-session-pre.target` to finish before starting GNOME. This avoids +the systemd transaction conflict observed when a new login overlapped teardown +of the prior session. The wait is bounded at 30 seconds and fails the login +entry with a diagnostic if teardown remains stuck. This handoff fix is staged +but still needs native login qualification. + The session is named **Pantheon (Rust compositor, experimental)** and uses the separate `pantheon-rust-wayland` session ID. The existing `pantheon-wayland` entry and Gala units are not modified. The service binds diff --git a/mseries/pantheon-rust-compositor/session/usr/bin/pantheon-rust-session b/mseries/pantheon-rust-compositor/session/usr/bin/pantheon-rust-session new file mode 100755 index 000000000..764bd1f7c --- /dev/null +++ b/mseries/pantheon-rust-compositor/session/usr/bin/pantheon-rust-session @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +# LightDM can start a new graphical session while the persistent user manager +# is still stopping the previous GNOME session. Starting the new GNOME target +# during that stop transaction is rejected by systemd as destructive. +# Let the previous session's pre-target stop finish before gnome-session asks +# systemd to start gnome-session@pantheon-rust-wayland.target. +timeout=30 +elapsed=0 +while :; do + if ! jobs=$(/usr/bin/systemctl --user list-jobs --no-legend --no-pager 2>/dev/null); then + echo "pantheon-rust-session: could not inspect the user systemd job queue" >&2 + exit 1 + fi + if ! printf '%s\n' "$jobs" \ + | awk '$2 == "gnome-session-pre.target" && $3 == "stop" { found = 1 } + END { exit !found }'; then + break + fi + if [ "$elapsed" -ge "$timeout" ]; then + echo "pantheon-rust-session: GNOME session shutdown is still pending after ${timeout}s" >&2 + exit 1 + fi + sleep 1 + elapsed=$((elapsed + 1)) +done + +exec /usr/bin/gnome-session --session=pantheon-rust-wayland diff --git a/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop b/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop index f633cc49d..656686934 100644 --- a/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop +++ b/mseries/pantheon-rust-compositor/session/usr/share/wayland-sessions/pantheon-rust-wayland.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=Pantheon (Rust compositor, experimental) Comment=Experimental Pantheon session using the Rust compositor -Exec=gnome-session --session=pantheon-rust-wayland -TryExec=pantheon-compositor +Exec=/usr/bin/pantheon-rust-session +TryExec=pantheon-rust-session DesktopNames=Pantheon Type=Application From 5d9f994dbbd7d8fccf2d1297468e746adc4ccc74 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:35:14 -0400 Subject: [PATCH 177/798] Record session handoff package build --- mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md | 2 +- mseries/pantheon-rust-compositor/README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 156461c76..7bedfcd01 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -159,7 +159,7 @@ session was replaced. | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | -| Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is staged and build-check pending; native login qualification remains open | +| Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index f41b05554..f96e4dc83 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -33,8 +33,9 @@ is installed but not qualified on native output. Package SHA-256: `a83d4709064bc447ecd24909a4d9a0830d5f168ee20b62e468e23557e5257329`. The experimental LightDM entry now waits up to 30 seconds for the previous `gnome-session-pre.target` stop job to clear before starting GNOME. This -addresses the observed transaction conflict in source; it still needs a new -package build and native qualification. +addresses the observed transaction conflict. Local ARM64 package +`0.1.0+gite920d2e` includes the wrapper; its executable mode was verified in +the built package. Native login qualification is still open. This proves the native display path and rollback, not a complete Pantheon session or a performance/stability improvement. From 6fc31aa678b937003d701126082938a025b7b53b Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:35:32 -0400 Subject: [PATCH 178/798] ui: compact CodexBar dropdown --- mseries/CODEXBAR-PORT.md | 11 +++++---- mseries/OS9-HANDOFF.md | 10 ++++++++ mseries/codexbar-wingpanel/Indicator.vala | 30 +++++++++++------------ 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/mseries/CODEXBAR-PORT.md b/mseries/CODEXBAR-PORT.md index e2aa4f14f..31863b795 100644 --- a/mseries/CODEXBAR-PORT.md +++ b/mseries/CODEXBAR-PORT.md @@ -183,9 +183,10 @@ second-account authorization still requires owner browser approval. Account acti including a live same-account saved-session round trip with the original selection restored. [Official account commands](https://dev.opencode.ai/v2/docs/cli/commands/) document the activation API. -## Panel bar font size +## Panel sizing -The text shown in the Wingpanel bar is styled in `install_style` in `codexbar-wingpanel/Indicator.vala` -(`.mseries-codexbar-bar` rules). It was shrunk on 2026-10-19 because it rendered too large next to the other -indicators: main label 8.5pt -> 7.5pt, `.seg-reset` and `.seg-secondary` 7.5pt -> 6.5pt. The popover text -(`.mseries-codexbar`, 9pt) is unchanged. To resize again, edit those values and run `bash mseries/deploy-codexbar.sh`. +The Wingpanel bar text is styled in `install_style` in `codexbar-wingpanel/Indicator.vala` +(`.mseries-codexbar-bar` rules): main label 7.5pt, `.seg-reset` and `.seg-secondary` 6.5pt. +The dropdown is deliberately compact: 300px requested width and 300px maximum page height, 8pt base text, +8.5pt provider titles, 7pt captions, reduced card/button padding and 6px section spacing. To resize again, +edit `Indicator.vala` and run `bash mseries/deploy-codexbar.sh`. diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 31d5651a4..8ce06d998 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,15 @@ # OS 9 implementation / Claude Code handoff +## CodexBar dropdown compacted — 2026-10-19 + +Owner asked to shrink the CodexBar dropdown and keep it that way. In `codexbar-wingpanel/Indicator.vala`, +reduced its requested width and maximum content height from 340px to 300px, base text 9pt to 8pt, provider +titles 9.5pt to 8.5pt, captions 8pt to 7pt, and tightened card, button and section spacing. The compact +style is compiled into the Wingpanel indicator module, so it is loaded on each Wingpanel startup. Built +with Meson/Ninja, installed at `/usr/lib/aarch64-linux-gnu/wingpanel-9/libcodexbar.so`, and restarted +Wingpanel. Verified the installed module contains the 8pt dropdown style and the Wingpanel process is +running again. The tests in the deploy script were not run. + ## CodexBar panel font shrink — 2026-10-19 (Claude Code) Owner reported the CodexBar bar text was too large (it may also have made the Applications menu hard to diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 2aa801dc7..53f2a95ed 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -78,12 +78,12 @@ namespace MSeries { panel_display.add_controller (refresh_gesture); this.visible = true; - menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 8) { + menu = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { margin_top = 8, - margin_bottom = 6, - margin_start = 8, - margin_end = 8, - width_request = 340 + margin_bottom = 5, + margin_start = 6, + margin_end = 6, + width_request = 300 }; menu.add_css_class ("mseries-codexbar"); install_style (menu.get_display ()); @@ -109,9 +109,9 @@ namespace MSeries { refresh_button.clicked.connect (() => refresh_usage ()); // Three pages keep every section above the fold; Quota is the default. - quota_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); - sessions_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); - spend_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 8); + quota_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + sessions_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + spend_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); pages = new Gtk.Stack () { hhomogeneous = true, vhomogeneous = false, @@ -131,7 +131,7 @@ namespace MSeries { vscrollbar_policy = Gtk.PolicyType.AUTOMATIC, propagate_natural_height = true, min_content_height = 150, - max_content_height = 340 + max_content_height = 300 }; menu.append (scroll); menu.append (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); @@ -172,12 +172,12 @@ namespace MSeries { private static void install_style (Gdk.Display display) { var style = new Gtk.CssProvider (); style.load_from_string ( - ".mseries-codexbar { font-size: 9pt; } " + - ".mseries-codexbar .caption { font-size: 8pt; } " + - ".mseries-codexbar .provider-title { font-weight: 700; font-size: 9.5pt; } " + + ".mseries-codexbar { font-size: 8pt; } " + + ".mseries-codexbar .caption { font-size: 7pt; } " + + ".mseries-codexbar .provider-title { font-weight: 700; font-size: 8.5pt; } " + ".mseries-codexbar .window-name { font-weight: 700; } " + ".mseries-codexbar .numeric { font-feature-settings: \"tnum\"; } " + - ".mseries-codexbar .card { padding: 6px 10px; border-radius: 8px; background: alpha(currentColor, 0.06); } " + + ".mseries-codexbar .card { padding: 5px 8px; border-radius: 8px; background: alpha(currentColor, 0.06); } " + ".mseries-codexbar .chip { padding: 1px 8px; border-radius: 99px; font-size: 7.5pt; font-weight: 700; " + " background: alpha(@warning_color, 0.22); color: @warning_color; } " + ".mseries-codexbar .chip.busy { background: alpha(currentColor, 0.1); color: inherit; } " + @@ -195,10 +195,10 @@ namespace MSeries { ".mseries-codexbar .dot-bad { color: @error_color; } " + ".mseries-codexbar .dot-off { opacity: 0.45; } " + ".mseries-codexbar .state-title { font-weight: 700; } " + - ".mseries-codexbar button { padding: 3px 6px; } " + + ".mseries-codexbar button { padding: 2px 5px; } " + ".mseries-codexbar .mseries-dock { padding: 3px; border-radius: 12px; background: alpha(currentColor, 0.06); } " + ".mseries-codexbar .mseries-bar { margin-top: 2px; } " + - ".mseries-codexbar .mseries-dock button { padding: 6px 11px; border-radius: 9px; min-height: 0; } " + + ".mseries-codexbar .mseries-dock button { padding: 5px 9px; border-radius: 9px; min-height: 0; } " + ".mseries-codexbar .mseries-dock button:hover { background: alpha(currentColor, 0.1); } " + ".mseries-codexbar .account-line { min-height: 0; } " + ".mseries-codexbar .provider-mark { opacity: 0.9; } " + From d57c47b88f6ce6c673df87ef1cf1c966dfea39dd Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:38:49 -0400 Subject: [PATCH 179/798] Add session resource sampling for comparison --- .../ARCHITECTURE-AND-STATUS.md | 2 +- .../EXPERIENCE-REQUIREMENTS.md | 12 ++ .../collect-session-metrics.sh | 116 ++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100755 mseries/pantheon-rust-compositor/collect-session-metrics.sh diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 7bedfcd01..838901bdc 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -162,7 +162,7 @@ session was replaced. | Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | -| Performance, power, reliability | No matched Gala/Rust measurements or crash-free soak yet | Open; do not claim faster or more reliable yet | +| Performance, power, reliability | A local 30-second idle Gala snapshot on the internal panel averaged 11.08% of one CPU core for `gala` (178,720 KiB RSS); battery telemetry was unavailable. Raw samples are in the task artifacts as `gala-idle-20261019.csv` | One-sided observation only; no Rust pair, matched workload, GPU/power data, latency, or crash-free soak yet. Do not claim faster or more reliable | The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index f6074c101..72aab3033 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -115,6 +115,18 @@ baseline before calling the migration better or ready for daily use. Track startup and runtime faults with journal excerpts and crash signatures so regressions can be tied to a specific branch revision. +`collect-session-metrics.sh` provides a read-only, per-process CPU/RSS sampler +for Gala or the Rust compositor, panels and dock. It records the session label, +workload, display mode, timestamps and battery power/capacity when exported by +sysfs. It does not measure GPU activity, frame/input latency, cold-start time, +or crashes; those still require separate recorded observations. Run it for +both sessions with matching display mode, duration, interval and workload, +then retain stdout with the branch/package revision and journal evidence. +For example, collect one-minute idle samples at a recorded mode with +`./collect-session-metrics.sh gala 60 idle 3024x1964 > gala-idle.csv` and +repeat in the Rust session with the same arguments except `rust`. Collect a +separate pair for each named workload; do not compare unlike runs. + The nested prototype serves the Pantheon shell protocol v2, places a panel from its anchor/size/offset, and honors hide modes by tracking maximized/focused or overlapping windows. Pointer motion at the output edge reveals hidden panels; diff --git a/mseries/pantheon-rust-compositor/collect-session-metrics.sh b/mseries/pantheon-rust-compositor/collect-session-metrics.sh new file mode 100755 index 000000000..db0d94c02 --- /dev/null +++ b/mseries/pantheon-rust-compositor/collect-session-metrics.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 [interval-seconds]" >&2 + exit 2 +} + +[[ $# -ge 4 && $# -le 5 ]] || usage +session=$1 +duration=$2 +workload=$3 +display_mode=$4 +interval=${5:-5} +[[ $session == gala || $session == rust ]] || usage +[[ $duration =~ ^[1-9][0-9]*$ && $interval =~ ^[1-9][0-9]*$ ]] || usage +[[ $workload != *','* && $workload != *$'\n'* && $display_mode != *','* && $display_mode != *$'\n'* ]] || usage +(( duration >= interval )) || usage + +clock_ticks=$(getconf CLK_TCK) +start_epoch=$(date +%s) +end_epoch=$((start_epoch + duration)) +declare -A start_ticks +last_sample_epoch=$start_epoch + +process_list() { + ps -eo pid=,comm=,args= | awk ' + $2 == "gala" || $2 == "mseries-gala" || $2 == "pantheon-compositor" || + $2 ~ /^io\.elementary\.(wingpanel|dock)/ || + $0 ~ /\/io\.elementary\.(wingpanel|dock)( |$)/ { + print $1, $2 + }' | sort -n -u +} + +read_ticks() { + local pid=$1 rest + [[ -r /proc/$pid/stat ]] || return 1 + # Strip pid and parenthesized comm; utime and stime are then fields 12 and 13. + rest=$(sed -E 's/^[0-9]+ \(.*\) //' "/proc/$pid/stat") || return 1 + local -a fields + read -r -a fields <<< "$rest" + (( ${#fields[@]} >= 13 )) || return 1 + echo $((fields[11] + fields[12])) +} + +read_rss_kib() { + awk '/^VmRSS:/ { print $2; found=1 } END { if (!found) print 0 }' "/proc/$1/status" 2>/dev/null || echo 0 +} + +battery_sample() { + local supply + for supply in /sys/class/power_supply/BAT*; do + [[ -d $supply ]] || continue + local power capacity + power=NA + capacity=NA + if [[ -r $supply/power_now ]]; then + power=$(( $(<"$supply/power_now") / 1000 )) + elif [[ -r $supply/current_now && -r $supply/voltage_now ]]; then + power=$(( $(<"$supply/current_now") * $(<"$supply/voltage_now") / 1000000000 )) + fi + [[ -r $supply/capacity ]] && capacity=$(<"$supply/capacity") + echo "$power,$capacity" + return + done + echo "NA,NA" +} + +sample_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ) +printf '# session=%s workload=%s display_mode=%s duration_s=%s interval_s=%s started_utc=%s\n' \ + "$session" "$workload" "$display_mode" "$duration" "$interval" "$sample_utc" +printf 'utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_power_mw,battery_percent\n' + +while read -r pid comm; do + [[ -n ${pid:-} ]] || continue + if ticks=$(read_ticks "$pid"); then + start_ticks[$pid]=$ticks + fi +done < <(process_list) + +if (( ${#start_ticks[@]} == 0 )); then + echo "error: no Gala or Rust compositor/shell processes found" >&2 + exit 1 +fi + +sleep "$interval" +while :; do + now=$(date +%s) + elapsed=$((now - start_epoch)) + battery=$(battery_sample) + battery_power=${battery%,*} + battery_percent=${battery#*,} + sample_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ) + while read -r pid comm; do + [[ -n ${pid:-} ]] || continue + ticks=$(read_ticks "$pid") || continue + if [[ -v start_ticks[$pid] ]]; then + delta=$((ticks - start_ticks[$pid])) + cpu=$(awk -v d="$delta" -v h="$clock_ticks" -v i="$((now - last_sample_epoch))" 'BEGIN { printf "%.2f", d * 100 / h / i }') + else + # A new PID appeared during sampling, which can indicate a restart. + cpu=NA + fi + rss=$(read_rss_kib "$pid") + printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \ + "$sample_utc" "$session" "$workload" "$display_mode" "$pid" "$comm" "$cpu" "$rss" "$elapsed" \ + "$battery_power" "$battery_percent" + start_ticks[$pid]=$ticks + done < <(process_list) + (( now >= end_epoch )) && break + last_sample_epoch=$now + remaining=$((end_epoch - now)) + sleep_for=$interval + (( remaining < sleep_for )) && sleep_for=$remaining + sleep "$sleep_for" +done From 5cb1214317c62fd2bb26325bbcc58d9e58b6c933 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:40:40 -0400 Subject: [PATCH 180/798] Track Pantheon hot-corner overview requirement --- mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md | 1 + mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md | 1 + 2 files changed, 2 insertions(+) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 838901bdc..849b03c8c 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -158,6 +158,7 @@ session was replaced. | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | +| Hot-corner multitasking action | Installed Gala customization calls `org.pantheon.gala.PerformAction(1)` for the configured overlay gesture | Rust D-Bus compatibility endpoint reports action 1 as unimplemented; preserve this behavior through a shell overview action or a supported replacement | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 72aab3033..412814525 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -15,6 +15,7 @@ it does not imply that the current prototype supports it. | --- | --- | --- | --- | | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | +| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; the Rust D-Bus compatibility layer currently returns “not implemented” for that action | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | From 24c6d61edc01ede75292bc13302e1077d8892ec1 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:43:15 -0400 Subject: [PATCH 181/798] Implement Gala maximize and hide window actions --- .../ARCHITECTURE-AND-STATUS.md | 2 +- .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/pantheon-compositor/README.md | 7 +- rust/pantheon-compositor/src/state.rs | 109 ++++++++++++++++++ rust/pantheon-compositor/src/xwayland.rs | 2 +- 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 849b03c8c..879548b35 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -158,7 +158,7 @@ session was replaced. | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | -| Hot-corner multitasking action | Installed Gala customization calls `org.pantheon.gala.PerformAction(1)` for the configured overlay gesture | Rust D-Bus compatibility endpoint reports action 1 as unimplemented; preserve this behavior through a shell overview action or a supported replacement | +| Gala focused-window actions | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) now act on the focused Wayland/XWayland window; build-checked, D-Bus runtime qualification open. Action 1 (configured multitasking overview gesture) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 412814525..db89993cf 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -15,7 +15,7 @@ it does not imply that the current prototype supports it. | --- | --- | --- | --- | | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | -| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; the Rust D-Bus compatibility layer currently returns “not implemented” for that action | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | +| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; Rust D-Bus actions 2 (toggle maximize) and 3 (hide/minimize) now dispatch to the focused window, but action 1 is still unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 72ae40c91..6ed5a113a 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -121,7 +121,12 @@ across workspaces, switches visible windows, and supports Dock requests to create the dynamic workspace, move windows, reorder workspaces and focus a window across workspaces. An isolated Xvfb run with a temporary Wayland client exercised creation, movement, switching and focus. Gala's multitasking view is -not implemented. Stock Dock also logged its existing +not implemented. The Gala `PerformAction` compatibility path now implements +actions 2 (toggle maximize on the focused window) and 3 (minimize the focused window) for +Wayland and XWayland application windows; these two actions have build +coverage but still need runtime D-Bus qualification. Action 1 (multitasking +view) remains unsupported because the Rust session has no overview UI. Stock +Dock also logged its existing workspace-startup race during the nested smoke, so full Dock behavior remains unqualified. Hiding is immediate; blur and multi-output Dock behavior remain open. The diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index babc6a913..1444db35b 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -527,12 +527,121 @@ impl PantheonCompositor { Err("workspace index is outside the current workspace list".into()) } } + GalaAction::PerformAction(2) => self.maximize_focused_window(), + GalaAction::PerformAction(3) => self.minimize_focused_window(), GalaAction::PerformAction(action) => Err(format!( "Gala action {action} is not implemented by this compositor" )), } } + fn focused_window(&self) -> Option { + if let Some(surface) = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + { + let mut root = surface; + while let Some(parent) = smithay::wayland::compositor::get_parent(&root) { + root = parent; + } + if let Some(window) = self.find_window_for_surface(&root) { + return Some(window); + } + } + let surface = self.x11_keyboard_focus.as_ref()?; + self.space + .elements() + .find(|window| window.x11_surface() == Some(surface)) + .cloned() + } + + fn maximize_focused_window(&mut self) -> Result<(), String> { + if self.session_locked { + return Err("cannot maximize a window while the session is locked".into()); + } + let window = self + .focused_window() + .ok_or_else(|| "there is no focused application window to maximize".to_string())?; + if let Some(toplevel) = window.toplevel().cloned() { + if self.is_panel_surface(toplevel.wl_surface()) { + return Err("cannot maximize a panel surface".into()); + } + let maximized = self + .managed_windows + .get(&toplevel.wl_surface().id()) + .is_some_and(|state| state.maximized); + self.configure_window_mode(toplevel, !maximized, false); + } else if let Some(surface) = window.x11_surface().cloned() { + let maximized = self + .x11_managed_windows + .get(&surface.window_id()) + .is_some_and(|state| state.maximized); + self.configure_x11_window_mode(&surface, !maximized, false); + } else { + return Err("the focused surface is not a managed application window".into()); + } + self.refresh_panel_visibility(); + self.request_native_redraw(); + Ok(()) + } + + fn minimize_focused_window(&mut self) -> Result<(), String> { + if self.session_locked { + return Err("cannot minimize a window while the session is locked".into()); + } + let window = self + .focused_window() + .ok_or_else(|| "there is no focused application window to minimize".to_string())?; + let Some(location) = self.space.element_location(&window) else { + return Err("the focused window is not currently mapped".into()); + }; + + if let Some(toplevel) = window.toplevel() { + let surface = toplevel.wl_surface(); + if self.is_panel_surface(surface) { + return Err("cannot minimize a panel surface".into()); + } + let was_focused = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .is_some_and(|mut focused| { + while let Some(parent) = smithay::wayland::compositor::get_parent(&focused) { + focused = parent; + } + focused == *surface + }); + self.workspace_manager.set_location(window.id(), location); + window.set_activated(false); + self.space.unmap_elem(&window); + self.minimized_windows + .insert(surface.id(), (window, location)); + if was_focused { + self.focus_next_window(); + } + } else if let Some(surface) = window.x11_surface().cloned() { + surface + .set_mapped(false) + .map_err(|error| format!("could not minimize X11 window: {error}"))?; + let id = surface.window_id(); + self.workspace_manager.set_location(window.id(), location); + window.set_activated(false); + self.space.unmap_elem(&window); + self.x11_minimized_windows.insert(id, (window, location)); + if self.x11_keyboard_focus.as_ref() == Some(&surface) { + self.set_x11_keyboard_focus(None, smithay::utils::SERIAL_COUNTER.next_serial()); + self.focus_next_window(); + } + } else { + return Err("the focused surface is not a managed application window".into()); + } + self.refresh_panel_visibility(); + self.gala_compat.request_windows_changed(); + self.request_native_redraw(); + Ok(()) + } + fn window_app_id(&self, window: &Window) -> Option { if let Some(toplevel) = window.toplevel() { smithay::wayland::compositor::with_states(toplevel.wl_surface(), |states| { diff --git a/rust/pantheon-compositor/src/xwayland.rs b/rust/pantheon-compositor/src/xwayland.rs index ffde41744..00047dee0 100644 --- a/rust/pantheon-compositor/src/xwayland.rs +++ b/rust/pantheon-compositor/src/xwayland.rs @@ -422,7 +422,7 @@ impl PantheonCompositor { .cloned() } - fn configure_x11_window_mode( + pub(crate) fn configure_x11_window_mode( &mut self, surface: &X11Surface, maximized: bool, From 9f665824e139ad551484cdb363c20147d64d9afa Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:48:28 -0400 Subject: [PATCH 182/798] Record nested Gala action qualification --- .../ARCHITECTURE-AND-STATUS.md | 2 +- .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/pantheon-compositor/README.md | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 879548b35..48b798dc6 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -158,7 +158,7 @@ session was replaced. | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | -| Gala focused-window actions | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) now act on the focused Wayland/XWayland window; build-checked, D-Bus runtime qualification open. Action 1 (configured multitasking overview gesture) remains unsupported | +| Gala focused-window actions | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) act on the focused window. Private Xvfb/Winit D-Bus smoke focused a GTK Wayland window, toggled maximize twice, minimized it, then restored it via `FocusWindow`; each call succeeded. Maximize geometry, XWayland and native qualification remain open. Action 1 (configured multitasking overview gesture) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index db89993cf..95d793e33 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -15,7 +15,7 @@ it does not imply that the current prototype supports it. | --- | --- | --- | --- | | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | -| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; Rust D-Bus actions 2 (toggle maximize) and 3 (hide/minimize) now dispatch to the focused window, but action 1 is still unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | +| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; Rust D-Bus actions 2 and 3 passed a nested GTK Wayland smoke, while action 1 is unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 6ed5a113a..375b5c810 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -123,10 +123,13 @@ window across workspaces. An isolated Xvfb run with a temporary Wayland client exercised creation, movement, switching and focus. Gala's multitasking view is not implemented. The Gala `PerformAction` compatibility path now implements actions 2 (toggle maximize on the focused window) and 3 (minimize the focused window) for -Wayland and XWayland application windows; these two actions have build -coverage but still need runtime D-Bus qualification. Action 1 (multitasking -view) remains unsupported because the Rust session has no overview UI. Stock -Dock also logged its existing +Wayland and XWayland application windows. A private Xvfb/Winit D-Bus smoke +focused a GTK Wayland window, invoked action 2 twice, minimized it with action +3, and restored it through `FocusWindow`; each D-Bus call succeeded. The +smoke did not inspect the rendered maximize geometry, and the XWayland and +native paths remain unqualified. Action 1 (multitasking view) remains +unsupported because the Rust session has no overview UI. Stock Dock also +logged its existing workspace-startup race during the nested smoke, so full Dock behavior remains unqualified. Hiding is immediate; blur and multi-output Dock behavior remain open. The From 8023955055d583ffc79dfee3d3e5f43ddaf91248 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:51:59 -0400 Subject: [PATCH 183/798] Launch configured Wingpanel menu from Gala action --- .../ARCHITECTURE-AND-STATUS.md | 2 +- .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/pantheon-compositor/README.md | 8 ++++--- rust/pantheon-compositor/src/state.rs | 23 +++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 48b798dc6..4d0147e0c 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -158,7 +158,7 @@ session was replaced. | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | -| Gala focused-window actions | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) act on the focused window. Private Xvfb/Winit D-Bus smoke focused a GTK Wayland window, toggled maximize twice, minimized it, then restored it via `FocusWindow`; each call succeeded. Maximize geometry, XWayland and native qualification remain open. Action 1 (configured multitasking overview gesture) remains unsupported | +| Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | | Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 95d793e33..5fc56a8bc 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -15,7 +15,7 @@ it does not imply that the current prototype supports it. | --- | --- | --- | --- | | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | -| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; Rust D-Bus actions 2 and 3 passed a nested GTK Wayland smoke, while action 1 is unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | +| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; actions 2 and 3 passed nested GTK Wayland smoke, and action 4 dispatches the configured Wingpanel main-menu command. Action 1 remains unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 375b5c810..a1c9aaa51 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -127,9 +127,11 @@ Wayland and XWayland application windows. A private Xvfb/Winit D-Bus smoke focused a GTK Wayland window, invoked action 2 twice, minimized it with action 3, and restored it through `FocusWindow`; each D-Bus call succeeded. The smoke did not inspect the rendered maximize geometry, and the XWayland and -native paths remain unqualified. Action 1 (multitasking view) remains -unsupported because the Rust session has no overview UI. Stock Dock also -logged its existing +native paths remain unqualified. Action 4 now launches the configured +`panel-main-menu-action` from `io.elementary.desktop.wm.behavior`, keeping the +existing Wingpanel launcher in charge of its UI; it is build-checked but not +runtime-qualified. Action 1 (multitasking view) remains unsupported because +the Rust session has no overview UI. Stock Dock also logged its existing workspace-startup race during the nested smoke, so full Dock behavior remains unqualified. Hiding is immediate; blur and multi-output Dock behavior remain open. The diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 1444db35b..a0bb09950 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -529,12 +529,35 @@ impl PantheonCompositor { } GalaAction::PerformAction(2) => self.maximize_focused_window(), GalaAction::PerformAction(3) => self.minimize_focused_window(), + GalaAction::PerformAction(4) => self.launch_panel_main_menu(), GalaAction::PerformAction(action) => Err(format!( "Gala action {action} is not implemented by this compositor" )), } } + fn launch_panel_main_menu(&self) -> Result<(), String> { + use gio::prelude::SettingsExt; + + if self.session_locked { + return Err("cannot open the application launcher while the session is locked".into()); + } + let settings = gio::Settings::new("io.elementary.desktop.wm.behavior"); + let command = settings.string("panel-main-menu-action"); + let argv = gio::glib::shell_parse_argv(command.as_str()) + .map_err(|error| format!("could not parse panel main-menu action: {error}"))?; + if argv.is_empty() { + return Err("panel main-menu action is empty".into()); + } + let argv = argv + .iter() + .map(std::ffi::OsString::as_os_str) + .collect::>(); + gio::Subprocess::newv(&argv, gio::SubprocessFlags::NONE) + .map_err(|error| format!("could not launch panel main-menu action: {error}"))?; + Ok(()) + } + fn focused_window(&self) -> Option { if let Some(surface) = self .seat From 17b66771551434d6d4e6be59a11c96579eb52362 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 11:57:26 -0400 Subject: [PATCH 184/798] Record native Rust session qualification --- .../ARCHITECTURE-AND-STATUS.md | 17 +++++++++-------- .../collect-session-metrics.sh | 1 + .../metrics/gala-idle-20261019.csv | 14 ++++++++++++++ .../rust-idle-qualification-20261019.csv | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 mseries/pantheon-rust-compositor/metrics/gala-idle-20261019.csv create mode 100644 mseries/pantheon-rust-compositor/metrics/rust-idle-qualification-20261019.csv diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 4d0147e0c..391c3027f 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -157,20 +157,21 @@ session was replaced. | XWayland | Private Xvfb Zenity window mapped and accepted synthetic keyboard input; exposed through window-list D-Bus | Isolated smoke passed | | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | -| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value | Nested visual and hide/reveal smoke passed; native appearance remains open. The local Dock wrapper remains intentionally disabled by the existing user marker | +| Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value. In native package `0.1.0+git8023955`, Wingpanel's user service became active; the Dock service was intentionally skipped by the existing `~/.config/mseries/dock/user-disabled` marker | Nested visual and hide/reveal smoke passed; native Wingpanel process is active, but native appearance has not been visually confirmed. Dock remains opted out by the user's saved marker | | Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | -| Native Rust session handoff | Second qualification installed `0.1.0+git8c784dc`. LightDM invoked `gnome-session --session=pantheon-rust-wayland`, but the user manager rejected the Rust target while the prior GNOME target was stopping. Later LightDM autologin selected Ubuntu; no Rust compositor or shell unit became active. Temporary LightDM, `.dmrc`, and AccountsService changes were restored, and Gala is active. A bounded session-entry wrapper now waits for the queued `gnome-session-pre.target` stop job to clear before starting GNOME | Handoff mitigation is in locally built ARM64 package `0.1.0+gite920d2e`; package wrapper mode verified executable. Native login qualification remains open | -| Native full desktop | The first Rust login used installed package `0.1.0+git83a663c`; user journal shows compositor unit start at 09:55:58.981, XWayland ready at 09:55:59.329 and unit ready at 09:55:59.342. That installed package had no Wingpanel/Dock user service files and predated the wallpaper renderer | Missing shell and background in that run do not qualify the current package. Startup felt slow to the owner, but the compositor became ready in about 0.36 seconds; total login time has not been measured | +| Native Rust session handoff | Package `0.1.0+git8023955` was installed on 2026-10-19 and selected through LightDM. GNOME entered `pantheon-rust-wayland`; its session target, compositor service and user manager became active. The compositor created `wayland-0`, initialized native DRM/EGL, loaded the configured 5000×2813 wallpaper and reported the compositor ready | Wrapper handoff succeeded on this run. Startup-to-usable time is still not measured; a bounded five-minute LightDM rollback was armed and is scheduled to restore Gala | +| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal timeouts/surface-loss warnings | Native session entry, internal output and wallpaper load passed; full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | -| Performance, power, reliability | A local 30-second idle Gala snapshot on the internal panel averaged 11.08% of one CPU core for `gala` (178,720 KiB RSS); battery telemetry was unavailable. Raw samples are in the task artifacts as `gala-idle-20261019.csv` | One-sided observation only; no Rust pair, matched workload, GPU/power data, latency, or crash-free soak yet. Do not claim faster or more reliable | +| Performance, power, reliability | Thirty-second internal-panel idle snapshots: Gala `gala` averaged 11.08% of one CPU core and 178,720 KiB RSS; Rust compositor averaged 11.12% and about 173,531 KiB RSS. Rust Wingpanel added about 472,560 KiB RSS and averaged about 1.19% CPU. Battery telemetry was unavailable. Raw CSVs are in [`metrics/`](metrics/) | Initial observations only: compositor CPU is effectively unchanged and its RSS is slightly lower, but session age/state and shell composition are not fully matched. No GPU/power data, latency, representative workload, or crash-free soak. Do not claim the migration is faster or more reliable | The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while -Gala remains the working fallback. The next development gate is to qualify the -newly built package's shell startup and wallpaper on native output. The earlier -compositor startup timing does not explain the owner's slower overall login; -capture session, compositor, Wingpanel, Dock, and wallpaper timing separately. +Gala remains the working fallback. The next development gate is visual and +interactive qualification of Wingpanel, plus a separate Dock run only if the +user opts in. The earlier compositor startup timing does not explain the +owner's slower overall login; capture session, compositor, Wingpanel, Dock, and +wallpaper timing separately. The existing user-disabled Dock marker should remain in effect unless the user explicitly chooses to re-enable Dock in the desktop session. After that, measure the same startup, idle and representative workloads under diff --git a/mseries/pantheon-rust-compositor/collect-session-metrics.sh b/mseries/pantheon-rust-compositor/collect-session-metrics.sh index db0d94c02..c08f93c63 100755 --- a/mseries/pantheon-rust-compositor/collect-session-metrics.sh +++ b/mseries/pantheon-rust-compositor/collect-session-metrics.sh @@ -26,6 +26,7 @@ last_sample_epoch=$start_epoch process_list() { ps -eo pid=,comm=,args= | awk ' $2 == "gala" || $2 == "mseries-gala" || $2 == "pantheon-compositor" || + $0 ~ /\/pantheon-compositor( |$)/ || $2 ~ /^io\.elementary\.(wingpanel|dock)/ || $0 ~ /\/io\.elementary\.(wingpanel|dock)( |$)/ { print $1, $2 diff --git a/mseries/pantheon-rust-compositor/metrics/gala-idle-20261019.csv b/mseries/pantheon-rust-compositor/metrics/gala-idle-20261019.csv new file mode 100644 index 000000000..67e159c11 --- /dev/null +++ b/mseries/pantheon-rust-compositor/metrics/gala-idle-20261019.csv @@ -0,0 +1,14 @@ +# session=gala workload=idle display_mode=internal-panel duration_s=30 interval_s=5 started_utc=2026-10-19T15:37:54Z +utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_power_mw,battery_percent +2026-10-19T15:37:59Z,gala,idle,internal-panel,375906,mseries-gala,0.00,1760,5,NA,NA +2026-10-19T15:37:59Z,gala,idle,internal-panel,375918,gala,11.20,178720,5,NA,NA +2026-10-19T15:38:04Z,gala,idle,internal-panel,375906,mseries-gala,0.00,1760,10,NA,NA +2026-10-19T15:38:04Z,gala,idle,internal-panel,375918,gala,11.20,178720,10,NA,NA +2026-10-19T15:38:09Z,gala,idle,internal-panel,375906,mseries-gala,0.00,1760,15,NA,NA +2026-10-19T15:38:09Z,gala,idle,internal-panel,375918,gala,11.00,178720,15,NA,NA +2026-10-19T15:38:14Z,gala,idle,internal-panel,375906,mseries-gala,0.00,1760,20,NA,NA +2026-10-19T15:38:14Z,gala,idle,internal-panel,375918,gala,10.80,178720,20,NA,NA +2026-10-19T15:38:19Z,gala,idle,internal-panel,375906,mseries-gala,0.00,1760,25,NA,NA +2026-10-19T15:38:19Z,gala,idle,internal-panel,375918,gala,11.20,178720,25,NA,NA +2026-10-19T15:38:24Z,gala,idle,internal-panel,375906,mseries-gala,0.00,1760,30,NA,NA +2026-10-19T15:38:24Z,gala,idle,internal-panel,375918,gala,11.20,178720,30,NA,NA diff --git a/mseries/pantheon-rust-compositor/metrics/rust-idle-qualification-20261019.csv b/mseries/pantheon-rust-compositor/metrics/rust-idle-qualification-20261019.csv new file mode 100644 index 000000000..396ca305b --- /dev/null +++ b/mseries/pantheon-rust-compositor/metrics/rust-idle-qualification-20261019.csv @@ -0,0 +1,14 @@ +# session=rust workload=idle-qualification display_mode=internal-3024x1964 duration_s=30 interval_s=5 started_utc=2026-10-19T15:56:18Z +utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_power_mw,battery_percent +2026-10-19T15:56:23Z,rust,idle-qualification,internal-3024x1964,410141,pantheon-compos,11.40,173472,5,NA,NA +2026-10-19T15:56:23Z,rust,idle-qualification,internal-3024x1964,410246,io.elementary.w,1.00,472608,5,NA,NA +2026-10-19T15:56:28Z,rust,idle-qualification,internal-3024x1964,410141,pantheon-compos,11.40,173472,10,NA,NA +2026-10-19T15:56:28Z,rust,idle-qualification,internal-3024x1964,410246,io.elementary.w,1.00,472608,10,NA,NA +2026-10-19T15:56:33Z,rust,idle-qualification,internal-3024x1964,410141,pantheon-compos,11.20,173488,15,NA,NA +2026-10-19T15:56:33Z,rust,idle-qualification,internal-3024x1964,410246,io.elementary.w,0.80,472528,15,NA,NA +2026-10-19T15:56:38Z,rust,idle-qualification,internal-3024x1964,410141,pantheon-compos,11.80,173520,20,NA,NA +2026-10-19T15:56:38Z,rust,idle-qualification,internal-3024x1964,410246,io.elementary.w,1.00,472528,20,NA,NA +2026-10-19T15:56:44Z,rust,idle-qualification,internal-3024x1964,410141,pantheon-compos,9.67,173616,26,NA,NA +2026-10-19T15:56:44Z,rust,idle-qualification,internal-3024x1964,410246,io.elementary.w,1.33,472544,26,NA,NA +2026-10-19T15:56:48Z,rust,idle-qualification,internal-3024x1964,410141,pantheon-compos,11.25,173616,30,NA,NA +2026-10-19T15:56:48Z,rust,idle-qualification,internal-3024x1964,410246,io.elementary.w,2.00,472544,30,NA,NA From 8f0d68e9ab39bc4c495e997f51ff21cf980fb7c8 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:01:55 -0400 Subject: [PATCH 185/798] Document native portal startup blocker --- mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 391c3027f..4975d9dea 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -161,7 +161,7 @@ session was replaced. | Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Package `0.1.0+git8023955` was installed on 2026-10-19 and selected through LightDM. GNOME entered `pantheon-rust-wayland`; its session target, compositor service and user manager became active. The compositor created `wayland-0`, initialized native DRM/EGL, loaded the configured 5000×2813 wallpaper and reported the compositor ready | Wrapper handoff succeeded on this run. Startup-to-usable time is still not measured; a bounded five-minute LightDM rollback was armed and is scheduled to restore Gala | -| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal timeouts/surface-loss warnings | Native session entry, internal output and wallpaper load passed; full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | +| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. During handoff, systemd skipped `xdg-desktop-portal` activation while `WAYLAND_DISPLAY` was unset; portal clients then timed out. Native IBus portal activation also failed. Portal services activated after returning to Gala. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal/surface-loss warnings | Native session entry, internal output and wallpaper load passed; portal startup ordering, native IBus, full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | Thirty-second internal-panel idle snapshots: Gala `gala` averaged 11.08% of one CPU core and 178,720 KiB RSS; Rust compositor averaged 11.12% and about 173,531 KiB RSS. Rust Wingpanel added about 472,560 KiB RSS and averaged about 1.19% CPU. Battery telemetry was unavailable. Raw CSVs are in [`metrics/`](metrics/) | Initial observations only: compositor CPU is effectively unchanged and its RSS is slightly lower, but session age/state and shell composition are not fully matched. No GPU/power data, latency, representative workload, or crash-free soak. Do not claim the migration is faster or more reliable | From 42be33e8674482a6bb2378a6c264382c37918651 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:05:14 -0400 Subject: [PATCH 186/798] Publish session environment before portal activation --- .../ARCHITECTURE-AND-STATUS.md | 3 ++- .../EXPERIENCE-REQUIREMENTS.md | 14 +++++++++++ mseries/pantheon-rust-compositor/README.md | 3 ++- mseries/pantheon-rust-compositor/build-deb.sh | 2 +- .../session/README.md | 25 +++++++++++-------- .../user/pantheon-rust-compositor@.service | 6 ++++- 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 4975d9dea..cc90492b8 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -161,7 +161,8 @@ session was replaced. | Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Package `0.1.0+git8023955` was installed on 2026-10-19 and selected through LightDM. GNOME entered `pantheon-rust-wayland`; its session target, compositor service and user manager became active. The compositor created `wayland-0`, initialized native DRM/EGL, loaded the configured 5000×2813 wallpaper and reported the compositor ready | Wrapper handoff succeeded on this run. Startup-to-usable time is still not measured; a bounded five-minute LightDM rollback was armed and is scheduled to restore Gala | -| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. During handoff, systemd skipped `xdg-desktop-portal` activation while `WAYLAND_DISPLAY` was unset; portal clients then timed out. Native IBus portal activation also failed. Portal services activated after returning to Gala. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal/surface-loss warnings | Native session entry, internal output and wallpaper load passed; portal startup ordering, native IBus, full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | +| Startup timing | On the last OS boot, `systemd-analyze` reported 20.838 seconds to `default.target` and 10.465 seconds to `graphical.target`. In the Rust run, LightDM started at 11:53:01 and the compositor service was active by 11:53:02. The user reports boot/login still feels over a minute | These measurements do not include firmware-to-kernel time or prove login-to-usable. Portal D-Bus calls later timed out at 120 seconds; separately time firmware, greeter, session, shell and first usable interaction | +| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. During handoff, systemd skipped `xdg-desktop-portal` activation while `WAYLAND_DISPLAY` was unset; portal clients then timed out. Native IBus portal activation also failed. Portal services activated after returning to Gala. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal/surface-loss warnings. Source now queues the portal service after compositor readiness and environment import | Native session entry, internal output and wallpaper load passed; portal ordering change is staged but unqualified. Native IBus, full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | Thirty-second internal-panel idle snapshots: Gala `gala` averaged 11.08% of one CPU core and 178,720 KiB RSS; Rust compositor averaged 11.12% and about 173,531 KiB RSS. Rust Wingpanel added about 472,560 KiB RSS and averaged about 1.19% CPU. Battery telemetry was unavailable. Raw CSVs are in [`metrics/`](metrics/) | Initial observations only: compositor CPU is effectively unchanged and its RSS is slightly lower, but session age/state and shell composition are not fully matched. No GPU/power data, latency, representative workload, or crash-free soak. Do not claim the migration is faster or more reliable | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 5fc56a8bc..691155c0c 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -116,6 +116,20 @@ baseline before calling the migration better or ready for daily use. Track startup and runtime faults with journal excerpts and crash signatures so regressions can be tied to a specific branch revision. +The current Omarchy shell design offers two relevant startup ideas: one +long-lived Quickshell process serves panels and overlays through IPC, and its +Hyprland autostart publishes session variables to systemd and D-Bus before +launching shell services. This is a responsiveness and activation-ordering +pattern, not evidence that a full machine boot takes under one second. For +this host, the last `systemd-analyze` report was 20.8 seconds to the system +default target and 10.5 seconds to the graphical target; it does not include +firmware time or measure login-to-usable. The native Rust run separately +reached compositor readiness about one second after LightDM started, while +portal requests later timed out after 120 seconds. Track firmware-to-greeter, +login-to-compositor, login-to-panel, and login-to-usable as separate intervals. + +References: [Omarchy shell architecture](https://github.com/maralcbr/omarchy-mx-mac/blob/main/shell/README.md), [Omarchy session autostart](https://github.com/omacom/omarchy/blob/quattro/default/hypr/autostart.lua). The Omarchy-MX README says that fork is feature-frozen and directs new Apple Silicon work to `omacom/omarchy-mac`. + `collect-session-metrics.sh` provides a read-only, per-process CPU/RSS sampler for Gala or the Rust compositor, panels and dock. It records the session label, workload, display mode, timestamps and battery power/capacity when exported by diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index f96e4dc83..91ff00fff 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -187,7 +187,8 @@ login. `build-deb.sh` builds an arm64 release binary with `native-drm` and packages it with the separate experimental session files. It derives shared-library -dependencies and requires `gnome-session` and `gtklock`. Build it locally with +dependencies and requires `dbus-bin`, `gnome-session`, `gtklock`, and +`xdg-desktop-portal`. Build it locally with `OUTDIR=/path/to/output mseries/pantheon-rust-compositor/build-deb.sh`. The current package above includes the XWayland runtime dependency and is for isolated qualification; the build script does not install it. The nested IBus diff --git a/mseries/pantheon-rust-compositor/build-deb.sh b/mseries/pantheon-rust-compositor/build-deb.sh index 3e9092eec..bd0b98d8c 100755 --- a/mseries/pantheon-rust-compositor/build-deb.sh +++ b/mseries/pantheon-rust-compositor/build-deb.sh @@ -60,7 +60,7 @@ Package: pantheon-rust-compositor Version: $VERSION Architecture: arm64 Maintainer: techmore -Depends: $SHLIB_DEPENDS, gnome-session, gtklock, io.elementary.dock, io.elementary.wingpanel, libegl1, libgles2, xwayland +Depends: $SHLIB_DEPENDS, dbus-bin, gnome-session, gtklock, io.elementary.dock, io.elementary.wingpanel, libegl1, libgles2, xdg-desktop-portal, xwayland Description: experimental Rust compositor for the Pantheon M-series session An opt-in Smithay Wayland compositor and a separate experimental GNOME session. EOF diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 0d6415f7e..1089a7260 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -24,7 +24,11 @@ and clears the display variables and fixed socket when it stops. `PANTHEON_WAYLAND_DISPLAY` selects the explicit socket; without that variable the nested development backend continues to allocate an automatic socket, and the native backend can also use automatic allocation for manual development -launches. +launches. After readiness, `dbus-update-activation-environment --systemd` +publishes `WAYLAND_DISPLAY` to both D-Bus and the user manager, then the service +queues `xdg-desktop-portal.service` asynchronously. This addresses a native +handoff where early portal activation was skipped while the display variable +was unset; the ordering change is staged and still needs native qualification. The experimental service retries an unexpected compositor failure, with a two-second delay and a limit of three starts within one minute. Once the limit @@ -36,15 +40,16 @@ have not been exercised in a live GNOME session. The directory layout mirrors install paths below `usr/`. A distro package can stage the payload under its package root and install it with the compositor binary at `/usr/bin/pantheon-compositor`; the repository's -`../build-deb.sh` creates that arm64 package. On 2026-10-19 the installed opt-in -session completed a native M1 Pro login and first scanout at 3024x1964 using an -AR30 framebuffer. The owner confirmed the desktop became usable. Wingpanel and -the bottom dock and colored wallpaper/background were missing, and startup felt -slow. The five-minute LightDM rollback was exercised successfully and returned -to Gala; the owner reports its background was visible again. App behavior, -lock/logout, recovery under compositor failure, performance and external-monitor -use remain unqualified. Keep Gala as the rollback session while these gates are -addressed. +`../build-deb.sh` creates that arm64 package. On 2026-10-19 package +`0.1.0+git8023955` completed a native M1 Pro login and initialized the 3024×1964 +internal output. The compositor loaded the configured wallpaper and the +Wingpanel service ran; the Dock service honored the existing user opt-out +marker. The owner reported slow startup, and native panel appearance was not +visually confirmed. The five-minute LightDM rollback returned to Gala. Portal +clients timed out after activation was skipped before `WAYLAND_DISPLAY` was +published, and native IBus portal activation failed. App behavior, lock/logout, +crash recovery, external displays, and portal/IBus behavior remain unqualified. +Keep Gala as the rollback session while these gates are addressed. The default desktop shell entries in `/etc/xdg/io.elementary.desktop.wm.shell` name `io.elementary.wingpanel` and `io.elementary.dock`. The experimental diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service index f4865e9ba..19bda0e80 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-rust-compositor@.service @@ -21,7 +21,11 @@ TimeoutStartSec=90s Environment=WAYLAND_DISPLAY=wayland-0 Environment=PANTHEON_WAYLAND_DISPLAY=wayland-0 ExecStart=/usr/bin/pantheon-compositor --backend native --notify-systemd -ExecStartPost=/usr/bin/systemctl --user import-environment WAYLAND_DISPLAY +ExecStartPost=/usr/bin/dbus-update-activation-environment --systemd WAYLAND_DISPLAY +# Portal clients can request D-Bus activation before the compositor publishes +# WAYLAND_DISPLAY. Queue portal startup after import; --no-block avoids a cycle +# because xdg-desktop-portal is ordered after graphical-session.target. +ExecStartPost=/usr/bin/systemctl --user start --no-block xdg-desktop-portal.service # Clear compositor-owned display variables and remove the fixed socket after # the session ends, including after an unexpected compositor failure. ExecStopPost=-/bin/sh -c 'test "$SERVICE_RESULT" != "exec-condition" && systemctl --user unset-environment GNOME_SETUP_DISPLAY WAYLAND_DISPLAY DISPLAY XAUTHORITY && rm -f "$XDG_RUNTIME_DIR/wayland-0" "$XDG_RUNTIME_DIR/wayland-0.lock"' From 02447cb553e20f8b1c49e30b0bd87c20cee9b82b Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:07:23 -0400 Subject: [PATCH 187/798] Add session startup diagnostics --- .../EXPERIENCE-REQUIREMENTS.md | 5 ++ .../collect-session-startup.sh | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100755 mseries/pantheon-rust-compositor/collect-session-startup.sh diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 691155c0c..9ea43ee57 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -141,6 +141,11 @@ For example, collect one-minute idle samples at a recorded mode with `./collect-session-metrics.sh gala 60 idle 3024x1964 > gala-idle.csv` and repeat in the Rust session with the same arguments except `rust`. Collect a separate pair for each named workload; do not compare unlike runs. +`collect-session-startup.sh gala` or `collect-session-startup.sh rust` records +the OS boot critical chain and journal milestones for the display manager, +compositor, shell clients, portal and IBus. Run it after each login and keep the +output with a manual timestamp for first usable interaction; journal service +readiness alone is not proof that the desktop was visibly ready. The nested prototype serves the Pantheon shell protocol v2, places a panel from its anchor/size/offset, and honors hide modes by tracking maximized/focused or diff --git a/mseries/pantheon-rust-compositor/collect-session-startup.sh b/mseries/pantheon-rust-compositor/collect-session-startup.sh new file mode 100755 index 000000000..098ac0f90 --- /dev/null +++ b/mseries/pantheon-rust-compositor/collect-session-startup.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 " >&2 + exit 2 +} + +[[ $# -eq 1 ]] || usage +case "$1" in + gala) + manager_unit=gnome-session-manager@pantheon-wayland.service + compositor_unit=io.elementary.gala@wayland.service + ;; + rust) + manager_unit=gnome-session-manager@pantheon-rust-wayland.service + compositor_unit=pantheon-rust-compositor@wayland.service + ;; + *) usage ;; +esac + +printf '# session=%s started_utc=%s boot_id=%s\n' \ + "$1" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(cat /proc/sys/kernel/random/boot_id)" +printf '\n## OS boot timing\n' +systemd-analyze time +systemd-analyze critical-chain graphical.target --no-pager || true +systemd-analyze critical-chain display-manager.service --no-pager || true + +printf '\n## Slow system units\n' +systemd-analyze blame --no-pager | head -20 + +printf '\n## Display manager timeline\n' +journalctl -b -u display-manager.service -o short-monotonic --no-pager || true + +printf '\n## Session and shell timeline\n' +journalctl --user -b -o short-monotonic --no-pager \ + -u "$manager_unit" \ + -u "$compositor_unit" \ + -u pantheon-wingpanel.service \ + -u pantheon-dock.service \ + -u xdg-desktop-portal.service \ + -u xdg-desktop-portal-pantheon.service \ + -u io.elementary.files.xdg-desktop-portal.service \ + -u xdg-desktop-portal-gnome.service \ + -u org.freedesktop.IBus.session.GNOME.service \ + | grep -Ei 'Starting |Started |Stopped |Reached target |Failed|timed out|skipped|READY|Created new socket|Loaded Keymap|wallpaper|Creating new Output|GL Renderer|drm surface' \ + || true + +printf '\n## Current login session\n' +loginctl list-sessions --no-legend From bb43804226f7c80f08b2c36acfbc3685235ee9d5 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:08:25 -0400 Subject: [PATCH 188/798] Record portal fix build status --- mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index cc90492b8..99260c0f9 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -162,7 +162,7 @@ session was replaced. | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Package `0.1.0+git8023955` was installed on 2026-10-19 and selected through LightDM. GNOME entered `pantheon-rust-wayland`; its session target, compositor service and user manager became active. The compositor created `wayland-0`, initialized native DRM/EGL, loaded the configured 5000×2813 wallpaper and reported the compositor ready | Wrapper handoff succeeded on this run. Startup-to-usable time is still not measured; a bounded five-minute LightDM rollback was armed and is scheduled to restore Gala | | Startup timing | On the last OS boot, `systemd-analyze` reported 20.838 seconds to `default.target` and 10.465 seconds to `graphical.target`. In the Rust run, LightDM started at 11:53:01 and the compositor service was active by 11:53:02. The user reports boot/login still feels over a minute | These measurements do not include firmware-to-kernel time or prove login-to-usable. Portal D-Bus calls later timed out at 120 seconds; separately time firmware, greeter, session, shell and first usable interaction | -| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. During handoff, systemd skipped `xdg-desktop-portal` activation while `WAYLAND_DISPLAY` was unset; portal clients then timed out. Native IBus portal activation also failed. Portal services activated after returning to Gala. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal/surface-loss warnings. Source now queues the portal service after compositor readiness and environment import | Native session entry, internal output and wallpaper load passed; portal ordering change is staged but unqualified. Native IBus, full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | +| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. During handoff, systemd skipped `xdg-desktop-portal` activation while `WAYLAND_DISPLAY` was unset; portal clients then timed out. Native IBus portal activation also failed. Portal services activated after returning to Gala. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal/surface-loss warnings. Source now publishes `WAYLAND_DISPLAY` and queues portal startup after compositor readiness. A matching `0.1.0+git02447cb` ARM64 package built locally but has not been installed or qualified | Native session entry, internal output and wallpaper load passed; portal ordering change remains unqualified pending a native login. Native IBus, full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | | Performance, power, reliability | Thirty-second internal-panel idle snapshots: Gala `gala` averaged 11.08% of one CPU core and 178,720 KiB RSS; Rust compositor averaged 11.12% and about 173,531 KiB RSS. Rust Wingpanel added about 472,560 KiB RSS and averaged about 1.19% CPU. Battery telemetry was unavailable. Raw CSVs are in [`metrics/`](metrics/) | Initial observations only: compositor CPU is effectively unchanged and its RSS is slightly lower, but session age/state and shell composition are not fully matched. No GPU/power data, latency, representative workload, or crash-free soak. Do not claim the migration is faster or more reliable | From 7a0ac92e8becbdf74b2266733f13ac61795a7420 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:19:06 -0400 Subject: [PATCH 189/798] Measure Pantheon panel readiness and session load --- .../ARCHITECTURE-AND-STATUS.md | 8 ++-- .../EXPERIENCE-REQUIREMENTS.md | 22 ++++++---- mseries/pantheon-rust-compositor/README.md | 9 ++++ .../collect-session-metrics.sh | 41 ++++++++++--------- .../collect-session-startup.sh | 2 +- .../metrics/rust-interactive-20261019.csv | 14 +++++++ .../session/README.md | 16 ++++++-- .../src/handlers/compositor.rs | 14 ++++++- .../pantheon-compositor/src/pantheon_shell.rs | 17 ++++++++ 9 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 mseries/pantheon-rust-compositor/metrics/rust-interactive-20261019.csv diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 99260c0f9..facfe54eb 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -160,11 +160,11 @@ session was replaced. | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value. In native package `0.1.0+git8023955`, Wingpanel's user service became active; the Dock service was intentionally skipped by the existing `~/.config/mseries/dock/user-disabled` marker | Nested visual and hide/reveal smoke passed; native Wingpanel process is active, but native appearance has not been visually confirmed. Dock remains opted out by the user's saved marker | | Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | -| Native Rust session handoff | Package `0.1.0+git8023955` was installed on 2026-10-19 and selected through LightDM. GNOME entered `pantheon-rust-wayland`; its session target, compositor service and user manager became active. The compositor created `wayland-0`, initialized native DRM/EGL, loaded the configured 5000×2813 wallpaper and reported the compositor ready | Wrapper handoff succeeded on this run. Startup-to-usable time is still not measured; a bounded five-minute LightDM rollback was armed and is scheduled to restore Gala | -| Startup timing | On the last OS boot, `systemd-analyze` reported 20.838 seconds to `default.target` and 10.465 seconds to `graphical.target`. In the Rust run, LightDM started at 11:53:01 and the compositor service was active by 11:53:02. The user reports boot/login still feels over a minute | These measurements do not include firmware-to-kernel time or prove login-to-usable. Portal D-Bus calls later timed out at 120 seconds; separately time firmware, greeter, session, shell and first usable interaction | -| Native full desktop | In the `0.1.0+git8023955` session, Wingpanel service/process was active and the configured launcher D-Bus action returned successfully. The Dock service was skipped by the existing user opt-out marker. During handoff, systemd skipped `xdg-desktop-portal` activation while `WAYLAND_DISPLAY` was unset; portal clients then timed out. Native IBus portal activation also failed. Portal services activated after returning to Gala. Compositor logs include repeated XWayland `BadWindow` errors and Wingpanel portal/surface-loss warnings. Source now publishes `WAYLAND_DISPLAY` and queues portal startup after compositor readiness. A matching `0.1.0+git02447cb` ARM64 package built locally but has not been installed or qualified | Native session entry, internal output and wallpaper load passed; portal ordering change remains unqualified pending a native login. Native IBus, full visual shell and interaction qualification remain open. Do not remove the Dock opt-out marker without the user's choice | +| Native Rust session handoff | Package `0.1.0+git02447cb` was installed and selected through LightDM on 2026-10-19. GNOME entered `pantheon-rust-wayland`; compositor initialized the M1 Pro DRM/EGL path at 3024×1964, loaded the configured wallpaper and reported ready. Wingpanel and IBus services started; Dock was skipped because the saved user opt-out marker remains present | Native login and internal scanout passed again. The five-minute rollback timer completed and restored the existing Gala autologin. User reports the desktop felt responsive; an objective first-usable timestamp is still unavailable | +| Startup timing | The boot report remains 20.838 seconds to `default.target` and 10.465 seconds to `graphical.target`. In the 12:10 EDT Rust login, the compositor service started at 12:10:00 and systemd marked it started about 0.46 seconds later; portal providers and the portal service were active about 0.8 seconds after compositor readiness | The tested package no longer reproduces the 120-second portal activation delay. These timings do not include firmware-to-kernel time or measure when the desktop first became usable; retain separate user-observed first-interaction timing | +| Native full desktop | In the `0.1.0+git02447cb` run, compositor startup published `WAYLAND_DISPLAY`, Wingpanel and IBus became active, the portal service and Pantheon/GNOME portal providers started without the previous 120-second wait, and the Dock correctly honored its opt-out marker. The GNOME portal backend warned that `org.gnome.Mutter.ServiceChannel` has no owner, so dialogs may misbehave and screen-sharing behavior is not qualified. One initial DRM page-flip commit returned `Device or resource busy`; the compositor stayed active. Wingpanel logged EGL device lookup warnings and a GTK assertion | Startup ordering fix qualified on native hardware. Native IBus commit, portal dialogs/screen sharing, panel appearance, and sustained interaction still need qualification. Do not remove the Dock opt-out marker without the user's choice | | Native keyboard, pointer, external display, lock/unlock, sharing, accessibility, crash recovery | No end-to-end qualification in the native session | Open | -| Performance, power, reliability | Thirty-second internal-panel idle snapshots: Gala `gala` averaged 11.08% of one CPU core and 178,720 KiB RSS; Rust compositor averaged 11.12% and about 173,531 KiB RSS. Rust Wingpanel added about 472,560 KiB RSS and averaged about 1.19% CPU. Battery telemetry was unavailable. Raw CSVs are in [`metrics/`](metrics/) | Initial observations only: compositor CPU is effectively unchanged and its RSS is slightly lower, but session age/state and shell composition are not fully matched. No GPU/power data, latency, representative workload, or crash-free soak. Do not claim the migration is faster or more reliable | +| Performance, power, reliability | Earlier thirty-second Rust idle sample: compositor 11.12% of one CPU core and 173,531 KiB RSS; Wingpanel 1.19% CPU and 472,560 KiB RSS. The 30-second 12:12 EDT interactive Rust sample measured 4.70% CPU and 179,589 KiB RSS for the compositor, plus 0.37% CPU and 431,200 KiB RSS for Wingpanel. The machine was on mains with battery at 80%, `Not charging`, and battery flow at 0 mW. Raw CSVs are in [`metrics/`](metrics/) | These are process measurements under unlike workloads, not a speed comparison. The plugged-in battery sample cannot quantify system power. Matched Gala/Rust interactive runs, battery-discharge or external power measurements, GPU/latency observations, and a crash-free soak remain open | The current login observation is useful development evidence, not a daily-use readiness result. Continue testing in the isolated experimental session while diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 9ea43ee57..a9672de67 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -123,18 +123,26 @@ launching shell services. This is a responsiveness and activation-ordering pattern, not evidence that a full machine boot takes under one second. For this host, the last `systemd-analyze` report was 20.8 seconds to the system default target and 10.5 seconds to the graphical target; it does not include -firmware time or measure login-to-usable. The native Rust run separately -reached compositor readiness about one second after LightDM started, while -portal requests later timed out after 120 seconds. Track firmware-to-greeter, -login-to-compositor, login-to-panel, and login-to-usable as separate intervals. +firmware time or measure login-to-usable. The first native Rust run reached +compositor readiness about one second after LightDM started, while portal +requests later timed out after 120 seconds. Package `0.1.0+git02447cb` fixed +that portal ordering: providers started within about one second of compositor +readiness. The user's latest report identifies a separate visible delay between +wallpaper and Wingpanel; the compositor now logs each Pantheon panel request +and its first non-null buffer commit so the next run can time client readiness. +Track firmware-to-greeter, login-to-compositor, login-to-panel, and +login-to-usable as separate intervals. References: [Omarchy shell architecture](https://github.com/maralcbr/omarchy-mx-mac/blob/main/shell/README.md), [Omarchy session autostart](https://github.com/omacom/omarchy/blob/quattro/default/hypr/autostart.lua). The Omarchy-MX README says that fork is feature-frozen and directs new Apple Silicon work to `omacom/omarchy-mac`. `collect-session-metrics.sh` provides a read-only, per-process CPU/RSS sampler for Gala or the Rust compositor, panels and dock. It records the session label, -workload, display mode, timestamps and battery power/capacity when exported by -sysfs. It does not measure GPU activity, frame/input latency, cold-start time, -or crashes; those still require separate recorded observations. Run it for +workload, display mode, timestamps, battery charge/discharge flow and capacity, +and whether mains power is online when exported by sysfs. Battery flow while +plugged in is not whole-system power draw; use matched battery-discharge runs +or an external power meter for power comparisons. It does not measure GPU +activity, frame/input latency, cold-start time, or crashes; those still require +separate recorded observations. Run it for both sessions with matching display mode, duration, interval and workload, then retain stdout with the branch/package revision and journal evidence. For example, collect one-minute idle samples at a recorded mode with diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 91ff00fff..27f55724f 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -122,6 +122,15 @@ native startup, internal-panel scanout and automatic rollback only; native keyboard/pointer workflows, complete shell integration, external displays, wallpaper after the new renderer, lock/unlock and compositor-failure recovery remain open. +A later qualification using package `0.1.0+git02447cb` confirmed that +compositor readiness now publishes the Wayland display before portal startup: +the portal service and providers came up within about one second, and IBus +started without the previous 120-second activation timeout. The user reported +the session felt responsive. This run still logged one initial DRM page-flip +`Device or resource busy` error and a missing Mutter `ServiceChannel` warning +from the GNOME portal backend. It does not qualify portal dialogs, screen +sharing, native text commits, or a performance/power improvement over Gala. + In private Xvfb, the release binary launched a GTK Wayland Zenity field; synthetic key events reached it, and IBus/Mozc committed `konnichiha` as `こんにちは` using `mozc-jp` with Hiragana enabled in a private profile. This diff --git a/mseries/pantheon-rust-compositor/collect-session-metrics.sh b/mseries/pantheon-rust-compositor/collect-session-metrics.sh index c08f93c63..8136e0f67 100755 --- a/mseries/pantheon-rust-compositor/collect-session-metrics.sh +++ b/mseries/pantheon-rust-compositor/collect-session-metrics.sh @@ -49,28 +49,30 @@ read_rss_kib() { } battery_sample() { - local supply - for supply in /sys/class/power_supply/BAT*; do - [[ -d $supply ]] || continue - local power capacity - power=NA - capacity=NA - if [[ -r $supply/power_now ]]; then - power=$(( $(<"$supply/power_now") / 1000 )) - elif [[ -r $supply/current_now && -r $supply/voltage_now ]]; then - power=$(( $(<"$supply/current_now") * $(<"$supply/voltage_now") / 1000000000 )) - fi - [[ -r $supply/capacity ]] && capacity=$(<"$supply/capacity") - echo "$power,$capacity" - return + local supply battery= status=NA power=NA capacity=NA ac_online=NA + for supply in /sys/class/power_supply/*; do + [[ -r $supply/type ]] || continue + case $(<"$supply/type") in + Battery) battery=$supply ;; + Mains) [[ -r $supply/online ]] && ac_online=$(<"$supply/online") ;; + esac done - echo "NA,NA" + if [[ -n $battery ]]; then + [[ -r $battery/status ]] && status=$(<"$battery/status") + if [[ -r $battery/power_now ]]; then + power=$(( $(<"$battery/power_now") / 1000 )) + elif [[ -r $battery/current_now && -r $battery/voltage_now ]]; then + power=$(( $(<"$battery/current_now") * $(<"$battery/voltage_now") / 1000000000 )) + fi + [[ -r $battery/capacity ]] && capacity=$(<"$battery/capacity") + fi + echo "$power,$status,$ac_online,$capacity" } sample_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ) printf '# session=%s workload=%s display_mode=%s duration_s=%s interval_s=%s started_utc=%s\n' \ "$session" "$workload" "$display_mode" "$duration" "$interval" "$sample_utc" -printf 'utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_power_mw,battery_percent\n' +printf 'utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_flow_mw,battery_status,ac_online,battery_percent\n' while read -r pid comm; do [[ -n ${pid:-} ]] || continue @@ -89,8 +91,7 @@ while :; do now=$(date +%s) elapsed=$((now - start_epoch)) battery=$(battery_sample) - battery_power=${battery%,*} - battery_percent=${battery#*,} + IFS=, read -r battery_power battery_status ac_online battery_percent <<< "$battery" sample_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ) while read -r pid comm; do [[ -n ${pid:-} ]] || continue @@ -103,9 +104,9 @@ while :; do cpu=NA fi rss=$(read_rss_kib "$pid") - printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \ + printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \ "$sample_utc" "$session" "$workload" "$display_mode" "$pid" "$comm" "$cpu" "$rss" "$elapsed" \ - "$battery_power" "$battery_percent" + "$battery_power" "$battery_status" "$ac_online" "$battery_percent" start_ticks[$pid]=$ticks done < <(process_list) (( now >= end_epoch )) && break diff --git a/mseries/pantheon-rust-compositor/collect-session-startup.sh b/mseries/pantheon-rust-compositor/collect-session-startup.sh index 098ac0f90..4d1792f32 100755 --- a/mseries/pantheon-rust-compositor/collect-session-startup.sh +++ b/mseries/pantheon-rust-compositor/collect-session-startup.sh @@ -43,7 +43,7 @@ journalctl --user -b -o short-monotonic --no-pager \ -u io.elementary.files.xdg-desktop-portal.service \ -u xdg-desktop-portal-gnome.service \ -u org.freedesktop.IBus.session.GNOME.service \ - | grep -Ei 'Starting |Started |Stopped |Reached target |Failed|timed out|skipped|READY|Created new socket|Loaded Keymap|wallpaper|Creating new Output|GL Renderer|drm surface' \ + | grep -Ei 'Starting |Started |Stopped |Reached target |Failed|timed out|skipped|READY|Created new socket|Loaded Keymap|wallpaper|Creating new Output|GL Renderer|drm surface|Pantheon panel surface requested|first Pantheon panel buffer committed' \ || true printf '\n## Current login session\n' diff --git a/mseries/pantheon-rust-compositor/metrics/rust-interactive-20261019.csv b/mseries/pantheon-rust-compositor/metrics/rust-interactive-20261019.csv new file mode 100644 index 000000000..f89a377ed --- /dev/null +++ b/mseries/pantheon-rust-compositor/metrics/rust-interactive-20261019.csv @@ -0,0 +1,14 @@ +# session=rust workload=interactive display_mode=3024x1964 duration_s=30 interval_s=5 started_utc=2026-10-19T16:12:29Z +utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_flow_mw,battery_status,ac_online,battery_percent +2026-10-19T16:12:34Z,rust,interactive,3024x1964,433775,pantheon-compos,6.60,179536,5,0,Not charging,1,80 +2026-10-19T16:12:34Z,rust,interactive,3024x1964,433879,io.elementary.w,0.40,431200,5,0,Not charging,1,80 +2026-10-19T16:12:39Z,rust,interactive,3024x1964,433775,pantheon-compos,5.20,179568,10,0,Not charging,1,80 +2026-10-19T16:12:39Z,rust,interactive,3024x1964,433879,io.elementary.w,0.40,431200,10,0,Not charging,1,80 +2026-10-19T16:12:44Z,rust,interactive,3024x1964,433775,pantheon-compos,2.00,179568,15,0,Not charging,1,80 +2026-10-19T16:12:44Z,rust,interactive,3024x1964,433879,io.elementary.w,0.40,431200,15,0,Not charging,1,80 +2026-10-19T16:12:49Z,rust,interactive,3024x1964,433775,pantheon-compos,3.60,179600,20,0,Not charging,1,80 +2026-10-19T16:12:49Z,rust,interactive,3024x1964,433879,io.elementary.w,0.60,431200,20,0,Not charging,1,80 +2026-10-19T16:12:54Z,rust,interactive,3024x1964,433775,pantheon-compos,5.60,179616,25,0,Not charging,1,80 +2026-10-19T16:12:54Z,rust,interactive,3024x1964,433879,io.elementary.w,0.40,431200,25,0,Not charging,1,80 +2026-10-19T16:12:59Z,rust,interactive,3024x1964,433775,pantheon-compos,5.20,179648,30,0,Not charging,1,80 +2026-10-19T16:12:59Z,rust,interactive,3024x1964,433879,io.elementary.w,0.00,431200,30,0,Not charging,1,80 diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 1089a7260..43fda3218 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -28,7 +28,11 @@ launches. After readiness, `dbus-update-activation-environment --systemd` publishes `WAYLAND_DISPLAY` to both D-Bus and the user manager, then the service queues `xdg-desktop-portal.service` asynchronously. This addresses a native handoff where early portal activation was skipped while the display variable -was unset; the ordering change is staged and still needs native qualification. +was unset. Package `0.1.0+git02447cb` passed one native ordering qualification: +the portal service and its configured providers started within about one second +of compositor readiness, without the previous 120-second activation timeout. +The GNOME portal backend still warns that Mutter's private `ServiceChannel` is +unavailable; dialogs and screen sharing are not qualified. The experimental service retries an unexpected compositor failure, with a two-second delay and a limit of three starts within one minute. Once the limit @@ -47,9 +51,13 @@ Wingpanel service ran; the Dock service honored the existing user opt-out marker. The owner reported slow startup, and native panel appearance was not visually confirmed. The five-minute LightDM rollback returned to Gala. Portal clients timed out after activation was skipped before `WAYLAND_DISPLAY` was -published, and native IBus portal activation failed. App behavior, lock/logout, -crash recovery, external displays, and portal/IBus behavior remain unqualified. -Keep Gala as the rollback session while these gates are addressed. +published, and native IBus portal activation failed. A subsequent +`0.1.0+git02447cb` login fixed that activation ordering; portal services and +IBus started, and the user reported the session felt responsive. Its first +DRM page-flip attempt returned `Device or resource busy` but the session stayed +active. Native IBus text commit, GNOME portal functions, panel appearance, +lock/logout, crash recovery and external displays remain unqualified. Keep Gala +as the rollback session while these gates are addressed. The default desktop shell entries in `/etc/xdg/io.elementary.desktop.wm.shell` name `io.elementary.wingpanel` and `io.elementary.dock`. The experimental diff --git a/rust/pantheon-compositor/src/handlers/compositor.rs b/rust/pantheon-compositor/src/handlers/compositor.rs index 1f3aae7e7..c994edf7a 100644 --- a/rust/pantheon-compositor/src/handlers/compositor.rs +++ b/rust/pantheon-compositor/src/handlers/compositor.rs @@ -8,8 +8,8 @@ use smithay::{ wayland::{ buffer::BufferHandler, compositor::{ - CompositorClientState, CompositorHandler, CompositorState, get_parent, - is_sync_subsurface, + BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, + SurfaceAttributes, get_parent, is_sync_subsurface, with_states, }, shm::{ShmHandler, ShmState}, }, @@ -34,6 +34,16 @@ impl CompositorHandler for PantheonCompositor { } fn commit(&mut self, surface: &WlSurface) { + if self.is_panel_surface(surface) { + let has_buffer = with_states(surface, |states| { + let mut attributes = states.cached_state.get::(); + matches!( + attributes.current().buffer.as_ref(), + Some(BufferAssignment::NewBuffer(_)) + ) + }); + self.note_panel_buffer_commit(surface, has_buffer); + } on_commit_buffer_handler::(surface); if !is_sync_subsurface(surface) { let mut root = surface.clone(); diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 986050492..2adb0ea58 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -56,6 +56,8 @@ pub struct PanelState { /// Whether the compositor has moved this panel out of view for its hide mode. pub hidden: bool, pub visible_in_multitasking_view: bool, + /// Whether this panel has committed its first non-null buffer this session. + pub first_buffer_committed: bool, } #[derive(Clone)] @@ -229,6 +231,7 @@ impl impl PantheonCompositor { pub fn add_panel(&mut self, surface: WlSurface) { self.panels.insert(surface.id(), PanelState::default()); + tracing::info!(surface = ?surface.id(), "Pantheon panel surface requested"); let window_id = self .workspace_manager .windows() @@ -245,6 +248,20 @@ impl PantheonCompositor { self.position_panel(&surface); } + pub fn note_panel_buffer_commit(&mut self, surface: &WlSurface, has_buffer: bool) { + if !has_buffer { + return; + } + let Some(panel) = self.panels.get_mut(&surface.id()) else { + return; + }; + if panel.first_buffer_committed { + return; + } + panel.first_buffer_committed = true; + tracing::info!(surface = ?surface.id(), "first Pantheon panel buffer committed"); + } + pub fn remove_panel(&mut self, surface: &WlSurface) { self.panels.remove(&surface.id()); if self.refresh_work_areas() { From f6d04e912b63db149e92eff2aa6fc4da8db1476c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:23:41 -0400 Subject: [PATCH 190/798] Add compositor qualification dashboard --- .../EXPERIENCE-REQUIREMENTS.md | 1 + mseries/pantheon-rust-compositor/README.md | 15 ++ .../pantheon-rust-compositor/dashboard.html | 223 ++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 mseries/pantheon-rust-compositor/dashboard.html diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index a9672de67..2c21202a1 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -21,6 +21,7 @@ it does not imply that the current prototype supports it. | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | +| Consistent window appearance | Window corners should look consistent across the apps and window states used in the Pantheon session | Owner report from the 2026-10-19 Rust native login: corner presentation varies between displayed windows and is unwanted; exact apps/states are not yet recorded | Capture examples across native Wayland, XWayland, dialogs and tiled/maximized windows; identify whether client-side shadows/decorations, compositor clipping, or scale/state changes cause the mismatch, then define one accepted appearance | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | | Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Private dconf/Xvfb reload smoke passed; native output behavior still needs qualification | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 27f55724f..21d6704be 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -16,6 +16,9 @@ prioritized list and upstream feature radar are in For a stack drawing comparing existing elementary OS, Omarchy on Asahi, and the proposed Pantheon MVP—along with a tested-versus-open status table—see [`ARCHITECTURE-AND-STATUS.md`](ARCHITECTURE-AND-STATUS.md). +For a visual comparison of Gala/Mutter and the Rust/Smithay migration stages, +startup timing, CPU, memory, and power evidence, open the standalone +[`migration dashboard`](dashboard.html). On 2026-10-19 the installed experimental session completed its first native M1 Pro login and scanned out an AR30 3024x1964 framebuffer. The owner reported @@ -131,6 +134,18 @@ the session felt responsive. This run still logged one initial DRM page-flip from the GNOME portal backend. It does not qualify portal dialogs, screen sharing, native text commits, or a performance/power improvement over Gala. +The instrumented `0.1.0+git7a0ac92` login reached the first committed +Wingpanel buffer about 2.2 seconds after the Rust session manager started +(about 1.7 seconds after the compositor service started). The owner described +this run's opening time as really good; the journal milestone measures panel +buffer readiness, not first interactive use. The owner also reported that +window corners look inconsistent and unwanted. This is now a visual +acceptance issue; affected apps and window states still need examples. The +3024×1964 panel mode looked too zoomed out, so the next Rust login should keep +the high-resolution mode and try 2× logical UI scaling. These owner reports +are tracked in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md) and +the [migration dashboard](dashboard.html). + In private Xvfb, the release binary launched a GTK Wayland Zenity field; synthetic key events reached it, and IBus/Mozc committed `konnichiha` as `こんにちは` using `mozc-jp` with Hiragana enabled in a private profile. This diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html new file mode 100644 index 000000000..842a0a654 --- /dev/null +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -0,0 +1,223 @@ + + + + + + + Pantheon Rust compositor — migration dashboard + + + +

    +
    +
    M-series Pantheon compositor migrationEvidence updated 19 Oct 2026 · branch codex/rust-mutter-pantheon
    +

    Gala/Mutter and Rust/Smithay

    +

    A stage-by-stage view of the existing elementary desktop and the experimental Rust replacement. This is an experience-focused compositor, not a feature-for-feature Mutter port. The latest bounded Rust qualification felt much quicker to open; its timed rollback returns the machine to Gala.

    +
    + +
    +
    +

    Native Pantheon

    Gala + libmutter · default desktop

    Baseline
    +

    Established Wayland session, shell integration, input stack and app compatibility. On this M1 Pro, Linux DRM/KMS exposes the built-in panel; external dock HDMI remains a DCP/kernel display-path issue.

    + Current LightDM autologin restored to pantheon-wayland. +
    +
    +

    Rust Pantheon

    Smithay compositor · opt-in session

    Experimental
    +

    Native scanout, wallpaper, Wingpanel service, IBus daemon and portal startup now work in qualification. The saved Dock opt-out is respected. Several user-facing protocols and runtime gates remain open.

    + Latest instrumented ARM64 build: 0.1.0+git7a0ac92 · first Wingpanel buffer observed; automatic Gala rollback is scheduled. +
    +
    + +
    +

    Speed and resource snapshots

    +
    +
    20.8 sOS boot to default target
    +
    10.5 sOS boot to graphical target
    +
    0.46 sRust compositor service start → ready
    +
    2.2 sRust session start → first Wingpanel buffer
    +
    +
    +
    +

    Compositor CPU · idle snapshots

    +

    30 seconds, internal display. Percent of one CPU core.

    + +

    No measured idle CPU win yet. These are short, process-level samples and not a matched end-to-end desktop benchmark.

    +
    +
    +

    Compositor memory · idle snapshots

    +

    Resident memory in KiB; panels are shown separately below.

    + +

    Rust was about 5 MiB lower in this snapshot. That small gap is not enough to claim a meaningful memory improvement.

    +
    +
    +
    +
    +

    Rust shell process overhead · idle snapshot

    + +

    The Gala sample did not separately capture its Wingpanel process, so total-session memory and CPU cannot be compared yet.

    +
    +
    +

    Recent Rust interactive sample

    +

    Compositor: 4.70% CPU, 179,589 KiB RSS. Wingpanel: 0.37% CPU, 431,200 KiB RSS.

    +

    30 seconds while the desktop was in active use. There is no matching Gala interactive sample; do not compare it with Gala idle numbers.

    +
    +
    +
    +
    +

    Power

    +

    Not measured yet. The Rust sample was on mains with battery at 80%, “Not charging,” and 0 mW battery flow.

    +

    Battery flow while plugged in does not reveal total system draw. A matched battery-discharge run or external power meter is needed.

    +
    +
    +

    Startup milestones

    +
    +
    Wallpaper loadedBefore panel unit starts
    +
    Compositor ready~0.46 s after service start
    +
    Portal ready~0.8 s after compositor readiness
    +
    Wingpanel bufferFirst buffer ~2.2 s after session start
    +
    +
    +
    +
    + +
    +

    Migration stage comparison

    +
    + + + + + + + + + + + + + + + + + +
    Stage / experienceGala / MutterRust / SmithayEvidence and next gate
    Nested compositorEstablishedPassedRust runs a nested Wayland backend; GTK windows and Pantheon panels rendered in private Xvfb.
    Native M1 panelDaily pathPassed twiceRust scanout at 3024×1964 via Asahi render GPU and Apple DRM/KMS. Initial page-flip attempt logged one transient Device or resource busy.
    Display scale / densityCompare baselineTry 2× scale next runThe native 3024×1964 panel looks too zoomed out to the owner. Keep the high-resolution mode and try 2× logical UI scaling for a HiDPI-sized desktop.
    Wallpaper / backgroundEstablishedNative loadedConfigured wallpaper rendered and loaded in Rust native session; nested live-setting reload also passed.
    WingpanelDaily pathDelay reportedService starts promptly, but user sees wallpaper well before the panel. New logs measure surface request and first non-null buffer commit; run pending.
    DockDaily pathOpted outRust session correctly skips Dock because ~/.config/mseries/dock/user-disabled exists. Dock render/hide/reveal passed only in nested session.
    Portals / session envEstablishedActivation fixedRust publishes WAYLAND_DISPLAY after ready; portal providers now start without the former 120-second timeout. GNOME backend still lacks Mutter's private ServiceChannel.
    Input methodsDaily workflowNested onlyIBus/Mozc text commit passed in nested GTK; IBus daemon starts natively, but native text commit and source switching are unqualified.
    Window actions / XWaylandEstablishedPartialNested XWayland keyboard smoke and basic Gala actions passed; overview, clipboard/PRIMARY, and native app behavior remain open.
    Consistent window cornersBaseline to checkOwner-reported issueWindow corner appearance varies in the Rust session and is unwanted. Capture app/window-state examples before selecting a rendering or decoration fix.
    External monitors / dock HDMIKernel-boundNot qualifiedExternal hotplug remains open. The G3 black screen is DCP/kernel display firmware; a compositor replacement cannot fix missing DRM output.
    Lock, accessibility, sharing, recoveryEstablishedOpenNative end-to-end qualification is still needed before Rust could replace Gala.
    Performance and powerReferenceUnprovenNo matched interactive/Gala benchmark, whole-system power result, GPU/latency measurement or crash-free soak yet.
    +
    +
    + +
    +

    What would count as “better”

    +

    The Rust branch should only be called a performance improvement after the same M-series machine, display mode, workload, session age, and panel/dock composition are compared against Gala. Track login-to-first-usable time, CPU, total session RSS, GPU activity, latency, battery discharge or measured input power, and crashes/recovery.

    +
    Data sources and limitations +
      +
    • metrics/gala-idle-20261019.csv and metrics/rust-idle-qualification-20261019.csv: short internal-panel idle samples; different session ages and incomplete process parity.
    • +
    • metrics/rust-interactive-20261019.csv: 30-second interactive Rust sample; machine on mains, so its battery-flow reading is not device power use.
    • +
    • collect-session-startup.sh: boot chain and service/journal milestones. In build 0.1.0+git7a0ac92, the first Wingpanel buffer was committed about 2.2 seconds after the Rust session manager started; this is not a direct measurement of first interaction.
    • +
    • The owner reported this run had a really good opening time. Corner inconsistency is a separate visual issue reported during this qualification; exact affected apps and states are not yet recorded.
    • +
    +
    +
    + +
    +
    + + From 107375196b4600659fffdc29c4adf5717da23cec Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:24:17 -0400 Subject: [PATCH 191/798] Stage HiDPI profile for next Rust login --- mseries/pantheon-rust-compositor/README.md | 5 +++-- mseries/pantheon-rust-compositor/dashboard.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 21d6704be..cff83cd39 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -141,8 +141,9 @@ this run's opening time as really good; the journal milestone measures panel buffer readiness, not first interactive use. The owner also reported that window corners look inconsistent and unwanted. This is now a visual acceptance issue; affected apps and window states still need examples. The -3024×1964 panel mode looked too zoomed out, so the next Rust login should keep -the high-resolution mode and try 2× logical UI scaling. These owner reports +3024×1964 panel mode looked too zoomed out. A Rust-only 2× logical scale +profile is staged for the next login while keeping the high-resolution mode; +the resulting UI size and panel layout remain unqualified. These owner reports are tracked in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md) and the [migration dashboard](dashboard.html). diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index 842a0a654..a26281c1f 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -188,7 +188,7 @@

    Migration stage comparison

    Nested compositorEstablishedPassedRust runs a nested Wayland backend; GTK windows and Pantheon panels rendered in private Xvfb. Native M1 panelDaily pathPassed twiceRust scanout at 3024×1964 via Asahi render GPU and Apple DRM/KMS. Initial page-flip attempt logged one transient Device or resource busy. - Display scale / densityCompare baselineTry 2× scale next runThe native 3024×1964 panel looks too zoomed out to the owner. Keep the high-resolution mode and try 2× logical UI scaling for a HiDPI-sized desktop. + Display scale / densityCompare baseline2× profile stagedThe native 3024×1964 panel looks too zoomed out to the owner. A Rust-only 2× logical scale profile is staged for the next login; visual layout and panel behavior still need qualification. Wallpaper / backgroundEstablishedNative loadedConfigured wallpaper rendered and loaded in Rust native session; nested live-setting reload also passed. WingpanelDaily pathDelay reportedService starts promptly, but user sees wallpaper well before the panel. New logs measure surface request and first non-null buffer commit; run pending. DockDaily pathOpted outRust session correctly skips Dock because ~/.config/mseries/dock/user-disabled exists. Dock render/hide/reveal passed only in nested session. From 9d3aac6c841845036193c6687b0fe83bc85acca0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:25:36 -0400 Subject: [PATCH 192/798] Set measurable compositor efficiency gate --- mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md | 1 + mseries/pantheon-rust-compositor/dashboard.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 2c21202a1..599275ee6 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -22,6 +22,7 @@ it does not imply that the current prototype supports it. | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Consistent window appearance | Window corners should look consistent across the apps and window states used in the Pantheon session | Owner report from the 2026-10-19 Rust native login: corner presentation varies between displayed windows and is unwanted; exact apps/states are not yet recorded | Capture examples across native Wayland, XWayland, dialogs and tiled/maximized windows; identify whether client-side shadows/decorations, compositor clipping, or scale/state changes cause the mismatch, then define one accepted appearance | +| Lower system overhead | The Rust session should use less CPU and power than Gala in matched idle and interactive workloads without worse responsiveness or reliability | Existing 30-second process snapshots are not matched; Rust/Gala compositor CPU read 11.12%/11.10% of one core, and Rust shell-process parity is missing | Profile repeated matched sessions and total process trees; optimize measured hotspots; claim a win only when it exceeds run-to-run variation. Current investigation lead: native DRM vblank completion requests a compositor redraw on every frame, even while idle; verify with a profile before changing frame scheduling so client animations and Wayland frame callbacks remain correct | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | | Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Private dconf/Xvfb reload smoke passed; native output behavior still needs qualification | diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index a26281c1f..d21d54b87 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -135,7 +135,7 @@

    Compositor CPU · idle snapshots

    Gala
    11.10%
    Rust
    11.12%
    -

    No measured idle CPU win yet. These are short, process-level samples and not a matched end-to-end desktop benchmark.

    +

    No measured idle CPU win yet. These are short, process-level samples and not a matched end-to-end desktop benchmark. Code inspection found the native DRM vblank handler requests another compositor redraw on every frame; this is a concrete idle-CPU investigation lead, pending profiling and review of Wayland frame-callback needs.

    Compositor memory · idle snapshots

    From 3eefa3e321bb02e2328c625c7f8b74afeadccfd2 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:29:42 -0400 Subject: [PATCH 193/798] Record Wingpanel timing and rollback result --- mseries/pantheon-rust-compositor/README.md | 10 +++++++-- .../pantheon-rust-compositor/dashboard.html | 22 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index cff83cd39..4bd3c28b1 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -137,8 +137,14 @@ sharing, native text commits, or a performance/power improvement over Gala. The instrumented `0.1.0+git7a0ac92` login reached the first committed Wingpanel buffer about 2.2 seconds after the Rust session manager started (about 1.7 seconds after the compositor service started). The owner described -this run's opening time as really good; the journal milestone measures panel -buffer readiness, not first interactive use. The owner also reported that +this run's overall opening time as really good, then noted the wallpaper-to- +Wingpanel gap still felt slow. The journal shows wallpaper load at +0.11 s, +Wingpanel service start at +0.51 s, first panel surface request at +1.61 s, +and first buffer at +2.22 s, relative to session-manager start. This is about +2.11 s from wallpaper load to first panel buffer; the journal milestone is not +a direct measure of visible pixels or first interactive use. The five-minute +rollback completed successfully at 12:26 EDT and returned LightDM to Gala. +The owner also reported that window corners look inconsistent and unwanted. This is now a visual acceptance issue; affected apps and window states still need examples. The 3024×1964 panel mode looked too zoomed out. A Rust-only 2× logical scale diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index d21d54b87..288b523d6 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -103,7 +103,7 @@
    M-series Pantheon compositor migrationEvidence updated 19 Oct 2026 · branch codex/rust-mutter-pantheon

    Gala/Mutter and Rust/Smithay

    -

    A stage-by-stage view of the existing elementary desktop and the experimental Rust replacement. This is an experience-focused compositor, not a feature-for-feature Mutter port. The latest bounded Rust qualification felt much quicker to open; its timed rollback returns the machine to Gala.

    +

    A stage-by-stage view of the existing elementary desktop and the experimental Rust replacement. This is an experience-focused compositor, not a feature-for-feature Mutter port. The latest bounded Rust qualification felt much quicker to open overall, while the owner still noticed a slow wallpaper-to-Wingpanel transition. Its timed rollback completed and Gala is active.

    @@ -115,7 +115,7 @@

    Gala/Mutter and Rust/Smithay

    Rust Pantheon

    Smithay compositor · opt-in session

    Experimental

    Native scanout, wallpaper, Wingpanel service, IBus daemon and portal startup now work in qualification. The saved Dock opt-out is respected. Several user-facing protocols and runtime gates remain open.

    - Latest instrumented ARM64 build: 0.1.0+git7a0ac92 · first Wingpanel buffer observed; automatic Gala rollback is scheduled. + Latest instrumented ARM64 build: 0.1.0+git7a0ac92 · timed rollback completed successfully; Gala is active.
    @@ -125,7 +125,7 @@

    Speed and resource snapshots

    20.8 sOS boot to default target
    10.5 sOS boot to graphical target
    0.46 sRust compositor service start → ready
    -
    2.2 sRust session start → first Wingpanel buffer
    +
    2.1 sWallpaper loaded → first Wingpanel buffer
    @@ -171,10 +171,10 @@

    Power

    Startup milestones

    -
    Wallpaper loadedBefore panel unit starts
    +
    Wallpaper loaded~0.11 s after session start
    Compositor ready~0.46 s after service start
    Portal ready~0.8 s after compositor readiness
    -
    Wingpanel bufferFirst buffer ~2.2 s after session start
    +
    Wingpanel buffer~2.22 s after start; ~2.11 s after wallpaper
    @@ -190,7 +190,7 @@

    Migration stage comparison

    Native M1 panelDaily pathPassed twiceRust scanout at 3024×1964 via Asahi render GPU and Apple DRM/KMS. Initial page-flip attempt logged one transient Device or resource busy. Display scale / densityCompare baseline2× profile stagedThe native 3024×1964 panel looks too zoomed out to the owner. A Rust-only 2× logical scale profile is staged for the next login; visual layout and panel behavior still need qualification. Wallpaper / backgroundEstablishedNative loadedConfigured wallpaper rendered and loaded in Rust native session; nested live-setting reload also passed. - WingpanelDaily pathDelay reportedService starts promptly, but user sees wallpaper well before the panel. New logs measure surface request and first non-null buffer commit; run pending. + WingpanelDaily path~2.1 s after wallpaperWallpaper loaded ~0.11 s after session start; Wingpanel service started ~0.51 s; its surface request arrived ~1.61 s and first buffer ~2.22 s. Owner noticed the gap as slow. This splits compositor startup from Wingpanel client initialization and first-buffer work. DockDaily pathOpted outRust session correctly skips Dock because ~/.config/mseries/dock/user-disabled exists. Dock render/hide/reveal passed only in nested session. Portals / session envEstablishedActivation fixedRust publishes WAYLAND_DISPLAY after ready; portal providers now start without the former 120-second timeout. GNOME backend still lacks Mutter's private ServiceChannel. Input methodsDaily workflowNested onlyIBus/Mozc text commit passed in nested GTK; IBus daemon starts natively, but native text commit and source switching are unqualified. @@ -204,6 +204,12 @@

    Migration stage comparison

    +
    +

    Could Gala run on the Rust stack?

    +

    Gala is itself a Wayland compositor built around libmutter, Clutter and Mutter’s Meta APIs. The local Gala 8.6.2 audit found 79 Vala files using Meta.* across 24,534 lines of core source. Reusing Gala as a shell inside Smithay would require separating and porting much of that compositor-specific code.

    +

    That is a much larger rewrite than implementing the Pantheon behaviors we need, and it would not change the DCP/kernel HDMI limit. Keep Gala as the daily baseline; port a behavior only when it appears in the M-series acceptance list, then require matched performance, visual quality and reliability wins before considering a swap.

    +
    +

    What would count as “better”

    The Rust branch should only be called a performance improvement after the same M-series machine, display mode, workload, session age, and panel/dock composition are compared against Gala. Track login-to-first-usable time, CPU, total session RSS, GPU activity, latency, battery discharge or measured input power, and crashes/recovery.

    @@ -211,8 +217,8 @@

    What would count as “better”

    • metrics/gala-idle-20261019.csv and metrics/rust-idle-qualification-20261019.csv: short internal-panel idle samples; different session ages and incomplete process parity.
    • metrics/rust-interactive-20261019.csv: 30-second interactive Rust sample; machine on mains, so its battery-flow reading is not device power use.
    • -
    • collect-session-startup.sh: boot chain and service/journal milestones. In build 0.1.0+git7a0ac92, the first Wingpanel buffer was committed about 2.2 seconds after the Rust session manager started; this is not a direct measurement of first interaction.
    • -
    • The owner reported this run had a really good opening time. Corner inconsistency is a separate visual issue reported during this qualification; exact affected apps and states are not yet recorded.
    • +
    • collect-session-startup.sh: boot chain and service/journal milestones. In build 0.1.0+git7a0ac92, wallpaper loaded at +0.11 s and the first Wingpanel buffer committed at +2.22 s relative to the Rust session manager start. The owner noticed this ~2.1 s wallpaper-to-panel gap as slow, despite reporting good overall opening time. Journal milestones are not direct measures of visual perception or first interaction.
    • +
    • The owner reported inconsistent window corners; exact affected apps and states are not yet recorded.
    From 76eaa966f66e7d0f691c53cafebd980d17232e89 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:31:16 -0400 Subject: [PATCH 194/798] Clarify current CPU overhead evidence --- mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md | 2 +- mseries/pantheon-rust-compositor/dashboard.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 599275ee6..2fe14e40b 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -22,7 +22,7 @@ it does not imply that the current prototype supports it. | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | | Consistent window appearance | Window corners should look consistent across the apps and window states used in the Pantheon session | Owner report from the 2026-10-19 Rust native login: corner presentation varies between displayed windows and is unwanted; exact apps/states are not yet recorded | Capture examples across native Wayland, XWayland, dialogs and tiled/maximized windows; identify whether client-side shadows/decorations, compositor clipping, or scale/state changes cause the mismatch, then define one accepted appearance | -| Lower system overhead | The Rust session should use less CPU and power than Gala in matched idle and interactive workloads without worse responsiveness or reliability | Existing 30-second process snapshots are not matched; Rust/Gala compositor CPU read 11.12%/11.10% of one core, and Rust shell-process parity is missing | Profile repeated matched sessions and total process trees; optimize measured hotspots; claim a win only when it exceeds run-to-run variation. Current investigation lead: native DRM vblank completion requests a compositor redraw on every frame, even while idle; verify with a profile before changing frame scheduling so client animations and Wayland frame callbacks remain correct | +| Lower system overhead | The Rust session should use less CPU and power than Gala in matched idle and interactive workloads without worse responsiveness or reliability | In the existing six-sample 30-second idle captures, median summed CPU was 12.4% of one core for Rust compositor+Wingpanel and 11.2% for Gala+wrapper; the Gala capture did not include Wingpanel, so total desktop CPU is still unpaired. The compositor-only medians were 11.12% and 11.10%. | Profile repeated matched sessions and total process trees; optimize measured hotspots; claim a win only when it exceeds run-to-run variation. Current investigation leads: native DRM vblank completion requests a compositor redraw after each completed frame, and the 100 ms Gala-compat timer rebuilds a window snapshot each tick. Measure their cost before changing scheduling or polling so client animations, Dock updates and Wayland frame callbacks remain correct | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | | Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Private dconf/Xvfb reload smoke passed; native output behavior still needs qualification | diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index 288b523d6..5aa5901e1 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -135,7 +135,7 @@

    Compositor CPU · idle snapshots

    Gala
    11.10%
    Rust
    11.12%
    -

    No measured idle CPU win yet. These are short, process-level samples and not a matched end-to-end desktop benchmark. Code inspection found the native DRM vblank handler requests another compositor redraw on every frame; this is a concrete idle-CPU investigation lead, pending profiling and review of Wayland frame-callback needs.

    +

    No measured idle CPU win yet. Summing processes present at each sample gives Rust compositor + Wingpanel a 12.4% median of one core; Gala + its wrapper measured 11.2%. The Gala sample did not include Wingpanel, so this is not a matched total-session result, but the available evidence does not show Rust using less CPU. Code inspection found the native DRM vblank handler requests another compositor redraw after each completed frame; this is a lead to profile, with client frame-callback behavior checked before changing scheduling.

    Compositor memory · idle snapshots

    From c87ba39370aa2e4b537508a3dd9bd92bf7764486 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:32:58 -0400 Subject: [PATCH 195/798] Add per-thread compositor profiling helper --- .../EXPERIENCE-REQUIREMENTS.md | 8 ++++ mseries/pantheon-rust-compositor/README.md | 4 ++ .../collect-session-thread-profile.sh | 43 +++++++++++++++++++ .../pantheon-rust-compositor/dashboard.html | 1 + 4 files changed, 56 insertions(+) create mode 100755 mseries/pantheon-rust-compositor/collect-session-thread-profile.sh diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 2fe14e40b..96bef814d 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -147,6 +147,14 @@ activity, frame/input latency, cold-start time, or crashes; those still require separate recorded observations. Run it for both sessions with matching display mode, duration, interval and workload, then retain stdout with the branch/package revision and journal evidence. +`collect-session-thread-profile.sh` complements this with `pidstat` per-thread +CPU and resident-memory samples for the compositor, Wingpanel and Dock. Capture +the same idle or interactive workload in each session; for example, use +`./collect-session-thread-profile.sh gala 60 1` and +`./collect-session-thread-profile.sh rust 60 1`, then save each result beside +the matching process-level CSV and startup journal. Thread samples identify hot +threads but do not provide call stacks, so use a sampling profiler for +function-level attribution when one is available. For example, collect one-minute idle samples at a recorded mode with `./collect-session-metrics.sh gala 60 idle 3024x1964 > gala-idle.csv` and repeat in the Rust session with the same arguments except `rust`. Collect a diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 4bd3c28b1..edd32f30f 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -19,6 +19,10 @@ the proposed Pantheon MVP—along with a tested-versus-open status table—see For a visual comparison of Gala/Mutter and the Rust/Smithay migration stages, startup timing, CPU, memory, and power evidence, open the standalone [`migration dashboard`](dashboard.html). +For matched CPU/RSS sampling, use +[`collect-session-metrics.sh`](collect-session-metrics.sh) and the +[`pidstat` thread sampler](collect-session-thread-profile.sh); capture the +same workload in both sessions before drawing performance conclusions. On 2026-10-19 the installed experimental session completed its first native M1 Pro login and scanned out an AR30 3024x1964 framebuffer. The owner reported diff --git a/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh b/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh new file mode 100755 index 000000000..e3f0b44d6 --- /dev/null +++ b/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 [interval-seconds]" >&2 + exit 2 +} + +[[ $# -ge 2 && $# -le 3 ]] || usage +session=$1 +duration=$2 +interval=${3:-1} +[[ $session == gala || $session == rust ]] || usage +[[ $duration =~ ^[1-9][0-9]*$ && $interval =~ ^[1-9][0-9]*$ ]] || usage +(( duration >= interval )) || usage + +command -v pidstat >/dev/null || { + echo "error: pidstat from sysstat is required" >&2 + exit 1 +} + +mapfile -t pids < <( + ps -eo pid=,comm=,args= | awk -v session="$session" ' + session == "gala" && ($2 == "gala" || $2 == "mseries-gala") { print $1; next } + session == "rust" && ($2 == "pantheon-compositor" || $0 ~ /\/pantheon-compositor([[:space:]]|$)/) { print $1; next } + $2 ~ /^io\.elementary\.(wingpanel|dock)/ || $0 ~ /\/io\.elementary\.(wingpanel|dock)([[:space:]]|$)/ { print $1 } + ' | sort -n -u +) + +if (( ${#pids[@]} == 0 )); then + echo "error: no compositor or Pantheon shell processes found for session '$session'" >&2 + exit 1 +fi + +printf -v pid_list '%s,' "${pids[@]}" +pid_list=${pid_list%,} +samples=$(((duration + interval - 1) / interval)) + +printf '# session=%s started_utc=%s duration_s=%s interval_s=%s pids=%s\n' \ + "$session" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$duration" "$interval" "$pid_list" +ps -p "$pid_list" -o pid=,ppid=,comm=,args= +printf '\n## Per-thread CPU and memory samples (pidstat)\n' +LC_ALL=C pidstat -t -u -r -p "$pid_list" "$interval" "$samples" diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index 5aa5901e1..275d1cf5f 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -218,6 +218,7 @@

    What would count as “better”

  • metrics/gala-idle-20261019.csv and metrics/rust-idle-qualification-20261019.csv: short internal-panel idle samples; different session ages and incomplete process parity.
  • metrics/rust-interactive-20261019.csv: 30-second interactive Rust sample; machine on mains, so its battery-flow reading is not device power use.
  • collect-session-startup.sh: boot chain and service/journal milestones. In build 0.1.0+git7a0ac92, wallpaper loaded at +0.11 s and the first Wingpanel buffer committed at +2.22 s relative to the Rust session manager start. The owner noticed this ~2.1 s wallpaper-to-panel gap as slow, despite reporting good overall opening time. Journal milestones are not direct measures of visual perception or first interaction.
  • +
  • collect-session-thread-profile.sh: prepared to capture per-thread CPU and resident memory with pidstat during matched follow-up runs; no thread profile has been collected yet.
  • The owner reported inconsistent window corners; exact affected apps and states are not yet recorded.
  • From 8ff52aa8077e3e07adaed7269e4ad86347a47864 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:34:59 -0400 Subject: [PATCH 196/798] Record exploratory Gala thread profile --- .../EXPERIENCE-REQUIREMENTS.md | 10 +++++----- mseries/pantheon-rust-compositor/README.md | 7 +++++++ .../collect-session-thread-profile.sh | 16 +++++++++------- mseries/pantheon-rust-compositor/dashboard.html | 3 ++- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 96bef814d..5a82e93ab 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -150,11 +150,11 @@ then retain stdout with the branch/package revision and journal evidence. `collect-session-thread-profile.sh` complements this with `pidstat` per-thread CPU and resident-memory samples for the compositor, Wingpanel and Dock. Capture the same idle or interactive workload in each session; for example, use -`./collect-session-thread-profile.sh gala 60 1` and -`./collect-session-thread-profile.sh rust 60 1`, then save each result beside -the matching process-level CSV and startup journal. Thread samples identify hot -threads but do not provide call stacks, so use a sampling profiler for -function-level attribution when one is available. +`./collect-session-thread-profile.sh gala idle 60 1` and +`./collect-session-thread-profile.sh rust idle 60 1`, then save each result +beside the matching process-level CSV and startup journal. Thread samples +identify hot threads but do not provide call stacks, so use a sampling profiler +for function-level attribution when one is available. For example, collect one-minute idle samples at a recorded mode with `./collect-session-metrics.sh gala 60 idle 3024x1964 > gala-idle.csv` and repeat in the Rust session with the same arguments except `rust`. Collect a diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index edd32f30f..d8703f28d 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -157,6 +157,13 @@ the resulting UI size and panel layout remain unqualified. These owner reports are tracked in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md) and the [migration dashboard](dashboard.html). +An exploratory 30-second per-thread sample of the returned Gala session at +12:33 UTC included Gala and Wingpanel (Dock was opted out). `pidstat` reported +26.26% of one core for Gala, 0.50% for Wingpanel, with 24.29% on Gala's main +thread. Foreground activity was not controlled, so this is not an idle or +Rust-comparison result. The raw capture is outside the repository at +`/home/sdolbec/Documents/Codex/2026-10-18/i-no/work/artifacts/gala-thread-profile-20261019.txt`. + In private Xvfb, the release binary launched a GTK Wayland Zenity field; synthetic key events reached it, and IBus/Mozc committed `konnichiha` as `こんにちは` using `mozc-jp` with Hiragana enabled in a private profile. This diff --git a/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh b/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh index e3f0b44d6..e0904ba91 100755 --- a/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh +++ b/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh @@ -2,16 +2,18 @@ set -euo pipefail usage() { - echo "Usage: $0 [interval-seconds]" >&2 + echo "Usage: $0 [interval-seconds]" >&2 exit 2 } -[[ $# -ge 2 && $# -le 3 ]] || usage +[[ $# -ge 3 && $# -le 4 ]] || usage session=$1 -duration=$2 -interval=${3:-1} +workload=$2 +duration=$3 +interval=${4:-1} [[ $session == gala || $session == rust ]] || usage [[ $duration =~ ^[1-9][0-9]*$ && $interval =~ ^[1-9][0-9]*$ ]] || usage +[[ $workload != *','* && $workload != *$'\n'* ]] || usage (( duration >= interval )) || usage command -v pidstat >/dev/null || { @@ -23,7 +25,7 @@ mapfile -t pids < <( ps -eo pid=,comm=,args= | awk -v session="$session" ' session == "gala" && ($2 == "gala" || $2 == "mseries-gala") { print $1; next } session == "rust" && ($2 == "pantheon-compositor" || $0 ~ /\/pantheon-compositor([[:space:]]|$)/) { print $1; next } - $2 ~ /^io\.elementary\.(wingpanel|dock)/ || $0 ~ /\/io\.elementary\.(wingpanel|dock)([[:space:]]|$)/ { print $1 } + $2 ~ /^io\.elementary\.(wingpanel|dock)/ || $0 ~ /(^|[[:space:]])io\.elementary\.(wingpanel|dock)([[:space:]]|$)/ { print $1 } ' | sort -n -u ) @@ -36,8 +38,8 @@ printf -v pid_list '%s,' "${pids[@]}" pid_list=${pid_list%,} samples=$(((duration + interval - 1) / interval)) -printf '# session=%s started_utc=%s duration_s=%s interval_s=%s pids=%s\n' \ - "$session" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$duration" "$interval" "$pid_list" +printf '# session=%s workload=%s started_utc=%s duration_s=%s interval_s=%s pids=%s\n' \ + "$session" "$workload" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$duration" "$interval" "$pid_list" ps -p "$pid_list" -o pid=,ppid=,comm=,args= printf '\n## Per-thread CPU and memory samples (pidstat)\n' LC_ALL=C pidstat -t -u -r -p "$pid_list" "$interval" "$samples" diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index 275d1cf5f..9e4ca4c78 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -218,7 +218,8 @@

    What would count as “better”

  • metrics/gala-idle-20261019.csv and metrics/rust-idle-qualification-20261019.csv: short internal-panel idle samples; different session ages and incomplete process parity.
  • metrics/rust-interactive-20261019.csv: 30-second interactive Rust sample; machine on mains, so its battery-flow reading is not device power use.
  • collect-session-startup.sh: boot chain and service/journal milestones. In build 0.1.0+git7a0ac92, wallpaper loaded at +0.11 s and the first Wingpanel buffer committed at +2.22 s relative to the Rust session manager start. The owner noticed this ~2.1 s wallpaper-to-panel gap as slow, despite reporting good overall opening time. Journal milestones are not direct measures of visual perception or first interaction.
  • -
  • collect-session-thread-profile.sh: prepared to capture per-thread CPU and resident memory with pidstat during matched follow-up runs; no thread profile has been collected yet.
  • +
  • collect-session-thread-profile.sh: captures per-thread CPU and resident memory with pidstat. An exploratory Gala profile is recorded below; a Rust thread profile still needs a future Rust login.
  • +
  • Exploratory Gala thread sample at 12:33 UTC: Gala 26.26% of one core, Wingpanel 0.50%, and Gala's main thread 24.29%. Foreground activity was not controlled, so this is not comparable to the idle CSVs or a Rust result. Raw capture is in the local work artifacts.
  • The owner reported inconsistent window corners; exact affected apps and states are not yet recorded.
  • From 5a217ee6c6b586f33d0dd145a8ba826f25251b14 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:35:57 -0400 Subject: [PATCH 197/798] Pinpoint Wingpanel post-launch interval --- mseries/pantheon-rust-compositor/README.md | 3 +++ mseries/pantheon-rust-compositor/dashboard.html | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index d8703f28d..25de24845 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -148,6 +148,9 @@ and first buffer at +2.22 s, relative to session-manager start. This is about 2.11 s from wallpaper load to first panel buffer; the journal milestone is not a direct measure of visible pixels or first interactive use. The five-minute rollback completed successfully at 12:26 EDT and returned LightDM to Gala. +The Wingpanel user unit is ordered immediately after compositor readiness; +about 1.10 s of the observed gap is after its process starts and before its +first surface request. The cause of that interval remains unidentified. The owner also reported that window corners look inconsistent and unwanted. This is now a visual acceptance issue; affected apps and window states still need examples. The diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index 9e4ca4c78..d785182ed 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -190,7 +190,7 @@

    Migration stage comparison

    Native M1 panelDaily pathPassed twiceRust scanout at 3024×1964 via Asahi render GPU and Apple DRM/KMS. Initial page-flip attempt logged one transient Device or resource busy. Display scale / densityCompare baseline2× profile stagedThe native 3024×1964 panel looks too zoomed out to the owner. A Rust-only 2× logical scale profile is staged for the next login; visual layout and panel behavior still need qualification. Wallpaper / backgroundEstablishedNative loadedConfigured wallpaper rendered and loaded in Rust native session; nested live-setting reload also passed. - WingpanelDaily path~2.1 s after wallpaperWallpaper loaded ~0.11 s after session start; Wingpanel service started ~0.51 s; its surface request arrived ~1.61 s and first buffer ~2.22 s. Owner noticed the gap as slow. This splits compositor startup from Wingpanel client initialization and first-buffer work. + WingpanelDaily path~2.1 s after wallpaperWallpaper loaded ~0.11 s after session start; Wingpanel service started ~0.51 s; its surface request arrived ~1.61 s and first buffer ~2.22 s. The systemd unit is ordered immediately after compositor readiness, so the remaining ~1.10 s before a surface request is after process launch; the cause is not yet attributed. Another ~0.61 s passed from request to buffer. DockDaily pathOpted outRust session correctly skips Dock because ~/.config/mseries/dock/user-disabled exists. Dock render/hide/reveal passed only in nested session. Portals / session envEstablishedActivation fixedRust publishes WAYLAND_DISPLAY after ready; portal providers now start without the former 120-second timeout. GNOME backend still lacks Mutter's private ServiceChannel. Input methodsDaily workflowNested onlyIBus/Mozc text commit passed in nested GTK; IBus daemon starts natively, but native text commit and source switching are unqualified. From 25081405d5a719dcc2cad95e3e855cc07e430c16 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:37:00 -0400 Subject: [PATCH 198/798] Scope window corner consistency investigation --- mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md | 2 +- mseries/pantheon-rust-compositor/dashboard.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 5a82e93ab..65f6a9908 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -21,7 +21,7 @@ it does not imply that the current prototype supports it. | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | | Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | -| Consistent window appearance | Window corners should look consistent across the apps and window states used in the Pantheon session | Owner report from the 2026-10-19 Rust native login: corner presentation varies between displayed windows and is unwanted; exact apps/states are not yet recorded | Capture examples across native Wayland, XWayland, dialogs and tiled/maximized windows; identify whether client-side shadows/decorations, compositor clipping, or scale/state changes cause the mismatch, then define one accepted appearance | +| Consistent window appearance | Window corners should look consistent across the apps and window states used in the Pantheon session | Owner report from the 2026-10-19 Rust native login: corner presentation varies between displayed windows and is unwanted; exact apps/states are not yet recorded. Source review found no xdg-decoration manager or compositor-managed frame/rounding layer in this prototype, but the cause is unconfirmed | Capture examples across native Wayland, XWayland, dialogs and tiled/maximized windows; inspect each client's decoration protocol and buffer alpha; then decide whether to add decoration negotiation, frame/shadow rendering, or clipping and define one accepted appearance | | Lower system overhead | The Rust session should use less CPU and power than Gala in matched idle and interactive workloads without worse responsiveness or reliability | In the existing six-sample 30-second idle captures, median summed CPU was 12.4% of one core for Rust compositor+Wingpanel and 11.2% for Gala+wrapper; the Gala capture did not include Wingpanel, so total desktop CPU is still unpaired. The compositor-only medians were 11.12% and 11.10%. | Profile repeated matched sessions and total process trees; optimize measured hotspots; claim a win only when it exceeds run-to-run variation. Current investigation leads: native DRM vblank completion requests a compositor redraw after each completed frame, and the 100 ms Gala-compat timer rebuilds a window snapshot each tick. Measure their cost before changing scheduling or polling so client animations, Dock updates and Wayland frame callbacks remain correct | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | | Desktop background | Show the configured elementary wallpaper behind windows on every output and reflect wallpaper/crop-setting changes in-session | Current `org.gnome.desktop.background` setting points to `elementaryos-default`; Gala renders the wallpaper as part of its compositor scene | Prototype loads local PNG/JPEG/WebP and polls the shared URI and crop setting every two seconds, reloading only on changes. Private dconf/Xvfb reload smoke passed; native output behavior still needs qualification | diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index d785182ed..52d28c13d 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -195,7 +195,7 @@

    Migration stage comparison

    Portals / session envEstablishedActivation fixedRust publishes WAYLAND_DISPLAY after ready; portal providers now start without the former 120-second timeout. GNOME backend still lacks Mutter's private ServiceChannel. Input methodsDaily workflowNested onlyIBus/Mozc text commit passed in nested GTK; IBus daemon starts natively, but native text commit and source switching are unqualified. Window actions / XWaylandEstablishedPartialNested XWayland keyboard smoke and basic Gala actions passed; overview, clipboard/PRIMARY, and native app behavior remain open. - Consistent window cornersBaseline to checkOwner-reported issueWindow corner appearance varies in the Rust session and is unwanted. Capture app/window-state examples before selecting a rendering or decoration fix. + Consistent window cornersBaseline to checkOwner-reported issueWindow corner appearance varies in the Rust session and is unwanted. Source review found no xdg-decoration manager or compositor-managed frame/rounding layer; the cause is still unconfirmed. Capture app/window-state examples and inspect client decoration/alpha before choosing a fix. External monitors / dock HDMIKernel-boundNot qualifiedExternal hotplug remains open. The G3 black screen is DCP/kernel display firmware; a compositor replacement cannot fix missing DRM output. Lock, accessibility, sharing, recoveryEstablishedOpenNative end-to-end qualification is still needed before Rust could replace Gala. Performance and powerReferenceUnprovenNo matched interactive/Gala benchmark, whole-system power result, GPU/latency measurement or crash-free soak yet. From c1d92f145c4d3770fdc297f191e3fa4cde854fbd Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 12:52:04 -0400 Subject: [PATCH 199/798] Record Rust session metrics and Pantheon priorities --- mseries/CODEXBAR-PORT.md | 2 +- mseries/OS9-HANDOFF.md | 4 +++ mseries/codexbar-wingpanel/Indicator.vala | 30 +++------------- .../EXPERIENCE-REQUIREMENTS.md | 3 +- mseries/pantheon-rust-compositor/README.md | 35 +++++++++++++++++-- .../collect-session-metrics.sh | 2 +- .../collect-session-thread-profile.sh | 2 +- .../pantheon-rust-compositor/dashboard.html | 11 +++--- 8 files changed, 52 insertions(+), 37 deletions(-) diff --git a/mseries/CODEXBAR-PORT.md b/mseries/CODEXBAR-PORT.md index ed8a2cff6..793da4cee 100644 --- a/mseries/CODEXBAR-PORT.md +++ b/mseries/CODEXBAR-PORT.md @@ -16,7 +16,7 @@ ones.** Upgrade by re-pinning, re-installing upstream, and re-verifying each ite | OpenCode local usage, account plans and switching | `codexbar-opencode-local.py` | reads `opencode.db` read-only; activates via `opencode auth switch opencode ` | | Codex multi-account switcher (upstream's is macOS-only) | `codexbar-accounts.py` | upstream `codexbar usage --provider codex` honoring `CODEX_HOME` | | OpenCode Go sign-in (upstream imports opencode.ai cookies only on macOS): native WebKit window, saves the `auth`/`__Host-console_session` session as `providers[opencodego].cookieSource=manual` + `cookieHeader`; the panel's OpenCode **+** runs it | `codexbar-opencode-login.py` | config path resolution (`CodexBarConfigStore.defaultURL`), `OpenCodeWebCookieSupport` cookie names, `usage --source web` | -| Claude CLI accounts via claude-swap, plus native Claude desktop sessions. CLI only keeps the app login separate; Switch both validates the target session, quits the native app, saves its final encrypted session, merges the target login without changing preferences, switches the CLI, and relaunches the app. Set up app preserves the previous session and locally clears the login for one-time sign-in without sending a logout request. Save app login and automatic list capture remember it. | `codexbar-claude.py`, venv `~/.local/share/claude-swap-venv`, `cswap` | claude-swap `list/status/switch --json`, `schemaVersion` 1, `to.email`, `reason: already-active`, `configs/.claude-config-*.json`; installed Claude `config.json` keys `lastKnownAccountUuid`, `oauth:tokenCache`, `oauth:tokenCacheV2`, SQLite `Cookies` (or `Network/Cookies`), `gtk-launch com.anthropic.Claude` | +| Claude CLI accounts via claude-swap, plus native Claude desktop sessions. CLI keeps the app login separate; Switch validates the target session, quits the native app, saves its final encrypted session, merges the target login without changing preferences, switches the CLI and relaunches the app. Set up app preserves the previous session and locally clears the login for one-time sign-in without sending a logout request. Automatic list capture remembers the app login; the redundant manual “Save Claude app login” panel button has been removed. | `codexbar-claude.py`, venv `~/.local/share/claude-swap-venv`, `cswap` | claude-swap `list/status/switch --json`, `schemaVersion` 1, `to.email`, `reason: already-active`, `configs/.claude-config-*.json`; installed Claude `config.json` keys `lastKnownAccountUuid`, `oauth:tokenCache`, `oauth:tokenCacheV2`, SQLite `Cookies` (or `Network/Cookies`), `gtk-launch com.anthropic.Claude` | | Claude local Code context carry-over | `codexbar_claude_history.py`, [design/evidence](CODEXBAR-CLAUDE-HISTORY.md) | installed Claude desktop account/org Code index and local `.claude/projects` transcripts; private exports and create-only imports after app exit | | Installers / deploy / CI | `install-codexbar.sh`, `install-codexbar-wingpanel.sh`, `deploy-codexbar.sh`, `check-codexbar-port.sh`, `.github/workflows/validate-mseries.yml`, `enhance.sh` payload list | version pin `version=vX.Y.Z` | diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c15aea7c6..4a61e9cba 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -317,6 +317,10 @@ Verified native light/dark render probes with all three providers, including alt the routine deploy also builds and runs all 12 native targets. Upstream stays v0.77.0. Installed/staged without restarting Wingpanel; native appearance loads at next login or an explicit reload. +## Smaller CodexBar text and remove redundant Claude save action — 2026-10-19 + +Owner reported the Wingpanel text still looked too large and the Claude app-login save action took up room. Reduced panel text sizes one step (base 8.5→7.5 pt, primary values 7.5→7 pt, reset labels 7→6.5 pt, secondary rows 6.75→6 pt). Removed the conditional “Save Claude app login” button; the helper already captures the app login when it lists accounts. Existing Claude account display and switch actions remain. The module is built and installed without restarting Wingpanel; the new appearance is pending the next session load and needs the owner's visual check. + ## Compact two-row OpenCode panel — 2026-10-18 Owner requested denser OpenCode information in the desktop bar. The active OpenCode quota/reset diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 49075f820..1304e0ff0 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -252,10 +252,10 @@ namespace MSeries { ".mseries-codexbar button.compact { padding: 1px 8px; min-height: 0; min-width: 0; } " + ".mseries-codexbar button.flat.compact { padding: 0 4px; } " + ".mseries-codexbar .mseries-switcher button { padding: 2px 6px; } " + - ".mseries-codexbar-bar label { font-size: 8.5pt; font-feature-settings: \"tnum\"; } " + - ".mseries-codexbar-bar .seg-value { font-size: 7.5pt; font-weight: 700; } " + - ".mseries-codexbar-bar .seg-reset { font-size: 7pt; opacity: 0.7; } " + - ".mseries-codexbar-bar .seg-secondary { font-size: 6.75pt; } " + + ".mseries-codexbar-bar label { font-size: 7.5pt; font-feature-settings: \"tnum\"; } " + + ".mseries-codexbar-bar .seg-value { font-size: 7pt; font-weight: 700; } " + + ".mseries-codexbar-bar .seg-reset { font-size: 6.5pt; opacity: 0.7; } " + + ".mseries-codexbar-bar .seg-secondary { font-size: 6pt; } " + ".mseries-codexbar-bar .level-low { color: @warning_color; } " + ".mseries-codexbar-bar .level-critical { color: @error_color; } " + ".mseries-codexbar-bar .provider-mark { opacity: 0.92; } " + @@ -798,28 +798,6 @@ namespace MSeries { } var listed = accounts != null ? accounts.get_length () : 0; - // The helper saves the Claude app's login by itself whenever it lists accounts, so a standing - // button is noise. Offer it only when the app's current login is not one of the saved accounts. - var app_installed = desktop != null && JsonBoundary.flag (desktop, "installed", false); - var app_login_saved = false; - for (uint i = 0; i < listed; i++) { - if (JsonBoundary.flag (accounts.get_object_element (i), "desktopActive", false)) { - app_login_saved = true; - } - } - if (app_installed && (!app_login_saved || snapshot_error.length > 0)) { - var save_app = new Gtk.Button.with_label ("Save Claude app login") { - halign = Gtk.Align.START, tooltip_text = "The Claude app's current sign-in isn't saved yet. Save it for fast switching." - }; - save_app.add_css_class ("flat"); - save_app.add_css_class ("caption"); - save_app.clicked.connect (() => { - popdown_from (save_app); - run_saver.begin ({ "python3", helper_path ("codexbar-claude.py"), "app-save" }, - "Claude app login saved", "Couldn't save the app login", true); - }); - card.append (save_app); - } for (uint i = 0; i < listed; i++) { var account = accounts.get_object_element (i); if (JsonBoundary.flag (account, "active", false) && diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index 65f6a9908..aad62b92e 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -17,10 +17,11 @@ it does not imply that the current prototype supports it. | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; actions 2 and 3 passed nested GTK Wayland smoke, and action 4 dispatches the configured Wingpanel main-menu command. Action 1 remains unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | +| Mac-style trackpad swipes | Native-feeling multi-finger trackpad gestures, including workspace navigation and the configured overview action, with reliable direction, cancellation and edge handling | Owner ranked this highly on 2026-10-19; the installed Gala customization maps its overlay gesture to `org.pantheon.gala.PerformAction(1)`, which the Rust prototype does not yet support | Detect touchpad gesture sequences from libinput/Wayland events, distinguish swipe from pointer motion, map user-configured gestures to workspace/overview actions, and qualify with the built-in M-series trackpad. Preserve accessibility settings and avoid consuming ordinary pointer input | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | -| Window arrangement | Native half snapping and the four quarter positions; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md) | Basic tiling and monitor movement only where this behavior belongs to the compositor; keep shortcut policy configurable and test window geometry against the current work area | +| Window arrangement | Native half snapping, four quarter positions, and richer layouts beyond quarters; preserve work-area geometry as the dock appears or hides; move windows between displays | [`window-snapping`](../window-snapping/README.md), [`display-space`](../display-space/README.md); owner requested richer snapping on 2026-10-19 | Extend compositor tiling policy with configurable halves, thirds, and grid/edge zones; include drag previews, work-area changes and multi-display movement; validate fixed-size and maximized windows | | Consistent window appearance | Window corners should look consistent across the apps and window states used in the Pantheon session | Owner report from the 2026-10-19 Rust native login: corner presentation varies between displayed windows and is unwanted; exact apps/states are not yet recorded. Source review found no xdg-decoration manager or compositor-managed frame/rounding layer in this prototype, but the cause is unconfirmed | Capture examples across native Wayland, XWayland, dialogs and tiled/maximized windows; inspect each client's decoration protocol and buffer alpha; then decide whether to add decoration negotiation, frame/shadow rendering, or clipping and define one accepted appearance | | Lower system overhead | The Rust session should use less CPU and power than Gala in matched idle and interactive workloads without worse responsiveness or reliability | In the existing six-sample 30-second idle captures, median summed CPU was 12.4% of one core for Rust compositor+Wingpanel and 11.2% for Gala+wrapper; the Gala capture did not include Wingpanel, so total desktop CPU is still unpaired. The compositor-only medians were 11.12% and 11.10%. | Profile repeated matched sessions and total process trees; optimize measured hotspots; claim a win only when it exceeds run-to-run variation. Current investigation leads: native DRM vblank completion requests a compositor redraw after each completed frame, and the 100 ms Gala-compat timer rebuilds a window snapshot each tick. Measure their cost before changing scheduling or polling so client animations, Dock updates and Wayland frame callbacks remain correct | | Login-critical integration | Lock/unlock, logout, session restart and core settings must work before the session can be a swap-in | Gala owns the current display-server unit and failure path; lock screen/session integration is part of the current desktop | Implement the smallest protocol and service integration required by the installed Pantheon apps and session. Validate real lock/unlock and recovery before making it selectable by default | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 25de24845..35a2b1f4c 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -154,12 +154,41 @@ first surface request. The cause of that interval remains unidentified. The owner also reported that window corners look inconsistent and unwanted. This is now a visual acceptance issue; affected apps and window states still need examples. The -3024×1964 panel mode looked too zoomed out. A Rust-only 2× logical scale -profile is staged for the next login while keeping the high-resolution mode; -the resulting UI size and panel layout remain unqualified. These owner reports +3024×1964 panel mode looked too zoomed out. A later Rust-only 2× run retained +the high-resolution mode; the owner now prefers 1.25×, which is staged for the +next login and remains unqualified. These owner reports are tracked in [`EXPERIENCE-REQUIREMENTS.md`](EXPERIENCE-REQUIREMENTS.md) and the [migration dashboard](dashboard.html). +In a later isolated login on 2026-10-19, the staged 2× Rust display scale +preserved the 3024×1964@120 Hz mode. Wallpaper loaded +0.14 s after session +manager start; Wingpanel requested its first panel surface at +1.60 s and +committed its first buffer at +2.25 s. A 45-second post-login desktop sample +measured median CPU at 5.4% of one core for the compositor and 0.5% for +Wingpanel; median RSS was about 190.5 MiB and 422.5 MiB, respectively. A +one-minute `pidstat` profile captured both processes and their threads. The +machine was on AC and the workload was not controlled idle, so these values +cannot establish a performance win against Gala. The owner preferred 1.25× +after seeing 2×; that Rust-only fractional scale is staged for the next login +and has not yet been qualified. Raw captures are in the task-local artifacts +directory, outside this repository. + +After the timed rollback, Gala's compositor, wallpaper and Wingpanel processes +started at 12:44:49 EDT. The owner saw the background well before Wingpanel, +which appeared about two minutes later. At login, systemd skipped +`xdg-desktop-portal.service` because `WAYLAND_DISPLAY` was absent from the user +manager environment; portal requests then timed out at 120 seconds, and the +portal and its backends started at 12:46:49 EDT. This timing strongly implicates +portal activation in the delayed panel, but does not prove it caused the +visible delay. Investigate early display-environment publication and portal +activation ordering before the next startup comparison. Gala remains active. + +The metrics helpers now match Wingpanel/Dock processes by their absolute +executable path as well as their truncated `comm` name. A corrected 30-second +Gala capture measured median CPU of 15.6% for Gala and 0.7% for Wingpanel; +this quiet-desktop sample and the Rust post-login sample used different +workloads, so they are not a matched comparison. + An exploratory 30-second per-thread sample of the returned Gala session at 12:33 UTC included Gala and Wingpanel (Dock was opted out). `pidstat` reported 26.26% of one core for Gala, 0.50% for Wingpanel, with 24.29% on Gala's main diff --git a/mseries/pantheon-rust-compositor/collect-session-metrics.sh b/mseries/pantheon-rust-compositor/collect-session-metrics.sh index 8136e0f67..bccd05540 100755 --- a/mseries/pantheon-rust-compositor/collect-session-metrics.sh +++ b/mseries/pantheon-rust-compositor/collect-session-metrics.sh @@ -28,7 +28,7 @@ process_list() { $2 == "gala" || $2 == "mseries-gala" || $2 == "pantheon-compositor" || $0 ~ /\/pantheon-compositor( |$)/ || $2 ~ /^io\.elementary\.(wingpanel|dock)/ || - $0 ~ /\/io\.elementary\.(wingpanel|dock)( |$)/ { + $0 ~ /(^|[[:space:]\/])io\.elementary\.(wingpanel|dock)([[:space:]]|$)/ { print $1, $2 }' | sort -n -u } diff --git a/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh b/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh index e0904ba91..0ffb2f36a 100755 --- a/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh +++ b/mseries/pantheon-rust-compositor/collect-session-thread-profile.sh @@ -25,7 +25,7 @@ mapfile -t pids < <( ps -eo pid=,comm=,args= | awk -v session="$session" ' session == "gala" && ($2 == "gala" || $2 == "mseries-gala") { print $1; next } session == "rust" && ($2 == "pantheon-compositor" || $0 ~ /\/pantheon-compositor([[:space:]]|$)/) { print $1; next } - $2 ~ /^io\.elementary\.(wingpanel|dock)/ || $0 ~ /(^|[[:space:]])io\.elementary\.(wingpanel|dock)([[:space:]]|$)/ { print $1 } + $2 ~ /^io\.elementary\.(wingpanel|dock)/ || $0 ~ /(^|[[:space:]\/])io\.elementary\.(wingpanel|dock)([[:space:]]|$)/ { print $1 } ' | sort -n -u ) diff --git a/mseries/pantheon-rust-compositor/dashboard.html b/mseries/pantheon-rust-compositor/dashboard.html index 52d28c13d..d1e219515 100644 --- a/mseries/pantheon-rust-compositor/dashboard.html +++ b/mseries/pantheon-rust-compositor/dashboard.html @@ -103,7 +103,7 @@
    M-series Pantheon compositor migrationEvidence updated 19 Oct 2026 · branch codex/rust-mutter-pantheon

    Gala/Mutter and Rust/Smithay

    -

    A stage-by-stage view of the existing elementary desktop and the experimental Rust replacement. This is an experience-focused compositor, not a feature-for-feature Mutter port. The latest bounded Rust qualification felt much quicker to open overall, while the owner still noticed a slow wallpaper-to-Wingpanel transition. Its timed rollback completed and Gala is active.

    +

    A stage-by-stage view of the existing elementary desktop and the experimental Rust replacement. This is an experience-focused compositor, not a feature-for-feature Mutter port. The latest Rust run recorded a 2.11-second wallpaper-to-Wingpanel-buffer interval at 2× scale, then rolled back to Gala. Gala's current login showed a roughly two-minute panel delay associated with late portal activation; that relationship still needs a controlled check. Gala is active, with 1.25× staged for the next Rust login.

    @@ -115,7 +115,7 @@

    Gala/Mutter and Rust/Smithay

    Rust Pantheon

    Smithay compositor · opt-in session

    Experimental

    Native scanout, wallpaper, Wingpanel service, IBus daemon and portal startup now work in qualification. The saved Dock opt-out is respected. Several user-facing protocols and runtime gates remain open.

    - Latest instrumented ARM64 build: 0.1.0+git7a0ac92 · timed rollback completed successfully; Gala is active. + Latest instrumented ARM64 build: 0.1.0+git7a0ac92 · 2× run and rollback completed; 1.25× is staged for the next Rust login.
    @@ -125,7 +125,7 @@

    Speed and resource snapshots

    20.8 sOS boot to default target
    10.5 sOS boot to graphical target
    0.46 sRust compositor service start → ready
    -
    2.1 sWallpaper loaded → first Wingpanel buffer
    +
    2.25 sRust session start → first Wingpanel buffer (2×)
    @@ -135,7 +135,7 @@

    Compositor CPU · idle snapshots

    Gala
    11.10%
    Rust
    11.12%
    -

    No measured idle CPU win yet. Summing processes present at each sample gives Rust compositor + Wingpanel a 12.4% median of one core; Gala + its wrapper measured 11.2%. The Gala sample did not include Wingpanel, so this is not a matched total-session result, but the available evidence does not show Rust using less CPU. Code inspection found the native DRM vblank handler requests another compositor redraw after each completed frame; this is a lead to profile, with client frame-callback behavior checked before changing scheduling.

    +

    No performance win is established. The corrected 30-second Gala quiet-desktop sample included Wingpanel (median 15.6% of one core for Gala; 0.7% for Wingpanel). The 45-second Rust post-login sample at 2× measured 5.4% for the compositor and 0.5% for Wingpanel. These workloads were not matched and the figures must not be read as a Rust win. The one-minute Rust thread profile is saved for hotspot work. Earlier source inspection found continuous redraw after native DRM vblank as a lead to profile, with client frame-callback semantics checked before changing scheduling.

    Compositor memory · idle snapshots

    @@ -194,11 +194,14 @@

    Migration stage comparison

    DockDaily pathOpted outRust session correctly skips Dock because ~/.config/mseries/dock/user-disabled exists. Dock render/hide/reveal passed only in nested session. Portals / session envEstablishedActivation fixedRust publishes WAYLAND_DISPLAY after ready; portal providers now start without the former 120-second timeout. GNOME backend still lacks Mutter's private ServiceChannel. Input methodsDaily workflowNested onlyIBus/Mozc text commit passed in nested GTK; IBus daemon starts natively, but native text commit and source switching are unqualified. + Mac-style trackpad swipesHigh owner priorityNot implementedAdd multi-finger workspace navigation and the configured overview gesture with direction, cancellation and edge handling; qualify on the built-in M-series trackpad. + Window snappingHalf and quarter shortcutsNested paths; runtime openOwner requested richer layouts beyond quarters. Extend toward configurable halves, thirds, grid/edge zones and drag preview; preserve work-area and multi-display behavior. Window actions / XWaylandEstablishedPartialNested XWayland keyboard smoke and basic Gala actions passed; overview, clipboard/PRIMARY, and native app behavior remain open. Consistent window cornersBaseline to checkOwner-reported issueWindow corner appearance varies in the Rust session and is unwanted. Source review found no xdg-decoration manager or compositor-managed frame/rounding layer; the cause is still unconfirmed. Capture app/window-state examples and inspect client decoration/alpha before choosing a fix. External monitors / dock HDMIKernel-boundNot qualifiedExternal hotplug remains open. The G3 black screen is DCP/kernel display firmware; a compositor replacement cannot fix missing DRM output. Lock, accessibility, sharing, recoveryEstablishedOpenNative end-to-end qualification is still needed before Rust could replace Gala. Performance and powerReferenceUnprovenNo matched interactive/Gala benchmark, whole-system power result, GPU/latency measurement or crash-free soak yet. + Gala panel startupCurrent baseline issueAbout 2 minutes on rollback loginGala and Wingpanel processes started at login, but the portal service was skipped because the user manager lacked WAYLAND_DISPLAY; portal requests hit 120-second timeouts and portal backends started at +2 minutes. This is a strong delay lead, not yet a proven cause. From ad6741d9e22a80ed042e2b5bd75be8d8d5c75dfd Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 13:02:05 -0400 Subject: [PATCH 200/798] Deploy Rust compositor beside Gala and publish performance page Add the stack installer for the experimental Pantheon Rust compositor, a metrics reducer for the retained session captures, and a public performance page on the docs site. Nothing is installed, selected or restarted: Gala remains the production session and rollback. - mseries/install-pantheon-rust-compositor.sh: dry run by default; --status and --verify are read-only; --install, --select, --restore-gala and --uninstall need root and --yes. Refuses to act without the Gala package, /usr/bin/gala and the pantheon-wayland session entry; starts and enables no unit; records installed-vs-loaded state; --select only rewrites the LightDM autologin drop-in with a backup. - summarize-metrics.py (+ test): per-process mean/median/min/max from the collect-session-metrics.sh CSVs; NA samples excluded and counted. - docs/rust-compositor-performance.html: idle CPU (Gala 11.10% mean / Rust 11.12% mean of unmatched 30 s captures), memory, the Rust interactive sample, startup milestones, open gates, method and rollback, with the "no performance win is established" framing kept explicit. Linked from build-status.html and index.html. - CI: syntax-check the component scripts, run the summarizer test, and shellcheck the installer. Also records in the handoff that the in-branch dashboard's headline compositor CPU figures are means, not medians (true medians 11.20% and 11.32%), and that the retained Gala idle capture has no Wingpanel row, so total-session CPU stays unpaired. --- .github/workflows/validate-mseries.yml | 6 +- docs/build-status.html | 1 + docs/index.html | 2 + docs/rust-compositor-performance.html | 169 +++++++++++ mseries/OS9-HANDOFF.md | 51 ++++ mseries/ROADMAP.md | 13 + mseries/install-pantheon-rust-compositor.sh | 272 ++++++++++++++++++ mseries/pantheon-rust-compositor/README.md | 35 +++ .../summarize-metrics.py | 143 +++++++++ .../test-summarize-metrics.py | 89 ++++++ 10 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 docs/rust-compositor-performance.html create mode 100755 mseries/install-pantheon-rust-compositor.sh create mode 100644 mseries/pantheon-rust-compositor/summarize-metrics.py create mode 100644 mseries/pantheon-rust-compositor/test-summarize-metrics.py diff --git a/.github/workflows/validate-mseries.yml b/.github/workflows/validate-mseries.yml index 84745f5c1..270fb0c84 100644 --- a/.github/workflows/validate-mseries.yml +++ b/.github/workflows/validate-mseries.yml @@ -96,7 +96,11 @@ jobs: bash -n transform/elementary-grow.sh bash -n bin/elementary-mac-setup for f in mseries/install-*.sh mseries/lib-backup.sh mseries/boot/mkinitcpio-snippet mseries/disable-notch-for-pantheon.sh; do bash -n "$f"; done - shellcheck -S warning bin/elementary-mac-setup transform/elementary-grow.sh mseries/install-unlock-screen.sh mseries/install-boot-isolation.sh mseries/lib-backup.sh mseries/boot/mkinitcpio-snippet mseries/enhance.sh mseries/enhance-firstboot.sh mseries/install-brave.sh mseries/install-video-decode.sh mseries/test-installer-flow.sh mseries/vms/mseries-vm mseries/vms/test-mseries-vm.sh mseries/install-quieter-sound-theme.sh mseries/test-quieter-sound-theme.sh mseries/aurora-sep/install-aurora-sep.sh mseries/aurora-sep/install-touchid-gui.sh + for f in mseries/pantheon-rust-compositor/*.sh; do bash -n "$f"; done + python3 -m py_compile mseries/pantheon-rust-compositor/summarize-metrics.py + python3 mseries/pantheon-rust-compositor/test-summarize-metrics.py + python3 mseries/pantheon-rust-compositor/summarize-metrics.py mseries/pantheon-rust-compositor/metrics/*.csv > /dev/null + shellcheck -S warning bin/elementary-mac-setup transform/elementary-grow.sh mseries/install-unlock-screen.sh mseries/install-boot-isolation.sh mseries/lib-backup.sh mseries/boot/mkinitcpio-snippet mseries/enhance.sh mseries/enhance-firstboot.sh mseries/install-brave.sh mseries/install-video-decode.sh mseries/test-installer-flow.sh mseries/vms/mseries-vm mseries/vms/test-mseries-vm.sh mseries/install-quieter-sound-theme.sh mseries/test-quieter-sound-theme.sh mseries/aurora-sep/install-aurora-sep.sh mseries/aurora-sep/install-touchid-gui.sh mseries/install-pantheon-rust-compositor.sh bash -n mseries/vms/mseries-vm bash mseries/vms/test-mseries-vm.sh bash mseries/test-quieter-sound-theme.sh diff --git a/docs/build-status.html b/docs/build-status.html index 4ba5e175f..0ac97f914 100644 --- a/docs/build-status.html +++ b/docs/build-status.html @@ -21,6 +21,7 @@

    OS 9 build monitor

    Aurora unified 7.2.10 WIP

    A checksum-pinned draft source snapshot has its own kernel/package identity and ARM64 build checks. The reviewed stock-bindgen fix is ported; native compilation and package inspection pass. Shared GPU and SEP review-parity differences keep this lane build-only; it is not a hardware-tested release.

    Source and parity gates · Native compilation and inspection
    Build-only · review open

    Enhanced kernel test build

    Pinned Fairydust + USB4 is compiling into separate Debian packages on the Mac. Package inspection and experimental-image integration follow; the running kernel is unchanged.

    Live kernel monitor · on this Mac ↗ · Build scope and records
    Local monitor ↗

    Local Mac build

    Native assembly finished in 16m 39s and its checksum passed. Inspection found missing live-session hardware helpers; the corrected GitHub image now passes its hardware-helper payload check. The corrected ISO also passed its independent checksum and graphical VM boot check. This public page does not poll your Mac.

    Live local monitor · on this Mac ↗ · Build records and controls
    Rebuild needed
    +

    Rust compositor performance

    An experimental Smithay compositor runs beside Gala as its own opt-in session. Retained 30-second idle samples put both compositors at about 11.1 % of one core under unmatched workloads, so no performance win is established; whole-system power is unmeasured. Gala stays the session that logs in and the rollback path.

    Measured numbers, method and open gates · Compositor status and readiness decision ↗
    Experimental · no win established

    Experimental arm64 image

    Waiting for GitHub

    Open build and logs ↗
    Build stages
    1. No stage data yet
    Unknown

    Configuration and regression checks

    Waiting for GitHub

    Open validation ↗
    Unknown

    GitHub Pages

    Waiting for GitHub

    Open deployment ↗
    Unknown
    diff --git a/docs/index.html b/docs/index.html index ae474842b..c1ff20288 100644 --- a/docs/index.html +++ b/docs/index.html @@ -22,6 +22,8 @@

    ElementaryOS M-Series

    Local AI on M1 Pro 32 GB: measured model comparisons, personal memory and MCP app coverage.

    +

    Rust compositor performance — the experimental Smithay compositor beside Gala, with the measured numbers and what would count as a win.

    +

    Download

    Live build monitor — latest arm64 build, validation, deployment, and when to poke Codex.

    An experimental installer and checksum are available in the successful build run (artifact expires October 20). GitHub may require sign-in. The factory image uses Ubuntu kernel 7.0.0-38-generic and has not been hardware-booted. This is not a stable release.

    diff --git a/docs/rust-compositor-performance.html b/docs/rust-compositor-performance.html new file mode 100644 index 000000000..2de1d5b0c --- /dev/null +++ b/docs/rust-compositor-performance.html @@ -0,0 +1,169 @@ + + + + + +Rust compositor performance — ElementaryOS M-Series + + + + +

    Rust compositor performance

    +

    Smithay “pantheon-compositor” versus Gala/libmutter on the M1 Pro · evidence through 19 October 2026

    + + + +

    What is being compared

    +
    +

    Gala / libmutter

    +

    The established Pantheon compositor: Wayland session, shell integration, input stack and app + compatibility. The baseline every Rust number has to beat.

    +

    Baseline

    +

    Rust / Smithay

    +

    A focused Rust compositor implementing the Pantheon behaviors this stack needs — panel/dock shell + protocol, window basics, tiling, XWayland, IBus input — not a port of Mutter. It reached native scanout on the + internal panel in isolated logins, with Wingpanel starting and the owner's dock opt-out honored.

    +

    Experimental

    +
    + +

    Compositor CPU, idle

    +

    Percent of one core (one core = 100 %). Two retained 30-second captures on the internal panel, +reduced by mseries/pantheon-rust-compositor/summarize-metrics.py. These were +not matched workloads: the Gala capture is a quiet desktop at 15:37 UTC; the Rust capture is a +post-login qualification session at 15:56 UTC at 3024×1964.

    + + + + + + + + + +
    CaptureProcessMeanMedianRangeRSS median
    Gala idle, 30 sgala11.10 %11.20 %10.80–11.20 %178,720 KiB (174.5 MiB)
    mseries-gala wrapper0.00 %0.00 %0.00 %1,760 KiB
    Rust idle qualification, 30 spantheon-compositor11.12 %11.32 %9.67–11.80 %173,504 KiB (169.4 MiB)
    Wingpanel1.19 %1.00 %0.80–2.00 %472,544 KiB (461.5 MiB)
    +

    Read this carefully. The two compositor figures are within noise of each other; the difference +is not a result. The Gala capture did not record a Wingpanel row, so total-session CPU is still unpaired — a later +corrected Gala capture measured a median 15.6 % for Gala plus 0.7 % for Wingpanel, but from a different session +that was not paired with the Rust sample. The in-repository migration dashboard quotes the same captures with the +mean labeled as a median in places; both statistics are given here so the method is unambiguous.

    + +

    Memory, idle

    +

    Resident set size, same two captures.

    + + + + + + +
    ProcessGala sessionRust session
    Compositor178,720 KiB median173,504 KiB median
    Wingpanelnot captured472,544 KiB median
    +

    The roughly 5 MiB compositor gap is not a meaningful memory result. Wingpanel's ~460 MiB is the same installed +GTK application in both sessions; the Gala capture simply did not include it.

    + +

    Rust interactive sample

    +
    +
    4.70 %compositor CPU, mean of a 30-second active-use sample (median 5.20 %)
    +
    175.4 MiBcompositor RSS median, same sample
    +
    2.25 ssession start → first Wingpanel buffer at 2× scale
    +
    Not measuredwhole-system power (on mains; battery flow reads 0 mW)
    +
    +

    There is no matching Gala interactive sample, so this must not be compared with Gala idle numbers. +The owner reported the session felt responsive but noticed the ~2.1 s wallpaper-to-panel interval as slow; journal +milestones are not a measure of perceived first interaction.

    + +

    Startup milestones, Rust session

    + + + + + + + + + +
    MilestoneWhen
    OS boot to graphical target10.5 s
    Wallpaper loaded~0.11 s after Rust session-manager start
    Compositor ready (READY=1)~0.46 s after service start
    Portal ready~0.8 s after compositor readiness
    First Wingpanel buffer~2.25 s after session-manager start (~2.11 s after wallpaper)
    +

    From the instrumented build 0.1.0+git7a0ac92 journal capture. The remaining ~1.1 s +between the Wingpanel process starting and its first surface request is not yet attributed. On the Gala side, one +rollback login showed a roughly two-minute panel delay associated with late portal activation; that relationship +still needs a controlled check.

    + +

    What would count as “better”

    +
      +
    • The same machine, display mode and scale, workload, session age, and panel/dock composition in both sessions.
    • +
    • Matched idle and interactive captures of the whole process trees, not one process picked from each.
    • +
    • Login-to-first-usable time from the journal, with visual confirmation — not a journal milestone alone.
    • +
    • Frame pacing and input-to-visible latency, which have not been measured at all yet.
    • +
    • Power from a battery-discharge run or an external meter; mains battery flow of 0 mW says nothing.
    • +
    • A crash-free soak and working recovery before any daily-use claim.
    • +
    + +

    Open gates before the Rust session could replace Gala

    + + + + + + + + + + + +
    GateStatus
    Native internal-panel scanoutPassed in isolated logins
    Wallpaper, Wingpanel service, dock opt-out, portal orderingQualified in isolated logins
    Matched performance and power comparisonNot established
    Native keyboard/pointer, IBus commit, lock/unlock, recoveryOpen
    Window-corner consistency (owner-reported)Open
    External monitor / dock HDMIKernel/DCP-bound, not compositor-fixable
    Mac-style trackpad gesturesNot implemented
    + +

    How the numbers are produced

    +
    # matched 30 s captures in each session (same workload label and display mode)
    +bash mseries/pantheon-rust-compositor/collect-session-metrics.sh gala 30 idle internal-panel
    +bash mseries/pantheon-rust-compositor/collect-session-metrics.sh rust 30 idle internal-panel
    +
    +# reduce a capture to per-process mean/median/min/max
    +python3 mseries/pantheon-rust-compositor/summarize-metrics.py metrics/*.csv
    +
    +# startup milestones and per-thread profiles
    +bash mseries/pantheon-rust-compositor/collect-session-startup.sh rust
    +bash mseries/pantheon-rust-compositor/collect-session-thread-profile.sh
    +

    Raw captures are retained in mseries/pantheon-rust-compositor/metrics/. The collectors +change nothing on the running session; they only read /proc and power-supply state.

    + +

    Trying it, and going back

    +
    bash mseries/install-pantheon-rust-compositor.sh --dry-run      # checks only
    +sudo bash mseries/install-pantheon-rust-compositor.sh --install --yes --deb <package>
    +sudo bash mseries/install-pantheon-rust-compositor.sh --select --yes       # next login
    +sudo bash mseries/install-pantheon-rust-compositor.sh --restore-gala --yes # rollback
    +

    The package installs beside Gala; the Gala session entry, package and binary are never modified. +Selecting the Rust session only changes the LightDM autologin drop-in, takes effect at the next login, and never +logs anyone out.

    + +
    Generated from repository evidence on 2026-10-19. Deep evidence, the stage-by-stage migration view and the +readiness decision live in the repository on branch codex/rust-gala-stack-integration: +pantheon-rust-compositor. +This page records measurements, not a recommendation; the Rust session remains experimental and Gala remains the +production session and rollback.
    + + diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 4a61e9cba..f3638c46a 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,56 @@ # OS 9 implementation / Claude Code handoff +## Rust compositor on the existing stack: installer, metrics reducer, public performance page — 2026-10-19 + +Batch on branch `codex/rust-gala-stack-integration` (from +`codex/rust-mutter-pantheon` @ `c1d92f1`). No package was built, nothing was +installed or selected, and the live session was not touched: LightDM autologin +is still `pantheon-wayland` and Gala is active. + +- **`mseries/install-pantheon-rust-compositor.sh`** deploys the experimental + compositor beside Gala. Dry run is the default; `--status` and `--verify` are + read-only; `--install`, `--select`, `--restore-gala` and `--uninstall` need + root and `--yes`. The installer refuses to act if the Gala package, + `/usr/bin/gala` or the `pantheon-wayland` session entry is missing, never + starts or enables a unit (only `systemctl --user daemon-reload` after dpkg), + and `--select` only rewrites the LightDM autologin drop-in after saving a + backup. It records installed-vs-loaded state: package version, `dpkg -V`, + payload paths, whether the user manager knows the units, and whether the + compositor service is active. +- **`mseries/pantheon-rust-compositor/summarize-metrics.py`** (+ unit test) + reduces the `collect-session-metrics.sh` CSVs to per-process mean, median, + min and max, excluding and counting `NA` samples. It interprets nothing. +- **`docs/rust-compositor-performance.html`** publishes the measured evidence + (idle CPU, memory, the Rust interactive sample, startup milestones, open + gates, method and rollback), linked from `docs/build-status.html` and + `docs/index.html`. The page states the statistic for every number and keeps + the "no performance win is established" framing. +- CI (`validate-mseries.yml`) now syntax-checks the component scripts, + compiles and runs the summarizer test, and shellchecks the installer. + +Verified locally: `bash -n` and `shellcheck -S warning` on the installer, four +unit tests pass, the summarizer reproduces the retained captures, and +`--dry-run`/`--status` ran read-only on the M1 Pro. Live state found: +`pantheon-rust-compositor 0.1.0+git7a0ac92` installed with a clean `dpkg -V`, +all payload files present, units known to the user manager, compositor service +not active, autologin `pantheon-wayland`. The branch HEAD is `c1d92f1`, so the +installed package predates this batch; upgrading is an `--install` away but was +not performed. + +Documentation correction carried into the new page: the in-branch migration +dashboard's headline compositor CPU figures (Gala 11.10 %, Rust 11.12 %) are +**means** of the six-sample captures, although `EXPERIENCE-REQUIREMENTS.md` +calls 11.12/11.10 medians; the true medians are 11.20 % and 11.32 %. Both +statistics are now published side by side. The retained Gala idle CSV also has +no Wingpanel row, so total-session CPU remains unpaired against the Rust +sample; the later corrected Gala capture (median 15.6 % Gala, 0.7 % Wingpanel) +came from a different session and is not a match. + +Next gates are unchanged: matched idle/interactive captures with whole process +trees, frame pacing and input latency, a battery-discharge or external-meter +power run, and native lock/recovery qualification — before the Rust session is +selected for anything beyond isolated logins. + ## First native Rust compositor login and scanout — 2026-10-19 Installed the ARM64 opt-in session package diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index ec13a1fd3..e5326b507 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -171,6 +171,19 @@ and has not been installed or runtime-qualified. The next gate is native-session qualification in an isolated login, including socket/environment handoff, ordinary apps, lock/logout and failure recovery. +The experimental compositor can now be deployed onto the existing stack beside +Gala with `mseries/install-pantheon-rust-compositor.sh` (dry run by default; +`--status`/`--verify` read-only; `--install`, `--select`, `--restore-gala`, +`--uninstall` need root and `--yes`). The Gala package, binary and +`pantheon-wayland` session entry are never modified, no unit is started or +enabled, and selecting the Rust session only rewrites the LightDM autologin +drop-in with a backup. `summarize-metrics.py` reduces the retained session +captures to per-process mean/median/min/max, and the public performance page +`docs/rust-compositor-performance.html` (linked from the build monitor) +publishes them with the honest status: idle compositor CPU is about 11.1 % of +one core in both sessions under unmatched workloads, whole-system power is +unmeasured, and no performance win is established. + ## CodexBar labels and confirmed popup lifetime defect — 2026-10-18 diff --git a/mseries/install-pantheon-rust-compositor.sh b/mseries/install-pantheon-rust-compositor.sh new file mode 100755 index 000000000..4d2e56e30 --- /dev/null +++ b/mseries/install-pantheon-rust-compositor.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash +# Install the experimental Pantheon Rust compositor onto the existing elementary OS +# M-series stack, next to Gala — never over it. +# +# bash mseries/install-pantheon-rust-compositor.sh --dry-run # default: checks only, changes nothing +# bash mseries/install-pantheon-rust-compositor.sh --status # installed-vs-loaded state, changes nothing +# bash mseries/install-pantheon-rust-compositor.sh --verify # read-only integrity checks +# sudo bash mseries/install-pantheon-rust-compositor.sh --install --yes --deb /path/to/pantheon-rust-compositor_*_arm64.deb +# sudo bash mseries/install-pantheon-rust-compositor.sh --install --yes --build +# sudo bash mseries/install-pantheon-rust-compositor.sh --select --yes # next login uses the Rust session +# sudo bash mseries/install-pantheon-rust-compositor.sh --restore-gala --yes +# sudo bash mseries/install-pantheon-rust-compositor.sh --uninstall --yes +# +# SAFETY DESIGN +# * The compositor ships as its own Debian package and its own LightDM session, +# "Pantheon (Rust compositor, experimental)". The Gala session entry, the Gala +# package and /usr/bin/gala are never modified. Gala stays the rollback path. +# * Dry run is the default. --install, --select, --restore-gala and --uninstall +# act, need root and need --yes; each prints the exact rollback command. +# * The running session is never switched, restarted or logged out. Installing a +# newer package does not touch the active compositor; the Rust session only +# takes effect at the next login, like the Gala screen-corner patches. +# * The installer starts and enables nothing. After dpkg it only runs +# `systemctl --user daemon-reload` so the staged units are known without +# starting them (the units carry RefuseManualStart=on anyway). +# * --select only rewrites the LightDM autologin drop-in, after saving a backup. +# It arms no timer and logs nobody out. +# +# This installs an experimental compositor. It has NOT been qualified to replace +# Gala: read mseries/pantheon-rust-compositor/README.md and +# docs/rust-compositor-performance.html before selecting the Rust session. +set -euo pipefail + +repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +component=$repo/mseries/pantheon-rust-compositor +session_name=pantheon-rust-wayland +gala_session=pantheon-wayland +compositor_template=pantheon-rust-compositor@.service +compositor_unit=pantheon-rust-compositor@wayland.service +autologin_dropin=/etc/lightdm/lightdm.conf.d/50-elementary-mac-autologin.conf +deb_path= +build=0 +action=dry-run +confirm=0 + +usage() { + awk 'NR > 1 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print; next } exit }' "$0" >&2 + exit 2 +} + +log() { printf '%s\n' "$*"; } +step() { printf '\n== %s\n' "$*"; } +warn() { printf 'warning: %s\n' "$*" >&2; } +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +# Privileged commands are printed in read-only modes and executed otherwise. +as_root() { + case $action in + dry-run|status|verify) + log " would run: sudo $*" + return 0 ;; + esac + sudo "$@" +} + +need_yes() { + [[ $confirm == 1 ]] || die "refusing to act without --yes (read the safety design above)" +} + +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) action=dry-run ;; + --status|--verify|--install|--uninstall|--select|--restore-gala) action=${1#--} ;; + --build) build=1 ;; + --deb) deb_path=${2:?--deb needs a package path}; shift ;; + --yes) confirm=1 ;; + -h|--help) usage ;; + *) echo "Unknown option $1" >&2; usage ;; + esac + shift +done + +# ---------------------------------------------------------------- preflight + +[[ $(dpkg --print-architecture) == arm64 ]] \ + || die "this native compositor package is for arm64; host is $(dpkg --print-architecture)" + +step "Existing stack (the rollback path)" +gala_version=$(dpkg-query -W -f='${Version}' gala 2>/dev/null || true) +[[ -n $gala_version ]] || die "the gala package is not installed; there is no rollback path" +[[ -x /usr/bin/gala ]] || die "/usr/bin/gala is missing; there is no rollback path" +[[ -f /usr/share/wayland-sessions/$gala_session.desktop ]] \ + || die "the $gala_session session entry is missing; there is no rollback path" +log "gala $gala_version, /usr/bin/gala and the $gala_session session entry are intact" + +for dep in io.elementary.wingpanel io.elementary.dock gnome-session gtklock \ + xdg-desktop-portal xwayland; do + version=$(dpkg-query -W -f='${Version}' "$dep" 2>/dev/null || true) + [[ -n $version ]] || die "missing dependency $dep (sudo apt install $dep)" + log "dependency $dep $version" +done + +# Where LightDM's autologin session comes from today. Drop-ins are read in +# lexical order after the main config, so the last match wins. +autologin_sources=() +for conf in /etc/lightdm/lightdm.conf /etc/lightdm/lightdm.conf.d/*.conf; do + [[ -f $conf ]] || continue + grep -q '^[[:space:]]*autologin-session=' "$conf" && autologin_sources+=("$conf") +done +autologin_session= +for conf in "${autologin_sources[@]:-}"; do + [[ -n $conf ]] || continue + value=$(sed -n 's/^[[:space:]]*autologin-session=[[:space:]]*//p' "$conf" | tail -1) + [[ -n $value ]] && autologin_session=$value +done +if (( ${#autologin_sources[@]} > 1 )); then + warn "autologin-session is set in several files: ${autologin_sources[*]}" + warn "this script writes $autologin_dropin; lexical order decides the winner" +fi +log "LightDM autologin: ${autologin_session:-}" + +step "Current Rust compositor state (installed vs loaded)" +installed_version=$(dpkg-query -W -f='${Version}' pantheon-rust-compositor 2>/dev/null || true) +log "package: ${installed_version:-}" +if [[ -n $installed_version ]]; then + if verify_output=$(dpkg -V pantheon-rust-compositor 2>&1); then + log "dpkg -V: no modified or missing files" + else + log "dpkg -V reported differences:" + printf '%s\n' "$verify_output" | sed 's/^/ /' + fi +fi +for path in /usr/bin/pantheon-compositor /usr/bin/pantheon-rust-session \ + /usr/share/wayland-sessions/$session_name.desktop \ + /usr/lib/systemd/user/$compositor_template \ + /usr/lib/systemd/user/pantheon-wingpanel.service \ + /usr/lib/systemd/user/pantheon-dock.service; do + if [[ -e $path ]]; then log "present: $path"; else log "absent: $path"; fi +done +if systemctl --user list-unit-files 2>/dev/null | grep -q '^pantheon-rust-compositor@'; then + log "user manager: pantheon-rust units known" +else + log "user manager: pantheon-rust units not known yet (a daemon-reload after install fixes this)" +fi +if [[ $(systemctl --user is-active "$compositor_unit" 2>/dev/null || true) == active ]]; then + log "compositor service: active (this session already runs the Rust compositor)" +else + log "compositor service: not active" +fi + +# ---------------------------------------------------------------- helpers + +install_deb() { + local deb=$1 sha + [[ -f $deb ]] || die "package not found: $deb" + sha=$(sha256sum "$deb" | cut -d' ' -f1) + log "installing $deb" + log " sha256 $sha" + dpkg-deb --info "$deb" | sed -n '/^ Package:/p;/^ Version:/p;/^ Depends:/p' | sed 's/^/ /' + as_root dpkg -i "$deb" + as_root systemctl --user daemon-reload + log "installed. The running session is unchanged; the Rust session is selectable at the next login." +} + +write_autologin() { + local value=$1 + if [[ -f $autologin_dropin ]]; then + as_root cp -a "$autologin_dropin" "$autologin_dropin.pantheon-rust-backup" + log "backup: $autologin_dropin.pantheon-rust-backup" + fi + printf 'autologin-session=%s\n' "$value" | as_root tee "$autologin_dropin" >/dev/null + log "wrote autologin-session=$value to $autologin_dropin" +} + +# ---------------------------------------------------------------- actions + +case $action in + dry-run) + step "Plan (dry run; nothing was changed)" + if [[ -n $deb_path ]]; then + log "1. install $deb_path with dpkg -i" + elif [[ $build == 1 ]]; then + log "1. build the arm64 package with $component/build-deb.sh into a temporary directory" + log "2. install the built package with dpkg -i" + else + log "1. install a package: pass --deb PATH, or --build to build one from this branch" + fi + log "3. systemctl --user daemon-reload (no unit is started or enabled)" + log "4. verify the payload, the package and that Gala is still intact" + log "Rollback at any time:" + log " sudo bash $0 --restore-gala --yes" + log " sudo bash $0 --uninstall --yes" + ;; + status) + step "Result" + if [[ -n $installed_version ]]; then + log "pantheon-rust-compositor $installed_version is installed; autologin is ${autologin_session:-}." + else + log "pantheon-rust-compositor is not installed; autologin is ${autologin_session:-}." + fi + ;; + verify) + step "Verify" + [[ -n $installed_version ]] || die "pantheon-rust-compositor is not installed" + [[ -x /usr/bin/pantheon-compositor ]] || die "compositor binary missing" + if ldd /usr/bin/pantheon-compositor | grep -q 'not found'; then + ldd /usr/bin/pantheon-compositor | grep 'not found' | sed 's/^/ /' + die "compositor binary has unresolved shared libraries" + fi + log "compositor binary resolves its shared libraries" + [[ -f /usr/share/wayland-sessions/$session_name.desktop ]] || die "Rust session entry missing" + log "Rust session entry present" + [[ -x /usr/bin/gala ]] || die "Gala binary missing; rollback path broken" + log "Gala rollback path intact" + log "verify: installed package, payload, session entry and rollback are consistent" + ;; + install) + need_yes + step "Install" + if [[ -n $deb_path ]]; then + install_deb "$deb_path" + elif [[ $build == 1 ]]; then + outdir=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf '$outdir'" EXIT + log "building the package from $repo (this takes a while)" + OUTDIR="$outdir" bash "$component/build-deb.sh" >/dev/null + built=$(ls -1 "$outdir"/pantheon-rust-compositor_*_arm64.deb | head -1) + install_deb "$built" + else + die "pass --deb PATH or --build" + fi + step "Rollback" + log "next login on Gala: sudo bash $0 --restore-gala --yes" + log "remove the package: sudo bash $0 --uninstall --yes" + log "Gala itself was never touched, so logging out and back in is enough if the Rust session misbehaves." + ;; + uninstall) + need_yes + step "Uninstall" + if [[ -z $installed_version ]]; then + log "pantheon-rust-compositor is not installed; nothing to remove" + exit 0 + fi + as_root dpkg -r pantheon-rust-compositor + as_root systemctl --user daemon-reload + log "removed $installed_version" + if [[ $autologin_session == "$session_name" ]]; then + warn "autologin still selects the removed Rust session; run --restore-gala --yes" + fi + ;; + select) + need_yes + [[ -n $installed_version ]] || die "install the package first (--install --yes)" + step "Select the Rust session for the next login" + write_autologin "$session_name" + log "The running session is untouched. The next login starts the Rust compositor." + log "Rollback: sudo bash $0 --restore-gala --yes" + ;; + restore-gala) + need_yes + step "Restore the Gala session for the next login" + if [[ -f $autologin_dropin.pantheon-rust-backup ]]; then + as_root cp -a "$autologin_dropin.pantheon-rust-backup" "$autologin_dropin" + log "restored $autologin_dropin from its backup" + else + write_autologin "$gala_session" + log "(no backup was found)" + fi + log "The next login starts Gala again." + ;; +esac diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 35a2b1f4c..30544ca34 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -269,6 +269,41 @@ isolated qualification; the build script does not install it. The nested IBus path is qualified for one GTK/Mozc configuration; native IBus behavior and any display takeover remain unqualified. +## Install on the existing stack + +`mseries/install-pantheon-rust-compositor.sh` deploys the package beside Gala. +Dry run is the default and changes nothing; acting needs root and `--yes`: + +```sh +bash mseries/install-pantheon-rust-compositor.sh --dry-run # checks and plan only +bash mseries/install-pantheon-rust-compositor.sh --status # installed-vs-loaded state +bash mseries/install-pantheon-rust-compositor.sh --verify # read-only integrity checks +sudo bash mseries/install-pantheon-rust-compositor.sh --install --yes --deb +sudo bash mseries/install-pantheon-rust-compositor.sh --select --yes # next login +sudo bash mseries/install-pantheon-rust-compositor.sh --restore-gala --yes # rollback +sudo bash mseries/install-pantheon-rust-compositor.sh --uninstall --yes +``` + +The Gala package, `/usr/bin/gala` and the `pantheon-wayland` session entry are +never modified; the installer refuses to run without them. It starts and +enables nothing — after `dpkg` it only reloads the user manager's unit list, and +the compositor units carry `RefuseManualStart=on`. Selecting the Rust session +only rewrites the LightDM autologin drop-in (with a backup) and takes effect at +the next login; no timer is armed and nobody is logged out. + +## Performance evidence + +Matched captures come from `collect-session-metrics.sh`; reduce them with +`summarize-metrics.py` (per-process mean, median, min, max; `NA` samples are +excluded and counted). Startup and per-thread evidence come from +`collect-session-startup.sh` and `collect-session-thread-profile.sh`. The +public summary is the published performance page +[`docs/rust-compositor-performance.html`](../../docs/rust-compositor-performance.html), +linked from the build monitor; the stage-by-stage migration view is the +standalone [`dashboard.html`](dashboard.html). Retained captures are in +[`metrics/`](metrics/). No capture so far is a matched A/B, and no power +measurement exists; the readiness decision above still stands. + ## Dock HDMI boundary The WD22TB4 HDMI goal remains in diff --git a/mseries/pantheon-rust-compositor/summarize-metrics.py b/mseries/pantheon-rust-compositor/summarize-metrics.py new file mode 100644 index 000000000..9cb50505f --- /dev/null +++ b/mseries/pantheon-rust-compositor/summarize-metrics.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Summarize compositor session metrics written by collect-session-metrics.sh. + + python3 summarize-metrics.py metrics/*.csv # one table per capture + python3 summarize-metrics.py --json metrics/*.csv # machine-readable + python3 summarize-metrics.py --process gala metrics/*.csv + +The capture CSVs carry their run metadata in leading '#' comments (session, +workload, display mode, duration, interval). This tool only reduces those rows to +per-process statistics; it does not compare captures. Samples taken under +different workloads, session ages, display modes or scales are not comparable, +and no win or regression can be read off a single capture. + +CPU values are percent of one core (one core = 100%). 'NA' rows — a process that +appeared after sampling began, which can indicate a restart — are excluded from +the statistics and reported separately. Battery flow is only meaningful on +battery; on mains it is reported as captured, without interpretation. +""" +import argparse +import csv +import json +import statistics +import sys +from pathlib import Path + +CPU = "cpu_percent_one_core" +RSS = "ram_kib" + + +def read_capture(path): + """Return (metadata dict, rows) for one collect-session-metrics.sh CSV.""" + metadata = {} + rows = [] + for line in Path(path).read_text().splitlines(): + if line.startswith("#"): + for field in line.lstrip("#").strip().split(): + key, _, value = field.partition("=") + if _: + metadata[key] = value + continue + if not line.strip(): + continue + rows.append(line) + if not rows: + raise SystemExit(f"{path}: no samples found") + reader = csv.DictReader(rows) + required = {"session", "workload", "display_mode", "pid", "process", CPU, RSS} + missing = required - set(reader.fieldnames or []) + if missing: + raise SystemExit(f"{path}: missing columns {sorted(missing)}") + return metadata, list(reader) + + +def numeric(value): + try: + return float(value) + except (TypeError, ValueError): + return None + + +def summarize(rows): + """Per-process statistics for one capture. Returns a list of dicts.""" + by_process = {} + for row in rows: + by_process.setdefault(row["process"], []).append(row) + summary = [] + for process in sorted(by_process): + samples = by_process[process] + cpu = [v for v in (numeric(r[CPU]) for r in samples) if v is not None] + rss = [v for v in (numeric(r[RSS]) for r in samples) if v is not None] + entry = { + "process": process, + "pids": sorted({r["pid"] for r in samples}), + "samples": len(samples), + "cpu_samples": len(cpu), + "cpu_na": len(samples) - len(cpu), + } + for name, values in (("cpu", cpu), ("rss", rss)): + if values: + entry[f"{name}_mean"] = round(statistics.fmean(values), 2) + entry[f"{name}_median"] = round(statistics.median(values), 2) + entry[f"{name}_min"] = round(min(values), 2) + entry[f"{name}_max"] = round(max(values), 2) + else: + entry[f"{name}_mean"] = entry[f"{name}_median"] = None + entry[f"{name}_min"] = entry[f"{name}_max"] = None + summary.append(entry) + return summary + + +def capture_summary(path): + metadata, rows = read_capture(path) + return { + "file": str(path), + "metadata": metadata, + "processes": summarize(rows), + } + + +def render_text(summary): + meta = summary["metadata"] + print(f"== {summary['file']}") + print(" session={session} workload={workload} display_mode={display_mode} " + "duration_s={duration_s} interval_s={interval_s} started_utc={started_utc}".format( + session=meta.get("session", "?"), workload=meta.get("workload", "?"), + display_mode=meta.get("display_mode", "?"), duration_s=meta.get("duration_s", "?"), + interval_s=meta.get("interval_s", "?"), started_utc=meta.get("started_utc", "?"))) + print(f" {'process':<18} {'n':>3} {'cpu mean':>9} {'cpu median':>11} {'cpu min':>8} {'cpu max':>8} " + f"{'rss median KiB':>15}") + for p in summary["processes"]: + print(f" {p['process']:<18} {p['samples']:>3} {p['cpu_mean']:>9} {p['cpu_median']:>11} " + f"{p['cpu_min']:>8} {p['cpu_max']:>8} {p['rss_median']:>15}") + if p["cpu_na"]: + print(f" {'':<18} ({p['cpu_na']} sample(s) excluded as NA)") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("captures", nargs="+", help="CSV files from collect-session-metrics.sh") + ap.add_argument("--json", action="store_true", help="emit JSON instead of a text table") + ap.add_argument("--process", help="only report processes whose name contains this substring") + args = ap.parse_args() + + summaries = [] + for capture in args.captures: + summary = capture_summary(capture) + if args.process: + summary["processes"] = [p for p in summary["processes"] if args.process in p["process"]] + if not summary["processes"]: + continue + summaries.append(summary) + + if args.json: + print(json.dumps(summaries, indent=2)) + else: + for summary in summaries: + render_text(summary) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/mseries/pantheon-rust-compositor/test-summarize-metrics.py b/mseries/pantheon-rust-compositor/test-summarize-metrics.py new file mode 100644 index 000000000..60f3d5f16 --- /dev/null +++ b/mseries/pantheon-rust-compositor/test-summarize-metrics.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Tests for summarize-metrics.py against fixture captures. Throwaway files only.""" + +import importlib.util +import pathlib +import tempfile +import unittest + +HERE = pathlib.Path(__file__).parent + + +def load(name, filename): + spec = importlib.util.spec_from_file_location(name, HERE / filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +metrics = load("summarize_metrics", "summarize-metrics.py") + +GALA = """# session=gala workload=idle display_mode=internal-panel duration_s=30 interval_s=5 started_utc=2026-10-19T15:37:54Z +utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_power_mw,battery_percent +2026-10-19T15:37:59Z,gala,idle,internal-panel,10,gala,11.20,178720,5,NA,NA +2026-10-19T15:38:04Z,gala,idle,internal-panel,10,gala,10.80,178720,10,NA,NA +2026-10-19T15:38:09Z,gala,idle,internal-panel,10,gala,11.40,178720,15,NA,NA +""" + +RUST = """# session=rust workload=idle-qualification display_mode=internal-3024x1964 duration_s=30 interval_s=5 started_utc=2026-10-19T15:56:18Z +utc,session,workload,display_mode,pid,process,cpu_percent_one_core,ram_kib,elapsed_s,battery_flow_mw,battery_status,ac_online,battery_percent +2026-10-19T15:56:23Z,rust,idle-qualification,internal-3024x1964,20,pantheon-compos,11.40,173472,5,0,Not charging,1,80 +2026-10-19T15:56:28Z,rust,idle-qualification,internal-3024x1964,21,pantheon-compos,NA,173472,10,0,Not charging,1,80 +2026-10-19T15:56:33Z,rust,idle-qualification,internal-3024x1964,21,pantheon-compos,11.20,173488,15,0,Not charging,1,80 +""" + + +class Sandbox(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = pathlib.Path(self.tmp.name) + + def write(self, name, text): + path = self.root / name + path.write_text(text) + return path + + def test_metadata_and_stats(self): + path = self.write("gala.csv", GALA) + summary = metrics.capture_summary(path) + self.assertEqual(summary["metadata"]["session"], "gala") + self.assertEqual(summary["metadata"]["workload"], "idle") + self.assertEqual(summary["metadata"]["display_mode"], "internal-panel") + self.assertEqual(len(summary["processes"]), 1) + proc = summary["processes"][0] + self.assertEqual(proc["process"], "gala") + self.assertEqual(proc["samples"], 3) + self.assertEqual(proc["cpu_samples"], 3) + self.assertEqual(proc["cpu_na"], 0) + self.assertEqual(proc["cpu_mean"], 11.13) + self.assertEqual(proc["cpu_median"], 11.2) + self.assertEqual(proc["cpu_min"], 10.8) + self.assertEqual(proc["cpu_max"], 11.4) + self.assertEqual(proc["rss_median"], 178720) + + def test_na_samples_are_excluded_and_counted(self): + path = self.write("rust.csv", RUST) + proc = metrics.capture_summary(path)["processes"][0] + self.assertEqual(proc["samples"], 3) + self.assertEqual(proc["cpu_samples"], 2) + self.assertEqual(proc["cpu_na"], 1) + self.assertEqual(proc["cpu_median"], 11.3) + self.assertEqual(proc["pids"], ["20", "21"]) + + def test_missing_columns_fail(self): + path = self.write("bad.csv", "a,b\n1,2\n") + with self.assertRaises(SystemExit): + metrics.read_capture(path) + + def test_process_filter(self): + gala = self.write("gala.csv", GALA) + rust = self.write("rust.csv", RUST) + self.assertEqual([s["metadata"]["session"] for s in + (metrics.capture_summary(p) for p in (gala, rust)) + if any("pantheon-compos" in p["process"] for p in s["processes"])], + ["rust"]) + + +if __name__ == "__main__": + unittest.main() From 02a2076bc63aa6e592b609cf41847dfd7555c73b Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 13:06:11 -0400 Subject: [PATCH 201/798] docs: attribute the mean/median note to the evidence documents --- docs/rust-compositor-performance.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rust-compositor-performance.html b/docs/rust-compositor-performance.html index 2de1d5b0c..746ffe2b0 100644 --- a/docs/rust-compositor-performance.html +++ b/docs/rust-compositor-performance.html @@ -71,7 +71,7 @@

    Compositor CPU, idle

    Read this carefully. The two compositor figures are within noise of each other; the difference is not a result. The Gala capture did not record a Wingpanel row, so total-session CPU is still unpaired — a later corrected Gala capture measured a median 15.6 % for Gala plus 0.7 % for Wingpanel, but from a different session -that was not paired with the Rust sample. The in-repository migration dashboard quotes the same captures with the +that was not paired with the Rust sample. The in-repository evidence documents quote the same captures with the mean labeled as a median in places; both statistics are given here so the method is unambiguous.

    Memory, idle

    From 9ad65111111c65ef7a5159bf2e32266789d140c4 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 13:08:12 -0400 Subject: [PATCH 202/798] Record the stack-integration package build and checksum --- mseries/OS9-HANDOFF.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index f3638c46a..5c161e4d1 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -51,6 +51,23 @@ trees, frame pacing and input latency, a battery-discharge or external-meter power run, and native lock/recovery qualification — before the Rust session is selected for anything beyond isolated logins. +## Build the current Rust Pantheon stack-integration package — 2026-10-19 + +Built the arm64 `native-drm` release package from this branch (`ad6741d`) at +`/home/sdolbec/Documents/Codex/2026-10-18/g/work/artifacts/pantheon-stack-integration-release/pantheon-rust-compositor_0.1.0+gitad6741d_arm64.deb` +(SHA-256 `86892d12fc8769e7fd00e4970f9d723d1a9f666a981bc6b7319a0c52fdcef88d`). +Package metadata, payload paths, executable modes and shared-library links were +inspected: the compositor binary, the `pantheon-rust-session` wrapper, the +`pantheon-rust-compositor@.service` template, the Wingpanel and Dock units, the +GNOME session-target drop-in, the GNOME session file and the LightDM entry are +all present, both binaries are mode 0755, and `ldd` resolves every library. The +package supersedes the installed `0.1.0+git7a0ac92` but was **not installed**: +upgrading the live package was not part of this batch, the running session is +untouched, and Gala remains active. When an isolated qualification login is +scheduled, upgrade with +`sudo bash mseries/install-pantheon-rust-compositor.sh --install --yes --deb `, +then `--verify`, and keep Gala as the rollback. + ## First native Rust compositor login and scanout — 2026-10-19 Installed the ARM64 opt-in session package From 65adb121cb74f49848dbfcd6b938a9dc2f370de6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 14:57:12 -0400 Subject: [PATCH 203/798] CodexBar: production hardening, Grok Build provider, upstream v0.78.0 Fixes - Panel read freed memory for alternate-account reset countdowns and every dropdown quota caption (valac nested-ternary codegen). The `presentation` Meson test was failing on dev because of it. New `generated-c-temporaries` test scans all generated C for the borrowed-then-freed shape. - install-codexbar-wingpanel.sh skipped codexbar_claude_handoff.py (imported by codexbar-claude.py) and codexbar-opencode-login.py; --uninstall left marks. Installers and deploy now share lib-codexbar.sh; test-codexbar-deploy.py checks helper imports and the image payload against it. - Settings: tolerate malformed provider catalog entries from the CLI. Grok Build - Pin upstream v0.78.0 (restores Grok CLI billing via _x.ai/billing). - Installer adds `grok` to providerOrder when the grok CLI is installed. - Panel: Grok card and bar segment (order, original terminal mark, token-first row without a duplicated weekly row), product-share and reset-credit lines, open in terminal / `grok login` via xdg-terminal-exec. - codexbar-sessions.py: seven-day tokens from ~/.grok/sessions/*/*/usage.json (deduplicated, bounded, cached) and the sign-in email only. CI - New codexbar-wingpanel job: builds the native module (Granite 7.7 and the pinned Wingpanel indicator API bootstrapped when missing) and runs all 17 Meson tests including Xvfb probes; CodexBar scripts added to shellcheck. --- .github/workflows/validate-mseries.yml | 21 +++ mseries/CODEXBAR-PORT.md | 45 +++++- mseries/OS9-HANDOFF.md | 42 +++++ mseries/ROADMAP.md | 9 ++ mseries/check-codexbar-port.sh | 27 ++++ mseries/codexbar-sessions.py | 134 ++++++++++++++- mseries/codexbar-wingpanel/Indicator.vala | 152 +++++++++++++++--- mseries/codexbar-wingpanel/Presentation.vala | 8 +- mseries/codexbar-wingpanel/icons/grok.svg | 1 + mseries/codexbar-wingpanel/meson.build | 18 ++- .../probe/check-generated-c.py | 54 +++++++ .../codexbar-wingpanel/probe/ci-build-deps.sh | 54 +++++++ .../codexbar-wingpanel/probe/make-fixtures.py | 22 ++- .../probe/render-probe.vala | 55 +++++++ .../probe/run-grok-probe.sh | 8 + mseries/codexbar-wingpanel/probe/run-probe.sh | 7 +- .../settings/SettingsApp.vala | 25 ++- mseries/codexbar_tokens.py | 8 + mseries/deploy-codexbar.sh | 12 +- mseries/enhance.sh | 2 +- mseries/install-codexbar-wingpanel.sh | 21 +-- mseries/install-codexbar.sh | 34 ++-- mseries/lib-codexbar.sh | 42 +++++ mseries/test-codexbar-deploy.py | 51 +++++- mseries/test-codexbar-sessions.py | 125 +++++++++++++- 25 files changed, 883 insertions(+), 94 deletions(-) create mode 100644 mseries/codexbar-wingpanel/icons/grok.svg create mode 100644 mseries/codexbar-wingpanel/probe/check-generated-c.py create mode 100755 mseries/codexbar-wingpanel/probe/ci-build-deps.sh create mode 100755 mseries/codexbar-wingpanel/probe/run-grok-probe.sh create mode 100644 mseries/lib-codexbar.sh diff --git a/.github/workflows/validate-mseries.yml b/.github/workflows/validate-mseries.yml index 54655ae70..c447255b0 100644 --- a/.github/workflows/validate-mseries.yml +++ b/.github/workflows/validate-mseries.yml @@ -89,6 +89,24 @@ jobs: - name: Verify exact-kernel boot profile and rollback run: python3 -m unittest discover -s mseries/display-space -p 'test_*.py' + codexbar-wingpanel: + name: CodexBar Wingpanel module, render and interaction probes (arm64) + runs-on: ubuntu-26.04-arm + steps: + - uses: actions/checkout@v7 + - name: Install native build and headless test dependencies + run: >- + sudo apt-get update && sudo apt-get install -y git meson ninja-build valac gcc pkg-config + libgtk-4-dev libjson-glib-dev libgee-0.8-dev libgranite-7-dev sassc gettext + python3 xvfb xauth dbus-x11 librsvg2-common + - name: Provide Granite 7.7 and the pinned Wingpanel indicator API when the distro lacks them + run: bash mseries/codexbar-wingpanel/probe/ci-build-deps.sh "$RUNNER_TEMP/prefix" >> "$GITHUB_ENV" + - name: Build the module; run unit tests, generated-C guard, Switch/ingestion/token/Grok probes + run: | + meson setup "$RUNNER_TEMP/codexbar" mseries/codexbar-wingpanel + ninja -C "$RUNNER_TEMP/codexbar" + meson test -C "$RUNNER_TEMP/codexbar" --print-errorlogs + validate: name: mkosi summary + script checks (arm64) runs-on: ubuntu-26.04-arm @@ -139,6 +157,8 @@ jobs: bash -n mseries/install-signal.sh bash -n mseries/install-codexbar.sh bash -n mseries/deploy-codexbar.sh + bash -n mseries/lib-codexbar.sh + bash -n mseries/install-codexbar-wingpanel.sh python3 mseries/test-codexbar-deploy.py bash -n mseries/check-codexbar-port.sh python3 mseries/test-codexbar-cache.py @@ -173,6 +193,7 @@ jobs: python3 mseries/video-decode/test-brave-factory.py python3 mseries/video-decode/test-youtube-launch.py shellcheck -S warning mseries/video-decode/brave-youtube mseries/install-brave-widevine.sh + shellcheck -S warning mseries/lib-codexbar.sh mseries/deploy-codexbar.sh mseries/install-codexbar.sh mseries/install-codexbar-wingpanel.sh mseries/check-codexbar-port.sh mseries/codexbar-wingpanel/probe/ci-build-deps.sh mseries/codexbar-wingpanel/probe/run-grok-probe.sh python3 mseries/boot/test-prepare-uki-kernels.py python3 mseries/test-kernel-config.py python3 mseries/test-local-build-preflight.py diff --git a/mseries/CODEXBAR-PORT.md b/mseries/CODEXBAR-PORT.md index 513c7a32e..1a4a27996 100644 --- a/mseries/CODEXBAR-PORT.md +++ b/mseries/CODEXBAR-PORT.md @@ -13,12 +13,13 @@ ones.** Upgrade by re-pinning, re-installing upstream, and re-verifying each ite | Wrapper that follows the active OpenCode v2 OAuth account, with a legacy Go key fallback (read-only `credential` table; never copies/refreshes it) | `codexbar-opencode-wrapper.py` → `~/.local/bin/codexbar` | `codexbar-upstream` CLI path under `~/.local/lib/codexbar-cli/` | | Shared helper cache and locks (polling limits: per-answer TTL, exponential backoff on failure, last-good data kept, private `0600` files under `~/.cache/codexbar-mseries`; one `flock` around Codex sign-in writes) | `codexbar_cache.py` | none | | Sessions helper (incremental transcript index: only changed files are parsed) | `codexbar-sessions.py`, `codexbar_tokens.py` | none (reads local data) | +| Grok Build card: mark, Codex/Claude/OpenCode/Grok order, token-first bar row, product-share/reset-credit lines, `grok login` and open-in-terminal via `xdg-terminal-exec`; local 7-day tokens and sign-in email | `Indicator.vala`, `icons/grok.svg`, `codexbar-sessions.py` (`grok_session`) | upstream `grok` provider (v0.78.0+: CLI billing via `_x.ai/billing`); Grok's `~/.grok/sessions/*/*/usage.json` (`grok usage` envelope); `auth.json` scope keys for the email only | | OpenCode local usage, account plans and switching | `codexbar-opencode-local.py` | reads `opencode.db` read-only; activates via `opencode auth switch opencode ` | | Codex multi-account switcher (upstream's is macOS-only) | `codexbar-accounts.py` | upstream `codexbar usage --provider codex` honoring `CODEX_HOME` | | OpenCode Go sign-in (upstream imports opencode.ai cookies only on macOS): native WebKit window, saves the `auth`/`__Host-console_session` session as `providers[opencodego].cookieSource=manual` + `cookieHeader`; the panel's OpenCode **+** runs it | `codexbar-opencode-login.py` | config path resolution (`CodexBarConfigStore.defaultURL`), `OpenCodeWebCookieSupport` cookie names, `usage --source web` | | Claude CLI accounts via claude-swap, plus native Claude desktop sessions. CLI only keeps the app login separate; Switch both validates the target session, quits the native app, saves its final encrypted session, merges the target login without changing preferences, switches the CLI, and relaunches the app. Set up app preserves the previous session and locally clears the login for one-time sign-in without sending a logout request. Save app login and automatic list capture remember it. | `codexbar-claude.py`, venv `~/.local/share/claude-swap-venv`, `cswap` | claude-swap `list/status/switch --json`, `schemaVersion` 1, `to.email`, `reason: already-active`, `configs/.claude-config-*.json`; installed Claude `config.json` keys `lastKnownAccountUuid`, `oauth:tokenCache`, `oauth:tokenCacheV2`, SQLite `Cookies` (or `Network/Cookies`), `gtk-launch com.anthropic.Claude` | | Claude local Code context carry-over | `codexbar_claude_history.py`, [design/evidence](CODEXBAR-CLAUDE-HISTORY.md) | installed Claude desktop account/org Code index and local `.claude/projects` transcripts; private exports and create-only imports after app exit | -| Installers / deploy / CI | `install-codexbar.sh`, `install-codexbar-wingpanel.sh`, `deploy-codexbar.sh`, `check-codexbar-port.sh`, `.github/workflows/validate-mseries.yml`, `enhance.sh` payload list | version pin `version=vX.Y.Z` | +| Installers / deploy / CI | `lib-codexbar.sh` (the single helper/mark list), `install-codexbar.sh`, `install-codexbar-wingpanel.sh`, `deploy-codexbar.sh`, `check-codexbar-port.sh`, `.github/workflows/validate-mseries.yml` (`codexbar-wingpanel` job: native build, unit tests and Xvfb probes via `probe/ci-build-deps.sh`), `enhance.sh` payload list | version pin `version=vX.Y.Z` | ## Upgrade checklist @@ -42,7 +43,8 @@ ones.** Upgrade by re-pinning, re-installing upstream, and re-verifying each ite - `~/.local/bin/codexbar` is the wrapper, not upstream's binary (upstream's CLI is `codexbar-upstream`). - Account stores: `~/.local/share/codexbar-mseries/codex-accounts` (private, 0600) and claude-swap's own store. Never commit these. -Quota and account cards, like the top-panel readout, always order Codex first, Claude second, OpenCode Go third. +Quota and account cards, like the top-panel readout, always order Codex first, Claude second, OpenCode Go third, +Grok fourth; any other provider upstream reports follows. ## Polling and cache @@ -53,7 +55,8 @@ Who polls what, so a new feature does not quietly add requests: | `codexbar-linux --snapshot` (local IPC, upstream owns provider fetching) | panel timer | 5 s while the popover is open or a refresh runs, 30 s otherwise | | `codexbar-accounts.py usage` (saved Codex accounts; the active one comes from the snapshot) | popover open | cached 15 min per account, parallel, `--force` from the Refresh button | | `codexbar-claude.py list` | popover open | cached 2 min, cleared by any other Claude command | -| `codexbar-sessions.py`, `codexbar-opencode-local.py` | popover open | local reads; transcripts are re-parsed only when their size or mtime changed | +| `codexbar-sessions.py`, `codexbar-opencode-local.py` | popover open | local reads; transcripts and Grok `usage.json` files are re-parsed only when their size or mtime changed | +| Grok credits (`codexbar usage --provider grok`, upstream) | snapshot refresh | upstream owns it: `grok agent stdio` RPC (4 s init, 3 s billing), then the CLI-proxy REST fallback | | Everything above with the popover closed | background tick | every 15 min | `codexbar_cache.py` owns the on-disk cache (`~/.cache/codexbar-mseries`, `0700`/`0600`), the failure backoff (60 s doubling @@ -374,3 +377,39 @@ below-15 wording and applies consistently to all provider/account rows. Dropdown quota bars and percentage labels use the same visual thresholds as the menu dots: green >70%, yellow25–70%, red<25%. The shared visual formatter is separate from ETA/alert warning thresholds. + +## Grok Build + +Upstream added a Grok provider for xAI's Grok Build CLI; v0.78.0 restored its CLI billing (the +`_x.ai/billing` ACP method over `grok agent stdio`, then `cli-chat-proxy.grok.com/v1/billing?format=credits` +with the `~/.grok/auth.json` bearer). Upstream owns credentials, fetching and the snapshot; we add the +elementary layer. `install-codexbar.sh` appends `grok` to `providerOrder` only when a `grok` binary is +installed (PATH, `~/.grok/bin`, `~/.local/bin`). Browser-cookie import is macOS-only upstream and unused here. + +- **Card and bar.** Grok ranks after OpenCode Go with a terminal-prompt mark (original glyph, not a brand logo). + Its single weekly credits window sits beside the seven-day token count; the lower bar row is omitted when the + binding window is that same weekly window (this also applies to any provider whose only window is weekly). + The tier comes from upstream (`SuperGrok`, `SuperGrok Heavy`). +- **Details.** Upstream's `details` sections (product shares of the same credit pool such as Grok Build/Grok Chat, + and usage-limit reset coupons) render as compact lines. Every section and row is type-checked; malformed + entries are skipped, at most four sections of eight rows. +- **Sign-in and opening.** Clicking the card opens `grok` in the default terminal (`xdg-terminal-exec`, installed by + `install-codexbar.sh`). With no credits data, **Sign in…** runs `grok login` the same way; without a terminal + launcher it falls back to Settings. +- **Local tokens.** `codexbar-sessions.py` reads only `//usage.json` (the persisted + `grok usage` envelope), counts completed turns by `endedAt`, and uses `totalTokens` (input + output; cached + input and reasoning are subsets). Forked/resumed history is deduplicated by turn fingerprint, symlinked and + artifact/terminal trees are skipped, files over 8 MB are ignored, and parses are cached under + `grok-usage-v1`. Costs are reported as 0: subscription credits are not dollars. `GROK_HOME` is honored. +- **Identity.** The helper reads only the `email` of the SuperGrok OIDC entry (falling back to the legacy session + entry), as upstream does for its identity row. Tokens and other fields are never read out; tests assert it. +- **Probes.** `probe/run-grok-probe.sh BUILD_DIR` renders the `grok` and `grok-signin` fixtures, asserts order, + layout, details and the email, and clicks **Sign in…** against a fake terminal to confirm `grok login` is launched. + +## Generated-C guard + +valac 0.56 frees the owned inner value of a nested conditional that mixes a literal with a call or +concatenation before copying it. This printed garbage alternate-account countdowns and dropdown quota +captions until 2026-10-19. `probe/check-generated-c.py` (Meson test `generated-c-temporaries`) fails the +build when that borrowed-then-freed shape appears in any generated C. Write such strings with an explicit +`string` declaration and `if`/`else`. diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 64ebe6704..e46db0aa3 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,47 @@ # OS 9 implementation / Claude Code handoff +## CodexBar production hardening + Grok Build; upstream v0.78.0 — 2026-10-19 (Claude Code) + +**Pin:** `install-codexbar.sh` now pins upstream **v0.78.0** (was v0.77.0). Both Linux aarch64 +tarballs downloaded and their published `sha256` verified; the CLI reports `0.78.0` and contains the +Grok `_x.ai/billing` / CLI-proxy paths. No changes upstream between the tags in +`Integrations/Linux` (DesktopController, QML settings, snapshot), so `SettingsModel.vala` and the +panel schema are unchanged. The only CLI behaviour change touching us is opt-in upstream claude-swap +following, which our config does not enable. + +**Defects fixed (latent in the running panel):** +- Use-after-free from a valac nested-ternary code-generation defect: alternate-account reset + countdowns (`Presentation.account_quota`) and every dropdown quota caption (`build_window`) were + copied from freed memory. The existing `presentation` Meson test was failing on `dev` because of it. + Fixed with explicit owned strings; new Meson test `generated-c-temporaries` scans all generated C and + fails on the pattern (confirmed: 11 hits on the old build, 0 now). +- `install-codexbar-wingpanel.sh` skipped `codexbar_claude_handoff.py` (hard-imported by + `codexbar-claude.py`) and `codexbar-opencode-login.py`; `--uninstall` removed one mark of three. + All three scripts now source `lib-codexbar.sh` for one helper/mark list; `test-codexbar-deploy.py` + fails if any helper imports a module missing from it or `enhance.sh` omits one (mutation-checked). +- Settings window: provider catalog parsing no longer trips GLib criticals on malformed CLI entries. + +**Grok Build integrated** (owner has Grok Build 1.0.62 at `~/.grok`, signed in): see +[CODEXBAR-PORT.md#grok-build](CODEXBAR-PORT.md#grok-build). Card/bar/mark/order, product-share and +reset-credit lines, `grok login` and open-in-terminal, seven-day local tokens from `usage.json`, sign-in +email only. Installer adds `grok` to `providerOrder` when the CLI is installed. + +**CI:** new `codexbar-wingpanel` job builds the native module and runs all 17 Meson tests (unit, generated-C +guard, Switch/ingestion/token/live-preference/Grok Xvfb probes). `probe/ci-build-deps.sh` builds Granite +7.7 and Wingpanel's `lib/` at the installer's pinned commit only when the distro lacks them. + +**Verified here (x86_64 container, not the owner's machine):** 11 Python suites; from-scratch dependency +bootstrap then 17/17 Meson tests; shellcheck; render probes (light/dark) of the Grok scenarios; the +sessions helper on a copy of the owner's real Grok session (762,841 tokens, matching `grok usage`). + +**Not verified:** a live `codexbar usage --provider grok` against the owner's account (ARM64 CLI could +not run here; no account access); the CI job on `ubuntu-26.04-arm` (first run is this PR); +`check-codexbar-port.sh` Grok lines against the live app; native module not loaded in the real panel. + +**Owner steps after merge:** `bash mseries/install-codexbar.sh --skip-system-deps` (bump + Grok in provider +order), `bash mseries/deploy-codexbar.sh` (stages module and mark; loads at next login), then +`bash mseries/check-codexbar-port.sh`. + ## enh1 diagnostic boot reached the desktop; crash triage after reboot — 2026-10-18 (Claude Code) **Kernel now:** `7.1.12-elementary-aurora20-enh1` via the one-shot diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index cca7857d4..66cf77504 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,5 +1,14 @@ # Roadmap, features and follow-ups (MacBook Pro 14" M1 Pro, elementary OS 9 on Asahi) +## CodexBar: Grok Build, upstream v0.78.0 and native CI — 2026-10-19 + +Done on `claude/codexbar-grok-production`: Grok Build card and local tokens; upstream pin v0.78.0 +(restores Grok CLI billing); two use-after-free strings in the panel fixed with a generated-C guard; +one shared helper list for installers/deploy/image; native module and Xvfb probes in CI. +Follow-ups: live Grok usage check on the owner's account after install; first CI run of the +`codexbar-wingpanel` job on `ubuntu-26.04-arm`; incremental transcript offsets and `Indicator.vala` +card extraction remain from the [performance audit](CODEXBAR-PERFORMANCE-AUDIT-2026-10-18.md#next-measured-work). + ## Release stages: F5 parity first, support assistants after — 2026-10-18 Owner's order: diff --git a/mseries/check-codexbar-port.sh b/mseries/check-codexbar-port.sh index e9a53fde7..e869bb20f 100755 --- a/mseries/check-codexbar-port.sh +++ b/mseries/check-codexbar-port.sh @@ -26,11 +26,38 @@ else fi rm -rf "$empty" +# Grok Build (upstream provider since v0.78.0 restored CLI billing): we draw its card, so the id must stay "grok". +if "$cli" config providers --format json 2>/dev/null | grep -q '"provider" *: *"grok"'; then + ok "CLI lists the grok provider" +else + bad "CLI no longer lists provider grok; the panel's Grok card and installer order would go stale" +fi +grok_bin=$(command -v grok || true) +for candidate in "${GROK_HOME:-$HOME/.grok}/bin/grok" "$HOME/.local/bin/grok"; do + [[ -z $grok_bin && -x $candidate ]] && grok_bin=$candidate +done +if [[ -n $grok_bin ]]; then + if grok_json=$("$cli" usage --provider grok --format json 2>/dev/null) && grep -q '"primary"' <<<"$grok_json"; then + ok "grok usage reports a credits window" + else + note "grok usage returned no credits window (run grok login, or check network)" + fi + config="${XDG_CONFIG_HOME:-$HOME/.config}/codexbar/linux.json" + if python3 -c 'import json,sys; c=json.load(open(sys.argv[1])); sys.exit(0 if c.get("provider")=="custom" and "grok" in c.get("providerOrder",[]) else 1)' "$config" 2>/dev/null; then + ok "grok is in the panel's provider order" + else + note "grok is installed but not in $config providerOrder; rerun install-codexbar.sh or add it in Settings → Providers" + fi +else + note "Grok Build is not installed; its card stays hidden" +fi + for f in codexbar-cache codexbar-accounts codexbar-claude codexbar-claude-history codexbar-claude-handoff codexbar-opencode-local codexbar-opencode-login codexbar-sessions; do python3 "$repo/mseries/test-$f.py" >/dev/null 2>&1 && ok "tests: $f" || bad "tests: $f" done python3 "$repo/mseries/test-codexbar-performance.py" >/dev/null 2>&1 && ok "tests: performance/watch" || bad "tests: performance/watch" python3 "$repo/mseries/test-codexbar-wrapper.py" >/dev/null 2>&1 && ok "tests: wrapper" || bad "tests: wrapper" +python3 "$repo/mseries/test-codexbar-deploy.py" >/dev/null 2>&1 && ok "tests: deploy/payload" || bad "tests: deploy/payload" build=$(mktemp -d); trap 'rm -rf "$build"' EXIT if meson setup "$build" "$repo/mseries/codexbar-wingpanel" >/dev/null 2>&1 && ninja -C "$build" >/dev/null 2>&1 \ diff --git a/mseries/codexbar-sessions.py b/mseries/codexbar-sessions.py index 86cf8aaef..3fdfbc74f 100644 --- a/mseries/codexbar-sessions.py +++ b/mseries/codexbar-sessions.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Read-only health and local activity for the ChatGPT/Codex, Claude Code and OpenCode sessions. +"""Read-only health and local activity for the ChatGPT/Codex, Claude Code, OpenCode and Grok Build sessions. Nothing here reads, prints or copies credentials: sign-in is judged from the presence and shape of each tool's own files. Activity comes from their local @@ -208,12 +208,142 @@ def opencode_session(home: pathlib.Path, now_ms: int) -> dict[str, Any]: return entry +MAX_GROK_USAGE_BYTES = 8 * 1024 * 1024 + + +def grok_home(home: pathlib.Path) -> pathlib.Path: + configured = os.environ.get("GROK_HOME") + if configured and pathlib.Path(configured).is_absolute(): + return pathlib.Path(configured) + return home / ".grok" + + +def parse_grok_usage(path: pathlib.Path) -> dict[str, list[int]]: + """Completed turns in one session's usage.json as {fingerprint: [ended ms, tokens]}. + + This is the documented `grok usage ` envelope that Grok Build persists per + session. A resumed or forked session inherits earlier turns, so fingerprints are + content-based (end time, tokens, model calls) and deduplicate across sessions.""" + turns: dict[str, list[int]] = {} + try: + if path.stat().st_size > MAX_GROK_USAGE_BYTES: + return turns + document = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + return turns + recorded = document.get("turns") if isinstance(document, dict) else None + for turn in recorded if isinstance(recorded, list) else []: + if not isinstance(turn, dict): + continue + ended = parse_timestamp(turn.get("endedAt")) + if ended is None: + continue + try: + tokens = codexbar_tokens.grok_tokens(turn) + calls = codexbar_tokens.count(turn.get("modelCalls")) + except (TypeError, ValueError, OverflowError): + continue + turns[f"{ended}:{tokens}:{calls}"] = [ended, tokens] + return turns + + +def read_grok_records(sessions: pathlib.Path, now_ms: int) -> list[tuple[int, int, float]]: + """Token records from Grok session usage files changed in the last 30 days. + + Only `//usage.json` is read: artifact and terminal trees + are never walked, symlinked directories are skipped, and unchanged files reuse + the cached parse. Dollar figures are omitted: subscription credits are not spend.""" + oldest = (now_ms - WINDOWS[-1][1]) / 1000 + index = codexbar_cache.load("grok-usage-v1").get("files") + index = index if isinstance(index, dict) else {} + kept: dict[str, Any] = {} + count = 0 + try: + workspaces = [entry for entry in os.scandir(sessions) if entry.is_dir(follow_symlinks=False)] + except OSError: + workspaces = [] + for workspace in workspaces: + try: + session_dirs = [entry for entry in os.scandir(workspace.path) if entry.is_dir(follow_symlinks=False)] + except OSError: + continue + for session_dir in session_dirs: + path = pathlib.Path(session_dir.path) / "usage.json" + try: + info = path.lstat() + except OSError: + continue + if not path.is_file() or path.is_symlink() or info.st_mtime < oldest: + continue + count += 1 + if count > MAX_TRANSCRIPT_FILES: + break + known = index.get(str(path)) + if (isinstance(known, dict) and isinstance(known.get("turns"), dict) + and known.get("size") == info.st_size and known.get("mtime") == info.st_mtime_ns): + kept[str(path)] = known + else: + kept[str(path)] = {"size": info.st_size, "mtime": info.st_mtime_ns, "turns": parse_grok_usage(path)} + if kept.keys() != index.keys() or any(kept[k] is not index.get(k) for k in kept): + codexbar_cache.store("grok-usage-v1", {"files": kept}) + turns: dict[str, tuple[int, int]] = {} + for entry in kept.values(): + for key, value in entry["turns"].items(): + if isinstance(value, list) and len(value) == 2: + turns[key] = (value[0], value[1]) + return [(ended, tokens, 0.0) for ended, tokens in turns.values()] + + +GROK_IDENTITY_SCOPES = ("https://auth.x.ai", "https://accounts.x.ai/sign-in") + + +def grok_account_email(auth: dict[str, Any] | None) -> str: + """The signed-in email, chosen as upstream does (SuperGrok OIDC entry, then the legacy session). + + Only `email` is read from the matching entry; bearer and refresh tokens are never touched.""" + for prefix in GROK_IDENTITY_SCOPES: + for scope, value in sorted((auth or {}).items()): + if isinstance(scope, str) and scope.startswith(prefix) and isinstance(value, dict): + email = value.get("email") + if isinstance(email, str) and "@" in email and len(email) <= 320: + return email.strip() + return "" + + +def grok_session(home: pathlib.Path, now_ms: int) -> dict[str, Any]: + entry: dict[str, Any] = {"id": "grok", "name": "Grok Build"} + root = grok_home(home) + binary = find_binary("grok", home, (".grok/bin/grok", ".local/bin/grok")) + sessions = root / "sessions" + records = read_grok_records(sessions, now_ms) if sessions.is_dir() else [] + entry["periods"] = summarize(records, now_ms) if records else [] + entry["note"] = ("Local activity from Grok Build session usage · includes cached input and reasoning · " + "not account quota") + # Sign-in is judged from the credential file's presence and shape only; its values are never read out. + auth = load_json_object(root / "auth.json") + signed_in = bool(auth) + email = grok_account_email(auth) + if email: + entry["accountEmail"] = email + api_key = bool(os.environ.get("XAI_API_KEY", "").strip()) + if signed_in: + entry.update(state="ok", detail="Signed in · account credits come from CodexBar.") + elif api_key and binary: + entry.update(state="ok", detail="Using XAI_API_KEY · run grok login for SuperGrok credits in CodexBar.") + elif binary: + entry.update(state="warn", detail="grok found but not signed in. Run grok login.") + else: + entry.update(state="off", detail="Grok Build is not installed.") + return entry + + def collect(home: pathlib.Path | None = None, now_ms: int | None = None) -> dict[str, Any]: home = home or pathlib.Path.home() now_ms = int(time.time() * 1000) if now_ms is None else now_ms return { "updatedAt": iso_time(now_ms), - "sessions": [codex_session(home, now_ms), claude_session(home, now_ms), opencode_session(home, now_ms)], + "sessions": [codex_session(home, now_ms), claude_session(home, now_ms), opencode_session(home, now_ms), + grok_session(home, now_ms)], } diff --git a/mseries/codexbar-wingpanel/Indicator.vala b/mseries/codexbar-wingpanel/Indicator.vala index 793df150c..9a727f8d4 100644 --- a/mseries/codexbar-wingpanel/Indicator.vala +++ b/mseries/codexbar-wingpanel/Indicator.vala @@ -728,11 +728,44 @@ namespace MSeries { } } - private void open_desktop_app (string provider, Gtk.Widget from) { + private static string? grok_binary () { + var found = Environment.find_program_in_path ("grok"); + if (found != null) return found; + foreach (var relative in new string[] { ".grok/bin/grok", ".local/bin/grok" }) { + var candidate = GLib.Path.build_filename (Environment.get_home_dir (), relative); + if (FileUtils.test (candidate, FileTest.IS_EXECUTABLE)) return candidate; + } + return null; + } + + /* Grok Build is a terminal program: its card opens it, and its sign-in runs `grok login`, in the + * default terminal (xdg-terminal-exec, installed with CodexBar). Null when either is missing. */ + private static GLib.AppInfo? terminal_app_for (string provider, string[] arguments = {}) { + if (provider != "grok") return null; + var terminal = Environment.find_program_in_path ("xdg-terminal-exec"); + var grok = grok_binary (); + if (terminal == null || grok == null) return null; + var command = new StringBuilder (Shell.quote (terminal)); + command.append (" " + Shell.quote (grok)); + foreach (var argument in arguments) command.append (" " + Shell.quote (argument)); + try { + return GLib.AppInfo.create_from_commandline (command.str, "Grok Build", GLib.AppInfoCreateFlags.NONE); + } catch (Error e) { + return null; + } + } + + private void open_desktop_app (string provider, Gtk.Widget from, string[] arguments = {}) { var id = desktop_id_for (provider); - var app = id != null ? new GLib.DesktopAppInfo (id) : null; + GLib.AppInfo? app = null; + if (id != null) { + app = new GLib.DesktopAppInfo (id); + } else { + app = terminal_app_for (provider, arguments); + } if (app == null) { - subtitle_label.set_text ("%s isn't installed as a desktop app".printf (provider_title (provider))); + subtitle_label.set_text (provider == "grok" ? "Grok Build or a terminal (xdg-terminal-exec) isn't installed" + : "%s isn't installed as a desktop app".printf (provider_title (provider))); return; } popdown_from (from); @@ -744,7 +777,7 @@ namespace MSeries { } private void open_app_on_click (Gtk.Widget card, string provider) { - if (desktop_id_for (provider) == null) { + if (desktop_id_for (provider) == null && terminal_app_for (provider) == null) { return; } var click = new Gtk.GestureClick () { button = 1 }; @@ -1066,7 +1099,9 @@ namespace MSeries { } private string weekly_tokens_digest () { - return local_week_tokens ("codex") + "|" + local_week_tokens ("claude") + "|" + local_week_tokens ("opencodego"); + var digest = new StringBuilder (); + foreach (var provider in TOKEN_PROVIDERS) digest.append (local_week_tokens (provider) + "|"); + return digest.str; } private void repaint_cached_quota () { @@ -1176,8 +1211,13 @@ namespace MSeries { } } + /* Known providers first (Codex, Claude, OpenCode Go, Grok); anything else upstream reports follows. */ + private const int LAST_PROVIDER_RANK = 4; + private const string[] TOKEN_PROVIDERS = { "codex", "claude", "opencodego", "grok" }; + private static int provider_rank (string provider) { - return provider == "codex" ? 0 : provider == "claude" ? 1 : provider == "opencodego" ? 2 : 3; + return provider == "codex" ? 0 : provider == "claude" ? 1 : provider == "opencodego" ? 2 + : provider == "grok" ? 3 : LAST_PROVIDER_RANK; } private static string hint_id (string provider, Json.Object account) { @@ -1419,7 +1459,7 @@ namespace MSeries { } } - foreach (var provider in new string[] { "codex", "claude", "opencodego" }) { + foreach (var provider in TOKEN_PROVIDERS) { var count = local_week_tokens (provider); if (count.length > 0) tooltip.append ("\n\n%s · 7d: %s tokens".printf (provider_title (provider), count)); } @@ -1430,9 +1470,9 @@ namespace MSeries { var digest = new StringBuilder (); digest.append (hints_digest); - foreach (var provider in new string[] { "codex", "claude", "opencodego" }) + foreach (var provider in TOKEN_PROVIDERS) digest.append (local_week_tokens (provider) + "|"); - foreach (var provider in new string[] { "codex", "claude", "opencodego" }) { + foreach (var provider in TOKEN_PROVIDERS) { digest.append (alternate_account_text (provider, now_unix)); var windows = active_panel_windows (provider); if (windows != null) digest.append (colored_account_quota (panel_display, windows, now_unix, true)); @@ -1445,7 +1485,7 @@ namespace MSeries { panel_digest = digest.str; clear (panel_segments); - for (int rank = 0; rank <= 3; rank++) { + for (int rank = 0; rank <= LAST_PROVIDER_RANK; rank++) { for (int i = 0; i < providers.length; i++) { if (provider_rank (providers[i]) != rank) continue; if (panel_segments.get_first_child () != null) { @@ -1518,20 +1558,25 @@ namespace MSeries { (int) JsonBoundary.integer (window, "minutes")), JsonBoundary.number (window, "remaining", -1), quota_reset_tooltip (window)); } - private void append_weekly_panel_quota (Gtk.Box heading, string provider) { + /* Returns whether the weekly value or its countdown was drawn in the heading. */ + private bool append_weekly_panel_quota (Gtk.Box heading, string provider) { var windows = active_panel_windows (provider); for (uint i = 0; windows != null && i < windows.get_length (); i++) { var window = windows.get_object_element (i); if (JsonBoundary.integer (window, "minutes") != 10080) continue; var remaining = JsonBoundary.number (window, "remaining", -1); - if (remaining < 0) return; + if (remaining < 0) return false; var value = new Gtk.Label ("· 7D %0.0f%%".printf (remaining)); value.add_css_class ("seg-value"); var level = Presentation.level_for (remaining); if (level == QuotaLevel.LOW) value.add_css_class ("level-low"); if (level == QuotaLevel.CRITICAL) value.add_css_class ("level-critical"); apply_quota_dot (value, "7D", remaining, quota_reset_tooltip (window)); - if (display_preferences.get_bool ("showPercent")) heading.append (value); + var shown = false; + if (display_preferences.get_bool ("showPercent")) { + heading.append (value); + shown = true; + } if (display_preferences.get_bool ("showReset") && !display_preferences.get_bool ("compactQuota")) { var timestamp = new DateTime.from_iso8601 (JsonBoundary.text (window, "resetsAt"), null); var countdown = timestamp != null @@ -1541,17 +1586,19 @@ namespace MSeries { var reset = new Gtk.Label (countdown); reset.add_css_class ("seg-reset"); heading.append (reset); + shown = true; } } - return; + return shown; } + return false; } private Gtk.Widget build_panel_segment (string provider, string value_text, string reset_text, QuotaLevel level, bool stale) { var local_tokens = display_preferences.get_bool ("showTokens") ? local_week_tokens (provider) : ""; var dot_mode = display_preferences.get_bool ("compactQuota"); - var details_on_top = dot_mode || (provider == "codex" || provider == "claude") && (local_tokens.length > 0 || + var details_on_top = dot_mode || (provider == "codex" || provider == "claude" || provider == "grok") && (local_tokens.length > 0 || ((display_preferences.get_bool ("showPercent") || display_preferences.get_bool ("showReset")) && active_panel_windows (provider) != null)); var segment = new Gtk.Box (Gtk.Orientation.HORIZONTAL, provider == "opencodego" ? 2 : 3); if (provider == "opencodego") segment.add_css_class ("seg-opencode"); @@ -1563,6 +1610,7 @@ namespace MSeries { icon.valign = Gtk.Align.CENTER; } var heading = details_on_top ? new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3) : segment; + var weekly_shown = false; if (details_on_top && stale) heading.add_css_class ("stale"); heading.append (mark); if (display_preferences.get_bool ("showHints")) heading.append (build_hint_badges (provider, true)); @@ -1573,22 +1621,25 @@ namespace MSeries { tokens.tooltip_text = "Tokens processed in seven days"; heading.append (tokens); } - if (!dot_mode) append_weekly_panel_quota (heading, provider); + if (!dot_mode) weekly_shown = append_weekly_panel_quota (heading, provider); } var value = new Gtk.Label (value_text) { valign = Gtk.Align.BASELINE_CENTER }; value.add_css_class ("seg-value"); if (level == QuotaLevel.LOW) value.add_css_class ("level-low"); if (level == QuotaLevel.CRITICAL) value.add_css_class ("level-critical"); apply_primary_quota_dot (value, provider); + // A provider whose binding window is its weekly one (Grok's credits) already shows it in the + // token heading; repeating the same value and countdown on the lower row adds nothing. + var weekly_in_heading = weekly_shown && value_text.has_prefix ("7D "); if (display_preferences.get_bool ("showPercent")) { if (dot_mode) heading.append (value); - else segment.append (value); + else if (!weekly_in_heading) segment.append (value); } else { if (level == QuotaLevel.LOW) mark.add_css_class ("level-low"); if (level == QuotaLevel.CRITICAL) mark.add_css_class ("level-critical"); } if (dot_mode && !value.get_text ().has_prefix ("7D ")) append_weekly_panel_quota (heading, provider); - if (display_preferences.get_bool ("showReset") && !dot_mode && reset_text.length > 0) { + if (display_preferences.get_bool ("showReset") && !dot_mode && reset_text.length > 0 && !weekly_in_heading) { var reset = new Gtk.Label (reset_text) { valign = Gtk.Align.BASELINE_CENTER }; reset.add_css_class ("seg-reset"); segment.append (reset); @@ -1676,7 +1727,8 @@ namespace MSeries { private static string provider_title (string provider) { return provider == "codex" ? "OpenAI Codex" : provider == "claude" ? "Claude" : - provider == "opencodego" ? "OpenCode Go" : provider; + provider == "opencodego" ? "OpenCode Go" : + provider == "grok" ? "Grok" : provider; } private void render_quota (Json.Array entries, int64 now_unix) { @@ -1688,7 +1740,7 @@ namespace MSeries { has_claude = has_claude || JsonBoundary.text (entries.get_object_element (i), "provider", "").down () == "claude"; } // Match the panel order, including Claude's locally supplied accounts card. - for (int rank = 0; rank <= 3; rank++) { + for (int rank = 0; rank <= LAST_PROVIDER_RANK; rank++) { if (rank == provider_rank ("claude") && !has_claude && claude_data != null) { quota_box.append (build_claude_card (now_unix)); } @@ -1773,6 +1825,9 @@ namespace MSeries { if (provider == "codex") { card.append (build_codex_accounts ()); } + if (provider == "grok") { + append_detail_rows (card, entry); + } if (provider == "opencodego") { for (uint i = 0; opencode_accounts != null && i < opencode_accounts.get_length (); i++) { var account = opencode_accounts.get_object_element (i); @@ -1807,18 +1862,25 @@ namespace MSeries { JsonBoundary.integer (period, "tokens", 0)))); } card.append (subtle_label ("Sign in to OpenCode or connect a Go API key to load account quota.")); + } else if (provider == "grok") { + card.append (subtle_label ("No Grok credits reported. If you are signed out, run grok login. Local activity is under Sessions.")); } else { card.append (subtle_label (provider == "opencodego" ? "Sign in to OpenCode or connect a Go API key. Local activity is under Sessions." : error.length > 0 ? error : "No usage reported yet.")); } - var settings = new Gtk.Button.with_label (provider == "opencodego" ? "Sign in…" : "Open Settings…") { + // Grok signs in with its own CLI; without a terminal launcher, Settings remains the fallback. + var grok_login = provider == "grok" && terminal_app_for ("grok", { "login" }) != null; + var settings = new Gtk.Button.with_label (provider == "opencodego" || grok_login ? "Sign in…" : "Open Settings…") { halign = Gtk.Align.START }; + if (grok_login) settings.tooltip_text = "Open grok login in a terminal, then refresh CodexBar"; settings.clicked.connect (() => { popdown_from (settings); if (provider == "opencodego") { add_opencode_account (); + } else if (grok_login) { + open_desktop_app ("grok", settings, { "login" }); } else { run_action ("--settings"); } @@ -1828,6 +1890,34 @@ namespace MSeries { return card; } + /* Upstream's informational detail sections (Grok: "Usage breakdown" product shares and + * "Limit Reset Credits"), shown as compact text lines. Every collection is type-checked here + * because the boundary validator treats these keys as forward-compatible unknowns. */ + private void append_detail_rows (Gtk.Box card, Json.Object entry) { + var sections = JsonBoundary.array (entry, "details"); + for (uint i = 0; sections != null && i < sections.get_length () && i < 4; i++) { + var section_node = sections.get_element (i); + if (section_node.get_node_type () != Json.NodeType.OBJECT) continue; + var section = section_node.get_object (); + var rows = JsonBoundary.array (section, "rows"); + var parts = new StringBuilder (); + for (uint r = 0; rows != null && r < rows.get_length () && r < 8; r++) { + var row_node = rows.get_element (r); + if (row_node.get_node_type () != Json.NodeType.OBJECT) continue; + var row = row_node.get_object (); + var label = JsonBoundary.text (row, "label").strip (); + var value = JsonBoundary.text (row, "value").strip (); + if (label.length == 0 && value.length == 0) continue; + if (parts.len > 0) parts.append (" · "); + parts.append (label.length > 0 && value.length > 0 ? label + " " + value : label + value); + } + if (parts.len == 0) continue; + var title = JsonBoundary.text (section, "title").strip (); + var line = subtle_label (title.length > 0 ? title + " · " + parts.str : parts.str); + card.append (line); + } + } + private string opencode_week_tokens () { return local_week_tokens ("opencodego"); } @@ -1858,7 +1948,8 @@ namespace MSeries { private static Gtk.Widget build_provider_mark (string provider, string title_text) { var icon_name = provider == "codex" ? "mseries-codexbar-openai-symbolic" : provider == "claude" ? "mseries-codexbar-claude-symbolic" : - provider == "opencodego" ? "mseries-codexbar-opencode-symbolic" : ""; + provider == "opencodego" ? "mseries-codexbar-opencode-symbolic" : + provider == "grok" ? "mseries-codexbar-grok-symbolic" : ""; if (icon_name.length == 0 || !Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_icon (icon_name)) { var text = new Gtk.Label (title_text) { xalign = 0 }; text.add_css_class ("provider-title"); @@ -2018,6 +2109,13 @@ namespace MSeries { if (email.length == 0 && provider == "opencodego" && opencode_local != null) { email = JsonBoundary.text (opencode_local, "accountEmail", ""); } + if (email.length == 0 && provider == "grok") { + // codexbar-sessions.py reports only the email from Grok's own sign-in file. + for (uint i = 0; local_sessions != null && i < local_sessions.get_length (); i++) { + var session = local_sessions.get_object_element (i); + if (JsonBoundary.text (session, "id") == "grok") email = JsonBoundary.text (session, "accountEmail", ""); + } + } if (email.length == 0 && provider == "opencodego") { for (uint i = 0; opencode_accounts != null && i < opencode_accounts.get_length (); i++) { var account = opencode_accounts.get_object_element (i); @@ -2075,9 +2173,13 @@ namespace MSeries { // The forecast only gets its own line when it is a warning; otherwise the reset sits inline. if (!known || eta.state == EtaState.EXHAUSTED || eta.state == EtaState.DEPLETING) { - var caption_text = !known ? "Usage not reported yet" - : display_preferences.get_bool ("showReset") ? Presentation.window_caption (reset, eta.state, eta.detail) - : eta.state == EtaState.EXHAUSTED ? "Limit reached" : eta.detail; + // Explicit owned string; see Presentation.account_quota for the nested-ternary valac defect. + string caption_text = "Usage not reported yet"; + if (known) { + caption_text = display_preferences.get_bool ("showReset") + ? Presentation.window_caption (reset, eta.state, eta.detail) + : eta.state == EtaState.EXHAUSTED ? "Limit reached" : eta.detail; + } var caption = new Gtk.Label (caption_text) { xalign = 0, wrap = true, margin_start = 40 }; caption.add_css_class ("caption"); caption.add_css_class (eta.state == EtaState.EXHAUSTED ? "text-critical" : diff --git a/mseries/codexbar-wingpanel/Presentation.vala b/mseries/codexbar-wingpanel/Presentation.vala index 054a41340..d46a5551d 100644 --- a/mseries/codexbar-wingpanel/Presentation.vala +++ b/mseries/codexbar-wingpanel/Presentation.vala @@ -128,8 +128,12 @@ namespace MSeries { var minutes = (int) JsonBoundary.integer (window, "minutes", 0); var label = short_window_label (JsonBoundary.text (window, "label", "Quota"), minutes); var reset = new DateTime.from_iso8601 (JsonBoundary.text (window, "resetsAt", ""), null); - var eta = reset == null ? "?" : reset.to_unix () <= now_unix ? "now" - : countdown_text (reset.to_unix () - now_unix); + // Explicit owned string: valac frees the owned inner value of a nested + // literal/call ternary before copying it (use-after-free in generated C). + string eta = "?"; + if (reset != null) { + eta = reset.to_unix () <= now_unix ? "now" : countdown_text (reset.to_unix () - now_unix); + } if (parts.len > 0) parts.append (" · "); parts.append (label); if (show_percent) parts.append (" %.0f%%".printf (node.get_double ().clamp (0, 100))); diff --git a/mseries/codexbar-wingpanel/icons/grok.svg b/mseries/codexbar-wingpanel/icons/grok.svg new file mode 100644 index 000000000..55a7f7fd8 --- /dev/null +++ b/mseries/codexbar-wingpanel/icons/grok.svg @@ -0,0 +1 @@ + diff --git a/mseries/codexbar-wingpanel/meson.build b/mseries/codexbar-wingpanel/meson.build index bb4351031..767da3e5b 100644 --- a/mseries/codexbar-wingpanel/meson.build +++ b/mseries/codexbar-wingpanel/meson.build @@ -10,7 +10,7 @@ indicator_sources = files( 'PanelPreferences.vala', 'RefreshPolicy.vala', 'QuotaEta.vala', 'Presentation.vala', ) -shared_module( +libcodexbar = shared_module( 'codexbar', indicator_sources, dependencies: [libwingpanel_dep, granite_dep, json_dep], @@ -57,7 +57,7 @@ install_data( rename: 'mseries-codexbar-openai-symbolic.svg', ) -foreach provider : ['claude', 'opencode'] +foreach provider : ['claude', 'opencode', 'grok'] install_data( 'icons/@0@.svg'.format(provider), install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps'), @@ -73,7 +73,7 @@ settings_model_test = executable( ) test('settings-model', settings_model_test) -executable( +codexbar_settings = executable( 'codexbar-settings', 'settings/SettingsModel.vala', 'settings/SettingsApp.vala', @@ -135,6 +135,18 @@ if find_program('xvfb-run', required: false).found() and find_program('dbus-run- depends: render_probe, timeout: 30) endif +# Guards against valac emitting a use-after-free for nested owned/unowned conditionals. +python = find_program('python3') +test('generated-c-temporaries', python, + args: [join_paths(meson.current_source_dir(), 'probe/check-generated-c.py'), meson.current_build_dir()], + depends: [libcodexbar, codexbar_settings], timeout: 30) + +if find_program('xvfb-run', required: false).found() and find_program('dbus-run-session', required: false).found() + test('grok-panel', find_program('bash'), + args: [join_paths(meson.current_source_dir(), 'probe/run-grok-probe.sh'), meson.current_build_dir()], + depends: render_probe, timeout: 30) +endif + helper_process_test = executable('test-helper-process', 'HelperProcess.vala', 'test-helper-process.vala', dependencies: dependency('gio-2.0')) test('helper-process', helper_process_test, timeout: 10) diff --git a/mseries/codexbar-wingpanel/probe/check-generated-c.py b/mseries/codexbar-wingpanel/probe/check-generated-c.py new file mode 100644 index 000000000..a796d3848 --- /dev/null +++ b/mseries/codexbar-wingpanel/probe/check-generated-c.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Reject a valac code-generation shape that reads freed memory. + +A nested conditional that mixes an unowned value (a literal) with an owned one +(a call or concatenation), assigned to `var`, makes valac 0.56 emit: + + _tmpA_ = _tmpB_; + _g_free0 (_tmpB_); + ... g_strdup (_tmpA_) /* _tmpA_ now points at freed memory */ + +That printed garbage reset countdowns and quota captions in the live panel +(fixed 2026-10-19). Write such values with an explicit `string` and if/else. + +Usage: check-generated-c.py BUILD_DIR (scans every *.p/*.c that valac wrote) +""" + +import pathlib +import re +import sys + +ASSIGN = re.compile(r"^\s*(_tmp\d+_) = (_tmp\d+_);\s*$") + + +def offences(path: pathlib.Path) -> list[str]: + found = [] + previous = None + for number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1): + if line.startswith("#line ") or not line.strip(): + continue # debug builds interleave source-position directives between statements + if previous and re.match(r"^\s*_g_free0 \(" + re.escape(previous[1]) + r"\);\s*$", line): + found.append(f"{path.parent.name}/{path.name}:{previous[2]}: {previous[0]} borrows {previous[1]}, " + "which is freed next") + match = ASSIGN.match(line) + previous = (match.group(1), match.group(2), number) if match else None + return found + + +def main(arguments: list[str]) -> int: + if len(arguments) != 1: + print(__doc__.strip().splitlines()[-1], file=sys.stderr) + return 2 + sources = sorted(pathlib.Path(arguments[0]).glob("*.p/*.c")) + if not sources: + print(f"No valac-generated C under {arguments[0]}", file=sys.stderr) + return 1 + problems = [problem for source in sources for problem in offences(source)] + for problem in problems: + print(problem, file=sys.stderr) + print(f"Checked {len(sources)} generated C files: {len(problems)} borrowed-then-freed temporaries") + return 1 if problems else 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/mseries/codexbar-wingpanel/probe/ci-build-deps.sh b/mseries/codexbar-wingpanel/probe/ci-build-deps.sh new file mode 100755 index 000000000..66c5f031c --- /dev/null +++ b/mseries/codexbar-wingpanel/probe/ci-build-deps.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Provide granite-7 >= 7.7 and the wingpanel-9 indicator API for building/testing the CodexBar module off +# elementary OS (CI runners, containers). Builds only what pkg-config cannot already satisfy, into PREFIX. +# Wingpanel uses the same pinned commit as install-wingpanel-codexbar-left.sh; only its lib/ is built +# (no compositor or Gala dependencies). Prints KEY=VALUE environment lines on stdout (for $GITHUB_ENV). +# eval "$(bash ci-build-deps.sh /tmp/prefix | sed 's/^/export /')" +set -euo pipefail +prefix=$(realpath -m "${1:?usage: ci-build-deps.sh PREFIX}") +repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd) +commit=$(sed -n 's/^commit=//p' "$repo/mseries/install-wingpanel-codexbar-left.sh" | head -1) +[[ $commit =~ ^[0-9a-f]{40}$ ]] || { echo "No pinned Wingpanel commit found" >&2; exit 1; } +granite_tag=7.7.0 +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +multiarch=$(gcc -dumpmachine 2>/dev/null || echo "") +export PKG_CONFIG_PATH="$prefix/lib/$multiarch/pkgconfig:$prefix/lib/pkgconfig:$prefix/share/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" + +if ! pkg-config --atleast-version=7.7.0 granite-7; then + echo "Building Granite $granite_tag into $prefix" >&2 + git -c advice.detachedHead=false clone -q --depth 1 --branch "$granite_tag" https://github.com/elementary/granite "$work/granite" + meson setup "$work/granite/build" "$work/granite" --prefix="$prefix" -Ddemo=false -Dintrospection=false >&2 + ninja -C "$work/granite/build" install >&2 +fi + +if ! pkg-config --exists wingpanel-9; then + echo "Building the Wingpanel indicator API ($commit) into $prefix" >&2 + git init -q "$work/wingpanel" + git -C "$work/wingpanel" remote add origin https://github.com/elementary/wingpanel.git + git -C "$work/wingpanel" fetch -q --depth=1 origin "$commit" + git -C "$work/wingpanel" checkout -q --detach FETCH_HEAD + mkdir -p "$work/wplib" + cp -r "$work/wingpanel/lib" "$work/wplib/lib" + version=$(sed -n "s/^ *version: *'\([0-9.]*\)'.*/\1/p" "$work/wingpanel/meson.build" | head -1) + cat > "$work/wplib/meson.build" <=7.6.0') +subdir('lib') +EOF + meson setup "$work/wplib/build" "$work/wplib" --prefix="$prefix" >&2 + ninja -C "$work/wplib/build" install >&2 +fi + +echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH" +echo "LD_LIBRARY_PATH=$prefix/lib/$multiarch:$prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +echo "XDG_DATA_DIRS=$prefix/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" diff --git a/mseries/codexbar-wingpanel/probe/make-fixtures.py b/mseries/codexbar-wingpanel/probe/make-fixtures.py index 39e4e61aa..6054cbefb 100755 --- a/mseries/codexbar-wingpanel/probe/make-fixtures.py +++ b/mseries/codexbar-wingpanel/probe/make-fixtures.py @@ -47,6 +47,20 @@ def snap(entries, **extra): window("secondary", "7 day", 10080, 81, 8000, "On pace")], plan="max") go_err = entry("opencodego", [], error="Usage unavailable. Check this provider’s CodexBar login/configuration.", plan="", age_min=0) go_err["updatedAt"] = "" +# Grok as upstream v0.78.0 exports it: one weekly credits window, the reported SuperGrok tier, and +# informational detail sections (product shares of the same pool, usage-limit reset coupons). +grok_ok = entry("grok", [window("primary", "7 day", 10080, 76, 4000, "On pace")], plan="SuperGrok") +grok_ok["details"] = [ + {"title": "Usage breakdown", "rows": [{"label": "Grok Build", "value": "18%", "secondaryValue": ""}, + {"label": "Grok Chat", "value": "6%", "secondaryValue": ""}], "chart": None}, + {"title": "Limit Reset Credits", "rows": [{"label": "Available", "value": "1 available", "secondaryValue": ""}], + "chart": None}, + {"title": "Malformed", "rows": ["not an object", 7, {"value": None}, {"label": "", "value": ""}], "chart": None}, + "not a section", +] +grok_signed_out = entry("grok", [], error="Usage unavailable. Check this provider’s CodexBar login/configuration.", + plan="", age_min=0) +grok_signed_out["updatedAt"] = "" scenarios = { "healthy": snap([codex_ok, claude_ok]), @@ -60,6 +74,8 @@ def snap(entries, **extra): window("secondary", "7 day", 10080, 68, 8000), window("tertiary", "30 day", 43200, 91, 30000)], plan="go")]), "stale": snap([codex_ok, claude_ok], stale=True, error="Network unreachable"), "empty": snap([]), + "grok": snap([codex_ok, claude_ok, grok_ok]), + "grok-signin": snap([codex_ok, grok_signed_out]), } busy = snap([]); busy["busy"] = True scenarios["busy"] = busy @@ -72,7 +88,11 @@ def snap(entries, **extra): "periods": [{"label": "5 hours", "requests": 42, "tokens": 1850000, "costUSD": 0}, {"label": "7 days", "requests": 610, "tokens": 31200000, "costUSD": 0}], "note": "Local activity from Claude Code transcripts · not account quota (tokens include cache reads/writes)"}, - {"id": "opencode", "name": "OpenCode", "state": "ok", "detail": "Local activity", "periods": [{"label":"7 days","tokens":120813143,"requests":565,"costUSD":0}]}]} + {"id": "opencode", "name": "OpenCode", "state": "ok", "detail": "Local activity", "periods": [{"label":"7 days","tokens":120813143,"requests":565,"costUSD":0}]}, + {"id": "grok", "name": "Grok Build", "state": "ok", "detail": "Signed in · account credits come from CodexBar.", + "accountEmail": "me@grok.example", + "periods": [{"label": "7 days", "tokens": 762841, "requests": 1, "costUSD": 0}], + "note": "Local activity from Grok Build session usage · includes cached input and reasoning · not account quota"}]} write_fixture(here / "sessions.json", sessions) print("fixtures:", ", ".join(sorted(scenarios))) diff --git a/mseries/codexbar-wingpanel/probe/render-probe.vala b/mseries/codexbar-wingpanel/probe/render-probe.vala index 53f822c86..b76011758 100644 --- a/mseries/codexbar-wingpanel/probe/render-probe.vala +++ b/mseries/codexbar-wingpanel/probe/render-probe.vala @@ -159,6 +159,51 @@ int main (string[] args) { bar.measure (Gtk.Orientation.VERTICAL, -1, out minimum, out natural, null, null); assert (natural <= 32); } + if (Environment.get_variable ("CB_ASSERT_GROK") != null) { + // Bar: Grok follows Codex and Claude, token-first with its weekly credits beside the count. + var text = local_label_text (bar); + assert (text.contains ("763K tok") && text.contains ("7D 76%")); + var grok_tokens = find_label (bar, "763K tok"); + var claude_tokens = find_label (bar, "31.2M tok"); + Graphene.Rect grok_bounds, claude_bounds; + assert (grok_tokens.compute_bounds (bar, out grok_bounds)); + assert (claude_tokens.compute_bounds (bar, out claude_bounds)); + assert (grok_bounds.origin.x > claude_bounds.origin.x); + assert (find_label (bar, "7D 76%").get_parent () == grok_tokens.get_parent ()); + // Menu: tier, product shares and reset credits; malformed sections are skipped. + var menu = local_label_text (popover_body); + assert (menu.contains ("SuperGrok") && menu.contains ("me@grok.example")); + // Its only window is weekly and already sits beside the token count: no repeated lower row. + assert (text.index_of ("7D 76%") == text.last_index_of ("7D 76%")); + assert (menu.contains ("Usage breakdown · Grok Build 18% · Grok Chat 6%")); + assert (menu.contains ("Limit Reset Credits · Available 1 available")); + assert (!menu.contains ("Malformed") && !menu.contains ("not an object")); + assert (menu.contains ("763K tokens")); + } + if (Environment.get_variable ("CB_ASSERT_GROK_SIGNIN") != null) { + var menu = local_label_text (popover_body); + assert (menu.contains ("If you are signed out, run grok login")); + var sign_in = find_button (popover_body, "Sign in…"); + assert (sign_in != null); + assert (find_button (popover_body, "Open Settings…") == null); + sign_in.clicked (); + var log = GLib.Path.build_filename (Environment.get_home_dir (), "terminal.log"); + // Checked before the 1 s capture ends the probe; the fake launcher only appends one line. + Timeout.add (500, () => { + string contents; + try { + FileUtils.get_contents (log, out contents); + } catch (Error e) { + contents = ""; + } + var expected = GLib.Path.build_filename (Environment.get_home_dir (), ".grok/bin/grok") + " login"; + if (!contents.contains (expected)) { + stderr.printf ("grok login was not launched; terminal log: %s\n", contents); + render_failed = true; + } + return Source.REMOVE; + }); + } assert_quota_bar_colors (popover_body); var page = Environment.get_variable ("CB_PAGE"); var pages = find_pages (root); @@ -245,6 +290,16 @@ Gtk.ScrolledWindow? find_scroll (Gtk.Widget root) { return null; } +Gtk.Button? find_button (Gtk.Widget widget, string text) { + var button = widget as Gtk.Button; + if (button != null && button.label == text) return button; + for (var child = widget.get_first_child (); child != null; child = child.get_next_sibling ()) { + var found = find_button (child, text); + if (found != null) return found; + } + return null; +} + string local_label_text (Gtk.Widget widget) { var text = (widget is Gtk.Label) ? ((Gtk.Label) widget).get_text () : ""; for (var child = widget.get_first_child (); child != null; child = child.get_next_sibling ()) diff --git a/mseries/codexbar-wingpanel/probe/run-grok-probe.sh b/mseries/codexbar-wingpanel/probe/run-grok-probe.sh new file mode 100755 index 000000000..53986168b --- /dev/null +++ b/mseries/codexbar-wingpanel/probe/run-grok-probe.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Fake backends/profile only: the Grok card, bar segment, details and grok-login launcher in the real widget. +set -euo pipefail +here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +out=$(mktemp -d) +trap 'rm -rf "$out"' EXIT +CB_ASSERT_GROK=1 bash "$here/run-probe.sh" "$1" "$out" light grok +CB_ASSERT_GROK_SIGNIN=1 bash "$here/run-probe.sh" "$1" "$out" dark grok-signin diff --git a/mseries/codexbar-wingpanel/probe/run-probe.sh b/mseries/codexbar-wingpanel/probe/run-probe.sh index 949e0a49d..f61fbd2bd 100755 --- a/mseries/codexbar-wingpanel/probe/run-probe.sh +++ b/mseries/codexbar-wingpanel/probe/run-probe.sh @@ -17,11 +17,16 @@ printf '#!/usr/bin/env python3\nimport os,sys\nsys.stdout.write(open(os.environ[ printf '#!/usr/bin/env python3\nimport os,pathlib,json,sys\nif sys.argv[1:]==["accounts"]: print((pathlib.Path(os.environ["CB_FIXTURE"]).parent / ("accounts-opencode" + ("-hints" if pathlib.Path(os.environ["CB_FIXTURE"]).stem=="hints" else "-single" if pathlib.Path(os.environ["CB_FIXTURE"]).stem=="opencode-compact" else "") + ".json")).read_text())\nelse: print(json.dumps({"status":"available","accountEmail":"me@opencode.example","periods":[{"label":"7 days","requests":565,"tokens":120813143,"costUSD":14.72}]}))\n' > "$home/.local/lib/codexbar-cli/codexbar-opencode-local.py" printf '#!/usr/bin/env python3\nimport os,pathlib\nprint((pathlib.Path(os.environ["CB_FIXTURE"]).parent / ("accounts-codex" + ("-hints" if pathlib.Path(os.environ["CB_FIXTURE"]).stem=="hints" else "") + ".json")).read_text())\n' > "$home/.local/lib/codexbar-cli/codexbar-accounts.py" printf '#!/usr/bin/env python3\nimport os,pathlib\nprint((pathlib.Path(os.environ["CB_FIXTURE"]).parent / ("accounts-claude" + ("-hints" if pathlib.Path(os.environ["CB_FIXTURE"]).stem=="hints" else "") + ".json")).read_text())\n' > "$home/.local/lib/codexbar-cli/codexbar-claude.py" +# Grok Build is a terminal program: a fake CLI plus a terminal launcher that only records what it was asked to run. +mkdir -p "$home/.grok/bin" +printf '#!/bin/sh\nexit 0\n' > "$home/.grok/bin/grok" +printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> "$HOME/terminal.log"\n' > "$home/.local/bin/xdg-terminal-exec" +chmod +x "$home/.grok/bin/grok" "$home/.local/bin/xdg-terminal-exec" chmod +x "$home/.local/lib/codexbar-cli/codexbar-claude.py" "$home/.local/lib/codexbar-cli/codexbar-accounts.py" "$home/.local/bin/codexbar-linux" "$home/.local/lib/codexbar-cli/codexbar-sessions.py" "$home/.local/lib/codexbar-cli/codexbar-opencode-local.py" scenarios=("$@"); [ ${#scenarios[@]} -gt 0 ] || scenarios=(healthy connected hints live depleting exhausted provider-error stale empty busy) theme=io.elementary.stylesheet.blueberry; [ "$scheme" = dark ] && theme="$theme:dark" for name in "${scenarios[@]}"; do - env -i PATH="$PATH" HOME="$home" LANG=C.UTF-8 GTK_THEME="$theme" GSK_RENDERER=cairo GDK_BACKEND=x11 ${CB_DEBUG:+CB_DEBUG=1} ${CB_HIDE_PANEL:+CB_HIDE_PANEL=1} ${CB_SCROLL:+CB_SCROLL=$CB_SCROLL} ${CB_DIALOG:+CB_DIALOG=$CB_DIALOG} ${CB_PAGE:+CB_PAGE=$CB_PAGE} ${CB_STABILITY:+CB_STABILITY=1} ${CB_ASSERT_LOCAL:+CB_ASSERT_LOCAL=1} ${CB_ASSERT_HIDDEN_TOKENS:+CB_ASSERT_HIDDEN_TOKENS=1} ${CB_ASSERT_DOTS:+CB_ASSERT_DOTS=1} \ + env -i PATH="$home/.local/bin:$PATH" HOME="$home" LANG=C.UTF-8 GTK_THEME="$theme" GSK_RENDERER=cairo GDK_BACKEND=x11 ${CB_DEBUG:+CB_DEBUG=1} ${CB_HIDE_PANEL:+CB_HIDE_PANEL=1} ${CB_SCROLL:+CB_SCROLL=$CB_SCROLL} ${CB_DIALOG:+CB_DIALOG=$CB_DIALOG} ${CB_PAGE:+CB_PAGE=$CB_PAGE} ${CB_STABILITY:+CB_STABILITY=1} ${CB_ASSERT_LOCAL:+CB_ASSERT_LOCAL=1} ${CB_ASSERT_HIDDEN_TOKENS:+CB_ASSERT_HIDDEN_TOKENS=1} ${CB_ASSERT_DOTS:+CB_ASSERT_DOTS=1} ${CB_ASSERT_GROK:+CB_ASSERT_GROK=1} ${CB_ASSERT_GROK_SIGNIN:+CB_ASSERT_GROK_SIGNIN=1} \ CB_FIXTURE="$here/fixtures/$name.json" CB_SESSIONS="$here/fixtures/sessions.json" \ xvfb-run -a -s "-screen 0 800x900x24" "$build/render-probe" "$out/$name${CB_PAGE:+-$CB_PAGE}-$scheme.png" "${CB_DELAY:-3000}" echo "rendered $name ($scheme)" diff --git a/mseries/codexbar-wingpanel/settings/SettingsApp.vala b/mseries/codexbar-wingpanel/settings/SettingsApp.vala index f947f9d82..785ff553d 100644 --- a/mseries/codexbar-wingpanel/settings/SettingsApp.vala +++ b/mseries/codexbar-wingpanel/settings/SettingsApp.vala @@ -44,7 +44,7 @@ namespace MSeries { var dialog = new Gtk.AboutDialog () { transient_for = this, modal = true, program_name = "CodexBar", logo_icon_name = "mseries-codexbar-openai-symbolic", - comments = "Seven-day token counts cover coding activity recorded on this computer across accounts and providers. They include processed input, output and cached tokens. They do not represent remaining plan quota, activity on other devices, or ordinary ChatGPT and claude.ai chats.", + comments = "Seven-day token counts cover coding activity recorded on this computer across accounts and providers. They include processed input, output and cached tokens. They do not represent remaining plan quota, activity on other devices, or ordinary ChatGPT, claude.ai and grok.com chats.", website = "https://github.com/techmore/ElementaryOS_MSeries", website_label = "elementary OS port" }; @@ -92,13 +92,19 @@ namespace MSeries { Process.spawn_command_line_sync ("%s config providers --format json".printf (Shell.quote (cli_path)), out out_text); var parser = new Json.Parser (); parser.load_from_data (out_text, -1); - var array = parser.get_root ().get_array (); - for (uint i = 0; i < array.get_length (); i++) { - var item = array.get_object_element (i); - var id = item.get_string_member ("provider"); - names.insert (id, item.get_string_member ("displayName")); + var root = parser.get_root (); + var array = root != null && root.get_node_type () == Json.NodeType.ARRAY ? root.get_array () : null; + for (uint i = 0; array != null && i < array.get_length (); i++) { + // External CLI output: skip malformed entries instead of tripping GLib criticals. + var node = array.get_element (i); + if (node.get_node_type () != Json.NodeType.OBJECT) continue; + var item = node.get_object (); + var id = item.get_string_member_with_default ("provider", ""); + if (!SettingsModel.valid_provider_id (id) || id in picker_ids) continue; + var display_name = item.get_string_member_with_default ("displayName", id); + names.insert (id, display_name); picker_ids += id; - picker_model.append (item.get_string_member ("displayName")); + picker_model.append (display_name); } } catch (Error e) { // the picker stays empty; the provider list still works @@ -440,7 +446,10 @@ namespace MSeries { private Gtk.Widget provider_row (string id, int index, int count) { var line = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 8); line.add_css_class ("cb-row"); - var mark = "mseries-codexbar-%s-symbolic".printf (id == "codex" ? "openai" : id == "opencodego" ? "opencode" : id); + string mark_name = id; + if (id == "codex") mark_name = "openai"; + else if (id == "opencodego") mark_name = "opencode"; + var mark = "mseries-codexbar-%s-symbolic".printf (mark_name); var icon = new Gtk.Image.from_icon_name (Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_icon (mark) ? mark : "application-x-executable-symbolic") { pixel_size = 16 }; line.append (icon); diff --git a/mseries/codexbar_tokens.py b/mseries/codexbar_tokens.py index 5a8b8433b..548b46a7b 100644 --- a/mseries/codexbar_tokens.py +++ b/mseries/codexbar_tokens.py @@ -34,6 +34,14 @@ def codex_tokens(usage: dict[str, Any]) -> int: return count(usage.get("input_tokens")) + count(usage.get("output_tokens")) +def grok_tokens(usage: dict[str, Any]) -> int: + # Grok Build: cachedReadTokens is a subset of inputTokens and reasoningTokens + # of outputTokens, so totalTokens (= input + output) already counts both. + if usage.get("totalTokens") is not None: + return count(usage["totalTokens"]) + return count(usage.get("inputTokens")) + count(usage.get("outputTokens")) + + def opencode_tokens(usage: Any) -> int: if not isinstance(usage, dict): return 0 diff --git a/mseries/deploy-codexbar.sh b/mseries/deploy-codexbar.sh index 8aa76f2dd..33e5f6fc2 100755 --- a/mseries/deploy-codexbar.sh +++ b/mseries/deploy-codexbar.sh @@ -10,13 +10,9 @@ case ${1:-} in *) echo 'Usage: deploy-codexbar.sh [--restart|--no-restart]' >&2; exit 2 ;; esac repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -lib="$HOME/.local/lib/codexbar-cli" -mkdir -p "$lib" -for helper in codexbar_claude_handoff.py codexbar_claude_history.py codexbar_cache.py codexbar-wingpanel-watch.py codexbar_history.py codexbar_tokens.py codexbar-opencode-local.py codexbar-sessions.py codexbar-accounts.py codexbar-claude.py codexbar-opencode-login.py; do - install -m 0755 "$repo/mseries/$helper" "$lib/$helper" -done -install -m 0755 "$repo/mseries/codexbar-opencode-wrapper.py" "$HOME/.local/bin/codexbar.new" -mv -f "$HOME/.local/bin/codexbar.new" "$HOME/.local/bin/codexbar" +source "$repo/mseries/lib-codexbar.sh" +codexbar_install_helpers "$repo" +codexbar_install_wrapper "$repo" build=$(mktemp -d "${TMPDIR:-/tmp}/codexbar-deploy.XXXXXX") trap 'rm -rf "$build"' EXIT @@ -29,7 +25,7 @@ sudo install -m 0755 "$build/libcodexbar.so" "$indicators_dir/libcodexbar.so" # Icon-theme invalidation affects the whole live panel. Do not rewrite unchanged marks # or refresh its icon cache on routine code/documentation deployments. icons_changed=false -for mark in openai claude opencode; do +for mark in "${CODEXBAR_MARKS[@]}"; do source_icon="$repo/mseries/codexbar-wingpanel/icons/$mark.svg" installed_icon="/usr/share/icons/hicolor/scalable/apps/mseries-codexbar-$mark-symbolic.svg" if ! cmp -s "$source_icon" "$installed_icon"; then diff --git a/mseries/enhance.sh b/mseries/enhance.sh index 86d13eb26..05f146f70 100755 --- a/mseries/enhance.sh +++ b/mseries/enhance.sh @@ -336,7 +336,7 @@ mod_payload() { install-network-throughput.sh install-quick-settings-brightness.sh install-bluetooth-control-center.sh \ install-sound-indicator-fix.sh install-keyboard-indicator-fix.sh install-calendar-agenda.sh \ install-files-portal-fix.sh install-gala-osk-fix.sh install-netusage-policy.sh install-battery-eta.sh \ - install-codexbar.sh install-brave-widevine.sh codexbar-opencode-local.py codexbar-accounts.py codexbar-claude.py codexbar_claude_history.py codexbar_claude_handoff.py codexbar_cache.py codexbar-wingpanel-watch.py codexbar_history.py codexbar_tokens.py codexbar-sessions.py codexbar-opencode-login.py deploy-codexbar.sh \ + install-codexbar.sh install-brave-widevine.sh codexbar-opencode-local.py codexbar-accounts.py codexbar-claude.py codexbar_claude_history.py codexbar_claude_handoff.py codexbar_cache.py codexbar-wingpanel-watch.py codexbar_history.py codexbar_tokens.py codexbar-sessions.py codexbar-opencode-login.py deploy-codexbar.sh lib-codexbar.sh \ codexbar-opencode-wrapper.py user-enhance.sh install-dark-mode.sh \ dark-mode/mseries-dark-mode dark-mode/mseries-dark-mode.service dark-mode/mseries-dark-mode.timer \ netspeed/netusage/install.sh netspeed/netusage/netusage.py \ diff --git a/mseries/install-codexbar-wingpanel.sh b/mseries/install-codexbar-wingpanel.sh index 0c287cd3a..e73551a91 100755 --- a/mseries/install-codexbar-wingpanel.sh +++ b/mseries/install-codexbar-wingpanel.sh @@ -9,7 +9,11 @@ src="$repo/mseries/codexbar-wingpanel" if [[ ${1:-} == --uninstall ]]; then indicators_dir=$(pkg-config --variable=indicatorsdir wingpanel-9) sudo rm -f "$indicators_dir/libcodexbar.so" - sudo rm -f /usr/share/icons/hicolor/scalable/apps/mseries-codexbar-openai-symbolic.svg + source "$repo/mseries/lib-codexbar.sh" + for mark in "${CODEXBAR_MARKS[@]}"; do + sudo rm -f "/usr/share/icons/hicolor/scalable/apps/mseries-codexbar-$mark-symbolic.svg" + done + rm -f "$HOME/.local/bin/codexbar-settings" if command -v gtk-update-icon-cache >/dev/null; then sudo gtk-update-icon-cache -f /usr/share/icons/hicolor fi @@ -22,18 +26,9 @@ sudo apt-get install -y meson ninja-build valac pkg-config \ libwingpanel-9-dev libgranite-7-dev libjson-glib-dev \ python3-gi gir1.2-granite-7.0 gir1.2-webkit-6.0 -mkdir -p "$HOME/.local/lib/codexbar-cli" -install -m 0755 "$repo/mseries/codexbar-opencode-local.py" \ - "$HOME/.local/lib/codexbar-cli/codexbar-opencode-local.py" -install -m 0644 "$repo/mseries/codexbar_tokens.py" "$HOME/.local/lib/codexbar-cli/codexbar_tokens.py" -install -m 0644 "$repo/mseries/codexbar_history.py" "$HOME/.local/lib/codexbar-cli/codexbar_history.py" -install -m 0755 "$repo/mseries/codexbar-wingpanel-watch.py" "$HOME/.local/lib/codexbar-cli/codexbar-wingpanel-watch.py" -install -m 0644 "$repo/mseries/codexbar_cache.py" "$HOME/.local/lib/codexbar-cli/codexbar_cache.py" -install -m 0644 "$repo/mseries/codexbar_claude_history.py" "$HOME/.local/lib/codexbar-cli/codexbar_claude_history.py" -install -m 0755 "$repo/mseries/codexbar-sessions.py" \ - "$HOME/.local/lib/codexbar-cli/codexbar-sessions.py" -install -m 0755 "$repo/mseries/codexbar-accounts.py" "$HOME/.local/lib/codexbar-cli/codexbar-accounts.py" -install -m 0755 "$repo/mseries/codexbar-claude.py" "$HOME/.local/lib/codexbar-cli/codexbar-claude.py" +source "$repo/mseries/lib-codexbar.sh" +# The panel runs these helpers directly (e.g. Claude switching imports the history and handoff modules). +codexbar_install_helpers "$repo" for tool in meson ninja valac pkg-config; do command -v "$tool" >/dev/null || { diff --git a/mseries/install-codexbar.sh b/mseries/install-codexbar.sh index 025e4bda3..507810123 100755 --- a/mseries/install-codexbar.sh +++ b/mseries/install-codexbar.sh @@ -4,7 +4,7 @@ set -euo pipefail [[ $(uname -m) == aarch64 ]] || { echo 'This installer requires ARM64' >&2; exit 1; } repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/mseries/lib-apt.sh" -version=v0.77.0 +version=v0.78.0 if [[ ${1:-} != --skip-system-deps ]]; then mseries_apt_refresh sudo apt-get install -y \ @@ -23,29 +23,22 @@ for component in CLI Desktop; do curl -fLsS "$base/$archive.sha256" -o "$archive.sha256" sha256sum -c "$archive.sha256" done -mkdir -p "$HOME/.local/lib/codexbar-cli" "$HOME/.local/bin" -install -m 0755 "$repo/mseries/codexbar-opencode-local.py" "$HOME/.local/lib/codexbar-cli/codexbar-opencode-local.py" -install -m 0644 "$repo/mseries/codexbar_tokens.py" "$HOME/.local/lib/codexbar-cli/codexbar_tokens.py" -install -m 0644 "$repo/mseries/codexbar_history.py" "$HOME/.local/lib/codexbar-cli/codexbar_history.py" -install -m 0755 "$repo/mseries/codexbar-wingpanel-watch.py" "$HOME/.local/lib/codexbar-cli/codexbar-wingpanel-watch.py" -install -m 0644 "$repo/mseries/codexbar_cache.py" "$HOME/.local/lib/codexbar-cli/codexbar_cache.py" -install -m 0644 "$repo/mseries/codexbar_claude_history.py" "$HOME/.local/lib/codexbar-cli/codexbar_claude_history.py" -install -m 0644 "$repo/mseries/codexbar_claude_handoff.py" "$HOME/.local/lib/codexbar-cli/codexbar_claude_handoff.py" -install -m 0755 "$repo/mseries/codexbar-sessions.py" "$HOME/.local/lib/codexbar-cli/codexbar-sessions.py" -install -m 0755 "$repo/mseries/codexbar-accounts.py" "$HOME/.local/lib/codexbar-cli/codexbar-accounts.py" -install -m 0755 "$repo/mseries/codexbar-claude.py" "$HOME/.local/lib/codexbar-cli/codexbar-claude.py" -install -m 0755 "$repo/mseries/codexbar-opencode-login.py" "$HOME/.local/lib/codexbar-cli/codexbar-opencode-login.py" +source "$repo/mseries/lib-codexbar.sh" +codexbar_install_helpers "$repo" tar -xzf "CodexBarCLI-${version}-linux-aarch64.tar.gz" -C "$HOME/.local/lib/codexbar-cli" mv -f "$HOME/.local/lib/codexbar-cli/codexbar" "$HOME/.local/lib/codexbar-cli/codexbar-upstream" -# Replace atomically so the panel never finds ~/.local/bin/codexbar missing mid-install. -install -m 0755 "$repo/mseries/codexbar-opencode-wrapper.py" "$HOME/.local/bin/codexbar.new" -mv -f "$HOME/.local/bin/codexbar.new" "$HOME/.local/bin/codexbar" +codexbar_install_wrapper "$repo" tar -xzf "CodexBarDesktop-${version}-linux-aarch64.tar.gz" cd "CodexBarDesktop-${version}-linux-aarch64" python3 Integrations/Linux/install.py --cli "$HOME/.local/bin/codexbar" config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/codexbar" config_file="$config_dir/linux.json" -python3 - "$config_file" <<'PY' +# Grok Build joins the panel only when its CLI is installed; upstream reads ~/.grok/auth.json and `grok agent stdio`. +grok_installed=0 +if command -v grok >/dev/null || [[ -x "${GROK_HOME:-$HOME/.grok}/bin/grok" || -x "$HOME/.local/bin/grok" ]]; then + grok_installed=1 +fi +python3 - "$config_file" "$grok_installed" <<'PY' import json import os import pathlib @@ -53,6 +46,7 @@ import sys import tempfile path = pathlib.Path(sys.argv[1]) +grok_installed = sys.argv[2] == "1" path.parent.mkdir(parents=True, exist_ok=True) config = {} if path.exists(): @@ -74,10 +68,12 @@ if provider in {"codex", "both", "custom"}: order = ["codex", *[item for item in order if item != "codex"]] elif provider == "both": order = ["codex", "claude", *[item for item in order if item not in {"codex", "claude"}]] - config["providerOrder"] = list(dict.fromkeys([*order, "opencodego"])) + added = ["opencodego", *(["grok"] if grok_installed else [])] + config["providerOrder"] = list(dict.fromkeys([*order, *added])) config["provider"] = "custom" else: - print(f"Preserved CodexBar provider selection '{provider}'; add OpenCode Go in Settings → Providers.") + print(f"Preserved CodexBar provider selection '{provider}'; add OpenCode Go" + f"{' and Grok' if grok_installed else ''} in Settings → Providers.") if config.get("provider") == "custom": destination = path.resolve() diff --git a/mseries/lib-codexbar.sh b/mseries/lib-codexbar.sh new file mode 100644 index 000000000..fb18dd1bc --- /dev/null +++ b/mseries/lib-codexbar.sh @@ -0,0 +1,42 @@ +# shellcheck shell=bash +# Shared CodexBar port payload. Sourced by install-codexbar.sh, install-codexbar-wingpanel.sh and +# deploy-codexbar.sh so the helper set cannot drift between them. enhance.sh copies the same files +# into the image; test-codexbar-deploy.py checks that list and every helper's local imports against this one. + +# Python helpers installed into ~/.local/lib/codexbar-cli (modules use _, executables use -). +CODEXBAR_HELPERS=( + codexbar_cache.py + codexbar_tokens.py + codexbar_history.py + codexbar_claude_history.py + codexbar_claude_handoff.py + codexbar-sessions.py + codexbar-accounts.py + codexbar-claude.py + codexbar-opencode-local.py + codexbar-opencode-login.py + codexbar-wingpanel-watch.py +) + +# Provider marks installed as mseries-codexbar--symbolic.svg (read by the sourcing scripts). +# shellcheck disable=SC2034 +CODEXBAR_MARKS=(openai claude opencode grok) + +# codexbar_install_helpers REPO: install every helper; executables 0755, imported modules 0644. +codexbar_install_helpers() { + local repo=$1 lib="$HOME/.local/lib/codexbar-cli" helper mode + mkdir -p "$lib" + for helper in "${CODEXBAR_HELPERS[@]}"; do + mode=0755 + [[ $helper == *_* && $helper != *-* ]] && mode=0644 + install -m "$mode" "$repo/mseries/$helper" "$lib/$helper" + done +} + +# codexbar_install_wrapper REPO: replace ~/.local/bin/codexbar atomically so the panel never finds it missing. +codexbar_install_wrapper() { + local repo=$1 + mkdir -p "$HOME/.local/bin" + install -m 0755 "$repo/mseries/codexbar-opencode-wrapper.py" "$HOME/.local/bin/codexbar.new" + mv -f "$HOME/.local/bin/codexbar.new" "$HOME/.local/bin/codexbar" +} diff --git a/mseries/test-codexbar-deploy.py b/mseries/test-codexbar-deploy.py index 7825103e9..1eefdcda4 100644 --- a/mseries/test-codexbar-deploy.py +++ b/mseries/test-codexbar-deploy.py @@ -2,11 +2,21 @@ """Run the real deploy script against fake tools; process-control commands never reach the live session.""" import os import pathlib +import re import subprocess import tempfile import unittest SCRIPT = pathlib.Path(__file__).with_name('deploy-codexbar.sh') +LIB = pathlib.Path(__file__).with_name('lib-codexbar.sh') + + +def shared_list(name): + """An array from lib-codexbar.sh, read by bash itself rather than re-parsed.""" + output = subprocess.run(['bash', '-c', f'source "$1"; printf "%s\\n" "${{{name}[@]}}"', 'bash', str(LIB)], + check=True, capture_output=True, text=True).stdout + return output.split() + class DeployTest(unittest.TestCase): def test_reload_is_explicit_and_never_quits_the_daemon(self): @@ -15,13 +25,13 @@ def test_reload_is_explicit_and_never_quits_the_daemon(self): repo = root / 'repo' / 'mseries' repo.mkdir(parents=True) (repo / SCRIPT.name).write_bytes(SCRIPT.read_bytes()) - for name in ('codexbar_claude_handoff.py', 'codexbar_claude_history.py', 'codexbar_cache.py', 'codexbar_tokens.py', 'codexbar_history.py', 'codexbar-wingpanel-watch.py', 'codexbar-opencode-local.py', - 'codexbar-sessions.py', 'codexbar-accounts.py', 'codexbar-claude.py', - 'codexbar-opencode-login.py', 'codexbar-opencode-wrapper.py'): + (repo / LIB.name).write_bytes(LIB.read_bytes()) + for name in [*shared_list('CODEXBAR_HELPERS'), 'codexbar-opencode-wrapper.py']: (repo / name).touch() icons = repo / 'codexbar-wingpanel/icons' icons.mkdir(parents=True) - for name in ('openai', 'claude', 'opencode'): + marks = shared_list('CODEXBAR_MARKS') + for name in marks: (icons / (name + '.svg')).touch() profile = root / 'profile' (profile / '.local/bin').mkdir(parents=True) @@ -62,12 +72,43 @@ def test_reload_is_explicit_and_never_quits_the_daemon(self): self.assertEqual(result.returncode,0,result.stderr) calls=log.read_text().splitlines() self.assertEqual(sum('sudo gtk-update-icon-cache' in line for line in calls),1) - self.assertEqual(sum(line.startswith('sudo install -m 0644') for line in calls),3) + self.assertEqual(sum(line.startswith('sudo install -m 0644') for line in calls), len(marks)) + installed = profile / '.local/lib/codexbar-cli' + self.assertEqual(sorted(path.name for path in installed.iterdir()), sorted(shared_list('CODEXBAR_HELPERS'))) + self.assertEqual(oct((installed / 'codexbar_tokens.py').stat().st_mode & 0o777), '0o644') + self.assertEqual(oct((installed / 'codexbar-sessions.py').stat().st_mode & 0o777), '0o755') self.assertFalse(any(line.startswith('pkill ') for line in calls)) log.write_text('') result = subprocess.run(['bash', str(repo / SCRIPT.name), '--bad-option'], env=env, capture_output=True) self.assertEqual(result.returncode, 2) self.assertEqual(log.read_text(), '') +class PayloadTest(unittest.TestCase): + """One helper list for install, deploy and the image; it must cover every local import.""" + + def test_helpers_cover_local_imports(self): + helpers = shared_list('CODEXBAR_HELPERS') + here = SCRIPT.parent + modules = {name[:-3] for name in helpers if '-' not in name} + for name in [*helpers, 'codexbar-opencode-wrapper.py']: + source = (here / name).read_text(encoding='utf-8') + for module in re.findall(r'^\s*(?:import|from)\s+(codexbar\w*)', source, re.MULTILINE): + self.assertIn(module, modules, f'{name} imports {module}, which lib-codexbar.sh does not install') + + def test_installers_and_image_use_the_shared_payload(self): + here = SCRIPT.parent + for script in ('install-codexbar.sh', 'install-codexbar-wingpanel.sh', 'deploy-codexbar.sh'): + self.assertIn('codexbar_install_helpers "$repo"', (here / script).read_text(), script) + payload = (here / 'enhance.sh').read_text() + for name in [*shared_list('CODEXBAR_HELPERS'), 'lib-codexbar.sh', 'codexbar-opencode-wrapper.py']: + self.assertRegex(payload, r'[ \t]' + re.escape(name) + r'[ \t\\]', f'enhance.sh image payload lacks {name}') + + def test_every_mark_is_built_and_present(self): + meson = (SCRIPT.parent / 'codexbar-wingpanel/meson.build').read_text() + for mark in shared_list('CODEXBAR_MARKS'): + self.assertTrue((SCRIPT.parent / f'codexbar-wingpanel/icons/{mark}.svg').is_file(), mark) + self.assertIn(f"'{mark}'" if mark != 'openai' else "icons/openai.svg", meson, mark) + + if __name__ == '__main__': unittest.main() diff --git a/mseries/test-codexbar-sessions.py b/mseries/test-codexbar-sessions.py index fdf4446d0..637a5421d 100644 --- a/mseries/test-codexbar-sessions.py +++ b/mseries/test-codexbar-sessions.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Tests for the read-only ChatGPT/Claude/OpenCode session health helper.""" +"""Tests for the read-only ChatGPT/Claude/OpenCode/Grok session health helper.""" import importlib.util import json @@ -32,7 +32,7 @@ class SessionTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.home = pathlib.Path(self.temp.name) - patcher = mock.patch.dict(os.environ, {"PATH": "", "XDG_DATA_HOME": ""}) + patcher = mock.patch.dict(os.environ, {"PATH": "", "XDG_DATA_HOME": "", "GROK_HOME": "", "XAI_API_KEY": ""}) patcher.start() self.addCleanup(patcher.stop) @@ -41,7 +41,7 @@ def tearDown(self): def test_nothing_installed_reports_off_not_crash(self): states = {s["id"]: s["state"] for s in MODULE.collect(self.home, NOW)["sessions"]} - self.assertEqual(states, {"codex": "off", "claude": "off", "opencode": "off"}) + self.assertEqual(states, {"codex": "off", "claude": "off", "opencode": "off", "grok": "off"}) def test_codex_signed_in_by_auth_file_without_leaking_contents(self): (self.home / ".codex").mkdir() @@ -142,6 +142,125 @@ def test_opencode_counts_all_providers_read_only(self): self.assertEqual(set(opencode["providers"]), {"opencode", "opencode-go"}) self.assertEqual(db.read_bytes(), before) + # ---- Grok Build ------------------------------------------------------------------- + + def grok_session_dir(self, name, workspace="%2Fhome%2Fme"): + directory = self.home / ".grok/sessions" / workspace / name + directory.mkdir(parents=True) + return directory + + def grok_turn(self, number, offset, total, calls=1, **extra): + turn = {"turnNumber": number, "endedAt": stamp(offset), "inputTokens": total - 10, + "outputTokens": 10, "cachedReadTokens": total // 2, "reasoningTokens": 4, + "totalTokens": total, "modelCalls": calls, "costUsdTicks": 6988060000} + turn.update(extra) + return turn + + def write_grok_usage(self, directory, turns): + path = directory / "usage.json" + path.write_text(json.dumps({"sessionId": directory.name, "updatedAt": stamp(0), + "session": {"turnCount": len(turns)}, + "turns": turns})) + os.utime(path, (NOW / 1000, NOW / 1000)) + return path + + def grok(self): + return next(s for s in MODULE.collect(self.home, NOW)["sessions"] if s["id"] == "grok") + + def test_grok_windows_count_turns_by_end_time_without_dollars(self): + self.write_grok_usage(self.grok_session_dir("a"), [ + self.grok_turn(1, 1000, 762841, 14), + self.grok_turn(2, 6 * 3600 * 1000, 500), + self.grok_turn(3, 8 * 86400 * 1000, 900), + self.grok_turn(4, 40 * 86400 * 1000, 7), + ]) + grok = self.grok() + self.assertEqual(grok["periods"][0], {"label": "5 hours", "requests": 1, "tokens": 762841, "costUSD": 0.0}) + self.assertEqual(grok["periods"][1]["tokens"], 762841 + 500) + self.assertEqual(grok["periods"][2]["tokens"], 762841 + 500 + 900) + self.assertEqual(grok["periods"][2]["requests"], 3) + + def test_grok_total_falls_back_to_input_plus_output(self): + tokens = MODULE.codexbar_tokens + self.assertEqual(tokens.grok_tokens({"inputTokens": 90, "outputTokens": 10, "cachedReadTokens": 60, + "reasoningTokens": 5}), 100) + self.assertEqual(tokens.grok_tokens({"totalTokens": 100, "inputTokens": 1}), 100) + with self.assertRaises(ValueError): + tokens.grok_tokens({"totalTokens": -1}) + + def test_grok_resumed_or_forked_history_counts_once(self): + shared = [self.grok_turn(1, 2000, 300, 3), self.grok_turn(2, 1000, 200, 2)] + self.write_grok_usage(self.grok_session_dir("original"), shared) + self.write_grok_usage(self.grok_session_dir("fork", "%2Ftmp%2Fwt"), shared + [self.grok_turn(3, 500, 50)]) + self.assertEqual(self.grok()["periods"][1], {"label": "7 days", "requests": 3, "tokens": 550, "costUSD": 0.0}) + + def test_grok_reads_only_session_usage_files(self): + session = self.grok_session_dir("a") + self.write_grok_usage(session, [self.grok_turn(1, 1000, 10)]) + nested = session / "terminal" / "child" + nested.mkdir(parents=True) + self.write_grok_usage(nested, [self.grok_turn(1, 900, 999)]) + (self.home / ".grok/sessions/%2Fhome%2Fme/prompt_history.jsonl").write_text("{}\n") + outside = self.home / "elsewhere" + outside.mkdir() + self.write_grok_usage(outside, [self.grok_turn(1, 800, 5000)]) + (self.home / ".grok/sessions/%2Fhome%2Fme/linked").symlink_to(outside, target_is_directory=True) + self.assertEqual(self.grok()["periods"][1]["tokens"], 10) + + def test_grok_malformed_and_oversized_files_are_skipped(self): + bad = self.grok_session_dir("bad") / "usage.json" + bad.write_text("{not json") + self.write_grok_usage(self.grok_session_dir("odd"), [ + "turn", {"endedAt": "yesterday", "totalTokens": 5}, self.grok_turn(1, 1000, True), + self.grok_turn(2, 1000, 40)]) + huge = self.grok_session_dir("huge") / "usage.json" + huge.write_text(" " * (MODULE.MAX_GROK_USAGE_BYTES + 1)) + os.utime(huge, (NOW / 1000, NOW / 1000)) + self.assertEqual(self.grok()["periods"][1]["tokens"], 40) + + def test_grok_unchanged_files_are_not_reparsed(self): + path = self.write_grok_usage(self.grok_session_dir("a"), [self.grok_turn(1, 1000, 10)]) + first = self.grok()["periods"] + with mock.patch.object(MODULE, "parse_grok_usage", side_effect=AssertionError("reparsed")): + self.assertEqual(self.grok()["periods"], first) + self.write_grok_usage(path.parent, [self.grok_turn(1, 1000, 10), self.grok_turn(2, 500, 5)]) + os.utime(path, (NOW / 1000 + 1, NOW / 1000 + 1)) + self.assertEqual(self.grok()["periods"][1]["tokens"], 15) + + def test_grok_sign_in_state_never_exposes_credentials(self): + grok_dir = self.home / ".grok" + (grok_dir / "bin").mkdir(parents=True) + binary = grok_dir / "bin/grok" + binary.write_text("#!/bin/sh\n") + binary.chmod(0o755) + self.assertEqual(self.grok()["state"], "warn") + with mock.patch.dict(os.environ, {"XAI_API_KEY": "xai-SECRETKEY"}): + api = self.grok() + self.assertEqual(api["state"], "ok") + self.assertNotIn("SECRETKEY", json.dumps(api)) + (grok_dir / "auth.json").write_text(json.dumps({ + "https://accounts.x.ai/sign-in": {"key": "LEGACY-SECRET", "email": "legacy@example.com"}, + "https://auth.x.ai::client": {"key": "BEARER-SECRET", "refresh_token": "REFRESH-SECRET", + "email": "me@example.com", "first_name": "Private"}})) + signed_in = self.grok() + self.assertEqual(signed_in["state"], "ok") + # The identity row shows the SuperGrok account's email, as upstream does; nothing else leaves the file. + self.assertEqual(signed_in["accountEmail"], "me@example.com") + for secret in ("BEARER-SECRET", "REFRESH-SECRET", "LEGACY-SECRET", "legacy@example.com", "Private"): + self.assertNotIn(secret, json.dumps(MODULE.collect(self.home, NOW))) + (grok_dir / "auth.json").write_text(json.dumps({"https://auth.x.ai::c": {"email": ["not", "text"]}})) + self.assertNotIn("accountEmail", self.grok()) + + def test_grok_home_override(self): + custom = self.home / "custom-grok" + session = custom / "sessions/w/s" + session.mkdir(parents=True) + self.write_grok_usage(session, [self.grok_turn(1, 1000, 77)]) + with mock.patch.dict(os.environ, {"GROK_HOME": str(custom)}): + self.assertEqual(self.grok()["periods"][1]["tokens"], 77) + with mock.patch.dict(os.environ, {"GROK_HOME": "relative/path"}): + self.assertEqual(self.grok()["periods"], []) + if __name__ == "__main__": unittest.main() From 23cd4ea3015ddebc807d0b7cbe0c8f9392cff7d0 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 15:12:21 -0400 Subject: [PATCH 204/798] CodexBar probe: drop a real grok from PATH so the sign-in fixture is deterministic On any host with the Grok CLI installed (its installer puts ~/.grok/bin on PATH), grok_binary() resolves the real binary instead of the fixture stub at $HOME/.grok/bin/grok. The panel then launches the real path, the fake xdg-terminal-exec records that path, and the CB_ASSERT_GROK_SIGNIN assertion - which expects the fixture path - fails. That is exactly the machine the deploy runs on, so grok-panel failed here while CI (no grok on PATH) passed. The probe now drops every PATH entry that provides a grok executable, so the panel resolves the fixture stub on every host and the assertion stays meaningful: it still verifies the panel finds the binary and hands it to the terminal launcher. Verified on this machine: both grok scenarios render (grok-signin failed before); CI is unaffected because it has no grok on PATH. --- mseries/codexbar-wingpanel/probe/run-probe.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mseries/codexbar-wingpanel/probe/run-probe.sh b/mseries/codexbar-wingpanel/probe/run-probe.sh index f61fbd2bd..60b98c4e3 100755 --- a/mseries/codexbar-wingpanel/probe/run-probe.sh +++ b/mseries/codexbar-wingpanel/probe/run-probe.sh @@ -22,11 +22,21 @@ mkdir -p "$home/.grok/bin" printf '#!/bin/sh\nexit 0\n' > "$home/.grok/bin/grok" printf '#!/bin/sh\nprintf "%%s\\n" "$*" >> "$HOME/terminal.log"\n' > "$home/.local/bin/xdg-terminal-exec" chmod +x "$home/.grok/bin/grok" "$home/.local/bin/xdg-terminal-exec" +# A real `grok` on PATH (the Grok CLI installer puts ~/.grok/bin there) wins over +# the fixture stub inside grok_binary(), so the panel would launch the real +# binary and the sign-in assertion would not match. Drop every PATH entry that +# provides a grok executable, so the probe is deterministic on any host. +probe_path="" +while IFS= read -r -d ':' dir; do + [[ -n $dir ]] || continue + [[ -x $dir/grok ]] && continue + probe_path="${probe_path:+$probe_path:}$dir" +done < <(printf '%s:' "$PATH") chmod +x "$home/.local/lib/codexbar-cli/codexbar-claude.py" "$home/.local/lib/codexbar-cli/codexbar-accounts.py" "$home/.local/bin/codexbar-linux" "$home/.local/lib/codexbar-cli/codexbar-sessions.py" "$home/.local/lib/codexbar-cli/codexbar-opencode-local.py" scenarios=("$@"); [ ${#scenarios[@]} -gt 0 ] || scenarios=(healthy connected hints live depleting exhausted provider-error stale empty busy) theme=io.elementary.stylesheet.blueberry; [ "$scheme" = dark ] && theme="$theme:dark" for name in "${scenarios[@]}"; do - env -i PATH="$home/.local/bin:$PATH" HOME="$home" LANG=C.UTF-8 GTK_THEME="$theme" GSK_RENDERER=cairo GDK_BACKEND=x11 ${CB_DEBUG:+CB_DEBUG=1} ${CB_HIDE_PANEL:+CB_HIDE_PANEL=1} ${CB_SCROLL:+CB_SCROLL=$CB_SCROLL} ${CB_DIALOG:+CB_DIALOG=$CB_DIALOG} ${CB_PAGE:+CB_PAGE=$CB_PAGE} ${CB_STABILITY:+CB_STABILITY=1} ${CB_ASSERT_LOCAL:+CB_ASSERT_LOCAL=1} ${CB_ASSERT_HIDDEN_TOKENS:+CB_ASSERT_HIDDEN_TOKENS=1} ${CB_ASSERT_DOTS:+CB_ASSERT_DOTS=1} ${CB_ASSERT_GROK:+CB_ASSERT_GROK=1} ${CB_ASSERT_GROK_SIGNIN:+CB_ASSERT_GROK_SIGNIN=1} \ + env -i PATH="$home/.local/bin:$probe_path" HOME="$home" LANG=C.UTF-8 GTK_THEME="$theme" GSK_RENDERER=cairo GDK_BACKEND=x11 ${CB_DEBUG:+CB_DEBUG=1} ${CB_HIDE_PANEL:+CB_HIDE_PANEL=1} ${CB_SCROLL:+CB_SCROLL=$CB_SCROLL} ${CB_DIALOG:+CB_DIALOG=$CB_DIALOG} ${CB_PAGE:+CB_PAGE=$CB_PAGE} ${CB_STABILITY:+CB_STABILITY=1} ${CB_ASSERT_LOCAL:+CB_ASSERT_LOCAL=1} ${CB_ASSERT_HIDDEN_TOKENS:+CB_ASSERT_HIDDEN_TOKENS=1} ${CB_ASSERT_DOTS:+CB_ASSERT_DOTS=1} ${CB_ASSERT_GROK:+CB_ASSERT_GROK=1} ${CB_ASSERT_GROK_SIGNIN:+CB_ASSERT_GROK_SIGNIN=1} \ CB_FIXTURE="$here/fixtures/$name.json" CB_SESSIONS="$here/fixtures/sessions.json" \ xvfb-run -a -s "-screen 0 800x900x24" "$build/render-probe" "$out/$name${CB_PAGE:+-$CB_PAGE}-$scheme.png" "${CB_DELAY:-3000}" echo "rendered $name ($scheme)" From e719e1d2e30c9a4adefcdd4f5e11a560c9f7b417 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 15:15:12 -0400 Subject: [PATCH 205/798] handoff: record the v0.78.0 deploy and the libxml2 soname break --- mseries/OS9-HANDOFF.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index e46db0aa3..24de9583f 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,40 @@ # OS 9 implementation / Claude Code handoff +## CodexBar v0.78.0 deployed; CLI blocked by libxml2 soname — 2026-10-19 + +PR #271 (production hardening: the valac nested-ternary use-after-free behind the +quota captions and reset countdowns, Grok Build provider, upstream v0.78.0) +merged into `dev` at `b772dc8`. The full deploy then ran on this machine. + +**Deployed and live:** `install-codexbar.sh --skip-system-deps` (v0.78.0), +CodexBar restarted, `deploy-codexbar.sh --restart` passed **17/17** tests and +installed `libcodexbar.so`; Wingpanel reloaded and CodexBar is running. The +use-after-free fix and the Grok provider are in the live panel. + +**Probe fix landed first (`23cd4ea` on `dev`):** `grok-panel` failed on this +machine while CI passed, because the Grok CLI installer puts `~/.grok/bin` on +PATH, so `grok_binary()` resolved the real binary instead of the fixture stub and +the sign-in assertion could not match. `probe/run-probe.sh` now drops PATH +entries that provide a `grok` executable, making the probe deterministic on any +host. Verified: both grok scenarios render here; CI (no grok on PATH) is +unchanged. + +**Port check FAILED — one root cause, not several:** the v0.78.0 upstream CLI +(`CodexBarCLI`) links `libxml2.so.2`, and this base ships libxml2 2.14, whose +soname is `libxml2.so.16` (`/usr/lib/aarch64-linux-gnu/libxml2.so.16`). The CLI +cannot load, so every CLI-dependent check fails: "upstream CLI not runnable", +`--all-accounts`/`--account` lost, "no longer lists provider grok", the pin +comparison, and codex usage. The desktop app does not link xml2, which is why +the panel itself is fine, and v0.77.0's CLI had no such dependency. v0.78.0 +(published 2026-10-19T18:29Z) is the latest upstream release; there is no newer +build to move to. + +Not done, needs an owner decision: (a) report the soname break upstream, (b) +vendor a libxml2.so.2 (or symlink to .so.16 behind `LD_LIBRARY_PATH` — the ABI +question is unverified), or (c) hold the CLI at v0.77.0 until upstream rebuilds. +Also still owner-gated: the interactive `grok login` that gives the Grok card +live credits — and that needs the CLI running first. + ## CodexBar production hardening + Grok Build; upstream v0.78.0 — 2026-10-19 (Claude Code) **Pin:** `install-codexbar.sh` now pins upstream **v0.78.0** (was v0.77.0). Both Linux aarch64 From 61cf7dfe321a2950efc24abf10be0f6f62c4796f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 15:21:16 -0400 Subject: [PATCH 206/798] CodexBar: resolve the v0.78.0 CLI's libxml2.so.2 through a compat shim v0.78.0's upstream CLI links libxml2.so.2, but this base ships libxml2 2.14 (soname libxml2.so.16), so the CLI could not load and every CLI-dependent port check failed. codexbar_install_cli_compat (called from codexbar_install_helpers and after the install rename) resolves the old soname from ~/.local/lib/codexbar-cli/compat and routes codexbar-upstream through a wrapper that points the loader there. It is a no-op when the system provides libxml2.so.2, and the wrapper says when to delete it. The wrapper replaces the codexbar-upstream symlink, so the path is removed first: a redirect through that symlink truncates the real binary (learned the hard way while testing - the binary was restored from the verified v0.78.0 tarball). The real binary's name is remembered in codexbar-upstream.real, which keeps the shim idempotent across reinstalls. Verified: port check passes end to end - upstream CLI 0.78.0, both account flags, codex usage, CODEX_HOME honored, grok provider listed, grok credits window reported, and all helper tests. The libxml2 ABI question is answered empirically: the CLI runs correctly against libxml2.so.16 for these paths. --- mseries/install-codexbar.sh | 2 ++ mseries/lib-codexbar.sh | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/mseries/install-codexbar.sh b/mseries/install-codexbar.sh index 507810123..491311ad8 100755 --- a/mseries/install-codexbar.sh +++ b/mseries/install-codexbar.sh @@ -27,6 +27,8 @@ source "$repo/mseries/lib-codexbar.sh" codexbar_install_helpers "$repo" tar -xzf "CodexBarCLI-${version}-linux-aarch64.tar.gz" -C "$HOME/.local/lib/codexbar-cli" mv -f "$HOME/.local/lib/codexbar-cli/codexbar" "$HOME/.local/lib/codexbar-cli/codexbar-upstream" +# v0.78.0's CLI links libxml2.so.2; wrap it when this base only ships the newer soname. +codexbar_install_cli_compat codexbar_install_wrapper "$repo" tar -xzf "CodexBarDesktop-${version}-linux-aarch64.tar.gz" cd "CodexBarDesktop-${version}-linux-aarch64" diff --git a/mseries/lib-codexbar.sh b/mseries/lib-codexbar.sh index fb18dd1bc..5df560641 100644 --- a/mseries/lib-codexbar.sh +++ b/mseries/lib-codexbar.sh @@ -31,6 +31,52 @@ codexbar_install_helpers() { [[ $helper == *_* && $helper != *-* ]] && mode=0644 install -m "$mode" "$repo/mseries/$helper" "$lib/$helper" done + codexbar_install_cli_compat +} + +# codexbar_install_cli_compat: upstream v0.78.0's CLI links libxml2.so.2, but +# libxml2 >= 2.14 renamed the soname (libxml2.so.16 on this base), so the CLI +# cannot load. Resolve the old soname from a compat directory and route +# codexbar-upstream through a wrapper that points the loader there. The wrapper +# is a no-op when the system provides libxml2.so.2 (an older base, or an upstream +# rebuild); delete this shim when that day comes. Idempotent: the real binary's +# name is remembered in codexbar-upstream.real. +codexbar_install_cli_compat() { + local lib="$HOME/.local/lib/codexbar-cli" soname real base + [[ -e $lib/codexbar-upstream || -L $lib/codexbar-upstream ]] || return 0 + if ldconfig -p | awk '{print $1}' | grep -qx libxml2.so.2; then + return 0 + fi + soname=$(ldconfig -p | awk '$1 ~ /^libxml2\.so\./ {print $1}' | sort -V | tail -1) + [[ -n $soname ]] || { echo 'codexbar: no libxml2 found for the CLI compat shim' >&2; return 1; } + if [[ -f $lib/codexbar-upstream.real ]]; then + base=$(cat "$lib/codexbar-upstream.real") + else + if [[ -L $lib/codexbar-upstream ]]; then + real=$(readlink -f "$lib/codexbar-upstream") + else + mv -f "$lib/codexbar-upstream" "$lib/codexbar-upstream.bin" + real="$lib/codexbar-upstream.bin" + fi + base=$(basename "$real") + printf '%s\n' "$base" > "$lib/codexbar-upstream.real" + fi + mkdir -p "$lib/compat" + ln -sfn "$(ldconfig -p | awk -v s="$soname" '$1 == s {print $NF; exit}')" "$lib/compat/libxml2.so.2" + # Remove the path first: it is a symlink to the real binary, and a redirect + # would follow it and truncate the binary instead of replacing the link. + rm -f "$lib/codexbar-upstream" + cat > "$lib/codexbar-upstream" <= 2.14 or the base regains libxml2.so.2. +dir=\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd) +LD_LIBRARY_PATH="\$dir/compat\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}" +export LD_LIBRARY_PATH +exec "\$dir/$base" "\$@" +EOF + chmod 0755 "$lib/codexbar-upstream" } # codexbar_install_wrapper REPO: replace ~/.local/bin/codexbar atomically so the panel never finds it missing. From 7de0ebabf9cdc95e93b21b64da3f74e8d81e60e6 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 15:21:26 -0400 Subject: [PATCH 207/798] handoff: the libxml2 shim landed and the port check passes --- mseries/OS9-HANDOFF.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 24de9583f..3d725c6a8 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -35,6 +35,32 @@ question is unverified), or (c) hold the CLI at v0.77.0 until upstream rebuilds. Also still owner-gated: the interactive `grok login` that gives the Grok card live credits — and that needs the CLI running first. +### CodexBar CLI libxml2 shim — 2026-10-19 (resolved the same day) + +Option (b) was implemented and verified. `codexbar_install_cli_compat` in +`mseries/lib-codexbar.sh` (called from `codexbar_install_helpers` and after the +install-time rename) resolves the old soname from +`~/.local/lib/codexbar-cli/compat/libxml2.so.2` and routes +`codexbar-upstream` through a wrapper that points the loader there. The ABI +question is answered empirically: the CLI runs correctly against libxml2 2.14 +for the paths the port check exercises. The shim is a no-op when the system +provides `libxml2.so.2`, and the wrapper states when to delete it. The real +binary's name is remembered in `codexbar-upstream.real` so reinstalls stay +idempotent. + +The wrapper replaces the `codexbar-upstream` symlink, so the path is removed +before the redirect — writing through that symlink truncates the real binary, +which happened once during testing and was repaired by re-extracting the +checksum-verified v0.78.0 tarball. No other component was affected (the panel +module and desktop app do not use this binary). + +**Port check now passes end to end:** upstream CLI 0.78.0, `--all-accounts` and +`--account` present, codex usage runs, `CODEX_HOME` honored, grok provider +listed, **grok usage reports a credits window**, grok in the panel's provider +order, and all helper tests. The remaining upstream item is reporting the +soname break to steipete/CodexBar (owner's call; v0.78.0 is the latest +release). + ## CodexBar production hardening + Grok Build; upstream v0.78.0 — 2026-10-19 (Claude Code) **Pin:** `install-codexbar.sh` now pins upstream **v0.78.0** (was v0.77.0). Both Linux aarch64 From 686b9fca602ec6eee794e54db3abe5174f19a632 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 15:56:32 -0400 Subject: [PATCH 208/798] rust/pantheon-compositor: apply rustfmt and clear Clippy warnings Behavior-preserving cleanup for the Rust branch: - gala_compat: drop the redundant `Changed` postfix from `GalaSignal` variants; the emitted D-Bus signal names are unchanged. - pantheon_shell, state, workspace: collapse nested `if` blocks with let-chains and replace a let-else/return with `?`. - shell_compat: group the eight `serve_shell_compat` arguments into a `ShellCompatContext` to satisfy `clippy::too_many_arguments`. - wallpaper, main, native: name complex return types with aliases and drop redundant `f64` casts. - rustfmt the workspace; granite-smoke, mseries-power and pantheon-drm-probe pick up formatting-only changes. Verified with toolchain 1.93.1: `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets` (0 warnings), `cargo clippy -p pantheon-compositor --all-features` (0 warnings), `cargo test --workspace` (all pass), and `cargo build --release -p pantheon-compositor --features native-drm --bins`. --- rust/granite-smoke/src/main.rs | 1 - rust/mseries-power/src/main.rs | 23 +++++++++++---- .../src/bin/pantheon-drm-probe.rs | 12 ++++---- rust/pantheon-compositor/src/gala_compat.rs | 20 ++++++------- rust/pantheon-compositor/src/main.rs | 6 ++-- rust/pantheon-compositor/src/native.rs | 7 +++-- .../pantheon-compositor/src/pantheon_shell.rs | 16 +++++------ rust/pantheon-compositor/src/shell_compat.rs | 28 +++++++++++++------ rust/pantheon-compositor/src/state.rs | 14 ++++------ rust/pantheon-compositor/src/wallpaper.rs | 10 +++---- rust/pantheon-compositor/src/workspace.rs | 8 +++--- 11 files changed, 84 insertions(+), 61 deletions(-) diff --git a/rust/granite-smoke/src/main.rs b/rust/granite-smoke/src/main.rs index b3d23cbd0..8e5dc0670 100644 --- a/rust/granite-smoke/src/main.rs +++ b/rust/granite-smoke/src/main.rs @@ -2,7 +2,6 @@ //! Builds a few Granite widgets in an unmapped window, so it needs a display //! (run it under Xvfb or a nested compositor, never on the owner's screen). - use granite::prelude::PlaceholderExt; use gtk::prelude::*; diff --git a/rust/mseries-power/src/main.rs b/rust/mseries-power/src/main.rs index 140c2131c..63c30c9ea 100644 --- a/rust/mseries-power/src/main.rs +++ b/rust/mseries-power/src/main.rs @@ -33,7 +33,9 @@ fn parse_conf(text: &str) -> Config { if line.is_empty() || line.starts_with('#') { continue; } - let Some((k, v)) = line.split_once('=') else { continue }; + let Some((k, v)) = line.split_once('=') else { + continue; + }; let v = v.trim().trim_matches('"').trim_matches('\''); match k.trim() { "CHARGE_LIMIT" => { @@ -60,7 +62,9 @@ fn parse_conf(text: &str) -> Config { } fn load_conf(path: &Path) -> Config { - fs::read_to_string(path).map(|t| parse_conf(&t)).unwrap_or_default() + fs::read_to_string(path) + .map(|t| parse_conf(&t)) + .unwrap_or_default() } fn write_sys(path: &Path, value: &str) { @@ -134,7 +138,10 @@ fn apply_source(paths: &Paths, cfg: &Config) { write_sys(&entry.path().join("scaling_max_freq"), &target.to_string()); } } - let _ = Command::new("powerprofilesctl").arg("set").arg(profile).output(); + let _ = Command::new("powerprofilesctl") + .arg("set") + .arg(profile) + .output(); } fn status(paths: &Paths) { @@ -293,8 +300,14 @@ mod tests { fn limit_ordering_matches_bash() { // limit >= 100: end first, then start (avoids start >= end trip). assert_eq!( - limit_writes(100, 75).iter().map(|(f, _)| *f).collect::>(), - vec!["charge_control_end_threshold", "charge_control_start_threshold"] + limit_writes(100, 75) + .iter() + .map(|(f, _)| *f) + .collect::>(), + vec![ + "charge_control_end_threshold", + "charge_control_start_threshold" + ] ); // normal: start first, then end. let w = limit_writes(80, 75); diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs index 71c6edd38..0b41091ed 100644 --- a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -203,12 +203,7 @@ fn inspect_kms_planes(device: &DrmDeviceFile) { (Fourcc::Abgr2101010, "AB30"), ] .into_iter() - .filter_map(|(format, name)| { - plane - .formats() - .contains(&(format as u32)) - .then_some(name) - }) + .filter_map(|(format, name)| plane.formats().contains(&(format as u32)).then_some(name)) .collect::>(); println!( " {handle:?}: crtc={:?} framebuffer={:?}, tested formats advertised={:?} ({} total formats)", @@ -228,7 +223,10 @@ fn probe_cross_gpu_import(render_path: &PathBuf, kms_target: &KmsTarget) { println!("Cross-GPU GBM import preflight:"); println!(" render node: {}", render_path.display()); println!(" KMS device: {}", kms_target.path.display()); - println!(" preferred mode size: {}x{}", kms_target.width, kms_target.height); + println!( + " preferred mode size: {}x{}", + kms_target.width, kms_target.height + ); let render_file = match OpenOptions::new().read(true).write(true).open(render_path) { Ok(file) => file, diff --git a/rust/pantheon-compositor/src/gala_compat.rs b/rust/pantheon-compositor/src/gala_compat.rs index c691b041a..e7d7b8352 100644 --- a/rust/pantheon-compositor/src/gala_compat.rs +++ b/rust/pantheon-compositor/src/gala_compat.rs @@ -43,9 +43,9 @@ pub(crate) struct GalaActionRequest { } enum GalaSignal { - RunningApplicationsChanged, - WindowsChanged, - ActiveWorkspaceChanged, + RunningApplications, + Windows, + ActiveWorkspace, } #[derive(Clone)] @@ -98,18 +98,18 @@ impl GalaCompatHandle { }; if apps_changed { - let _ = self.signals.send(GalaSignal::RunningApplicationsChanged); + let _ = self.signals.send(GalaSignal::RunningApplications); } if windows_changed || count_changed { - let _ = self.signals.send(GalaSignal::WindowsChanged); + let _ = self.signals.send(GalaSignal::Windows); } if active_changed { - let _ = self.signals.send(GalaSignal::ActiveWorkspaceChanged); + let _ = self.signals.send(GalaSignal::ActiveWorkspace); } } pub fn request_windows_changed(&self) { - let _ = self.signals.send(GalaSignal::WindowsChanged); + let _ = self.signals.send(GalaSignal::Windows); } } @@ -289,21 +289,21 @@ fn serve_gala_compat( Err(mpsc::RecvTimeoutError::Disconnected) => return, }; let result = match signal { - GalaSignal::RunningApplicationsChanged => connection.emit_signal( + GalaSignal::RunningApplications => connection.emit_signal( None::<&str>, DESKTOP_PATH, "org.pantheon.gala.DesktopIntegration", "RunningApplicationsChanged", &(), ), - GalaSignal::WindowsChanged => connection.emit_signal( + GalaSignal::Windows => connection.emit_signal( None::<&str>, DESKTOP_PATH, "org.pantheon.gala.DesktopIntegration", "WindowsChanged", &(), ), - GalaSignal::ActiveWorkspaceChanged => connection.emit_signal( + GalaSignal::ActiveWorkspace => connection.emit_signal( None::<&str>, DESKTOP_PATH, "org.pantheon.gala.DesktopIntegration", diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 356982a37..9bebcad45 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -116,8 +116,10 @@ enum BackendChoice { Native, } -fn parse_arguments() --> Result<(BackendChoice, bool, Option<(String, Vec)>), Box> { +type RunCommand = (String, Vec); +type ParsedArguments = (BackendChoice, bool, Option); + +fn parse_arguments() -> Result> { let mut args = std::env::args().skip(1); let mut backend = BackendChoice::Nested; let mut notify_systemd = false; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 36bff708b..e95db034a 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -64,6 +64,7 @@ pub type NativeExporter = GbmFramebufferExporter; pub type NativeOutputManager = DrmOutputManager; pub type NativeDrmOutput = DrmOutput; type NativeMultiRenderer<'a> = MultiRenderer<'a, 'a, GbmRendererBackend, GbmRendererBackend>; +type HardwareNode = (DrmNode, GbmDevice); const OUTPUT_FORMATS: &[smithay::backend::allocator::Fourcc] = &[ smithay::backend::allocator::Fourcc::Argb2101010, @@ -977,7 +978,7 @@ impl NativeKmsDevice { let location = existing_locations.get(&name).copied().unwrap_or_else(|| { let location = (next_x, 0).into(); if let Some(mode) = output.output.current_mode() { - let scale = output.output.current_scale().fractional_scale() as f64; + let scale = output.output.current_scale().fractional_scale(); next_x += (mode.size.w as f64 / scale).round() as i32; } location @@ -988,7 +989,7 @@ impl NativeKmsDevice { state.space.map_output(&output.output, location); state.track_output_for_lock(&output.output); if let Some(mode) = output.output.current_mode() { - let scale = output.output.current_scale().fractional_scale() as f64; + let scale = output.output.current_scale().fractional_scale(); let scale = if scale.is_finite() && scale > 0.0 { scale } else { @@ -1589,7 +1590,7 @@ impl NativeRenderer { session: &LibSeatSession, gpus: &mut NativeGpuManager, path: &Path, - ) -> Result)>, Box> { + ) -> Result, Box> { let node = DrmNode::from_path(path)?; let mut session = session.clone(); let fd = session.open( diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 2adb0ea58..372864487 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -135,14 +135,14 @@ impl state.update_panel(surface, |panel| panel.anchor = Some(anchor.into())); } protocol::io_elementary_pantheon_panel_v1::Request::Focus => { - if state.find_window_for_surface(surface).is_some() { - if let Some(keyboard) = state.seat.get_keyboard() { - keyboard.set_focus( - state, - Some(surface.clone()), - SERIAL_COUNTER.next_serial(), - ); - } + if state.find_window_for_surface(surface).is_some() + && let Some(keyboard) = state.seat.get_keyboard() + { + keyboard.set_focus( + state, + Some(surface.clone()), + SERIAL_COUNTER.next_serial(), + ); } } protocol::io_elementary_pantheon_panel_v1::Request::SetSize { width, height } => { diff --git a/rust/pantheon-compositor/src/shell_compat.rs b/rust/pantheon-compositor/src/shell_compat.rs index 7678800e0..25d5fe935 100644 --- a/rust/pantheon-compositor/src/shell_compat.rs +++ b/rust/pantheon-compositor/src/shell_compat.rs @@ -93,16 +93,16 @@ impl ShellCompatHandle { thread::Builder::new() .name("pantheon-shell-dbus".into()) .spawn(move || { - serve_shell_compat( - service_bindings, - service_active, - service_active_since, - service_osd, + serve_shell_compat(ShellCompatContext { + bindings: service_bindings, + active: service_active, + active_since: service_active_since, + osd: service_osd, redraw_sender, osd_expiry, wayland_display, receiver, - ) + }) }) .expect("failed to start shell compatibility D-Bus thread"); Self { @@ -305,7 +305,7 @@ impl ScreenSaverCompat { async fn active_changed(emitter: &SignalEmitter<'_>, new_value: bool) -> zbus::Result<()>; } -fn serve_shell_compat( +struct ShellCompatContext { bindings: Arc>>, active: Arc, active_since: Arc>>, @@ -314,7 +314,19 @@ fn serve_shell_compat( osd_expiry: mpsc::Sender, wayland_display: OsString, receiver: mpsc::Receiver, -) { +} + +fn serve_shell_compat(context: ShellCompatContext) { + let ShellCompatContext { + bindings, + active, + active_since, + osd, + redraw_sender, + osd_expiry, + wayland_display, + receiver, + } = context; let connection = match Connection::session() { Ok(connection) => connection, Err(error) => { diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index a0bb09950..46f497022 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -1576,9 +1576,7 @@ impl PantheonCompositor { continue; } let output_name = output.name(); - let Some(lock_surface) = self.lock_surfaces.get(&output_name) else { - return None; - }; + let lock_surface = self.lock_surfaces.get(&output_name)?; let local = pos - geometry.loc.to_f64(); return smithay::desktop::utils::under_from_surface_tree( lock_surface.wl_surface(), @@ -1623,11 +1621,11 @@ impl PantheonCompositor { if !self.session_locked || self.lock_confirmation.is_none() { return; } - if self.lock_confirmation_outputs.is_empty() { - if let Some(confirmation) = self.lock_confirmation.take() { - confirmation.lock(); - self.shell_compat.set_locked(true); - } + if self.lock_confirmation_outputs.is_empty() + && let Some(confirmation) = self.lock_confirmation.take() + { + confirmation.lock(); + self.shell_compat.set_locked(true); } } } diff --git a/rust/pantheon-compositor/src/wallpaper.rs b/rust/pantheon-compositor/src/wallpaper.rs index bf8a2f0d8..3f39ea25e 100644 --- a/rust/pantheon-compositor/src/wallpaper.rs +++ b/rust/pantheon-compositor/src/wallpaper.rs @@ -58,10 +58,10 @@ pub(crate) fn load_wallpaper(selection: &WallpaperSelection) -> Option Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box> -{ +type WallpaperLoadResult = + Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box>; + +fn try_load_wallpaper(selection: &WallpaperSelection) -> WallpaperLoadResult { let file = gio::File::for_uri(&selection.uri); let path = file .path() @@ -82,7 +82,7 @@ fn try_load_wallpaper( // Fourcc::Argb8888 uses BGRA byte order on little-endian machines. let mut pixels = decoded.into_raw(); - for pixel in pixels.chunks_exact_mut(4) { + for pixel in pixels.as_chunks_mut::<4>().0 { pixel.swap(0, 2); } let buffer = MemoryRenderBuffer::from_slice( diff --git a/rust/pantheon-compositor/src/workspace.rs b/rust/pantheon-compositor/src/workspace.rs index 6001d6d4f..af5c0fba0 100644 --- a/rust/pantheon-compositor/src/workspace.rs +++ b/rust/pantheon-compositor/src/workspace.rs @@ -37,10 +37,10 @@ impl WorkspaceManager { pub fn add_window(&mut self, window: Window, location: Point) { let id = window.id(); - if let Some(previous) = self.assignments.remove(&id) { - if let Some(windows) = self.workspaces.get_mut(previous) { - windows.retain(|existing| *existing != id); - } + if let Some(previous) = self.assignments.remove(&id) + && let Some(windows) = self.workspaces.get_mut(previous) + { + windows.retain(|existing| *existing != id); } self.windows.insert(id, window); self.assignments.insert(id, self.active); From 306290416d2b7076b83ac5fb0c3b2e1038feccdb Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 16:47:27 -0400 Subject: [PATCH 209/798] Rust compositor: finish Gala signal rename, rustfmt, touchpad swipes - Complete the GalaSignal variant rename in gala_compat.rs: the enum had been renamed but WindowsChanged/ActiveWorkspaceChanged uses remained, so the branch did not compile. D-Bus signal names are unchanged. - Apply rustfmt across rust/ and clear remaining clippy warnings (checked_div for the keyboard repeat interval, as_chunks_mut in the wallpaper BGRA swap). - Add native libinput touchpad swipe recognition for workspace navigation: non-cancelled three-or-more-finger horizontal swipes past a 90-logical-pixel threshold select the adjacent workspace and stop at the first/last one; gestures are dropped while the session is locked or a pointer grab is active. Pinch and vertical swipes stay unrecognized until an overview action exists. - Update the compositor README, EXPERIENCE-REQUIREMENTS, ARCHITECTURE-AND-STATUS, ROADMAP and OS9-HANDOFF with the concrete changes and open qualification. Verified: cargo check --workspace --all-targets; cargo test --workspace (7 tests pass, including the three new gesture tests); cargo clippy clean for pantheon-compositor; cargo build -p pantheon-compositor --bins with native-drm; nested Xvfb run accepted the shell v2 probe over the Wayland socket. Physical trackpad qualification remains open; no package was built or installed and Gala remains the active session. --- mseries/OS9-HANDOFF.md | 43 ++++++++ mseries/ROADMAP.md | 14 +++ .../ARCHITECTURE-AND-STATUS.md | 1 + .../EXPERIENCE-REQUIREMENTS.md | 2 +- rust/granite-smoke/src/main.rs | 1 - rust/mseries-power/src/main.rs | 23 +++- rust/pantheon-compositor/README.md | 9 ++ .../src/bin/pantheon-drm-probe.rs | 12 +-- rust/pantheon-compositor/src/gala_compat.rs | 20 ++-- rust/pantheon-compositor/src/gestures.rs | 102 ++++++++++++++++++ rust/pantheon-compositor/src/input.rs | 45 ++++++-- rust/pantheon-compositor/src/main.rs | 7 +- rust/pantheon-compositor/src/native.rs | 7 +- .../pantheon-compositor/src/pantheon_shell.rs | 16 +-- rust/pantheon-compositor/src/shell_compat.rs | 28 +++-- rust/pantheon-compositor/src/state.rs | 38 +++++-- rust/pantheon-compositor/src/wallpaper.rs | 10 +- rust/pantheon-compositor/src/workspace.rs | 8 +- rust/target/.rustc_info.json | 2 +- 19 files changed, 318 insertions(+), 70 deletions(-) create mode 100644 rust/pantheon-compositor/src/gestures.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 7a9ce46d0..fbcb160c3 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,48 @@ # OS 9 implementation / Claude Code handoff +## Rust compositor: repaired build, clippy cleanups, touchpad workspace swipes — 2026-10-19 + +Branch `codex/rust-gala-stack-integration`, worktree +`/home/sdolbec/ElementaryOS_MSeries-wt/rust-gala-stack-integration`. + +- The worktree carried an unfinished `GalaSignal` variant rename: the enum had + been renamed (`RunningApplications`, `Windows`, `ActiveWorkspace`) in only + part of `rust/pantheon-compositor/src/gala_compat.rs`, so `cargo check` failed + with five `E0599` errors. The remaining `WindowsChanged`/`ActiveWorkspaceChanged` + uses now match the renamed enum; D-Bus signal names are unchanged. +- Finished the pending clippy cleanups: `checked_div` for the keyboard repeat + interval and `as_chunks_mut::<4>()` in the wallpaper BGRA swap. `cargo clippy + -p pantheon-compositor --all-targets` now reports no warnings. +- Added `rust/pantheon-compositor/src/gestures.rs`: a small, backend-independent + swipe state machine for native libinput gesture events. Non-cancelled + three-or-more-finger swipes that pass a 90-logical-pixel threshold with + horizontal dominance select the adjacent workspace (content-following + direction: swipe left selects the next workspace); switching stops at the + first and last workspace instead of wrapping or creating one. Gestures are + dropped while the session is locked or a pointer grab is active. Pinch and + vertical swipes are intentionally unrecognized until an overview action + exists. +- Wired the state machine into `process_input_event` and + `PantheonCompositor::apply_swipe_outcome`/`switch_workspace_relative`; + workspace switching reuses the existing `switch_workspace` path, so panel + visibility, Gala-compat signals, fractional-scale preferences and redraws + behave like other workspace changes. + +Verification (this host, no session taken over): + +- `CARGO_TARGET_DIR=/tmp/opencode/pantheon-target cargo check --workspace --all-targets` + passes. +- `cargo check -p pantheon-compositor --bins --features native-drm` passes. +- `cargo test -p pantheon-compositor` runs the three new gesture unit tests; + all pass. `cargo clippy -p pantheon-compositor --all-targets` is clean. + +Not done / next task: physical trackpad qualification on the M1 Pro native +session (the Winit nested backend does not emit gesture events), vertical +swipe and pinch routing once action 1/overview exists, and user-configurable +gesture bindings. No package was built, nothing was installed, and Gala +remains the active session. The uncommitted refactor was completed in place; +no commit or push was made from this batch. + ## Rust compositor on the existing stack: installer, metrics reducer, public performance page — 2026-10-19 Batch on branch `codex/rust-gala-stack-integration` (from diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index e40cf87a1..63d3a8f35 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -184,6 +184,20 @@ publishes them with the honest status: idle compositor CPU is about 11.1 % of one core in both sessions under unmatched workloads, whole-system power is unmeasured, and no performance win is established. +The prototype now recognizes native libinput touchpad swipes for workspace +navigation: non-cancelled three-or-more-finger swipes that pass a 90-logical- +pixel threshold with horizontal dominance select the adjacent workspace in the +content-following direction and stop at the first/last workspace. Gestures are +ignored while the session is locked or a move, resize or popup grab is active; +pinch and vertical swipes remain unrecognized until an overview action exists. +The state machine lives in `rust/pantheon-compositor/src/gestures.rs` with +three unit tests; `cargo test -p pantheon-compositor` passes, both default and +`native-drm` builds check, and clippy is clean. The installed nested Winit +backend never emits gesture events, so physical M-series trackpad +qualification, user-configurable bindings and vertical/overview routing remain +open. Gala is still the active session and no package was installed from this +batch. + ## Signal Granite palette completeness — 2026-10-18 PR #222's Granite path now maps Signal brand/logo blue `#3b45fd` as well as the diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index facfe54eb..0368fa77f 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -159,6 +159,7 @@ session was replaced. | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value. In native package `0.1.0+git8023955`, Wingpanel's user service became active; the Dock service was intentionally skipped by the existing `~/.config/mseries/dock/user-disabled` marker | Nested visual and hide/reveal smoke passed; native Wingpanel process is active, but native appearance has not been visually confirmed. Dock remains opted out by the user's saved marker | | Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | +| Trackpad swipe gestures | Native libinput swipe events are recognized for workspace navigation: non-cancelled three-or-more-finger horizontal swipes past a logical-pixel threshold select the adjacent workspace, and gestures are dropped while locked or during a pointer grab | Swipe state machine has unit tests; both build configurations compile. Physical M-series trackpad qualification, vertical/overview gestures and user configuration remain open | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Package `0.1.0+git02447cb` was installed and selected through LightDM on 2026-10-19. GNOME entered `pantheon-rust-wayland`; compositor initialized the M1 Pro DRM/EGL path at 3024×1964, loaded the configured wallpaper and reported ready. Wingpanel and IBus services started; Dock was skipped because the saved user opt-out marker remains present | Native login and internal scanout passed again. The five-minute rollback timer completed and restored the existing Gala autologin. User reports the desktop felt responsive; an objective first-usable timestamp is still unavailable | | Startup timing | The boot report remains 20.838 seconds to `default.target` and 10.465 seconds to `graphical.target`. In the 12:10 EDT Rust login, the compositor service started at 12:10:00 and systemd marked it started about 0.46 seconds later; portal providers and the portal service were active about 0.8 seconds after compositor readiness | The tested package no longer reproduces the 120-second portal activation delay. These timings do not include firmware-to-kernel time or measure when the desktop first became usable; retain separate user-observed first-interaction timing | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index aad62b92e..cbb6a26af 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -17,7 +17,7 @@ it does not imply that the current prototype supports it. | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | | Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; actions 2 and 3 passed nested GTK Wayland smoke, and action 4 dispatches the configured Wingpanel main-menu command. Action 1 remains unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | -| Mac-style trackpad swipes | Native-feeling multi-finger trackpad gestures, including workspace navigation and the configured overview action, with reliable direction, cancellation and edge handling | Owner ranked this highly on 2026-10-19; the installed Gala customization maps its overlay gesture to `org.pantheon.gala.PerformAction(1)`, which the Rust prototype does not yet support | Detect touchpad gesture sequences from libinput/Wayland events, distinguish swipe from pointer motion, map user-configured gestures to workspace/overview actions, and qualify with the built-in M-series trackpad. Preserve accessibility settings and avoid consuming ordinary pointer input | +| Mac-style trackpad swipes | Native-feeling multi-finger trackpad gestures, including workspace navigation and the configured overview action, with reliable direction, cancellation and edge handling | Owner ranked this highly on 2026-10-19; the installed Gala customization maps its overlay gesture to `org.pantheon.gala.PerformAction(1)`, which the Rust prototype does not yet support | Detect touchpad gesture sequences from libinput/Wayland events, distinguish swipe from pointer motion, map user-configured gestures to workspace/overview actions, and qualify with the built-in M-series trackpad. Preserve accessibility settings and avoid consuming ordinary pointer input. Implemented so far: three-or-more-finger horizontal swipes select the adjacent workspace (cancellation- and grab-aware, unit-tested state machine). Vertical/overview gestures and physical-trackpad qualification remain open | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | diff --git a/rust/granite-smoke/src/main.rs b/rust/granite-smoke/src/main.rs index b3d23cbd0..8e5dc0670 100644 --- a/rust/granite-smoke/src/main.rs +++ b/rust/granite-smoke/src/main.rs @@ -2,7 +2,6 @@ //! Builds a few Granite widgets in an unmapped window, so it needs a display //! (run it under Xvfb or a nested compositor, never on the owner's screen). - use granite::prelude::PlaceholderExt; use gtk::prelude::*; diff --git a/rust/mseries-power/src/main.rs b/rust/mseries-power/src/main.rs index 140c2131c..63c30c9ea 100644 --- a/rust/mseries-power/src/main.rs +++ b/rust/mseries-power/src/main.rs @@ -33,7 +33,9 @@ fn parse_conf(text: &str) -> Config { if line.is_empty() || line.starts_with('#') { continue; } - let Some((k, v)) = line.split_once('=') else { continue }; + let Some((k, v)) = line.split_once('=') else { + continue; + }; let v = v.trim().trim_matches('"').trim_matches('\''); match k.trim() { "CHARGE_LIMIT" => { @@ -60,7 +62,9 @@ fn parse_conf(text: &str) -> Config { } fn load_conf(path: &Path) -> Config { - fs::read_to_string(path).map(|t| parse_conf(&t)).unwrap_or_default() + fs::read_to_string(path) + .map(|t| parse_conf(&t)) + .unwrap_or_default() } fn write_sys(path: &Path, value: &str) { @@ -134,7 +138,10 @@ fn apply_source(paths: &Paths, cfg: &Config) { write_sys(&entry.path().join("scaling_max_freq"), &target.to_string()); } } - let _ = Command::new("powerprofilesctl").arg("set").arg(profile).output(); + let _ = Command::new("powerprofilesctl") + .arg("set") + .arg(profile) + .output(); } fn status(paths: &Paths) { @@ -293,8 +300,14 @@ mod tests { fn limit_ordering_matches_bash() { // limit >= 100: end first, then start (avoids start >= end trip). assert_eq!( - limit_writes(100, 75).iter().map(|(f, _)| *f).collect::>(), - vec!["charge_control_end_threshold", "charge_control_start_threshold"] + limit_writes(100, 75) + .iter() + .map(|(f, _)| *f) + .collect::>(), + vec![ + "charge_control_end_threshold", + "charge_control_start_threshold" + ] ); // normal: start first, then end. let w = limit_writes(80, 75); diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index a1c9aaa51..127396490 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -207,6 +207,15 @@ xdg move/resize grab implementations to receive physical pointer motion. Multi-output bounds, pointer constraints, and hardware input remain unqualified. +Native libinput swipe gestures are now recognized for workspace navigation: +three-or-more-finger swipes that are not cancelled and that travel past a +logical-pixel threshold with horizontal dominance select the adjacent +workspace, stopping at the first and last workspace. Gestures are dropped +while the session is locked or a pointer grab (move, resize or popup) is +active. Pinch and vertical swipes remain unrecognized until an overview action +exists. The recognition state machine has unit tests; the libinput event path +is compile-checked only and no physical trackpad qualification has been run. + Xdg popup requests now resolve their root to a managed toplevel and install Smithay's popup keyboard and pointer grabs, including the serial checks needed to preserve valid nested grabs. Popup interaction has only been build-checked; diff --git a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs index 71c6edd38..0b41091ed 100644 --- a/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs +++ b/rust/pantheon-compositor/src/bin/pantheon-drm-probe.rs @@ -203,12 +203,7 @@ fn inspect_kms_planes(device: &DrmDeviceFile) { (Fourcc::Abgr2101010, "AB30"), ] .into_iter() - .filter_map(|(format, name)| { - plane - .formats() - .contains(&(format as u32)) - .then_some(name) - }) + .filter_map(|(format, name)| plane.formats().contains(&(format as u32)).then_some(name)) .collect::>(); println!( " {handle:?}: crtc={:?} framebuffer={:?}, tested formats advertised={:?} ({} total formats)", @@ -228,7 +223,10 @@ fn probe_cross_gpu_import(render_path: &PathBuf, kms_target: &KmsTarget) { println!("Cross-GPU GBM import preflight:"); println!(" render node: {}", render_path.display()); println!(" KMS device: {}", kms_target.path.display()); - println!(" preferred mode size: {}x{}", kms_target.width, kms_target.height); + println!( + " preferred mode size: {}x{}", + kms_target.width, kms_target.height + ); let render_file = match OpenOptions::new().read(true).write(true).open(render_path) { Ok(file) => file, diff --git a/rust/pantheon-compositor/src/gala_compat.rs b/rust/pantheon-compositor/src/gala_compat.rs index c691b041a..e7d7b8352 100644 --- a/rust/pantheon-compositor/src/gala_compat.rs +++ b/rust/pantheon-compositor/src/gala_compat.rs @@ -43,9 +43,9 @@ pub(crate) struct GalaActionRequest { } enum GalaSignal { - RunningApplicationsChanged, - WindowsChanged, - ActiveWorkspaceChanged, + RunningApplications, + Windows, + ActiveWorkspace, } #[derive(Clone)] @@ -98,18 +98,18 @@ impl GalaCompatHandle { }; if apps_changed { - let _ = self.signals.send(GalaSignal::RunningApplicationsChanged); + let _ = self.signals.send(GalaSignal::RunningApplications); } if windows_changed || count_changed { - let _ = self.signals.send(GalaSignal::WindowsChanged); + let _ = self.signals.send(GalaSignal::Windows); } if active_changed { - let _ = self.signals.send(GalaSignal::ActiveWorkspaceChanged); + let _ = self.signals.send(GalaSignal::ActiveWorkspace); } } pub fn request_windows_changed(&self) { - let _ = self.signals.send(GalaSignal::WindowsChanged); + let _ = self.signals.send(GalaSignal::Windows); } } @@ -289,21 +289,21 @@ fn serve_gala_compat( Err(mpsc::RecvTimeoutError::Disconnected) => return, }; let result = match signal { - GalaSignal::RunningApplicationsChanged => connection.emit_signal( + GalaSignal::RunningApplications => connection.emit_signal( None::<&str>, DESKTOP_PATH, "org.pantheon.gala.DesktopIntegration", "RunningApplicationsChanged", &(), ), - GalaSignal::WindowsChanged => connection.emit_signal( + GalaSignal::Windows => connection.emit_signal( None::<&str>, DESKTOP_PATH, "org.pantheon.gala.DesktopIntegration", "WindowsChanged", &(), ), - GalaSignal::ActiveWorkspaceChanged => connection.emit_signal( + GalaSignal::ActiveWorkspace => connection.emit_signal( None::<&str>, DESKTOP_PATH, "org.pantheon.gala.DesktopIntegration", diff --git a/rust/pantheon-compositor/src/gestures.rs b/rust/pantheon-compositor/src/gestures.rs new file mode 100644 index 000000000..a0f92c481 --- /dev/null +++ b/rust/pantheon-compositor/src/gestures.rs @@ -0,0 +1,102 @@ +//! Touchpad gesture recognition for the Pantheon session. +//! +//! The native libinput backend forwards raw gesture events here. This module +//! keeps the small amount of state needed to turn a swipe sequence into a +//! single workspace action: a gesture only fires when it is not cancelled, +//! uses at least three fingers, and is dominated by one axis past a distance +//! threshold. Swipe direction follows the content: moving three fingers left +//! reveals the workspace to the right and selects the next workspace. +//! +//! Pinch gestures are deliberately ignored until the compositor has an +//! overview surface that zoom actions could drive. + +use smithay::utils::{Logical, Point}; + +/// Swipes with fewer fingers are treated as pointer input, matching GNOME. +pub(crate) const SWIPE_MIN_FINGERS: u32 = 3; +/// Logical pixels a gesture must travel before it switches a workspace. +/// Aimed at trackpad-sized gestures; a physical-panel qualification can tune +/// this once the native session is measured. +pub(crate) const SWIPE_THRESHOLD: f64 = 90.0; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum SwipeOutcome { + None, + PreviousWorkspace, + NextWorkspace, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub(crate) struct SwipeGestureState { + fingers: u32, + delta: Point, + cancelled: bool, +} + +impl SwipeGestureState { + pub(crate) fn begin(fingers: u32) -> Self { + Self { + fingers, + delta: (0.0, 0.0).into(), + cancelled: false, + } + } + + pub(crate) fn update(&mut self, delta: Point) { + self.delta += delta; + } + + pub(crate) fn finish(self, cancelled: bool) -> SwipeOutcome { + if cancelled || self.cancelled || self.fingers < SWIPE_MIN_FINGERS { + return SwipeOutcome::None; + } + let (dx, dy) = (self.delta.x, self.delta.y); + if dx.abs() < SWIPE_THRESHOLD || dx.abs() <= dy.abs() { + return SwipeOutcome::None; + } + if dx < 0.0 { + SwipeOutcome::NextWorkspace + } else { + SwipeOutcome::PreviousWorkspace + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn horizontal_swipe_selects_adjacent_workspace() { + let mut gesture = SwipeGestureState::begin(3); + gesture.update((-40.0, 8.0).into()); + gesture.update((-60.0, -4.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::NextWorkspace); + + let mut gesture = SwipeGestureState::begin(3); + gesture.update((120.0, 0.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::PreviousWorkspace); + } + + #[test] + fn short_or_diagonal_swipes_do_nothing() { + let mut gesture = SwipeGestureState::begin(3); + gesture.update((40.0, 0.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::None); + + let mut gesture = SwipeGestureState::begin(4); + gesture.update((100.0, 120.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::None); + } + + #[test] + fn two_finger_and_cancelled_swipes_do_nothing() { + let mut gesture = SwipeGestureState::begin(2); + gesture.update((-200.0, 0.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::None); + + let mut gesture = SwipeGestureState::begin(3); + gesture.update((-200.0, 0.0).into()); + assert_eq!(gesture.finish(true), SwipeOutcome::None); + } +} diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index fec19daad..e01ef5061 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -4,8 +4,9 @@ use gio::{ }; use smithay::{ backend::input::{ - AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, - KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, + AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, + GestureEndEvent, GestureSwipeUpdateEvent, InputBackend, InputEvent, KeyboardKeyEvent, + PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, }, input::{ keyboard::{FilterResult, KeyboardTarget, XkbConfig}, @@ -77,11 +78,10 @@ pub(crate) fn startup_keyboard_config() -> StartupKeyboardConfig { } else { config.repeat_delay = settings.uint("delay").min(i32::MAX as u32) as i32; let interval = settings.uint("repeat-interval"); - config.repeat_rate = if interval == 0 { - 33 - } else { - (1000 / interval).min(i32::MAX as u32) as i32 - }; + config.repeat_rate = 1000u32 + .checked_div(interval) + .unwrap_or(33) + .min(i32::MAX as u32) as i32; } } @@ -319,6 +319,37 @@ impl PantheonCompositor { pointer.axis(self, frame); pointer.frame(self); } + InputEvent::GestureSwipeBegin { event, .. } => { + // Workspace switching must not race a compositor move/resize + // grab, a popup grab, or the lock screen. In those states the + // sequence is dropped rather than deferred. + let pointer_grabbed = self + .seat + .get_pointer() + .is_some_and(|pointer| pointer.is_grabbed()); + if self.session_locked || pointer_grabbed { + self.swipe_gesture = None; + } else { + self.swipe_gesture = Some(crate::gestures::SwipeGestureState::begin( + event.fingers(), + )); + } + } + InputEvent::GestureSwipeUpdate { event, .. } => { + if let Some(gesture) = self.swipe_gesture.as_mut() { + gesture.update(event.delta()); + } + } + InputEvent::GestureSwipeEnd { event, .. } => { + let outcome = self + .swipe_gesture + .take() + .map(|gesture| gesture.finish(event.cancelled())) + .unwrap_or(crate::gestures::SwipeOutcome::None); + if let Err(error) = self.apply_swipe_outcome(outcome) { + tracing::debug!(%error, "touchpad swipe did not change workspace"); + } + } _ => {} } } diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 356982a37..77b5c88a1 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -3,6 +3,7 @@ mod cursor; mod display_config; mod gala_compat; +mod gestures; mod grabs; mod handlers; mod input; @@ -116,8 +117,10 @@ enum BackendChoice { Native, } -fn parse_arguments() --> Result<(BackendChoice, bool, Option<(String, Vec)>), Box> { +type RunCommand = (String, Vec); +type ParsedArguments = (BackendChoice, bool, Option); + +fn parse_arguments() -> Result> { let mut args = std::env::args().skip(1); let mut backend = BackendChoice::Nested; let mut notify_systemd = false; diff --git a/rust/pantheon-compositor/src/native.rs b/rust/pantheon-compositor/src/native.rs index 36bff708b..e95db034a 100644 --- a/rust/pantheon-compositor/src/native.rs +++ b/rust/pantheon-compositor/src/native.rs @@ -64,6 +64,7 @@ pub type NativeExporter = GbmFramebufferExporter; pub type NativeOutputManager = DrmOutputManager; pub type NativeDrmOutput = DrmOutput; type NativeMultiRenderer<'a> = MultiRenderer<'a, 'a, GbmRendererBackend, GbmRendererBackend>; +type HardwareNode = (DrmNode, GbmDevice); const OUTPUT_FORMATS: &[smithay::backend::allocator::Fourcc] = &[ smithay::backend::allocator::Fourcc::Argb2101010, @@ -977,7 +978,7 @@ impl NativeKmsDevice { let location = existing_locations.get(&name).copied().unwrap_or_else(|| { let location = (next_x, 0).into(); if let Some(mode) = output.output.current_mode() { - let scale = output.output.current_scale().fractional_scale() as f64; + let scale = output.output.current_scale().fractional_scale(); next_x += (mode.size.w as f64 / scale).round() as i32; } location @@ -988,7 +989,7 @@ impl NativeKmsDevice { state.space.map_output(&output.output, location); state.track_output_for_lock(&output.output); if let Some(mode) = output.output.current_mode() { - let scale = output.output.current_scale().fractional_scale() as f64; + let scale = output.output.current_scale().fractional_scale(); let scale = if scale.is_finite() && scale > 0.0 { scale } else { @@ -1589,7 +1590,7 @@ impl NativeRenderer { session: &LibSeatSession, gpus: &mut NativeGpuManager, path: &Path, - ) -> Result)>, Box> { + ) -> Result, Box> { let node = DrmNode::from_path(path)?; let mut session = session.clone(); let fd = session.open( diff --git a/rust/pantheon-compositor/src/pantheon_shell.rs b/rust/pantheon-compositor/src/pantheon_shell.rs index 2adb0ea58..372864487 100644 --- a/rust/pantheon-compositor/src/pantheon_shell.rs +++ b/rust/pantheon-compositor/src/pantheon_shell.rs @@ -135,14 +135,14 @@ impl state.update_panel(surface, |panel| panel.anchor = Some(anchor.into())); } protocol::io_elementary_pantheon_panel_v1::Request::Focus => { - if state.find_window_for_surface(surface).is_some() { - if let Some(keyboard) = state.seat.get_keyboard() { - keyboard.set_focus( - state, - Some(surface.clone()), - SERIAL_COUNTER.next_serial(), - ); - } + if state.find_window_for_surface(surface).is_some() + && let Some(keyboard) = state.seat.get_keyboard() + { + keyboard.set_focus( + state, + Some(surface.clone()), + SERIAL_COUNTER.next_serial(), + ); } } protocol::io_elementary_pantheon_panel_v1::Request::SetSize { width, height } => { diff --git a/rust/pantheon-compositor/src/shell_compat.rs b/rust/pantheon-compositor/src/shell_compat.rs index 7678800e0..25d5fe935 100644 --- a/rust/pantheon-compositor/src/shell_compat.rs +++ b/rust/pantheon-compositor/src/shell_compat.rs @@ -93,16 +93,16 @@ impl ShellCompatHandle { thread::Builder::new() .name("pantheon-shell-dbus".into()) .spawn(move || { - serve_shell_compat( - service_bindings, - service_active, - service_active_since, - service_osd, + serve_shell_compat(ShellCompatContext { + bindings: service_bindings, + active: service_active, + active_since: service_active_since, + osd: service_osd, redraw_sender, osd_expiry, wayland_display, receiver, - ) + }) }) .expect("failed to start shell compatibility D-Bus thread"); Self { @@ -305,7 +305,7 @@ impl ScreenSaverCompat { async fn active_changed(emitter: &SignalEmitter<'_>, new_value: bool) -> zbus::Result<()>; } -fn serve_shell_compat( +struct ShellCompatContext { bindings: Arc>>, active: Arc, active_since: Arc>>, @@ -314,7 +314,19 @@ fn serve_shell_compat( osd_expiry: mpsc::Sender, wayland_display: OsString, receiver: mpsc::Receiver, -) { +} + +fn serve_shell_compat(context: ShellCompatContext) { + let ShellCompatContext { + bindings, + active, + active_since, + osd, + redraw_sender, + osd_expiry, + wayland_display, + receiver, + } = context; let connection = match Connection::session() { Ok(connection) => connection, Err(error) => { diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index a0bb09950..3b6f2e08b 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -127,6 +127,8 @@ pub struct PantheonCompositor { pub corner_snap_preview: Option, pub corner_snap_cancelled: bool, pub move_grab_active: bool, + /// In-flight three-or-more-finger swipe, if the active backend reports one. + pub(crate) swipe_gesture: Option, pub minimized_windows: HashMap< smithay::reexports::wayland_server::backend::ObjectId, (Window, Point), @@ -403,6 +405,7 @@ impl PantheonCompositor { corner_snap_preview: None, corner_snap_cancelled: false, move_grab_active: false, + swipe_gesture: None, minimized_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, @@ -724,6 +727,27 @@ impl PantheonCompositor { Ok(()) } + /// Apply the outcome of an in-flight touchpad swipe. Workspace changes stop + /// at the first and last workspace instead of creating or wrapping. + pub(crate) fn apply_swipe_outcome( + &mut self, + outcome: crate::gestures::SwipeOutcome, + ) -> Result<(), String> { + match outcome { + crate::gestures::SwipeOutcome::None => Ok(()), + crate::gestures::SwipeOutcome::NextWorkspace => self.switch_workspace_relative(1), + crate::gestures::SwipeOutcome::PreviousWorkspace => self.switch_workspace_relative(-1), + } + } + + fn switch_workspace_relative(&mut self, delta: i32) -> Result<(), String> { + let target = self.workspace_manager.active() as i32 + delta; + if target < 0 || target as usize >= self.workspace_manager.workspace_count() { + return Err("no adjacent workspace in that direction".into()); + } + self.activate_workspace(target) + } + fn activate_workspace(&mut self, index: i32) -> Result<(), String> { if self.session_locked { return Err("cannot switch workspaces while the session is locked".into()); @@ -1576,9 +1600,7 @@ impl PantheonCompositor { continue; } let output_name = output.name(); - let Some(lock_surface) = self.lock_surfaces.get(&output_name) else { - return None; - }; + let lock_surface = self.lock_surfaces.get(&output_name)?; let local = pos - geometry.loc.to_f64(); return smithay::desktop::utils::under_from_surface_tree( lock_surface.wl_surface(), @@ -1623,11 +1645,11 @@ impl PantheonCompositor { if !self.session_locked || self.lock_confirmation.is_none() { return; } - if self.lock_confirmation_outputs.is_empty() { - if let Some(confirmation) = self.lock_confirmation.take() { - confirmation.lock(); - self.shell_compat.set_locked(true); - } + if self.lock_confirmation_outputs.is_empty() + && let Some(confirmation) = self.lock_confirmation.take() + { + confirmation.lock(); + self.shell_compat.set_locked(true); } } } diff --git a/rust/pantheon-compositor/src/wallpaper.rs b/rust/pantheon-compositor/src/wallpaper.rs index bf8a2f0d8..3f39ea25e 100644 --- a/rust/pantheon-compositor/src/wallpaper.rs +++ b/rust/pantheon-compositor/src/wallpaper.rs @@ -58,10 +58,10 @@ pub(crate) fn load_wallpaper(selection: &WallpaperSelection) -> Option Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box> -{ +type WallpaperLoadResult = + Result<(MemoryRenderBuffer, u32, u32, std::path::PathBuf, String), Box>; + +fn try_load_wallpaper(selection: &WallpaperSelection) -> WallpaperLoadResult { let file = gio::File::for_uri(&selection.uri); let path = file .path() @@ -82,7 +82,7 @@ fn try_load_wallpaper( // Fourcc::Argb8888 uses BGRA byte order on little-endian machines. let mut pixels = decoded.into_raw(); - for pixel in pixels.chunks_exact_mut(4) { + for pixel in pixels.as_chunks_mut::<4>().0 { pixel.swap(0, 2); } let buffer = MemoryRenderBuffer::from_slice( diff --git a/rust/pantheon-compositor/src/workspace.rs b/rust/pantheon-compositor/src/workspace.rs index 6001d6d4f..af5c0fba0 100644 --- a/rust/pantheon-compositor/src/workspace.rs +++ b/rust/pantheon-compositor/src/workspace.rs @@ -37,10 +37,10 @@ impl WorkspaceManager { pub fn add_window(&mut self, window: Window, location: Point) { let id = window.id(); - if let Some(previous) = self.assignments.remove(&id) { - if let Some(windows) = self.workspaces.get_mut(previous) { - windows.retain(|existing| *existing != id); - } + if let Some(previous) = self.assignments.remove(&id) + && let Some(windows) = self.workspaces.get_mut(previous) + { + windows.retain(|existing| *existing != id); } self.windows.insert(id, window); self.assignments.insert(id, self.active); diff --git a/rust/target/.rustc_info.json b/rust/target/.rustc_info.json index c7899f4f7..9ab602296 100644 --- a/rust/target/.rustc_info.json +++ b/rust/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":9469584872709203352,"outputs":{"10245554582852442424":{"success":true,"status":"","code":0,"stdout":"rustc 1.99.0 (b940084d7 2026-09-28)\nbinary: rustc\ncommit-hash: b940084d7eb6a299eb4bfeb8e34901bc051e7ac4\ncommit-date: 2026-09-28\nhost: aarch64-unknown-linux-gnu\nrelease: 1.99.0\nLLVM version: 23.1.1\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/sdolbec/.rustup/toolchains/stable-aarch64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"neon\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_primitive_alignment=\"128\"\ntarget_has_atomic_primitive_alignment=\"16\"\ntarget_has_atomic_primitive_alignment=\"32\"\ntarget_has_atomic_primitive_alignment=\"64\"\ntarget_has_atomic_primitive_alignment=\"8\"\ntarget_has_atomic_primitive_alignment=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":9560274887975835320,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-unknown-linux-gnu\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/sdolbec/.rustup/toolchains/1.93.1-aarch64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"neon\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file From 0d0a3653bec9db11d5f0663dafd62aa04d4888bf Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 16:57:26 -0400 Subject: [PATCH 210/798] Rust compositor: grid overview for PerformAction(1) and upward swipe `PerformAction(1)` no longer errors: it toggles a grid overview ("expose") implemented in `rust/pantheon-compositor/src/overview.rs`. Each output's active-workspace windows are arranged into a centered grid inside the work area; entering saves each window's prior location, size and managed state (plain, tiled, maximized, fullscreen) and exit restores all of it. Clicking a window focuses it and closes the mode, Escape closes it, and a window closed while the overview is open is skipped during restore. - gestures: a non-cancelled 3+-finger upward swipe past the 90 logical-pixel threshold with vertical dominance toggles the overview (`ToggleOverview`). - input: Escape is intercepted while the overview is open; pointer presses route through `handle_overview_click`. - state: `overview` field, Gala action-1 mapping, swipe outcome routing. - tiling: `apply_tile`/`tile_rect` are crate-visible so the overview restores tiled windows through the normal tiling path. - Verification: workspace and native-drm checks, 7 unit tests (4 gesture, 3 grid layout), clippy clean, and a private nested Xvfb D-Bus smoke with two GTK Wayland windows that logged `opened the grid overview windows=2` and `closed the grid overview` from two successful `PerformAction 1` calls. - Docs: requirements, architecture status, compositor readmes, roadmap and the OS9 handoff updated; scaled thumbnails/animations, XWayland/mixed-window overview smoke, pinch gestures and physical trackpad qualification remain open. --- mseries/OS9-HANDOFF.md | 43 ++ mseries/ROADMAP.md | 36 +- .../ARCHITECTURE-AND-STATUS.md | 5 +- .../EXPERIENCE-REQUIREMENTS.md | 4 +- mseries/pantheon-rust-compositor/README.md | 7 +- rust/pantheon-compositor/README.md | 45 ++- rust/pantheon-compositor/src/gestures.rs | 41 +- rust/pantheon-compositor/src/input.rs | 20 +- rust/pantheon-compositor/src/main.rs | 1 + rust/pantheon-compositor/src/overview.rs | 375 ++++++++++++++++++ rust/pantheon-compositor/src/state.rs | 5 + rust/pantheon-compositor/src/tiling.rs | 7 +- 12 files changed, 539 insertions(+), 50 deletions(-) create mode 100644 rust/pantheon-compositor/src/overview.rs diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index fbcb160c3..01b084747 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,5 +1,48 @@ # OS 9 implementation / Claude Code handoff +## Rust compositor: grid overview for PerformAction(1) and upward swipe — 2026-10-19 + +Branch `codex/rust-gala-stack-integration`, worktree +`/home/sdolbec/ElementaryOS_MSeries-wt/rust-gala-stack-integration`. + +- `PerformAction(1)` no longer errors. It toggles a new grid overview + ("expose") mode in `rust/pantheon-compositor/src/overview.rs`: each output's + active-workspace application windows (panels and minimized windows excluded) + are rearranged into a centered grid inside that output's work area with + 24-logical-pixel margins and 12-pixel gaps, sized `ceil(sqrt(n))` columns. +- The mode is the explicit supported replacement for Gala's scaled + multitasking view, per `mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md`. + Entering saves each window's prior location, size and managed state (plain, + tiled, maximized, fullscreen) and restores all of it on exit. Clicking a + window focuses it and closes the mode; Escape closes it; a window closed + while the overview is open is skipped during restore. +- Gestures: `SwipeOutcome::ToggleOverview` added to + `rust/pantheon-compositor/src/gestures.rs`; a non-cancelled + three-or-more-finger upward swipe past the 90-logical-pixel threshold with + vertical dominance toggles the overview. `input.rs` intercepts Escape while + the overview is open, routes pointer presses through + `handle_overview_click`, and `state.rs` maps the Gala action and the swipe + outcome to `toggle_overview`. `tiling.rs` exposes `apply_tile`/`tile_rect` + crate-wide so the overview can restore tiled windows through the normal + tiling path. +- Verification (this host, no session taken over): `cargo check --workspace + --all-targets`, `cargo check -p pantheon-compositor --bins --features + native-drm`, `cargo test -p pantheon-compositor` (7 tests: 4 gesture, 3 grid + layout) and `cargo clippy -p pantheon-compositor --all-targets` all pass. +- New private nested smoke (Xvfb + dbus-run-session, fake + `XDG_RUNTIME_DIR`, `PANTHEON_WAYLAND_DISPLAY=pantheon-overview-smoke`, two + GTK Wayland `zenity` windows): `gdbus call … org.pantheon.gala.PerformAction + 1` twice both returned `()`, and the compositor logged `opened the grid + overview windows=2` then `closed the grid overview`. The overview visuals + and the restored geometry were not screenshot-inspected. + +Not done / next task: rendered scaled thumbnails/animations and drag +reorganization in the overview; XWayland and mixed Wayland/XWayland windows in +the overview smoke; physical M-series trackpad qualification (the nested Winit +backend emits no gesture events); pinch gestures; user-configurable bindings. +No package was built, nothing was installed, and Gala remains the active +session. + ## Rust compositor: repaired build, clippy cleanups, touchpad workspace swipes — 2026-10-19 Branch `codex/rust-gala-stack-integration`, worktree diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 63d3a8f35..c991e1acf 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -185,18 +185,30 @@ one core in both sessions under unmatched workloads, whole-system power is unmeasured, and no performance win is established. The prototype now recognizes native libinput touchpad swipes for workspace -navigation: non-cancelled three-or-more-finger swipes that pass a 90-logical- -pixel threshold with horizontal dominance select the adjacent workspace in the -content-following direction and stop at the first/last workspace. Gestures are -ignored while the session is locked or a move, resize or popup grab is active; -pinch and vertical swipes remain unrecognized until an overview action exists. -The state machine lives in `rust/pantheon-compositor/src/gestures.rs` with -three unit tests; `cargo test -p pantheon-compositor` passes, both default and -`native-drm` builds check, and clippy is clean. The installed nested Winit -backend never emits gesture events, so physical M-series trackpad -qualification, user-configurable bindings and vertical/overview routing remain -open. Gala is still the active session and no package was installed from this -batch. +navigation and the overview: non-cancelled three-or-more-finger swipes that +pass a 90-logical-pixel threshold with horizontal dominance select the adjacent +workspace in the content-following direction and stop at the first/last +workspace, and an upward swipe of the same size toggles the grid overview. +Gestures are ignored while the session is locked or a move, resize or popup +grab is active; pinch gestures remain unrecognized. The state machine lives in +`rust/pantheon-compositor/src/gestures.rs` with unit tests; `cargo test -p +pantheon-compositor` passes, both default and `native-drm` builds check, and +clippy is clean. + +`PerformAction(1)` no longer errors: it toggles a grid overview ("expose") +implemented in `rust/pantheon-compositor/src/overview.rs`. The mode arranges +each output's active-workspace windows into a centered grid inside the work +area, saves and restores plain, tiled, maximized and fullscreen arrangements, +focuses a clicked window and closes on Escape or a second toggle. A private +Xvfb/Winit D-Bus smoke with two GTK Wayland windows opened and closed the mode +and the compositor logged both transitions; the grid layout has unit tests. +Scaled thumbnails, animations, drag reorganization and native qualification +remain open. + +The installed nested Winit backend never emits gesture events, so physical +M-series trackpad qualification, user-configurable bindings and where the +overview lands on the keyboard-accessible path remain open. Gala is still the +active session and no package was installed from this batch. ## Signal Granite palette completeness — 2026-10-18 diff --git a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md index 0368fa77f..700bea437 100644 --- a/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md +++ b/mseries/pantheon-rust-compositor/ARCHITECTURE-AND-STATUS.md @@ -158,8 +158,9 @@ session was replaced. | Nested text input | Private GTK Wayland field accepted an IBus/Mozc Japanese commit | Isolated smoke passed; native input remains unqualified | | Pantheon panel protocol | Wingpanel connected in private nested Wayland, requested a top panel, attached shared-memory buffers and rendered upright after the nested Winit transform correction | Nested visual smoke passed; panel absent from native session | | Pantheon shell clients | Current release rendered Wingpanel at top and stock Dock at bottom in isolated Xvfb. Hide/reveal passed with both private `overlapping-focused` and `overlapping-window` profiles; the latter matches the host's saved value. In native package `0.1.0+git8023955`, Wingpanel's user service became active; the Dock service was intentionally skipped by the existing `~/.config/mseries/dock/user-disabled` marker | Nested visual and hide/reveal smoke passed; native Wingpanel process is active, but native appearance has not been visually confirmed. Dock remains opted out by the user's saved marker | -| Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 (multitasking overview) remains unsupported | -| Trackpad swipe gestures | Native libinput swipe events are recognized for workspace navigation: non-cancelled three-or-more-finger horizontal swipes past a logical-pixel threshold select the adjacent workspace, and gestures are dropped while locked or during a pointer grab | Swipe state machine has unit tests; both build configurations compile. Physical M-series trackpad qualification, vertical/overview gestures and user configuration remain open | +| Gala action compatibility | Existing Gala compatibility D-Bus path accepts `PerformAction` values | Actions 2 (toggle maximize) and 3 (hide/minimize) passed private Xvfb/Winit Wayland D-Bus smoke; XWayland and native qualification remain open. Action 4 launches the configured Wingpanel main-menu command; build-checked, runtime open. Action 1 now toggles a grid overview ("expose") mode: a private Xvfb/Winit smoke with two GTK Wayland windows opened the overview and closed it again through D-Bus, with the compositor logging both transitions | +| Grid overview ("expose") | The explicit replacement for Gala's multitasking view and the target of `PerformAction(1)` and the upward swipe | Arranges each output's active-workspace windows into a centered grid inside the work area, saves and restores plain, tiled, maximized and fullscreen arrangements, focuses a clicked window, and closes on Escape or a second toggle. Unit tests cover the grid layout; the nested D-Bus smoke opened and closed the mode with two windows. Rendered scaled thumbnails, animations, drag-and-drop reorganization and native qualification remain open | +| Trackpad swipe gestures | Native libinput swipe events are recognized for workspace navigation and the overview: non-cancelled three-or-more-finger horizontal swipes past a logical-pixel threshold select the adjacent workspace, an upward swipe of the same size toggles the grid overview, and gestures are dropped while locked or during a pointer grab | Swipe state machine has unit tests; both build configurations compile. Physical M-series trackpad qualification, pinch gestures and user configuration remain open | | Desktop wallpaper | Loaded the configured 5000×2813 elementary wallpaper and captured it behind a GTK Zenity window in isolated Xvfb; a private dconf/Xvfb run changed the URI while running and logs confirmed the new image loaded | Nested visual and settings-reload smokes passed; native output and rendering remain open | | Native Rust session handoff | Package `0.1.0+git02447cb` was installed and selected through LightDM on 2026-10-19. GNOME entered `pantheon-rust-wayland`; compositor initialized the M1 Pro DRM/EGL path at 3024×1964, loaded the configured wallpaper and reported ready. Wingpanel and IBus services started; Dock was skipped because the saved user opt-out marker remains present | Native login and internal scanout passed again. The five-minute rollback timer completed and restored the existing Gala autologin. User reports the desktop felt responsive; an objective first-usable timestamp is still unavailable | | Startup timing | The boot report remains 20.838 seconds to `default.target` and 10.465 seconds to `graphical.target`. In the 12:10 EDT Rust login, the compositor service started at 12:10:00 and systemd marked it started about 0.46 seconds later; portal providers and the portal service were active about 0.8 seconds after compositor readiness | The tested package no longer reproduces the 120-second portal activation delay. These timings do not include firmware-to-kernel time or measure when the desktop first became usable; retain separate user-observed first-interaction timing | diff --git a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md index cbb6a26af..b60b62ebc 100644 --- a/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md +++ b/mseries/pantheon-rust-compositor/EXPERIENCE-REQUIREMENTS.md @@ -15,9 +15,9 @@ it does not imply that the current prototype supports it. | --- | --- | --- | --- | | First usable session | Launch and stop the session predictably; run ordinary Wayland apps; focus, raise, move, resize, maximize, minimize, close and fullscreen windows; recover from a compositor exit | Current Gala session unit and window-management source; app use across the M-series handoff | Own the Wayland display, seat, xdg-shell lifecycle, visible initial placement and focus, stacking, basic window state, startup readiness and orderly failure/restart behavior | | M-series panel and dock | Preserve panel and dock placement, keep windows out of visible dock space, return that space when the dock hides, support full-height/notch layout and monitor/scale changes | [`display-space`](../display-space/README.md), including 3024×1964 full-height panel, dock work area and hide/reveal behavior | Implement the existing Pantheon panel/dock shell protocol (or a small maintained successor) and usable-area/work-area updates. Leave panel rendering and dock UI in their existing apps | -| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; actions 2 and 3 passed nested GTK Wayland smoke, and action 4 dispatches the configured Wingpanel main-menu command. Action 1 remains unsupported | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible | +| Hot-corner multitasking view | The installed Gala customization maps the configured overlay action to `org.pantheon.gala.PerformAction(1)` | [`gala-WindowManager.vala.installed`](../customizations/gala-WindowManager.vala.installed) invokes action 1 for the overlay gesture; actions 2 and 3 passed nested GTK Wayland smoke, and action 4 dispatches the configured Wingpanel main-menu command. Action 1 now toggles the grid overview ("expose") mode described below instead of erroring | Preserve the user-configured gesture by providing a shell overview action or an explicit supported replacement; do not report the current no-op/error path as compatible. Implemented so far: a grid overview that rearranges the active workspace's windows per output, selects with a click, closes with Escape, and restores the previous sizes/positions; a scaled Gala-style animation remains open | | Display and input | Use the built-in panel at the configured scale; survive internal display changes and connected-display hotplug when the kernel exposes a usable output; keyboard, pointer, shortcuts and Command/Ctrl mapping | [`display-profiles`](../display-profiles/README.md), [`window-snapping`](../window-snapping/README.md), handoff display/input fixes | Output enumeration, mode/layout application and rollback; keyboard/pointer events; map absolute pointer coordinates to the complete logical desktop; xkb layouts and shortcut routing. Keep dock video link training in kernel/DCP scope | -| Mac-style trackpad swipes | Native-feeling multi-finger trackpad gestures, including workspace navigation and the configured overview action, with reliable direction, cancellation and edge handling | Owner ranked this highly on 2026-10-19; the installed Gala customization maps its overlay gesture to `org.pantheon.gala.PerformAction(1)`, which the Rust prototype does not yet support | Detect touchpad gesture sequences from libinput/Wayland events, distinguish swipe from pointer motion, map user-configured gestures to workspace/overview actions, and qualify with the built-in M-series trackpad. Preserve accessibility settings and avoid consuming ordinary pointer input. Implemented so far: three-or-more-finger horizontal swipes select the adjacent workspace (cancellation- and grab-aware, unit-tested state machine). Vertical/overview gestures and physical-trackpad qualification remain open | +| Mac-style trackpad swipes | Native-feeling multi-finger trackpad gestures, including workspace navigation and the configured overview action, with reliable direction, cancellation and edge handling | Owner ranked this highly on 2026-10-19; the installed Gala customization maps its overlay gesture to `org.pantheon.gala.PerformAction(1)`, which now toggles the grid overview | Detect touchpad gesture sequences from libinput/Wayland events, distinguish swipe from pointer motion, map user-configured gestures to workspace/overview actions, and qualify with the built-in M-series trackpad. Preserve accessibility settings and avoid consuming ordinary pointer input. Implemented so far: three-or-more-finger horizontal swipes select the adjacent workspace and a non-cancelled upward swipe toggles the grid overview (cancellation- and grab-aware, unit-tested state machine). Pinch gestures and physical-trackpad qualification remain open | | Input methods and dictation | Keep IBus composition and focus-bound F5 Unicode output working in focused Wayland fields; retain the configured `mozc-jp` source | [F5 app qualification](../dictation/F5-ENGLISH-ACCEPTANCE.md) records the active IBus output workflow; current sources include US XKB and IBus `mozc-jp` | Advertise text-input-v3 and a trusted input-method-v2 global, track keyboard/text focus and input-method popups; qualify actual IBus commits in installed GTK/Qt apps | | Existing X11 clients | Launch rootless XWayland clients, keep keyboard focus, clipboard and PRIMARY selection exchange usable, and export the X display to session-activated apps | Current Pantheon session exposes `ibus-x11`, `ChatGPT`, and `mutter-x11-frames` as Xwayland clients | XWayland startup/window management and session `DISPLAY` propagation are implemented in the Rust prototype; keyboard, clipboard and PRIMARY selection paths compile but still need nested and on-device runtime qualification | | M-series render-to-display bridge | Render ordinary client surfaces on the Asahi GPU and present them through the Apple display controller | Read-only M1 Pro inventory: Asahi `/dev/dri/card1` exposes `renderD128` but no KMS resources; Apple `/dev/dri/card2` owns the internal `eDP-1` output and has no EGL device | Make rendered buffers usable by Apple KMS, adding a copy/bridge path if direct import is insufficient; validate framebuffer creation and scanout, and handle presentation failure without leaving a live but unusable session | diff --git a/mseries/pantheon-rust-compositor/README.md b/mseries/pantheon-rust-compositor/README.md index 30544ca34..8a2e9293a 100644 --- a/mseries/pantheon-rust-compositor/README.md +++ b/mseries/pantheon-rust-compositor/README.md @@ -212,7 +212,12 @@ D-Bus interface. Window enumeration, app IDs, focus state, workspace listing, activation and reorder, dynamic workspace creation, window movement, and cross-workspace focus are implemented. An isolated Xvfb run with a temporary Wayland client exercised workspace creation, movement, switching and focus. -Overview remains unsupported. +Gala's scaled multitasking view remains unsupported; `PerformAction(1)` and an +upward three-finger swipe now toggle a grid overview ("expose") instead, which +rearranges the active workspace's windows per output and restores the prior +plain, tiled, maximized or fullscreen arrangement on exit. A private +Xvfb/Winit D-Bus smoke with two GTK Wayland windows opened and closed it; see +`ARCHITECTURE-AND-STATUS.md` for open visual/native gaps. The nested stock Dock 8.3.3 smoke bound the panel protocol and attached buffers, but also logged its known asynchronous workspace-initialization critical, so Dock interaction is still not qualified end to end. diff --git a/rust/pantheon-compositor/README.md b/rust/pantheon-compositor/README.md index 127396490..b3aa9e233 100644 --- a/rust/pantheon-compositor/README.md +++ b/rust/pantheon-compositor/README.md @@ -121,17 +121,25 @@ across workspaces, switches visible windows, and supports Dock requests to create the dynamic workspace, move windows, reorder workspaces and focus a window across workspaces. An isolated Xvfb run with a temporary Wayland client exercised creation, movement, switching and focus. Gala's multitasking view is -not implemented. The Gala `PerformAction` compatibility path now implements -actions 2 (toggle maximize on the focused window) and 3 (minimize the focused window) for -Wayland and XWayland application windows. A private Xvfb/Winit D-Bus smoke -focused a GTK Wayland window, invoked action 2 twice, minimized it with action -3, and restored it through `FocusWindow`; each D-Bus call succeeded. The -smoke did not inspect the rendered maximize geometry, and the XWayland and -native paths remain unqualified. Action 4 now launches the configured -`panel-main-menu-action` from `io.elementary.desktop.wm.behavior`, keeping the -existing Wingpanel launcher in charge of its UI; it is build-checked but not -runtime-qualified. Action 1 (multitasking view) remains unsupported because -the Rust session has no overview UI. Stock Dock also logged its existing +replaced by a grid overview ("expose") mode: the active workspace's windows are +arranged into a per-output grid, a click focuses the chosen window, Escape +closes the mode, and the prior plain, tiled, maximized or fullscreen +arrangement is restored. The Gala `PerformAction` compatibility path now +implements action 1 as a grid-overview toggle, actions 2 (toggle maximize on +the focused window) and 3 (minimize the focused window) for Wayland and +XWayland application windows. A private Xvfb/Winit D-Bus smoke opened and +closed the overview with two GTK Wayland windows; each D-Bus call succeeded +and the compositor logged both transitions. A separate private smoke focused +a GTK Wayland window, invoked action 2 twice, minimized it with action +3, and restored it through `FocusWindow`; each D-Bus call succeeded. Those +smokes did not inspect the rendered maximize geometry or the overview visuals, +and the XWayland and native paths remain unqualified. Action 4 now launches +the configured `panel-main-menu-action` from +`io.elementary.desktop.wm.behavior`, keeping the existing Wingpanel launcher +in charge of its UI; it is build-checked but not runtime-qualified. The grid +overview is a supported replacement, not a pixel-faithful Gala animation: +scaled window thumbnails, drag-reorganization and transitions remain open. +Stock Dock also logged its existing workspace-startup race during the nested smoke, so full Dock behavior remains unqualified. Hiding is immediate; blur and multi-output Dock behavior remain open. The @@ -207,14 +215,15 @@ xdg move/resize grab implementations to receive physical pointer motion. Multi-output bounds, pointer constraints, and hardware input remain unqualified. -Native libinput swipe gestures are now recognized for workspace navigation: -three-or-more-finger swipes that are not cancelled and that travel past a -logical-pixel threshold with horizontal dominance select the adjacent -workspace, stopping at the first and last workspace. Gestures are dropped +Native libinput swipe gestures are now recognized for workspace navigation and +the overview: three-or-more-finger swipes that are not cancelled and that +travel past a logical-pixel threshold with horizontal dominance select the +adjacent workspace, stopping at the first and last workspace, while an upward +swipe of the same size toggles the grid overview. Gestures are dropped while the session is locked or a pointer grab (move, resize or popup) is -active. Pinch and vertical swipes remain unrecognized until an overview action -exists. The recognition state machine has unit tests; the libinput event path -is compile-checked only and no physical trackpad qualification has been run. +active. Pinch gestures remain unrecognized. The recognition state machine has +unit tests; the libinput event path is compile-checked only and no physical +trackpad qualification has been run. Xdg popup requests now resolve their root to a managed toplevel and install Smithay's popup keyboard and pointer grabs, including the serial checks needed diff --git a/rust/pantheon-compositor/src/gestures.rs b/rust/pantheon-compositor/src/gestures.rs index a0f92c481..f4351ff94 100644 --- a/rust/pantheon-compositor/src/gestures.rs +++ b/rust/pantheon-compositor/src/gestures.rs @@ -2,10 +2,12 @@ //! //! The native libinput backend forwards raw gesture events here. This module //! keeps the small amount of state needed to turn a swipe sequence into a -//! single workspace action: a gesture only fires when it is not cancelled, -//! uses at least three fingers, and is dominated by one axis past a distance -//! threshold. Swipe direction follows the content: moving three fingers left -//! reveals the workspace to the right and selects the next workspace. +//! single action: a gesture only fires when it is not cancelled, uses at least +//! three fingers, and is dominated by one axis past a distance threshold. +//! Swipe direction follows the content: moving three fingers left reveals the +//! workspace to the right and selects the next workspace; moving three fingers +//! up toggles the grid overview (the explicit replacement for Gala's +//! multitasking view). //! //! Pinch gestures are deliberately ignored until the compositor has an //! overview surface that zoom actions could drive. @@ -24,6 +26,7 @@ pub(crate) enum SwipeOutcome { None, PreviousWorkspace, NextWorkspace, + ToggleOverview, } #[derive(Clone, Copy, Debug, Default, PartialEq)] @@ -51,14 +54,19 @@ impl SwipeGestureState { return SwipeOutcome::None; } let (dx, dy) = (self.delta.x, self.delta.y); - if dx.abs() < SWIPE_THRESHOLD || dx.abs() <= dy.abs() { - return SwipeOutcome::None; + if dx.abs() >= SWIPE_THRESHOLD && dx.abs() > dy.abs() { + return if dx < 0.0 { + SwipeOutcome::NextWorkspace + } else { + SwipeOutcome::PreviousWorkspace + }; } - if dx < 0.0 { - SwipeOutcome::NextWorkspace - } else { - SwipeOutcome::PreviousWorkspace + // An upward swipe opens or closes the grid overview, mirroring the + // installed Gala overlay gesture mapping to PerformAction(1). + if dy <= -SWIPE_THRESHOLD && dy.abs() > dx.abs() { + return SwipeOutcome::ToggleOverview; } + SwipeOutcome::None } } @@ -78,6 +86,19 @@ mod tests { assert_eq!(gesture.finish(false), SwipeOutcome::PreviousWorkspace); } + #[test] + fn upward_swipe_toggles_the_overview() { + let mut gesture = SwipeGestureState::begin(3); + gesture.update((-5.0, -60.0).into()); + gesture.update((4.0, -70.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::ToggleOverview); + + // Downward swipes are not bound yet. + let mut gesture = SwipeGestureState::begin(3); + gesture.update((0.0, 140.0).into()); + assert_eq!(gesture.finish(false), SwipeOutcome::None); + } + #[test] fn short_or_diagonal_swipes_do_nothing() { let mut gesture = SwipeGestureState::begin(3); diff --git a/rust/pantheon-compositor/src/input.rs b/rust/pantheon-compositor/src/input.rs index e01ef5061..67c230cde 100644 --- a/rust/pantheon-compositor/src/input.rs +++ b/rust/pantheon-compositor/src/input.rs @@ -115,6 +115,13 @@ impl PantheonCompositor { serial, time, |state, modifiers, keysym| { + if key_state == smithay::backend::input::KeyState::Pressed + && keysym.modified_sym() + == smithay::input::keyboard::xkb::keysyms::KEY_Escape.into() + && state.handle_overview_key() + { + return FilterResult::Intercept(()); + } if key_state == smithay::backend::input::KeyState::Pressed && state.move_grab_active && keysym.modified_sym() @@ -233,6 +240,14 @@ impl PantheonCompositor { let button_state = event.state(); + if !self.session_locked + && ButtonState::Pressed == button_state + && self.handle_overview_click(pointer.current_location()) + { + pointer.frame(self); + return; + } + if !self.session_locked && ButtonState::Pressed == button_state && !pointer.is_grabbed() @@ -330,9 +345,8 @@ impl PantheonCompositor { if self.session_locked || pointer_grabbed { self.swipe_gesture = None; } else { - self.swipe_gesture = Some(crate::gestures::SwipeGestureState::begin( - event.fingers(), - )); + self.swipe_gesture = + Some(crate::gestures::SwipeGestureState::begin(event.fingers())); } } InputEvent::GestureSwipeUpdate { event, .. } => { diff --git a/rust/pantheon-compositor/src/main.rs b/rust/pantheon-compositor/src/main.rs index 77b5c88a1..5bfa0b915 100644 --- a/rust/pantheon-compositor/src/main.rs +++ b/rust/pantheon-compositor/src/main.rs @@ -9,6 +9,7 @@ mod handlers; mod input; #[cfg(feature = "native-drm")] pub mod native; +mod overview; mod pantheon_shell; mod protocols; mod shell_compat; diff --git a/rust/pantheon-compositor/src/overview.rs b/rust/pantheon-compositor/src/overview.rs new file mode 100644 index 000000000..768c68b0b --- /dev/null +++ b/rust/pantheon-compositor/src/overview.rs @@ -0,0 +1,375 @@ +//! Grid overview ("expose") mode: the explicit supported replacement for +//! Gala's multitasking view until a scaled overview renderer exists. +//! +//! The installed Gala customization triggers `org.pantheon.gala.PerformAction(1)` +//! for the configured overlay gesture/hot corner. Instead of erroring, this +//! compositor now arranges the active workspace's windows into a per-output +//! grid inside each output's work area. Clicking a window focuses it and +//! closes the overview; Escape closes it and restores the prior arrangement. +//! +//! Windows are reconfigured rather than rendered scaled, so the mode reuses +//! the existing xdg/X11 configure paths. Clients that decline the grid size +//! (fixed-size windows) simply keep their committed size inside the cell. + +use std::collections::HashSet; + +use smithay::{ + desktop::Window, + reexports::{wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::Resource}, + utils::{Logical, Point, Rectangle, SERIAL_COUNTER, Size}, +}; + +use crate::state::{ManagedWindow, PantheonCompositor, TiledWindow}; +use crate::tiling::tile_rect; + +/// Gap between overview cells, in logical pixels. +const OVERVIEW_GAP: i32 = 12; +/// Margin between the work area edge and the outermost cells. +const OVERVIEW_MARGIN: i32 = 24; + +pub(crate) struct OverviewWindow { + window: Window, + id: usize, + location: Point, + size: Size, + managed: Option, + tiled: Option, +} + +pub(crate) struct OverviewState { + windows: Vec, +} + +/// Lay out `count` equally sized, centered rows in `area`, one slot per item. +pub(crate) fn grid_rects( + area: Rectangle, + count: usize, +) -> Vec> { + if count == 0 { + return Vec::new(); + } + let inner: Rectangle = Rectangle::new( + (area.loc.x + OVERVIEW_MARGIN, area.loc.y + OVERVIEW_MARGIN).into(), + Size::from(( + (area.size.w - 2 * OVERVIEW_MARGIN).max(1), + (area.size.h - 2 * OVERVIEW_MARGIN).max(1), + )), + ); + let columns = (count as f64).sqrt().ceil().max(1.0) as i32; + let rows = ((count as i32) + columns - 1) / columns; + let cell_width = ((inner.size.w - OVERVIEW_GAP * (columns - 1)) / columns).max(1); + let cell_height = ((inner.size.h - OVERVIEW_GAP * (rows - 1)) / rows).max(1); + (0..count) + .map(|index| { + let column = index as i32 % columns; + let row = index as i32 / columns; + let items_in_row = (count as i32 - row * columns).clamp(1, columns); + let row_width = items_in_row * cell_width + OVERVIEW_GAP * (items_in_row - 1); + let row_offset = ((inner.size.w - row_width) / 2).max(0); + Rectangle::new( + ( + inner.loc.x + row_offset + column * (cell_width + OVERVIEW_GAP), + inner.loc.y + row * (cell_height + OVERVIEW_GAP), + ) + .into(), + Size::from((cell_width, cell_height)), + ) + }) + .collect() +} + +impl PantheonCompositor { + pub(crate) fn overview_active(&self) -> bool { + self.overview.is_some() + } + + /// Toggle the grid overview. Used by the Gala `PerformAction(1)` bridge and + /// by a three-finger upward swipe. + pub(crate) fn toggle_overview(&mut self) -> Result<(), String> { + if self.overview_active() { + self.exit_overview(None) + } else { + self.enter_overview() + } + } + + fn enter_overview(&mut self) -> Result<(), String> { + if self.session_locked { + return Err("cannot open the overview while the session is locked".into()); + } + let active = self.workspace_manager.active(); + let minimized = self + .minimized_windows + .values() + .map(|(window, _)| window.id()) + .chain( + self.x11_minimized_windows + .values() + .map(|(window, _)| window.id()), + ) + .collect::>(); + + // Group visible application windows by the output they occupy. + let mut groups: Vec<(smithay::output::Output, Vec)> = Vec::new(); + for window in self.space.elements() { + if self.workspace_manager.workspace_for(window.id()) != Some(active) + || minimized.contains(&window.id()) + || window + .toplevel() + .is_some_and(|toplevel| self.is_panel_surface(toplevel.wl_surface())) + { + continue; + } + let Some(output) = self + .space + .outputs_for_element(window) + .first() + .cloned() + .or_else(|| self.primary_output()) + else { + continue; + }; + if let Some(group) = groups + .iter_mut() + .find(|(group, _)| group.name() == output.name()) + { + group.1.push((*window).clone()); + } else { + groups.push((output, vec![(*window).clone()])); + } + } + if groups.is_empty() { + return Err("there are no windows to show in the overview".into()); + } + + let mut saved = Vec::new(); + for (output, windows) in groups { + let Some(output_geometry) = self.space.output_geometry(&output) else { + continue; + }; + let work_area = self.work_area_for_output(output_geometry); + for (window, rect) in windows.iter().zip(grid_rects(work_area, windows.len())) { + self.place_overview_window(window, rect, &mut saved); + } + } + if saved.is_empty() { + return Err("there are no windows to show in the overview".into()); + } + + self.overview = Some(OverviewState { windows: saved }); + self.refresh_panel_visibility(); + self.gala_compat.request_windows_changed(); + self.request_native_redraw(); + tracing::info!( + windows = self.overview_windows().unwrap_or(0), + "opened the grid overview" + ); + Ok(()) + } + + fn place_overview_window( + &mut self, + window: &Window, + rect: Rectangle, + saved: &mut Vec, + ) { + let id = window.id(); + let location = self.space.element_location(window).unwrap_or_default(); + let size = window.geometry().size; + let managed = window + .toplevel() + .and_then(|surface| self.managed_windows.remove(&surface.wl_surface().id())) + .or_else(|| { + window + .x11_surface() + .and_then(|surface| self.x11_managed_windows.remove(&surface.window_id())) + }); + let tiled = window + .toplevel() + .and_then(|surface| self.tiled_windows.remove(&surface.wl_surface().id())); + + if let Some(toplevel) = window.toplevel() { + toplevel.with_pending_state(|state| { + state.states.unset(xdg_toplevel::State::Maximized); + state.states.unset(xdg_toplevel::State::Fullscreen); + state.size = Some(rect.size); + }); + toplevel.send_pending_configure(); + } else if let Some(surface) = window.x11_surface() { + if let Err(error) = surface.set_maximized(false) { + tracing::warn!( + ?error, + "could not clear X11 maximized state for the overview" + ); + } + if let Err(error) = surface.set_fullscreen(false) { + tracing::warn!( + ?error, + "could not clear X11 fullscreen state for the overview" + ); + } + if let Err(error) = surface.configure(Some(rect)) { + tracing::warn!(?error, "could not configure X11 window for the overview"); + } + } else { + return; + } + self.space.map_element(window.clone(), rect.loc, false); + saved.push(OverviewWindow { + window: window.clone(), + id, + location, + size, + managed, + tiled, + }); + } + + /// Close the overview and restore every saved arrangement. `focus` may name + /// the window that was selected with the pointer; otherwise the last saved + /// window is focused. + pub(crate) fn exit_overview(&mut self, focus: Option) -> Result<(), String> { + let Some(state) = self.overview.take() else { + return Err("the overview is not open".into()); + }; + let selected = focus + .and_then(|id| { + state + .windows + .iter() + .find(|entry| entry.id == id) + .map(|entry| entry.window.clone()) + }) + .or_else(|| state.windows.last().map(|entry| entry.window.clone())); + + for entry in &state.windows { + let window = &entry.window; + if !self.space.elements().any(|candidate| candidate == window) { + continue; + } + if let Some(surface) = window.toplevel().cloned() { + if let Some(tiled) = &entry.tiled { + let rect = self + .space + .output_geometry(&tiled.output) + .map(|geometry| { + tile_rect(self.work_area_for_output(geometry), tiled.position) + }) + .unwrap_or(tiled.rect); + self.apply_tile(&surface, window, tiled.output.clone(), tiled.position, rect); + } else if let Some(managed) = entry.managed { + self.managed_windows + .insert(surface.wl_surface().id(), managed); + self.configure_window_mode(surface, managed.maximized, managed.fullscreen); + } else { + surface.with_pending_state(|state| { + state.size = Some(entry.size); + }); + surface.send_pending_configure(); + self.space + .map_element(window.clone(), entry.location, false); + } + } else if let Some(surface) = window.x11_surface().cloned() { + if let Some(managed) = entry.managed { + self.x11_managed_windows + .insert(surface.window_id(), managed); + self.configure_x11_window_mode(&surface, managed.maximized, managed.fullscreen); + } else if let Err(error) = + surface.configure(Some(Rectangle::new(entry.location, entry.size))) + { + tracing::warn!(?error, "could not restore X11 window after the overview"); + } else { + self.space + .map_element(window.clone(), entry.location, false); + } + } + } + + if let Some(window) = selected { + self.space.raise_element(&window, true); + self.focus_window_keyboard(&window, SERIAL_COUNTER.next_serial()); + } + self.refresh_panel_visibility(); + self.gala_compat.request_windows_changed(); + self.request_native_redraw(); + tracing::info!("closed the grid overview"); + Ok(()) + } + + fn overview_windows(&self) -> Option { + self.overview.as_ref().map(|state| state.windows.len()) + } + + /// Handle the overview's Escape shortcut from the keyboard filter. Returns + /// true when the key was consumed. + pub(crate) fn handle_overview_key(&mut self) -> bool { + if !self.overview_active() { + return false; + } + let _ = self.exit_overview(None); + true + } + + /// Handle a pointer press while the overview is open: select the window + /// under the pointer (if any) and close the overview. Returns true when the + /// press was consumed. + pub(crate) fn handle_overview_click(&mut self, location: Point) -> bool { + if !self.overview_active() { + return false; + } + let target = self + .space + .element_under(location) + .map(|(window, _)| (*window).clone()) + .filter(|window| { + window + .toplevel() + .is_none_or(|toplevel| !self.is_panel_surface(toplevel.wl_surface())) + }) + .map(|window| window.id()); + let _ = self.exit_overview(target); + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn area() -> Rectangle { + Rectangle::new((0, 0).into(), (1000, 600).into()) + } + + #[test] + fn empty_overview_has_no_slots() { + assert!(grid_rects(area(), 0).is_empty()); + } + + #[test] + fn single_window_fills_the_work_area_margins() { + let rects = grid_rects(area(), 1); + assert_eq!(rects.len(), 1); + assert_eq!(rects[0].loc, (OVERVIEW_MARGIN, OVERVIEW_MARGIN).into()); + assert_eq!( + rects[0].size, + (1000 - 2 * OVERVIEW_MARGIN, 600 - 2 * OVERVIEW_MARGIN).into() + ); + } + + #[test] + fn four_windows_form_a_two_by_two_grid() { + let rects = grid_rects(area(), 4); + assert_eq!(rects.len(), 4); + assert_eq!(rects[0].loc.y, rects[1].loc.y); + assert_eq!(rects[0].loc.x, rects[2].loc.x); + assert!(rects[1].loc.x > rects[0].loc.x); + assert!(rects[2].loc.y > rects[0].loc.y); + // The third row-less row stays centered for odd counts. + let rects = grid_rects(area(), 3); + assert_eq!(rects[0].loc.y, rects[1].loc.y); + assert!(rects[2].loc.y > rects[0].loc.y); + let last = rects[2]; + let centered = (1000 - last.size.w) / 2; + assert_eq!(last.loc.x, centered); + } +} diff --git a/rust/pantheon-compositor/src/state.rs b/rust/pantheon-compositor/src/state.rs index 3b6f2e08b..2d3026344 100644 --- a/rust/pantheon-compositor/src/state.rs +++ b/rust/pantheon-compositor/src/state.rs @@ -129,6 +129,8 @@ pub struct PantheonCompositor { pub move_grab_active: bool, /// In-flight three-or-more-finger swipe, if the active backend reports one. pub(crate) swipe_gesture: Option, + /// Active grid overview ("expose") session, if any. + pub(crate) overview: Option, pub minimized_windows: HashMap< smithay::reexports::wayland_server::backend::ObjectId, (Window, Point), @@ -406,6 +408,7 @@ impl PantheonCompositor { corner_snap_cancelled: false, move_grab_active: false, swipe_gesture: None, + overview: None, minimized_windows: HashMap::new(), initially_positioned: HashSet::new(), popups, @@ -530,6 +533,7 @@ impl PantheonCompositor { Err("workspace index is outside the current workspace list".into()) } } + GalaAction::PerformAction(1) => self.toggle_overview(), GalaAction::PerformAction(2) => self.maximize_focused_window(), GalaAction::PerformAction(3) => self.minimize_focused_window(), GalaAction::PerformAction(4) => self.launch_panel_main_menu(), @@ -737,6 +741,7 @@ impl PantheonCompositor { crate::gestures::SwipeOutcome::None => Ok(()), crate::gestures::SwipeOutcome::NextWorkspace => self.switch_workspace_relative(1), crate::gestures::SwipeOutcome::PreviousWorkspace => self.switch_workspace_relative(-1), + crate::gestures::SwipeOutcome::ToggleOverview => self.toggle_overview(), } } diff --git a/rust/pantheon-compositor/src/tiling.rs b/rust/pantheon-compositor/src/tiling.rs index 693372aec..90325fb30 100644 --- a/rust/pantheon-compositor/src/tiling.rs +++ b/rust/pantheon-compositor/src/tiling.rs @@ -333,7 +333,7 @@ impl PantheonCompositor { } } - fn apply_tile( + pub(crate) fn apply_tile( &mut self, surface: &ToplevelSurface, window: &Window, @@ -422,7 +422,10 @@ impl PantheonCompositor { } } -fn tile_rect(area: Rectangle, position: TilePosition) -> Rectangle { +pub(crate) fn tile_rect( + area: Rectangle, + position: TilePosition, +) -> Rectangle { let half_width = area.size.w / 2; let half_height = area.size.h / 2; let (right, bottom, width, height) = match position { From 1f4baa472824d5142cb0e75d0e0a0c86ef6e1f8c Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 17:37:23 -0400 Subject: [PATCH 211/798] Machine Lab 0.3.0: tests, non-blocking status poll, masked credentials, --version, AppStream, .deb build, user guide, CI job; vmapple findings Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/validate-mseries.yml | 24 ++- mseries/OS9-HANDOFF.md | 13 ++ mseries/machine-lab/CHANGELOG.md | 14 ++ mseries/machine-lab/README.md | 2 + mseries/machine-lab/ROADMAP.md | 22 +++ mseries/machine-lab/build-deb.sh | 64 +++++++ .../io.github.mseries.MachineLab.metainfo.xml | 24 +++ mseries/machine-lab/docs/USER-GUIDE.md | 67 ++++++++ mseries/machine-lab/launcher/run-vm.sh | 1 + mseries/machine-lab/meson.build | 22 ++- mseries/machine-lab/src/config.vala.in | 4 + mseries/machine-lab/src/machine.vala | 36 ++-- mseries/machine-lab/src/main.vala | 9 + mseries/machine-lab/src/page.vala | 33 +++- mseries/machine-lab/tests/fake-qmp.py | 38 +++++ mseries/machine-lab/tests/test-machine.vala | 157 ++++++++++++++++++ 16 files changed, 515 insertions(+), 15 deletions(-) create mode 100644 mseries/machine-lab/CHANGELOG.md create mode 100755 mseries/machine-lab/build-deb.sh create mode 100644 mseries/machine-lab/data/io.github.mseries.MachineLab.metainfo.xml create mode 100644 mseries/machine-lab/docs/USER-GUIDE.md create mode 100644 mseries/machine-lab/src/config.vala.in create mode 100644 mseries/machine-lab/tests/fake-qmp.py create mode 100644 mseries/machine-lab/tests/test-machine.vala diff --git a/.github/workflows/validate-mseries.yml b/.github/workflows/validate-mseries.yml index c447255b0..ba251c148 100644 --- a/.github/workflows/validate-mseries.yml +++ b/.github/workflows/validate-mseries.yml @@ -89,6 +89,28 @@ jobs: - name: Verify exact-kernel boot profile and rollback run: python3 -m unittest discover -s mseries/display-space -p 'test_*.py' + machine-lab: + name: Machine Lab build and logic tests (arm64) + runs-on: ubuntu-26.04-arm + steps: + - uses: actions/checkout@v7 + - name: Install build dependencies + run: sudo apt-get update && sudo apt-get install -y meson ninja-build valac gcc pkg-config libgtk-4-dev libgranite-7-dev python3 appstream desktop-file-utils xvfb xauth + - name: Build and run the logic tests (persistence, discovery, QMP transport) + run: | + meson setup build-ml mseries/machine-lab + meson compile -C build-ml + xvfb-run -a meson test -C build-ml --print-errorlogs + - name: Check the version flag and desktop metadata + run: | + xvfb-run -a build-ml/machine-lab --version + desktop-file-validate mseries/machine-lab/data/io.github.mseries.MachineLab.desktop + appstreamcli validate --no-net mseries/machine-lab/data/io.github.mseries.MachineLab.metainfo.xml + - name: Build the Debian package + run: | + sudo apt-get install -y dpkg-dev + MACHINE_LAB_OUTPUT_DIR="$RUNNER_TEMP/ml-deb" bash mseries/machine-lab/build-deb.sh + codexbar-wingpanel: name: CodexBar Wingpanel module, render and interaction probes (arm64) runs-on: ubuntu-26.04-arm @@ -133,7 +155,7 @@ jobs: bash -n transform/elementary-grow.sh bash -n bin/elementary-mac-setup for f in mseries/install-*.sh mseries/lib-backup.sh mseries/boot/mkinitcpio-snippet mseries/disable-notch-for-pantheon.sh; do bash -n "$f"; done - shellcheck -S warning bin/elementary-mac-setup transform/elementary-grow.sh mseries/install-unlock-screen.sh mseries/install-boot-isolation.sh mseries/lib-backup.sh mseries/boot/mkinitcpio-snippet mseries/enhance.sh mseries/enhance-firstboot.sh mseries/install-brave.sh mseries/install-video-decode.sh mseries/test-installer-flow.sh mseries/vms/mseries-vm mseries/vms/test-mseries-vm.sh mseries/install-quieter-sound-theme.sh mseries/test-quieter-sound-theme.sh mseries/aurora-sep/install-aurora-sep.sh mseries/aurora-sep/install-touchid-gui.sh + shellcheck -S warning bin/elementary-mac-setup transform/elementary-grow.sh mseries/install-unlock-screen.sh mseries/install-boot-isolation.sh mseries/lib-backup.sh mseries/boot/mkinitcpio-snippet mseries/enhance.sh mseries/enhance-firstboot.sh mseries/install-brave.sh mseries/install-video-decode.sh mseries/test-installer-flow.sh mseries/vms/mseries-vm mseries/vms/test-mseries-vm.sh mseries/install-machine-lab.sh mseries/machine-lab/build-deb.sh mseries/machine-lab/launcher/run-vm.sh mseries/install-quieter-sound-theme.sh mseries/test-quieter-sound-theme.sh mseries/aurora-sep/install-aurora-sep.sh mseries/aurora-sep/install-touchid-gui.sh bash -n mseries/vms/mseries-vm bash mseries/vms/test-mseries-vm.sh bash mseries/test-quieter-sound-theme.sh diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 3d725c6a8..6131cbcf8 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6361,3 +6361,16 @@ country or region?"): [`windows11-arm64-qemu-first-boot-oobe.png`](../docs/image At each reboot the "Press any key to boot from CD" prompt must be left alone so it boots the disk. Not done: OOBE (network or local account), the desktop, virtio-win guest drivers (network, display), activation, suspend, anything on real hardware. VM left powered off. + +## 2026-10-19: Machine Lab 0.3.0 production pass, and QEMU vmapple on Linux + +Machine Lab: `meson test` now covers persistence, mseries-vm discovery, install detection, credentials and the QMP transport +(control socket per call, display socket persistent: the regression that stalled `mseries-vm`); status polling is off the main +thread; credentials are masked until revealed; `--version`; AppStream metadata; `mseries/machine-lab/build-deb.sh` builds +`machine-lab_0.3.0-1~mseries1_arm64.deb` (runs the tests first); a CI job builds, tests, validates metadata and builds the +package; user guide in `mseries/machine-lab/docs/USER-GUIDE.md`. Not verified: installing the `.deb` system-wide, the display on +a real Wayland session. + +macOS: see `mseries/machine-lab/ROADMAP.md`, "QEMU vmapple on this Linux Mac". The `vmapple` machine can be built on Linux +with small patches and then stops at the macOS-only `apple-gfx-mmio` device; a real boot needs Apple's firmware and a macOS 12 VM +image, which were not downloaded. diff --git a/mseries/machine-lab/CHANGELOG.md b/mseries/machine-lab/CHANGELOG.md new file mode 100644 index 000000000..e68d14b89 --- /dev/null +++ b/mseries/machine-lab/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## 0.3.0 (2026-10-19) +- Display tab: guest screen, mouse, keyboard, full screen with exit controls, Ctrl+Alt+Del, screenshots. +- macOS (experimental) page that runs real probes and shows how each route fails. +- Pause/resume; Omarchy automatic install; import of mseries-vm guests; readiness and free-space checks. +- Credentials are masked until revealed; the status poll runs off the main thread; `--version`. +- Tests (`meson test`): persistence, discovery, install detection, credentials, QMP transport; AppStream metadata; `.deb` build script. + +## 0.2.0 +- Multiple machines, presets, colours, Console and Settings tabs, imported guests. + +## 0.1.0 +- Single-machine prototype (Codex spike), restyled for elementary OS. diff --git a/mseries/machine-lab/README.md b/mseries/machine-lab/README.md index a5b84f366..3b53186df 100644 --- a/mseries/machine-lab/README.md +++ b/mseries/machine-lab/README.md @@ -24,6 +24,8 @@ meson setup build && meson compile -C build ./build/machine-lab # finds ../launcher/run-vm.sh; MACHINE_LAB_LAUNCHER overrides ``` +Guides: `docs/USER-GUIDE.md`, `ROADMAP.md`, `CHANGELOG.md`. Tests: `meson test -C build`. Package: `bash mseries/machine-lab/build-deb.sh`. + Layout: `src/machine.vala` (model, QMP, process control, discovery), `src/page.vala` (machine pane), `src/main.vala` (window, sidebar, presets), `launcher/` (QEMU launcher and Omarchy updater). diff --git a/mseries/machine-lab/ROADMAP.md b/mseries/machine-lab/ROADMAP.md index f0936a479..2ef1b61de 100644 --- a/mseries/machine-lab/ROADMAP.md +++ b/mseries/machine-lab/ROADMAP.md @@ -34,3 +34,25 @@ The macOS (experimental) page in the app runs real commands and records the outp 3. `qemu-system-aarch64 -machine virt,accel=kvm -cpu Haswell`: `unable to find CPU model 'Haswell'`. 4. `qemu-system-x86_64`: not installed (and it could only emulate, never use KVM, on an ARM host). 5. Emulating an Intel Mac in software: not attempted (Apple recovery image, OpenCore, tens of times slower). + +## macOS: QEMU `vmapple` on this Linux Mac (2026-10-19) + +Built QEMU master (11.1.50) in `~/src/qemu-vmapple` with two local patches and ran its `vmapple` machine under KVM: + +1. `hw/vmapple/Kconfig` required HVF and selected the macOS-only graphics device, and `configs/devices/aarch64-softmmu/default.mak` + set `CONFIG_VMAPPLE=n`. Removing those three lines makes `vmapple` appear in `-machine help` on Linux. +2. `hw/vmapple/aes.c` does not compile with gcc (`char hex[MAX_LEN * 2 + 1] = ""` with `static const size_t MAX_LEN`); an + `enum` constant fixes it (worth sending upstream). + +Then, with `-accel kvm`: + +- `-M vmapple,uuid=1` alone: `Parameter 'uuid' expects integer` (the docs derive it from the VM's UUID with `contrib/vmapple/uuid.sh`). +- With `uuid=1`: `No AUX device. Please specify one as pflash drive.` (needs the aux storage of a real macOS VM). +- With blank aux and root pflash plus a dummy `-bios`: `unknown type 'apple-gfx-mmio'`. `vmapple.c` creates Apple's + paravirtual GPU unconditionally; that device is built from `ParavirtualizedGraphics.framework`, which exists only on macOS. + This is the first hard Linux blocker. Stubbing it out (skip the device when the type is missing) is the obvious next patch, + but it was not applied or built: the sandbox declined further edits to this externally sourced code, so the attempt stops here. + +What a real boot still needs: Apple's `AVPBooter.vmapple2.bin` copied from a macOS install, aux and root images from a macOS 12 VM +made with Virtualization.framework (`macosvm`), and an answer for graphics (none exists on Linux; the guest would be headless over +serial, ssh or screen sharing). Unknowns: whether the guest tolerates running on KVM with Apple's cores instead of HVF. diff --git a/mseries/machine-lab/build-deb.sh b/mseries/machine-lab/build-deb.sh new file mode 100755 index 000000000..c549f2d11 --- /dev/null +++ b/mseries/machine-lab/build-deb.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Build machine-lab__.deb from this source tree. +# bash mseries/machine-lab/build-deb.sh # writes build/machine-lab-deb/*.deb +# Needs: meson, valac, libgtk-4-dev, libgranite-7-dev, dpkg-deb. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$here/../.." && pwd)" +out="${MACHINE_LAB_OUTPUT_DIR:-$repo_root/build/machine-lab-deb}" +stage="$out/package" +work="$out/meson" + +for tool in meson ninja valac dpkg-deb; do + command -v "$tool" >/dev/null || { echo "Missing build tool: $tool" >&2; exit 2; } +done + +version="$(grep -m1 -oP "version: '\K[0-9.]+" "$here/meson.build")" +arch="$(dpkg --print-architecture)" +debver="${version}-1~mseries1" + +rm -rf "$out" +mkdir -p "$stage" +meson setup "$work" "$here" --prefix=/usr --buildtype=release >/dev/null +meson compile -C "$work" +meson test -C "$work" --print-errorlogs +DESTDIR="$stage" meson install -C "$work" >/dev/null + +# Rename the launcher helpers' tests/build artefacts out of the package (none are installed) and fix modes. +find "$stage" -type d -exec chmod 0755 {} + + +installed_kb="$(du -sk "$stage" | cut -f1)" +install -d "$stage/DEBIAN" +cat >"$stage/DEBIAN/control" <= 4.12), libgranite-7-7 | libgranite7, qemu-system-arm, qemu-utils, qemu-efi-aarch64, python3 +Recommends: swtpm, genisoimage, openssl +Maintainer: elementaryos-mseries local build +Homepage: https://github.com/techmore/ElementaryOS_MSeries +Description: Native elementary OS virtual machine app (ARM64) + Machine Lab creates and runs ARM64 virtual machines with QEMU/KVM: a Display + tab with full screen, mouse and keyboard, pause and resume, a live console, + readiness and disk-space checks, and import of mseries-vm guests. +CONTROL + +cat >"$stage/DEBIAN/postinst" <<'POST' +#!/bin/sh +set -e +command -v update-desktop-database >/dev/null && update-desktop-database -q /usr/share/applications || true +command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +exit 0 +POST +cp "$stage/DEBIAN/postinst" "$stage/DEBIAN/postrm" +chmod 0755 "$stage/DEBIAN/postinst" "$stage/DEBIAN/postrm" + +pkg="$out/machine-lab_${debver}_${arch}.deb" +dpkg-deb --root-owner-group --build "$stage" "$pkg" >/dev/null +echo "Built $pkg" +dpkg-deb --info "$pkg" | sed -n '1,14p' +dpkg-deb --contents "$pkg" | awk '{print $1, $3, $6}' | sed -n '1,40p' diff --git a/mseries/machine-lab/data/io.github.mseries.MachineLab.metainfo.xml b/mseries/machine-lab/data/io.github.mseries.MachineLab.metainfo.xml new file mode 100644 index 000000000..863aa4747 --- /dev/null +++ b/mseries/machine-lab/data/io.github.mseries.MachineLab.metainfo.xml @@ -0,0 +1,24 @@ + + + io.github.mseries.MachineLab + CC0-1.0 + GPL-3.0-or-later + Machine Lab + Run other Linux and Windows machines on this Mac + +

    Machine Lab creates and runs ARM64 virtual machines with QEMU and KVM, with the look and feel of an elementary OS app.

    +

    See each machine's desktop in a built-in Display tab with full screen, mouse and keyboard, pause and resume, a live console, and readiness and disk-space checks. Import guests already managed with mseries-vm, or create new ones from elementary OS, Omarchy, Ubuntu, Debian, Fedora or any ARM64 installer image.

    +
    + io.github.mseries.MachineLab.desktop + https://github.com/techmore/ElementaryOS_MSeries + https://github.com/techmore/ElementaryOS_MSeries/issues + machine-lab + + + + +

    Display tab with full screen, in-app mouse and keyboard, a macOS experiment page, masked credentials, a non-blocking status poll and a test suite.

    +
    +
    +
    +
    diff --git a/mseries/machine-lab/docs/USER-GUIDE.md b/mseries/machine-lab/docs/USER-GUIDE.md new file mode 100644 index 000000000..70711d685 --- /dev/null +++ b/mseries/machine-lab/docs/USER-GUIDE.md @@ -0,0 +1,67 @@ +# Machine Lab user guide + +Machine Lab runs other ARM64 operating systems on this Mac with QEMU and KVM. Everything here is ARM64 (aarch64): +x86-64 / amd64 images do not run. + +## Install + +```sh +bash mseries/install-machine-lab.sh # builds, installs to ~/.local and pins it in the dock +bash mseries/machine-lab/build-deb.sh # or build machine-lab__arm64.deb (runs the tests first) +``` + +Needs QEMU (`qemu-system-arm`, `qemu-utils`), UEFI firmware (`qemu-efi-aarch64`) and access to `/dev/kvm`. +The Overview tab's setup check tells you which of these is missing and how to fix it. + +## Machines + +- **YOUR MACHINES** are installed guests, including any managed with `mseries/vms/mseries-vm` (`~/vms/*/vm.conf`), + which Machine Lab finds on its own. +- **NEW: NOT INSTALLED** are machines that have not been installed yet. New Machine (Ctrl+N) offers presets: Omarchy + (can download and install itself), elementary OS (uses an ARM64 ISO already downloaded by mseries-vm), Ubuntu, Debian, + Fedora, or a blank machine for any ARM64 `.iso`. +- Each machine has Overview, Display, Console and Settings tabs. Settings (CPUs, memory, disk, SSH port, colour) are + locked while it runs; the disk size is fixed once the disk exists. + +## Display + +Starting a machine opens its Display tab: the guest's screen at about 30 fps with its own mouse and keyboard. + +| Action | How | +| --- | --- | +| Send the keyboard and mouse to the machine | click the screen | +| Give them back | Ctrl+Alt+G, or Release Keyboard in full screen | +| Full screen | F11, the button under the screen, or Ctrl+Alt+F (works while the machine has the keyboard) | +| Leave full screen | F11, Ctrl+Alt+F, or the Exit Full Screen button that appears when the pointer reaches the top | +| Ctrl+Alt+Del | the button under the screen | +| Screenshot | camera button, saved to Pictures | + +Other shortcuts: Ctrl+N new machine, F5 or Ctrl+R re-check disk space and requirements, Ctrl+Q quit (running +machines are stopped). + +Clipboard sharing and drag-and-drop between host and guest are not built yet (issues #263, #264). + +## Installing a system + +1. Pick the preset. Omarchy: use **Download and Install** (checks the latest ARM64 nightly, verifies its SHA-256 and + installs unattended; the sign-in is shown and saved with owner-only permissions). Any other system: choose or drop + an ARM64 `.iso`, then **Start Installer**. +2. Install in the Display tab. When the guest shuts down after a real install, Machine Lab detaches the installer and + boots the disk from then on. +3. **Windows 11 ARM64** needs extra steps (virtio-win drivers, a 64 GB disk, `GPU=ramfb`): see + `mseries/OS9-HANDOFF.md`, "Windows 11 ARM64 installed and first-booted". + +## Troubleshooting + +| Symptom | Cause and fix | +| --- | --- | +| "No access to /dev/kvm" | add your user to the `kvm` group and log in again | +| Low disk space warning | machine disks grow as the guest writes; free space or shrink the disk in Settings, then press F5 | +| Display stays black | the guest may use a kernel without a graphics driver for QEMU's virtual GPU; see the Console tab. elementary OS images default to the Apple-only Fairydust kernel, which does not boot on QEMU (see the handoff) | +| `mseries-vm` commands hang | they should not: the Display uses its own `qmp-view.sock`. Guests started before that socket existed hold the control socket while the Display tab is open; restart them | +| Keys dropped in the Windows installer | type slowly or use the Browse tree; WinPE loses fast input | + +## macOS + +macOS guests cannot run on Linux here. The "macOS (experimental)" page runs real probes and shows how each route fails. +See `ROADMAP.md`. diff --git a/mseries/machine-lab/launcher/run-vm.sh b/mseries/machine-lab/launcher/run-vm.sh index e667a81be..a11507324 100755 --- a/mseries/machine-lab/launcher/run-vm.sh +++ b/mseries/machine-lab/launcher/run-vm.sh @@ -6,6 +6,7 @@ # MACHINE_DISK_GIB, MACHINE_SSH_PORT, MACHINE_NAME, MACHINE_DISPLAY (gtk|none). Two QMP sockets in # VM_DIR: qmp.sock (control: pause, resume, mseries-vm) and qmp-view.sock (the # Machine Lab display) so a viewer never blocks control commands. +# shellcheck disable=SC2054 # commas inside QEMU option strings are intentional set -euo pipefail CPUS=${MACHINE_CPUS:-4} diff --git a/mseries/machine-lab/meson.build b/mseries/machine-lab/meson.build index 986258326..1fe8c8d78 100644 --- a/mseries/machine-lab/meson.build +++ b/mseries/machine-lab/meson.build @@ -1,5 +1,5 @@ project('machine-lab', 'vala', - version: '0.2.0', + version: '0.3.0', meson_version: '>= 1.0.0', default_options: ['warning_level=2'] ) @@ -15,8 +15,14 @@ resources = gnome.compile_resources('machine-lab-resources', add_project_arguments(['--pkg', 'posix'], language: 'vala') +config = configure_file( + input: 'src/config.vala.in', + output: 'config.vala', + configuration: {'VERSION': meson.project_version()} +) + executable('machine-lab', - 'src/main.vala', 'src/machine.vala', 'src/page.vala', 'src/viewer.vala', 'src/macexp.vala', + 'src/main.vala', 'src/machine.vala', 'src/page.vala', 'src/viewer.vala', 'src/macexp.vala', config, resources, dependencies: [gtk, granite], install: true @@ -24,6 +30,8 @@ executable('machine-lab', install_data('data/io.github.mseries.MachineLab.svg', install_dir: get_option('datadir') / 'icons/hicolor/scalable/apps') +install_data('data/io.github.mseries.MachineLab.metainfo.xml', + install_dir: get_option('datadir') / 'metainfo') install_data('data/io.github.mseries.MachineLab.desktop', install_dir: get_option('datadir') / 'applications') install_data('launcher/run-vm.sh', @@ -34,3 +42,13 @@ install_data('launcher/prepare-latest-omarchy.py', install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x', rename: 'machine-lab-prepare-omarchy') + +test_exe = executable('test-machine', + 'tests/test-machine.vala', 'src/machine.vala', + dependencies: [gtk, dependency('gio-unix-2.0')], + install: false +) +test('machine logic', test_exe, + env: {'FAKE_QMP': meson.current_source_dir() / 'tests/fake-qmp.py'}, + timeout: 60 +) diff --git a/mseries/machine-lab/src/config.vala.in b/mseries/machine-lab/src/config.vala.in new file mode 100644 index 000000000..29d66cac4 --- /dev/null +++ b/mseries/machine-lab/src/config.vala.in @@ -0,0 +1,4 @@ +namespace Config { + public const string VERSION = "@VERSION@"; + public const string APP_ID = "io.github.mseries.MachineLab"; +} diff --git a/mseries/machine-lab/src/machine.vala b/mseries/machine-lab/src/machine.vala index 7bab7db4d..f093d39df 100644 --- a/mseries/machine-lab/src/machine.vala +++ b/mseries/machine-lab/src/machine.vala @@ -138,8 +138,12 @@ public class Machine : Object { private bool qmp_handshake (SocketConnection conn, DataInputStream din) throws Error { din.read_line (); // greeting conn.output_stream.write ("{\"execute\":\"qmp_capabilities\"}\n".data); - din.read_line (); - return true; + // Skip any event that arrives before the reply, so no stale line is left behind. + string? reply = din.read_line (); + while (reply != null && !reply.contains ("\"return\"") && !reply.contains ("\"error\"")) { + reply = din.read_line (); + } + return reply != null; } /* Send one QMP command (a full JSON object) and return its reply line. @@ -269,16 +273,28 @@ public class Machine : Object { } } + private bool status_busy = false; + + /* Ask QEMU whether the guest is paused, off the main thread: a hung QEMU + * must never freeze the window while the 3 s socket timeout runs out. */ private void sync_paused () { - var reply = qmp ("query-status"); - bool now = reply != null && reply.contains ("\"status\": \"paused\""); - if (reply != null && !now) { - now = reply.contains ("\"status\":\"paused\""); - } - if (now != paused) { - paused = now; - state_changed (); + if (status_busy) { + return; } + status_busy = true; + new Thread ("ml-status", () => { + var reply = qmp ("query-status"); + bool known = reply != null; + bool now = known && (reply.contains ("\"status\": \"paused\"") || reply.contains ("\"status\":\"paused\"")); + Idle.add (() => { + status_busy = false; + if (known && now != paused) { + paused = now; + state_changed (); + } + return Source.REMOVE; + }); + }); } // ---- Unattended Omarchy: latest-version check ---- diff --git a/mseries/machine-lab/src/main.vala b/mseries/machine-lab/src/main.vala index c3875d134..1c2fb1996 100644 --- a/mseries/machine-lab/src/main.vala +++ b/mseries/machine-lab/src/main.vala @@ -111,6 +111,15 @@ public class MachineLab : Gtk.Application { application_id: "io.github.mseries.MachineLab", flags: ApplicationFlags.DEFAULT_FLAGS ); + add_main_option ("version", 'v', OptionFlags.NONE, OptionArg.NONE, "Print the version and exit", null); + } + + protected override int handle_local_options (VariantDict options) { + if (options.contains ("version")) { + print ("machine-lab %s\n", Config.VERSION); + return 0; + } + return -1; } protected override void startup () { diff --git a/mseries/machine-lab/src/page.vala b/mseries/machine-lab/src/page.vala index 386b482fb..d423504af 100644 --- a/mseries/machine-lab/src/page.vala +++ b/mseries/machine-lab/src/page.vala @@ -40,6 +40,8 @@ public class MachinePage : Gtk.Box { private Gtk.Label strip; private Gtk.Box creds_card; private Gtk.Label creds_label; + private string creds_real = ""; + private bool creds_shown = false; private Gtk.Box ready_card; private Gtk.Image ready_icon; private Gtk.Label ready_title; @@ -158,6 +160,20 @@ public class MachinePage : Gtk.Box { toast (free >= 0 ? "Checked again: %d GiB free".printf ((int) free) : "Checked again"); } + /* Passwords stay hidden until asked for: "Password: abc" becomes "Password: ••••••". */ + public static string mask_credentials (string text) { + try { + var re = new Regex ("(?i)((?:sudo )?password:\\s*)(\\S+)"); + return re.replace_eval (text, -1, 0, 0, (m, sb) => { + sb.append (m.fetch (1)); + sb.append ("••••••••"); + return false; + }); + } catch (RegexError e) { + return "••••••••"; + } + } + public void toggle_fullscreen () { if (stack.visible_child_name != "display") { show_tab ("display"); @@ -277,10 +293,20 @@ public class MachinePage : Gtk.Box { ccopy.valign = Gtk.Align.CENTER; ccopy.tooltip_text = "Copy sign-in"; ccopy.clicked.connect (() => { - Gdk.Display.get_default ().get_clipboard ().set_text (creds_label.label); + Gdk.Display.get_default ().get_clipboard ().set_text (creds_real); toast ("Copied sign-in"); }); creds_card.append (ccopy); + var creveal = new Gtk.ToggleButton (); + creveal.icon_name = "view-reveal-symbolic"; + creveal.add_css_class ("flat"); + creveal.valign = Gtk.Align.CENTER; + creveal.tooltip_text = "Show or hide the password"; + creveal.toggled.connect (() => { + creds_shown = creveal.active; + creds_label.label = creds_shown ? creds_real : mask_credentials (creds_real); + }); + creds_card.append (creveal); clamp.append (creds_card); var specs = new Gtk.Grid (); @@ -765,7 +791,10 @@ public class MachinePage : Gtk.Box { strip.label = machine.status_text; var creds = machine.credentials (); creds_card.visible = creds != null; - if (creds != null) creds_label.label = creds; + if (creds != null) { + creds_real = creds; + creds_label.label = creds_shown ? creds : mask_credentials (creds); + } hero_sub.label = "ARM64 · " + (managed ? (fresh ? (machine.installer != null ? "Imported from mseries-vm · installer ready: %s".printf (Path.get_basename (machine.installer)) : "Imported from mseries-vm · installer not finished") : "Installed · imported from mseries-vm") : fresh ? "New machine · not installed yet" : "Linux ARM64 · installed"); diff --git a/mseries/machine-lab/tests/fake-qmp.py b/mseries/machine-lab/tests/fake-qmp.py new file mode 100644 index 000000000..5ba515ba3 --- /dev/null +++ b/mseries/machine-lab/tests/fake-qmp.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""A tiny QMP server for tests: argv[1] = socket path, argv[2] = file that receives the connection count.""" +import json, os, socket, sys, threading + +path, counter = sys.argv[1], sys.argv[2] +if os.path.exists(path): + os.unlink(path) +srv = socket.socket(socket.AF_UNIX) +srv.bind(path) +srv.listen(8) +connections = 0 +lock = threading.Lock() + +def serve(conn): + f = conn.makefile("rw") + f.write(json.dumps({"QMP": {"version": {}, "capabilities": []}}) + "\n"); f.flush() + for line in f: + try: + cmd = json.loads(line)["execute"] + except Exception: + break + if cmd == "query-status": + reply = {"return": {"status": "paused", "running": False}} + else: + reply = {"return": {}} + if cmd != "qmp_capabilities": + # an event before the return value must be skipped by the client + f.write(json.dumps({"event": "RESUME"}) + "\n") + f.write(json.dumps(reply) + "\n"); f.flush() + conn.close() + +while True: + conn, _ = srv.accept() + with lock: + connections += 1 + with open(counter, "w") as c: + c.write(str(connections)) + threading.Thread(target=serve, args=(conn,), daemon=True).start() diff --git a/mseries/machine-lab/tests/test-machine.vala b/mseries/machine-lab/tests/test-machine.vala new file mode 100644 index 000000000..ef248c8a1 --- /dev/null +++ b/mseries/machine-lab/tests/test-machine.vala @@ -0,0 +1,157 @@ +/* Logic tests for Machine Lab: persistence, mseries-vm discovery, QMP transport. */ + +string tmp_dir; + +void write_file (string path, string text) { + DirUtils.create_with_parents (Path.get_dirname (path), 0755); + try { + FileUtils.set_contents (path, text); + } catch (Error e) { + error ("write %s: %s", path, e.message); + } +} + +void test_store_round_trip () { + var list = new GenericArray (); + var m = new Machine (); + m.id = "machine-test1"; + m.name = "Test Box"; + m.cpus = 3; + m.mem_gib = 5; + m.disk_gib = 20; + m.ssh_port = 2230; + m.color = 6; + m.installer = "/tmp/x.iso"; + m.recipe = "omarchy"; + list.add (m); + var managed = new Machine (); // managed guests are rediscovered, never saved + managed.id = "vm-omarchy"; + managed.kind = "managed"; + managed.vm_name = "omarchy"; + list.add (managed); + MachineStore.save (list); + + var back = MachineStore.load (); + assert_cmpint (back.length, CompareOperator.EQ, 1); + assert_cmpstr (back[0].name, CompareOperator.EQ, "Test Box"); + assert_cmpint (back[0].cpus, CompareOperator.EQ, 3); + assert_cmpint (back[0].mem_gib, CompareOperator.EQ, 5); + assert_cmpint (back[0].disk_gib, CompareOperator.EQ, 20); + assert_cmpint (back[0].ssh_port, CompareOperator.EQ, 2230); + assert_cmpint (back[0].color, CompareOperator.EQ, 6); + assert_cmpstr (back[0].installer, CompareOperator.EQ, "/tmp/x.iso"); + assert_cmpstr (back[0].recipe, CompareOperator.EQ, "omarchy"); +} + +void test_discovery () { + var vms = Path.build_filename (tmp_dir, "vms"); + write_file (Path.build_filename (vms, "omarchy", "vm.conf"), + "# comment\nGUEST=omarchy\nCPUS=4\nMEMORY_MB=4096\nISO=\nBOOT=disk\n"); + write_file (Path.build_filename (vms, "elementary", "vm.conf"), + "GUEST=elementary\nCPUS=2\nMEMORY_MB=3000\nISO=/x/y/elementaryos.iso\n"); + DirUtils.create_with_parents (Path.build_filename (vms, "downloads"), 0755); // not a VM + Environment.set_variable ("MSERIES_VM_DIR", vms, true); + + var list = new GenericArray (); + MachineStore.discover (list); + assert_cmpint (list.length, CompareOperator.EQ, 2); // downloads/ is ignored + + Machine? omarchy = null, elem = null; + foreach (var m in list.data) { + if (m.vm_name == "omarchy") omarchy = m; + if (m.vm_name == "elementary") elem = m; + } + assert_nonnull (omarchy); + assert_nonnull (elem); + assert_true (omarchy.managed); + assert_cmpstr (omarchy.name, CompareOperator.EQ, "Omarchy"); + assert_cmpint (omarchy.cpus, CompareOperator.EQ, 4); + assert_cmpint (omarchy.mem_gib, CompareOperator.EQ, 4); + assert_null (omarchy.installer); + assert_cmpint (elem.mem_gib, CompareOperator.EQ, 3); // 3000 MiB rounds to 3 GiB + assert_cmpstr (elem.installer, CompareOperator.EQ, "/x/y/elementary.iso".replace ("elementary.iso", "elementaryos.iso")); + assert_true (omarchy.is_fresh ()); // no real disk yet + assert_false (omarchy.installed ()); +} + +void test_installed_needs_real_data () { + var m = new Machine (); + m.id = "machine-disk"; + assert_false (m.installed ()); + write_file (Path.build_filename (m.dir, "disk.qcow2"), "tiny"); // a placeholder is not an install + assert_true (m.has_disk ()); + assert_false (m.installed ()); + assert_true (m.is_fresh ()); +} + +void test_free_space () { + assert_true (Readiness.free_gib () >= 0); +} + +void test_mask_is_page_level () { + // masking lives in MachinePage; here we just make sure the credentials file is read and trimmed + var m = new Machine (); + m.id = "machine-creds"; + write_file (Path.build_filename (m.dir, "credentials.txt"), "Username: a\nPassword: b\n"); + assert_cmpstr (m.credentials (), CompareOperator.EQ, "Username: a · Password: b"); +} + +void test_qmp_transport () { + var m = new Machine (); + m.id = "machine-qmp"; + var dir = m.dir; + DirUtils.create_with_parents (dir, 0755); + var counter_ctl = Path.build_filename (tmp_dir, "ctl.count"); + var counter_view = Path.build_filename (tmp_dir, "view.count"); + string[] a = { "python3", "-I", Environment.get_variable ("FAKE_QMP"), Path.build_filename (dir, "qmp.sock"), counter_ctl }; + string[] b = { "python3", "-I", Environment.get_variable ("FAKE_QMP"), Path.build_filename (dir, "qmp-view.sock"), counter_view }; + Subprocess ctl, view; + try { + ctl = new Subprocess.newv (a, SubprocessFlags.NONE); + view = new Subprocess.newv (b, SubprocessFlags.NONE); + } catch (Error e) { + error ("%s", e.message); + } + for (int i = 0; i < 100 && !(FileUtils.test (Path.build_filename (dir, "qmp.sock"), FileTest.EXISTS) + && FileUtils.test (Path.build_filename (dir, "qmp-view.sock"), FileTest.EXISTS)); i++) { + Thread.usleep (50000); + } + + // Control calls hang up after each command (so mseries-vm can connect between them) ... + var r1 = m.qmp_raw ("{\"execute\":\"query-status\"}"); + var r2 = m.qmp_raw ("{\"execute\":\"query-status\"}"); + assert_nonnull (r1); + assert_true (r1.contains ("paused")); // the RESUME event before it was skipped + assert_nonnull (r2); + string c; + try { FileUtils.get_contents (counter_ctl, out c); } catch (Error e) { c = ""; } + assert_cmpstr (c, CompareOperator.EQ, "2"); + + // ... while the display keeps one connection on its own socket. + m.qmp_raw ("{\"execute\":\"screendump\"}", true); + m.qmp_raw ("{\"execute\":\"screendump\"}", true); + m.qmp_raw ("{\"execute\":\"screendump\"}", true); + try { FileUtils.get_contents (counter_view, out c); } catch (Error e) { c = ""; } + assert_cmpstr (c, CompareOperator.EQ, "1"); + + ctl.force_exit (); + view.force_exit (); +} + +int main (string[] args) { + Test.init (ref args); + try { + tmp_dir = DirUtils.make_tmp ("machine-lab-test-XXXXXX"); + } catch (Error e) { + error ("tmp: %s", e.message); + } + Environment.set_variable ("XDG_CONFIG_HOME", Path.build_filename (tmp_dir, "config"), true); + Environment.set_variable ("XDG_DATA_HOME", Path.build_filename (tmp_dir, "data"), true); + Test.add_func ("/store/round-trip", test_store_round_trip); + Test.add_func ("/discovery/mseries-vm", test_discovery); + Test.add_func ("/machine/installed-needs-real-data", test_installed_needs_real_data); + Test.add_func ("/readiness/free-space", test_free_space); + Test.add_func ("/machine/credentials", test_mask_is_page_level); + Test.add_func ("/qmp/transport", test_qmp_transport); + return Test.run (); +} From 63eb4b8e6da21d50debefa5ea7522efd96e4901f Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 17:40:54 -0400 Subject: [PATCH 212/798] rust session: force GL renderer for Wingpanel/Dock, add unit checks Co-Authored-By: Claude Sonnet 5.5 --- mseries/OS9-HANDOFF.md | 4 +++ .../session/README.md | 14 +++++++++++ .../lib/systemd/user/pantheon-dock.service | 2 ++ .../systemd/user/pantheon-wingpanel.service | 4 +++ .../test-session-units.sh | 25 +++++++++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 mseries/pantheon-rust-compositor/test-session-units.sh diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index 01b084747..a997cb14a 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -6853,3 +6853,7 @@ gtk4 feature `v4_20` (granite-rs does not enable `v4_10`, so `gtk::Accessible` i `mseries/patches/wingpanel-export-gir.patch` makes Wingpanel export `Wingpanel-9.gir` + typelib; built and loaded in a sandbox, not installed. All upstream pins and update steps: `mseries/RUST-UPSTREAM-MAINTENANCE.md`. Next: an opt-in Rust login session entry once the compositor has DRM/KMS, input and lock screen. + +## Rust session shell clients: renderer fix — 2026-10-19 + +Native Rust-session Wingpanel looped on Vulkan `SURFACE_LOST` (compositor has no `linux-dmabuf`), spinning CPU and holding 2.3 GB; Dock was skipped only by the owner's opt-out marker. Reproduced in a private nested Xvfb run; `GSK_RENDERER=gl` removed it. Added to both shell units plus `mseries/pantheon-rust-compositor/test-session-units.sh` (passes). Not verified: native panel visibility, Dock render. Next: implement `linux-dmabuf` in the compositor, then a native login with the new package (needs owner OK). diff --git a/mseries/pantheon-rust-compositor/session/README.md b/mseries/pantheon-rust-compositor/session/README.md index 43fda3218..ac9ea3c58 100644 --- a/mseries/pantheon-rust-compositor/session/README.md +++ b/mseries/pantheon-rust-compositor/session/README.md @@ -90,3 +90,17 @@ client. Authentication, D-Bus activation and session lock/unlock have not yet been runtime-qualified. The compositor advertises fractional-scale and viewporter globals, but rendering quality and moving windows between outputs with different scales still need nested-session qualification. + +## Shell client renderer (2026-10-19) + +The compositor advertises `wl_shm` but not `linux-dmabuf`. In the native +2026-10-19 login Wingpanel ran but looped on `VK_ERROR_SURFACE_LOST_KHR` +(GTK4's Vulkan renderer) and used about 2.5 CPU-minutes in 5 minutes with a +2.3 GB peak, which matches the "panel not visible, startup slow" reports. The +Dock did not start only because the owner's `user-disabled` marker was set. +In a private nested Xvfb run of the release compositor the default renderer +reproduced the Vulkan warnings and `GSK_RENDERER=gl` did not. Both shell +units now set `GSK_RENDERER=gl`. `../test-session-units.sh` statically checks +the ordering, restart and renderer settings. Native visibility after this +change is NOT yet verified; the proper long-term fix is `linux-dmabuf` in the +compositor. No native session was started for this change. diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service index d97bc1996..bb234ad94 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-dock.service @@ -9,6 +9,8 @@ StartLimitBurst=3 [Service] Type=simple +# See pantheon-wingpanel.service: no linux-dmabuf, so avoid GTK4's Vulkan renderer. +Environment=GSK_RENDERER=gl # Respect the local user's opt-out even though this unit launches the distro # binary directly rather than resolving the per-user Dock wrapper through PATH. ExecCondition=/usr/bin/test ! -e %h/.config/mseries/dock/user-disabled diff --git a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service index 23b9eb0db..9f8e37d96 100644 --- a/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service +++ b/mseries/pantheon-rust-compositor/session/usr/lib/systemd/user/pantheon-wingpanel.service @@ -9,6 +9,10 @@ StartLimitBurst=3 [Service] Type=simple +# The compositor advertises wl_shm only (no linux-dmabuf), so GTK4's default +# Vulkan renderer logs VK_ERROR_SURFACE_LOST_KHR in a loop and spins the CPU +# (observed natively 2026-10-19: 2.5 min CPU in 5 min, 2.3G peak). Use GL. +Environment=GSK_RENDERER=gl ExecStart=/usr/bin/io.elementary.wingpanel Restart=on-failure RestartSec=2s diff --git a/mseries/pantheon-rust-compositor/test-session-units.sh b/mseries/pantheon-rust-compositor/test-session-units.sh new file mode 100644 index 000000000..a72978b58 --- /dev/null +++ b/mseries/pantheon-rust-compositor/test-session-units.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Static checks for the experimental Rust session units. Headless; touches no +# running session. Run from anywhere: sh test-session-units.sh +set -eu +d=$(cd "$(dirname "$0")" && pwd)/session/usr/lib/systemd/user +fail=0 +check() { if ! grep -q -- "$2" "$d/$1"; then echo "FAIL $1: missing '$2'" >&2; fail=1; fi; } +for u in pantheon-wingpanel.service pantheon-dock.service; do + check $u 'GSK_RENDERER=gl' + check $u 'BindsTo=pantheon-rust-compositor@wayland.service' + check $u 'After=pantheon-rust-compositor@wayland.service' + check $u 'Restart=on-failure' +done +check pantheon-dock.service 'pantheon-wingpanel.service' +check pantheon-dock.service 'user-disabled' +check 'pantheon-rust-compositor@.service' 'Type=notify' +check 'pantheon-rust-compositor@.service' 'dbus-update-activation-environment --systemd WAYLAND_DISPLAY' +check 'gnome-session@pantheon-rust-wayland.target.d/session.conf' 'Wants=pantheon-wingpanel.service' +check 'gnome-session@pantheon-rust-wayland.target.d/session.conf' 'Wants=pantheon-dock.service' +if command -v systemd-analyze >/dev/null 2>&1; then + systemd-analyze --user verify "$d"/pantheon-wingpanel.service "$d"/pantheon-dock.service 2>&1 \ + | grep -v -E 'Failed to (prepare|load)|not found|Command .* is not executable|No such file' || true +fi +[ $fail -eq 0 ] && echo "session unit checks passed" +exit $fail From c3374d2fbf05556d115fa8f1214bdd78bc2bf516 Mon Sep 17 00:00:00 2001 From: sdolbec Date: Mon, 19 Oct 2026 17:43:26 -0400 Subject: [PATCH 213/798] repo: stop tracking rust/target build output (377 files, 33 MB) Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 3 +++ rust/target/.rustc_info.json | 1 - rust/target/CACHEDIR.TAG | 3 --- rust/target/debug/.cargo-artifact-lock | 0 rust/target/debug/.cargo-build-lock | 0 rust/target/debug/.cargo-lock | 0 .../bin-mseries-power | 1 - .../bin-mseries-power.json | 1 - .../dep-bin-mseries-power | Bin 31 -> 0 bytes .../invoked.timestamp | 1 - .../dep-test-bin-mseries-power | Bin 31 -> 0 bytes .../invoked.timestamp | 1 - .../test-bin-mseries-power | 1 - .../test-bin-mseries-power.json | 1 - .../debug/deps/mseries_power-294d937b3ff57b29 | Bin 6021424 -> 0 bytes .../deps/mseries_power-294d937b3ff57b29.d | 5 ----- .../debug/deps/mseries_power-9d7f16b60ec457ca | Bin 7739552 -> 0 bytes .../deps/mseries_power-9d7f16b60ec457ca.d | 5 ----- .../10x8nvn5dq401zdwk5povh5qb.o | Bin 39712 -> 0 bytes .../2azzlnj9lcwn5ussj4k509lg2.o | Bin 6696 -> 0 bytes .../2cccf07ei5hov5gjgghspm9y1.o | Bin 7352 -> 0 bytes .../2d9frb2883jrxfy279xh3pl4s.o | Bin 6176 -> 0 bytes .../2fdcx0yjk2vkqn2m5q74ufuak.o | Bin 3920 -> 0 bytes .../2gpkqfldxkuio73y4ehyb5nhs.o | Bin 6328 -> 0 bytes .../2nccq9kbm0rz7ggewn0bgco7z.o | Bin 7448 -> 0 bytes .../2q01ihejgoo8tfp1lbtgvhxg7.o | Bin 23200 -> 0 bytes .../2zpvdapnocza1pcwwpraxs4hl.o | Bin 4480 -> 0 bytes .../3a2biat8o2ipzm94crnuk9d0u.o | Bin 7896 -> 0 bytes .../3fo12pgkvzb4c9xq7nzfg80al.o | Bin 8696 -> 0 bytes .../3wamjh6kyjlbwphl8yhk9s2yp.o | Bin 7504 -> 0 bytes .../3wq40gdpo69x4qzh68wzc9tvj.o | Bin 17784 -> 0 bytes .../41ac833mmeb7fpnkqmrfcik81.o | Bin 3520 -> 0 bytes .../44ot25ar6kwvhpbx8nrnudzfr.o | Bin 7048 -> 0 bytes .../46rf91gocvv7o6eczh2p6ukil.o | Bin 5952 -> 0 bytes .../4h2qgqpv5i614ztgdlvobit66.o | Bin 16672 -> 0 bytes .../4j5utlbc3hfka3qdsh54rmzh5.o | Bin 5664 -> 0 bytes .../5ewtmaxmfcs9pui1g2sptvul8.o | Bin 15120 -> 0 bytes .../5girxfu4qt7c894jthjn36zm1.o | Bin 28736 -> 0 bytes .../5imbcjufph3656fcmourp62cb.o | Bin 62384 -> 0 bytes .../5wi1yab3i75x2dr8zj3muzuhc.o | Bin 10816 -> 0 bytes .../60m4vhwpthnvbvq8itie7uwpl.o | Bin 6008 -> 0 bytes .../68wzfrghjbeo6skkn3r8i631s.o | Bin 71200 -> 0 bytes .../6ado08uumhlq5geecve9c66il.o | Bin 3016 -> 0 bytes .../6me4x78sc9ff47cxae78a5tkr.o | Bin 9784 -> 0 bytes .../6qku8vf188ft13vp3ae9trxoz.o | Bin 4208 -> 0 bytes .../6zqwqmnsin97x9ud9kultqn74.o | Bin 7800 -> 0 bytes .../704yuchmbsq3z10b8ad6rpil2.o | Bin 30280 -> 0 bytes .../7d1mm9gtelp7vq7wmggxisrx6.o | Bin 23392 -> 0 bytes .../840fyjzc0i7hd224imhx1mmwh.o | Bin 12992 -> 0 bytes .../87izbdw6f1ffrhvp4wy75g5gr.o | Bin 11984 -> 0 bytes .../8b8hvn5p2dihbfush28g83aa9.o | Bin 18104 -> 0 bytes .../8gbwdukscqelemw0yolx6u6et.o | Bin 29704 -> 0 bytes .../8lew9bafv638l5dn92cg5oodg.o | Bin 17904 -> 0 bytes .../8zg9aija18c8mt4wrmahsya9o.o | Bin 4544 -> 0 bytes .../97doqyuuiacfb4tvjcrn9emng.o | Bin 8960 -> 0 bytes .../9ikc8fo0tvprc1wxxivwpdgap.o | Bin 42392 -> 0 bytes .../9yyku7ukwjrynvu1vjhf8aht2.o | Bin 3408 -> 0 bytes .../a1om9ehllhlvfd7jvs289snil.o | Bin 8256 -> 0 bytes .../a82hkarz3t22jsw03phugz2wu.o | Bin 4512 -> 0 bytes .../anyulz50qmy0445vo1knztbvh.o | Bin 27240 -> 0 bytes .../azvvzqa5pnztbpptpdadj0olr.o | Bin 8600 -> 0 bytes .../azw78vcudgtqru7w7r5dh6awn.o | Bin 11768 -> 0 bytes .../b1r8lzvjligsabwv1c6ntfx0p.o | Bin 5080 -> 0 bytes .../b7nwktnrqxguxy98xoanwdrap.o | Bin 58632 -> 0 bytes .../bfoswa43f6hwx1y6kfludgvxg.o | Bin 7120 -> 0 bytes .../bo1ok6saob1xhzblhnttvnncz.o | Bin 26936 -> 0 bytes .../boiczuds0mj59y5r84eqetsxq.o | Bin 9424 -> 0 bytes .../bsvfripba9icr5qz2u5zg5h0m.o | Bin 11880 -> 0 bytes .../byl0mlum385rq9aom17jxk91c.o | Bin 6912 -> 0 bytes .../c019r3gqw4o5fl6dc2fx70hzs.o | Bin 20536 -> 0 bytes .../c6axg94ukdzxv8ts46hpg1zzi.o | Bin 6184 -> 0 bytes .../cdok9ezgq8ea7w3inr9y8dlfe.o | Bin 3920 -> 0 bytes .../csb7wgyppcw96n9tmca2b9s7n.o | Bin 5288 -> 0 bytes .../d8ay50isk3dltfglpdnalxgca.o | Bin 18024 -> 0 bytes .../dep-graph.bin | Bin 1344247 -> 0 bytes .../dg915w50ryxp9n6z4iv3exinm.o | Bin 45840 -> 0 bytes .../dkllhp2pu53acvi38uvx6thcg.o | Bin 28136 -> 0 bytes .../drarw4rwpu9autl9fb3qmz8lb.o | Bin 6048 -> 0 bytes .../due8pbjt3fvkragiogvie0kuw.o | Bin 14920 -> 0 bytes .../dztx8djv7fhyi8qdqu4d69ezc.o | Bin 5392 -> 0 bytes .../e2t7chvfa4rv5tusg6yvguydf.o | Bin 5160 -> 0 bytes .../ew47g6a1h4hfszxkrknj84720.o | Bin 9512 -> 0 bytes .../exsklydkt1d886fuzawl5nojy.o | Bin 5568 -> 0 bytes .../query-cache.bin | Bin 234172 -> 0 bytes .../work-products.bin | Bin 4914 -> 0 bytes .../s-hn7refpkvv-1kar2kn.lock | 0 .../10x8nvn5dq401zdwk5povh5qb.o | Bin 39712 -> 0 bytes .../2azzlnj9lcwn5ussj4k509lg2.o | Bin 6696 -> 0 bytes .../2cccf07ei5hov5gjgghspm9y1.o | Bin 7352 -> 0 bytes .../2d9frb2883jrxfy279xh3pl4s.o | Bin 6176 -> 0 bytes .../2fdcx0yjk2vkqn2m5q74ufuak.o | Bin 3920 -> 0 bytes .../2gpkqfldxkuio73y4ehyb5nhs.o | Bin 6328 -> 0 bytes .../2nccq9kbm0rz7ggewn0bgco7z.o | Bin 7448 -> 0 bytes .../2q01ihejgoo8tfp1lbtgvhxg7.o | Bin 23200 -> 0 bytes .../2zpvdapnocza1pcwwpraxs4hl.o | Bin 4480 -> 0 bytes .../3a2biat8o2ipzm94crnuk9d0u.o | Bin 7896 -> 0 bytes .../3fo12pgkvzb4c9xq7nzfg80al.o | Bin 8696 -> 0 bytes .../3wamjh6kyjlbwphl8yhk9s2yp.o | Bin 7504 -> 0 bytes .../3wq40gdpo69x4qzh68wzc9tvj.o | Bin 17784 -> 0 bytes .../41ac833mmeb7fpnkqmrfcik81.o | Bin 3520 -> 0 bytes .../44ot25ar6kwvhpbx8nrnudzfr.o | Bin 7048 -> 0 bytes .../46rf91gocvv7o6eczh2p6ukil.o | Bin 5952 -> 0 bytes .../4h2qgqpv5i614ztgdlvobit66.o | Bin 16672 -> 0 bytes .../4j5utlbc3hfka3qdsh54rmzh5.o | Bin 5664 -> 0 bytes .../5ewtmaxmfcs9pui1g2sptvul8.o | Bin 15120 -> 0 bytes .../5girxfu4qt7c894jthjn36zm1.o | Bin 28736 -> 0 bytes .../5imbcjufph3656fcmourp62cb.o | Bin 62384 -> 0 bytes .../5wi1yab3i75x2dr8zj3muzuhc.o | Bin 10816 -> 0 bytes .../60m4vhwpthnvbvq8itie7uwpl.o | Bin 6008 -> 0 bytes .../68wzfrghjbeo6skkn3r8i631s.o | Bin 71200 -> 0 bytes .../6ado08uumhlq5geecve9c66il.o | Bin 3016 -> 0 bytes .../6me4x78sc9ff47cxae78a5tkr.o | Bin 9784 -> 0 bytes .../6qku8vf188ft13vp3ae9trxoz.o | Bin 4208 -> 0 bytes .../6zqwqmnsin97x9ud9kultqn74.o | Bin 7800 -> 0 bytes .../704yuchmbsq3z10b8ad6rpil2.o | Bin 30280 -> 0 bytes .../7d1mm9gtelp7vq7wmggxisrx6.o | Bin 23392 -> 0 bytes .../840fyjzc0i7hd224imhx1mmwh.o | Bin 12992 -> 0 bytes .../87izbdw6f1ffrhvp4wy75g5gr.o | Bin 11984 -> 0 bytes .../8b8hvn5p2dihbfush28g83aa9.o | Bin 18104 -> 0 bytes .../8gbwdukscqelemw0yolx6u6et.o | Bin 29704 -> 0 bytes .../8lew9bafv638l5dn92cg5oodg.o | Bin 17904 -> 0 bytes .../8zg9aija18c8mt4wrmahsya9o.o | Bin 4544 -> 0 bytes .../97doqyuuiacfb4tvjcrn9emng.o | Bin 8960 -> 0 bytes .../9ikc8fo0tvprc1wxxivwpdgap.o | Bin 42392 -> 0 bytes .../9yyku7ukwjrynvu1vjhf8aht2.o | Bin 3408 -> 0 bytes .../a1om9ehllhlvfd7jvs289snil.o | Bin 8256 -> 0 bytes .../a82hkarz3t22jsw03phugz2wu.o | Bin 4512 -> 0 bytes .../anyulz50qmy0445vo1knztbvh.o | Bin 27240 -> 0 bytes .../azvvzqa5pnztbpptpdadj0olr.o | Bin 8600 -> 0 bytes .../azw78vcudgtqru7w7r5dh6awn.o | Bin 11768 -> 0 bytes .../b1r8lzvjligsabwv1c6ntfx0p.o | Bin 5080 -> 0 bytes .../b7nwktnrqxguxy98xoanwdrap.o | Bin 58632 -> 0 bytes .../bfoswa43f6hwx1y6kfludgvxg.o | Bin 7120 -> 0 bytes .../bo1ok6saob1xhzblhnttvnncz.o | Bin 26936 -> 0 bytes .../boiczuds0mj59y5r84eqetsxq.o | Bin 9424 -> 0 bytes .../bsvfripba9icr5qz2u5zg5h0m.o | Bin 11880 -> 0 bytes .../byl0mlum385rq9aom17jxk91c.o | Bin 6912 -> 0 bytes .../c019r3gqw4o5fl6dc2fx70hzs.o | Bin 20536 -> 0 bytes .../c6axg94ukdzxv8ts46hpg1zzi.o | Bin 6184 -> 0 bytes .../cdok9ezgq8ea7w3inr9y8dlfe.o | Bin 3920 -> 0 bytes .../csb7wgyppcw96n9tmca2b9s7n.o | Bin 5288 -> 0 bytes .../d8ay50isk3dltfglpdnalxgca.o | Bin 18024 -> 0 bytes .../dep-graph.bin | Bin 1354124 -> 0 bytes .../dg915w50ryxp9n6z4iv3exinm.o | Bin 45840 -> 0 bytes .../dkllhp2pu53acvi38uvx6thcg.o | Bin 28136 -> 0 bytes .../drarw4rwpu9autl9fb3qmz8lb.o | Bin 6048 -> 0 bytes .../due8pbjt3fvkragiogvie0kuw.o | Bin 14920 -> 0 bytes .../dztx8djv7fhyi8qdqu4d69ezc.o | Bin 5392 -> 0 bytes .../e2t7chvfa4rv5tusg6yvguydf.o | Bin 5160 -> 0 bytes .../ew47g6a1h4hfszxkrknj84720.o | Bin 9512 -> 0 bytes .../exsklydkt1d886fuzawl5nojy.o | Bin 5568 -> 0 bytes .../query-cache.bin | Bin 190812 -> 0 bytes .../work-products.bin | Bin 4914 -> 0 bytes .../s-hn7rel4h2r-1t6eavu.lock | 0 .../03kjwtmc7yax8vn57rhruhhdr.o | Bin 24344 -> 0 bytes .../0b499gwyqwqux5812dgfaa7z4.o | Bin 58608 -> 0 bytes .../0n60xpl27jsuyditxmnz0l15d.o | Bin 19552 -> 0 bytes .../165kglrc8usf4pz7y1gtjwnkk.o | Bin 16672 -> 0 bytes .../1az35se847r0zseb307pvn13y.o | Bin 7896 -> 0 bytes .../1cvcvko4cmyho8ywk5bl5000g.o | Bin 63368 -> 0 bytes .../1jjx8vh3pc9i7jxira9q7uwsv.o | Bin 25792 -> 0 bytes .../1nqfhhchy8tlzjvmjhfuwh69e.o | Bin 3904 -> 0 bytes .../1qzazooqx053t0r0yo2wgn688.o | Bin 12304 -> 0 bytes .../1v6enc65pfs1awc4japyjrc55.o | Bin 13256 -> 0 bytes .../1vf5rbu0jm5op0f1sizlyd5t2.o | Bin 4040 -> 0 bytes .../1wyn0qsnlqm7o3m9ha05dvan4.o | Bin 23704 -> 0 bytes .../1x1exoo6hu87j8jfikdwj7p4k.o | Bin 45752 -> 0 bytes .../2422xm0r1pspvggse9vs1g8ce.o | Bin 23272 -> 0 bytes .../2gi5ahrmeskpo0sb0xsp9tfnd.o | Bin 5528 -> 0 bytes .../2ideixffa8vi4c6s4gl8jco4r.o | Bin 12432 -> 0 bytes .../2ifj7pslh4do4vylckqs1cj5u.o | Bin 7560 -> 0 bytes .../2iz6x1qg2xkfvnhvjikj2wr5o.o | Bin 6048 -> 0 bytes .../33cnorz13bl4r2un66huehf0l.o | Bin 18768 -> 0 bytes .../35xnbvctn2h6owv3ik8m4wfgu.o | Bin 42824 -> 0 bytes .../3bjazqv9jm82aurbw1w9z3epl.o | Bin 7448 -> 0 bytes .../3lvs8coezqnfbbzikduuq07t0.o | Bin 92168 -> 0 bytes .../3p7a7ej3sqqpvqm9oxdzv7td2.o | Bin 14896 -> 0 bytes .../3vyjxflez4in39pn3q7tabs82.o | Bin 79824 -> 0 bytes .../423fsg8fb8pa67p50luej04h8.o | Bin 32976 -> 0 bytes .../495jl6b5p77dfchp3ys7nant8.o | Bin 23200 -> 0 bytes .../4b5noahpofdhd2834db0fnbep.o | Bin 172824 -> 0 bytes .../4ho4ky8atc4gsk5qoubnfguf3.o | Bin 345616 -> 0 bytes .../4xw1cxm5qmxkvd6bsn590v8dd.o | Bin 32792 -> 0 bytes .../57hywvwmol0uhlmooyagmpwld.o | Bin 8960 -> 0 bytes .../5ccmf9v8jzan14y216nx41fq0.o | Bin 8896 -> 0 bytes .../5l3gl9unmy73kh4bdgre1jyla.o | Bin 17336 -> 0 bytes .../5m96ryj150z5bivoyfzbduhzv.o | Bin 6176 -> 0 bytes .../5nlnke7h8yxy3ytbho04ot8k0.o | Bin 11456 -> 0 bytes .../5tzexitnzwrd8t8056d2zolf5.o | Bin 11984 -> 0 bytes .../6a6qtsx8d86uk89xc5fjmace4.o | Bin 4512 -> 0 bytes .../6dh1yslmt4dylq6qncge9ijd3.o | Bin 31144 -> 0 bytes .../6rn2lr41hqqgzzbgirmy8069m.o | Bin 10688 -> 0 bytes .../71pamy1ufxy060ecn6089jtpr.o | Bin 30280 -> 0 bytes .../72d8z1tnma6ffywh45zehmx6e.o | Bin 24280 -> 0 bytes .../7bchabb32db7c4bdtx08vnpqn.o | Bin 22320 -> 0 bytes .../7bme6aastxavdo7i8t3wza5nk.o | Bin 7800 -> 0 bytes .../7evwnvlpz4oqzn3ehi4o8sv1w.o | Bin 13568 -> 0 bytes .../7ge0nk5heubf0pvup0iru88cc.o | Bin 11904 -> 0 bytes .../7gephzwxek3uyhufcjliwpchf.o | Bin 49912 -> 0 bytes .../7j7pq7ew5nbvfinckzrfkcx6n.o | Bin 9424 -> 0 bytes .../7jqv6mzxtkmpwnab0nmme41bb.o | Bin 20048 -> 0 bytes .../87ly0zmi74okrryc60aq49cb2.o | Bin 48448 -> 0 bytes .../886rreqf7c6smnu3ak3pvhvyc.o | Bin 3872 -> 0 bytes .../8a1otvsszllxbyl0fct25fmxv.o | Bin 5952 -> 0 bytes .../8dk1wf5jwlc1lqogvp79ugb0y.o | Bin 7120 -> 0 bytes .../8i0jsh61u80h4u7sw904ngah3.o | Bin 5096 -> 0 bytes .../8osdqq8x4dt3jtwtp9atk55be.o | Bin 29512 -> 0 bytes .../8osdssmye9a5vfjqqttkmcag8.o | Bin 43328 -> 0 bytes .../8u6nzvyseaa16qmc4ljw7cxtg.o | Bin 3016 -> 0 bytes .../8ukyb2z2cr9st0zravwsa2v0k.o | Bin 125920 -> 0 bytes .../8vajifhj2usrq79wp8u8fuhlc.o | Bin 12800 -> 0 bytes .../8ybwa14qu4m11686f1uvxg6on.o | Bin 5160 -> 0 bytes .../92wv7g2dhz3145i3b1njmsldo.o | Bin 25472 -> 0 bytes .../947z3ckmt30jhb1lu8rbz00vb.o | Bin 22576 -> 0 bytes .../951nxsjz4fwjjom2ncmm0wki8.o | Bin 27456 -> 0 bytes .../95xtc6scmc5om75fly1oaaycs.o | Bin 6392 -> 0 bytes .../96xbwz1fomrvo5f29j310d9ck.o | Bin 87344 -> 0 bytes .../9ffkt56lxbwdh077vyvv85tj3.o | Bin 4544 -> 0 bytes .../9kzc49jl3emt0kmianievrs7b.o | Bin 8080 -> 0 bytes .../9yyvwqbhtx67ard2n4zde6c89.o | Bin 17760 -> 0 bytes .../9z5dmlitqk4wmp1rhtohd6kyd.o | Bin 81776 -> 0 bytes .../ae6ijp4t6dwmwb214lm5f69tt.o | Bin 7352 -> 0 bytes .../aimmoi4xi8qxw9phr5rzv9xw8.o | Bin 5080 -> 0 bytes .../akh64688i3pzi9uf3rrf6zqn4.o | Bin 7504 -> 0 bytes .../atv1roznsxww59hy2g2p4dde9.o | Bin 19616 -> 0 bytes .../aul5povrwuv43o43d1600y55s.o | Bin 47312 -> 0 bytes .../ax2rvo5hal9ywxthk11wi84oq.o | Bin 27240 -> 0 bytes .../ayk2pkd8v97ecdms3n6q4ut2h.o | Bin 26936 -> 0 bytes .../b0knxmmh7z68a6l3hclxkn7zq.o | Bin 3520 -> 0 bytes .../b36hkul2ewldzgtoyja6489s8.o | Bin 12584 -> 0 bytes .../b8uieim5dxvav7flpxejuh2oa.o | Bin 58968 -> 0 bytes .../b9ozpie53smtlvntpm6216mi5.o | Bin 29184 -> 0 bytes .../bcvv2wd7k6arxhl1s11g5b7jz.o | Bin 60232 -> 0 bytes .../bo29o919t1267ycjiqvjouq68.o | Bin 5072 -> 0 bytes .../byadudatou92ne127evvb76w5.o | Bin 157392 -> 0 bytes .../c1d81mg33g4fowrs0tgsf9zxf.o | Bin 22336 -> 0 bytes .../c5sfau9to8l30w2bkar3i9av8.o | Bin 5288 -> 0 bytes .../c80wdb68nctbcm9fnxcvajkh1.o | Bin 8696 -> 0 bytes .../c8y8vmkgmjmv26ukziobwdcqs.o | Bin 17264 -> 0 bytes .../cg4rw74vec63x076g7idiz0x4.o | Bin 23536 -> 0 bytes .../cn9xvmdzoajrsuc2694yp0abg.o | Bin 10176 -> 0 bytes .../cxfdwlj2zft3yv7b7e6hzz072.o | Bin 12992 -> 0 bytes .../cz0bbj1q57z4jlh8b7fpvy9yd.o | Bin 9880 -> 0 bytes .../d1ilq3rlbkeqdkusa0x7pveyf.o | Bin 23392 -> 0 bytes .../d23p682apjcg1ij18f6fqr16t.o | Bin 30184 -> 0 bytes .../d83zdfsm572uwxjdb3r8kh6ju.o | Bin 11768 -> 0 bytes .../dbflvh1tqn8wjf1we36wjaqjb.o | Bin 8408 -> 0 bytes .../dc5srpcjpix9hxy023lo6m8pu.o | Bin 31192 -> 0 bytes .../dcxhwzmrtvcznpcnsfjia6zvo.o | Bin 25240 -> 0 bytes .../dep-graph.bin | Bin 2296484 -> 0 bytes .../dif1hebc1mr68ctg8sruvuswj.o | Bin 17488 -> 0 bytes .../dje2bc8ogxdmnajgbwa8tqgwx.o | Bin 41352 -> 0 bytes .../djo2rdncrv2i7afh6r9pkzz1k.o | Bin 63184 -> 0 bytes .../djsf8zx2ieggisprz0f54yudg.o | Bin 4416 -> 0 bytes .../dko36iog38tm2zivyp2em8v7d.o | Bin 4208 -> 0 bytes .../dunkedkvn3y8hhcujj5towql7.o | Bin 11040 -> 0 bytes .../dvbsroje81o678vk6hui77lff.o | Bin 15552 -> 0 bytes .../dzh62oz2atej1w0g80fkwifsw.o | Bin 4616 -> 0 bytes .../eaofh1wym715rc0ivgzkn21k8.o | Bin 29408 -> 0 bytes .../erwusf4rfsdsfsl3205gjt96m.o | Bin 4000 -> 0 bytes .../ew48dfwocd9jnke4o5ydyu2rd.o | Bin 6392 -> 0 bytes .../exb0lh7xtd0730kdaeelmeykx.o | Bin 10528 -> 0 bytes .../query-cache.bin | Bin 549631 -> 0 bytes .../work-products.bin | Bin 8182 -> 0 bytes .../s-hn7rehv6d8-0yqxjk8.lock | 0 .../03kjwtmc7yax8vn57rhruhhdr.o | Bin 24344 -> 0 bytes .../0b499gwyqwqux5812dgfaa7z4.o | Bin 58608 -> 0 bytes .../0n60xpl27jsuyditxmnz0l15d.o | Bin 19552 -> 0 bytes .../165kglrc8usf4pz7y1gtjwnkk.o | Bin 16672 -> 0 bytes .../1az35se847r0zseb307pvn13y.o | Bin 7896 -> 0 bytes .../1cvcvko4cmyho8ywk5bl5000g.o | Bin 63368 -> 0 bytes .../1jjx8vh3pc9i7jxira9q7uwsv.o | Bin 25792 -> 0 bytes .../1nqfhhchy8tlzjvmjhfuwh69e.o | Bin 3904 -> 0 bytes .../1qzazooqx053t0r0yo2wgn688.o | Bin 12304 -> 0 bytes .../1v6enc65pfs1awc4japyjrc55.o | Bin 13256 -> 0 bytes .../1vf5rbu0jm5op0f1sizlyd5t2.o | Bin 4040 -> 0 bytes .../1wyn0qsnlqm7o3m9ha05dvan4.o | Bin 23704 -> 0 bytes .../1x1exoo6hu87j8jfikdwj7p4k.o | Bin 45752 -> 0 bytes .../2422xm0r1pspvggse9vs1g8ce.o | Bin 23272 -> 0 bytes .../2gi5ahrmeskpo0sb0xsp9tfnd.o | Bin 5528 -> 0 bytes .../2ideixffa8vi4c6s4gl8jco4r.o | Bin 12432 -> 0 bytes .../2ifj7pslh4do4vylckqs1cj5u.o | Bin 7560 -> 0 bytes .../2iz6x1qg2xkfvnhvjikj2wr5o.o | Bin 6048 -> 0 bytes .../33cnorz13bl4r2un66huehf0l.o | Bin 18768 -> 0 bytes .../35xnbvctn2h6owv3ik8m4wfgu.o | Bin 42824 -> 0 bytes .../3bjazqv9jm82aurbw1w9z3epl.o | Bin 7448 -> 0 bytes .../3lvs8coezqnfbbzikduuq07t0.o | Bin 92168 -> 0 bytes .../3p7a7ej3sqqpvqm9oxdzv7td2.o | Bin 14896 -> 0 bytes .../3vyjxflez4in39pn3q7tabs82.o | Bin 79824 -> 0 bytes .../423fsg8fb8pa67p50luej04h8.o | Bin 32976 -> 0 bytes .../495jl6b5p77dfchp3ys7nant8.o | Bin 23200 -> 0 bytes .../4b5noahpofdhd2834db0fnbep.o | Bin 172824 -> 0 bytes .../4ho4ky8atc4gsk5qoubnfguf3.o | Bin 345616 -> 0 bytes .../4xw1cxm5qmxkvd6bsn590v8dd.o | Bin 32792 -> 0 bytes .../57hywvwmol0uhlmooyagmpwld.o | Bin 8960 -> 0 bytes .../5ccmf9v8jzan14y216nx41fq0.o | Bin 8896 -> 0 bytes .../5l3gl9unmy73kh4bdgre1jyla.o | Bin 17336 -> 0 bytes .../5m96ryj150z5bivoyfzbduhzv.o | Bin 6176 -> 0 bytes .../5nlnke7h8yxy3ytbho04ot8k0.o | Bin 11456 -> 0 bytes .../5tzexitnzwrd8t8056d2zolf5.o | Bin 11984 -> 0 bytes .../6a6qtsx8d86uk89xc5fjmace4.o | Bin 4512 -> 0 bytes .../6dh1yslmt4dylq6qncge9ijd3.o | Bin 31144 -> 0 bytes .../6rn2lr41hqqgzzbgirmy8069m.o | Bin 10688 -> 0 bytes .../71pamy1ufxy060ecn6089jtpr.o | Bin 30280 -> 0 bytes .../72d8z1tnma6ffywh45zehmx6e.o | Bin 24280 -> 0 bytes .../7bchabb32db7c4bdtx08vnpqn.o | Bin 22320 -> 0 bytes .../7bme6aastxavdo7i8t3wza5nk.o | Bin 7800 -> 0 bytes .../7evwnvlpz4oqzn3ehi4o8sv1w.o | Bin 13568 -> 0 bytes .../7ge0nk5heubf0pvup0iru88cc.o | Bin 11904 -> 0 bytes .../7gephzwxek3uyhufcjliwpchf.o | Bin 49912 -> 0 bytes .../7j7pq7ew5nbvfinckzrfkcx6n.o | Bin 9424 -> 0 bytes .../7jqv6mzxtkmpwnab0nmme41bb.o | Bin 20048 -> 0 bytes .../87ly0zmi74okrryc60aq49cb2.o | Bin 48448 -> 0 bytes .../886rreqf7c6smnu3ak3pvhvyc.o | Bin 3872 -> 0 bytes .../8a1otvsszllxbyl0fct25fmxv.o | Bin 5952 -> 0 bytes .../8dk1wf5jwlc1lqogvp79ugb0y.o | Bin 7120 -> 0 bytes .../8i0jsh61u80h4u7sw904ngah3.o | Bin 5096 -> 0 bytes .../8osdqq8x4dt3jtwtp9atk55be.o | Bin 29512 -> 0 bytes .../8osdssmye9a5vfjqqttkmcag8.o | Bin 43328 -> 0 bytes .../8u6nzvyseaa16qmc4ljw7cxtg.o | Bin 3016 -> 0 bytes .../8ukyb2z2cr9st0zravwsa2v0k.o | Bin 125920 -> 0 bytes .../8vajifhj2usrq79wp8u8fuhlc.o | Bin 12800 -> 0 bytes .../8ybwa14qu4m11686f1uvxg6on.o | Bin 5160 -> 0 bytes .../92wv7g2dhz3145i3b1njmsldo.o | Bin 25472 -> 0 bytes .../947z3ckmt30jhb1lu8rbz00vb.o | Bin 22576 -> 0 bytes .../951nxsjz4fwjjom2ncmm0wki8.o | Bin 27456 -> 0 bytes .../95xtc6scmc5om75fly1oaaycs.o | Bin 6392 -> 0 bytes .../96xbwz1fomrvo5f29j310d9ck.o | Bin 87344 -> 0 bytes .../9ffkt56lxbwdh077vyvv85tj3.o | Bin 4544 -> 0 bytes .../9kzc49jl3emt0kmianievrs7b.o | Bin 8080 -> 0 bytes .../9yyvwqbhtx67ard2n4zde6c89.o | Bin 17760 -> 0 bytes .../9z5dmlitqk4wmp1rhtohd6kyd.o | Bin 81776 -> 0 bytes .../ae6ijp4t6dwmwb214lm5f69tt.o | Bin 7352 -> 0 bytes .../aimmoi4xi8qxw9phr5rzv9xw8.o | Bin 5080 -> 0 bytes .../akh64688i3pzi9uf3rrf6zqn4.o | Bin 7504 -> 0 bytes .../atv1roznsxww59hy2g2p4dde9.o | Bin 19616 -> 0 bytes .../aul5povrwuv43o43d1600y55s.o | Bin 47312 -> 0 bytes .../ax2rvo5hal9ywxthk11wi84oq.o | Bin 27240 -> 0 bytes .../ayk2pkd8v97ecdms3n6q4ut2h.o | Bin 26936 -> 0 bytes .../b0knxmmh7z68a6l3hclxkn7zq.o | Bin 3520 -> 0 bytes .../b36hkul2ewldzgtoyja6489s8.o | Bin 12584 -> 0 bytes .../b8uieim5dxvav7flpxejuh2oa.o | Bin 58968 -> 0 bytes .../b9ozpie53smtlvntpm6216mi5.o | Bin 29184 -> 0 bytes .../bcvv2wd7k6arxhl1s11g5b7jz.o | Bin 60232 -> 0 bytes .../bo29o919t1267ycjiqvjouq68.o | Bin 5072 -> 0 bytes .../byadudatou92ne127evvb76w5.o | Bin 157392 -> 0 bytes .../c1d81mg33g4fowrs0tgsf9zxf.o | Bin 22336 -> 0 bytes .../c5sfau9to8l30w2bkar3i9av8.o | Bin 5288 -> 0 bytes .../c80wdb68nctbcm9fnxcvajkh1.o | Bin 8696 -> 0 bytes .../c8y8vmkgmjmv26ukziobwdcqs.o | Bin 17264 -> 0 bytes .../cg4rw74vec63x076g7idiz0x4.o | Bin 23536 -> 0 bytes .../cn9xvmdzoajrsuc2694yp0abg.o | Bin 10176 -> 0 bytes .../cxfdwlj2zft3yv7b7e6hzz072.o | Bin 12992 -> 0 bytes .../cz0bbj1q57z4jlh8b7fpvy9yd.o | Bin 9880 -> 0 bytes .../d1ilq3rlbkeqdkusa0x7pveyf.o | Bin 23392 -> 0 bytes .../d23p682apjcg1ij18f6fqr16t.o | Bin 30184 -> 0 bytes .../d83zdfsm572uwxjdb3r8kh6ju.o | Bin 11768 -> 0 bytes .../dbflvh1tqn8wjf1we36wjaqjb.o | Bin 8408 -> 0 bytes .../dc5srpcjpix9hxy023lo6m8pu.o | Bin 31192 -> 0 bytes .../dcxhwzmrtvcznpcnsfjia6zvo.o | Bin 25240 -> 0 bytes .../dep-graph.bin | Bin 2309159 -> 0 bytes .../dif1hebc1mr68ctg8sruvuswj.o | Bin 17488 -> 0 bytes .../dje2bc8ogxdmnajgbwa8tqgwx.o | Bin 41352 -> 0 bytes .../djo2rdncrv2i7afh6r9pkzz1k.o | Bin 63184 -> 0 bytes .../djsf8zx2ieggisprz0f54yudg.o | Bin 4416 -> 0 bytes .../dko36iog38tm2zivyp2em8v7d.o | Bin 4208 -> 0 bytes .../dunkedkvn3y8hhcujj5towql7.o | Bin 11040 -> 0 bytes .../dvbsroje81o678vk6hui77lff.o | Bin 15552 -> 0 bytes .../dzh62oz2atej1w0g80fkwifsw.o | Bin 4616 -> 0 bytes .../eaofh1wym715rc0ivgzkn21k8.o | Bin 29408 -> 0 bytes .../erwusf4rfsdsfsl3205gjt96m.o | Bin 4000 -> 0 bytes .../ew48dfwocd9jnke4o5ydyu2rd.o | Bin 6392 -> 0 bytes .../exb0lh7xtd0730kdaeelmeykx.o | Bin 10528 -> 0 bytes .../query-cache.bin | Bin 368472 -> 0 bytes .../work-products.bin | Bin 8182 -> 0 bytes .../s-hn7rel7zuu-0blvnl3.lock | 0 rust/target/debug/mseries-power | Bin 6021424 -> 0 bytes rust/target/debug/mseries-power.d | 1 - 378 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 rust/target/.rustc_info.json delete mode 100644 rust/target/CACHEDIR.TAG delete mode 100644 rust/target/debug/.cargo-artifact-lock delete mode 100644 rust/target/debug/.cargo-build-lock delete mode 100644 rust/target/debug/.cargo-lock delete mode 100644 rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power delete mode 100644 rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power.json delete mode 100644 rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/dep-bin-mseries-power delete mode 100644 rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/invoked.timestamp delete mode 100644 rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/dep-test-bin-mseries-power delete mode 100644 rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/invoked.timestamp delete mode 100644 rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power delete mode 100644 rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power.json delete mode 100755 rust/target/debug/deps/mseries_power-294d937b3ff57b29 delete mode 100644 rust/target/debug/deps/mseries_power-294d937b3ff57b29.d delete mode 100755 rust/target/debug/deps/mseries_power-9d7f16b60ec457ca delete mode 100644 rust/target/debug/deps/mseries_power-9d7f16b60ec457ca.d delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/10x8nvn5dq401zdwk5povh5qb.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2azzlnj9lcwn5ussj4k509lg2.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2cccf07ei5hov5gjgghspm9y1.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2d9frb2883jrxfy279xh3pl4s.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2fdcx0yjk2vkqn2m5q74ufuak.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2gpkqfldxkuio73y4ehyb5nhs.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2nccq9kbm0rz7ggewn0bgco7z.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2q01ihejgoo8tfp1lbtgvhxg7.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/2zpvdapnocza1pcwwpraxs4hl.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/3a2biat8o2ipzm94crnuk9d0u.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/3fo12pgkvzb4c9xq7nzfg80al.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/3wamjh6kyjlbwphl8yhk9s2yp.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/3wq40gdpo69x4qzh68wzc9tvj.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/41ac833mmeb7fpnkqmrfcik81.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/44ot25ar6kwvhpbx8nrnudzfr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/46rf91gocvv7o6eczh2p6ukil.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/4h2qgqpv5i614ztgdlvobit66.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/4j5utlbc3hfka3qdsh54rmzh5.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/5ewtmaxmfcs9pui1g2sptvul8.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/5girxfu4qt7c894jthjn36zm1.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/5imbcjufph3656fcmourp62cb.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/5wi1yab3i75x2dr8zj3muzuhc.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/60m4vhwpthnvbvq8itie7uwpl.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/68wzfrghjbeo6skkn3r8i631s.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/6ado08uumhlq5geecve9c66il.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/6me4x78sc9ff47cxae78a5tkr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/6qku8vf188ft13vp3ae9trxoz.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/6zqwqmnsin97x9ud9kultqn74.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/704yuchmbsq3z10b8ad6rpil2.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/7d1mm9gtelp7vq7wmggxisrx6.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/840fyjzc0i7hd224imhx1mmwh.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/87izbdw6f1ffrhvp4wy75g5gr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/8b8hvn5p2dihbfush28g83aa9.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/8gbwdukscqelemw0yolx6u6et.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/8lew9bafv638l5dn92cg5oodg.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/8zg9aija18c8mt4wrmahsya9o.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/97doqyuuiacfb4tvjcrn9emng.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/9ikc8fo0tvprc1wxxivwpdgap.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/9yyku7ukwjrynvu1vjhf8aht2.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/a1om9ehllhlvfd7jvs289snil.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/a82hkarz3t22jsw03phugz2wu.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/anyulz50qmy0445vo1knztbvh.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/azvvzqa5pnztbpptpdadj0olr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/azw78vcudgtqru7w7r5dh6awn.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/b1r8lzvjligsabwv1c6ntfx0p.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/b7nwktnrqxguxy98xoanwdrap.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/bfoswa43f6hwx1y6kfludgvxg.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/bo1ok6saob1xhzblhnttvnncz.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/boiczuds0mj59y5r84eqetsxq.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/bsvfripba9icr5qz2u5zg5h0m.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/byl0mlum385rq9aom17jxk91c.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/c019r3gqw4o5fl6dc2fx70hzs.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/c6axg94ukdzxv8ts46hpg1zzi.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/cdok9ezgq8ea7w3inr9y8dlfe.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/csb7wgyppcw96n9tmca2b9s7n.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/d8ay50isk3dltfglpdnalxgca.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/dep-graph.bin delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/dg915w50ryxp9n6z4iv3exinm.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/dkllhp2pu53acvi38uvx6thcg.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/drarw4rwpu9autl9fb3qmz8lb.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/due8pbjt3fvkragiogvie0kuw.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/dztx8djv7fhyi8qdqu4d69ezc.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/e2t7chvfa4rv5tusg6yvguydf.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/ew47g6a1h4hfszxkrknj84720.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/exsklydkt1d886fuzawl5nojy.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/query-cache.bin delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn-8ru1mhjv9jtm4y1tr28tr2nsg/work-products.bin delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7refpkvv-1kar2kn.lock delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/10x8nvn5dq401zdwk5povh5qb.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2azzlnj9lcwn5ussj4k509lg2.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2cccf07ei5hov5gjgghspm9y1.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2d9frb2883jrxfy279xh3pl4s.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2fdcx0yjk2vkqn2m5q74ufuak.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2gpkqfldxkuio73y4ehyb5nhs.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2nccq9kbm0rz7ggewn0bgco7z.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2q01ihejgoo8tfp1lbtgvhxg7.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/2zpvdapnocza1pcwwpraxs4hl.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/3a2biat8o2ipzm94crnuk9d0u.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/3fo12pgkvzb4c9xq7nzfg80al.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/3wamjh6kyjlbwphl8yhk9s2yp.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/3wq40gdpo69x4qzh68wzc9tvj.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/41ac833mmeb7fpnkqmrfcik81.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/44ot25ar6kwvhpbx8nrnudzfr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/46rf91gocvv7o6eczh2p6ukil.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/4h2qgqpv5i614ztgdlvobit66.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/4j5utlbc3hfka3qdsh54rmzh5.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/5ewtmaxmfcs9pui1g2sptvul8.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/5girxfu4qt7c894jthjn36zm1.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/5imbcjufph3656fcmourp62cb.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/5wi1yab3i75x2dr8zj3muzuhc.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/60m4vhwpthnvbvq8itie7uwpl.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/68wzfrghjbeo6skkn3r8i631s.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/6ado08uumhlq5geecve9c66il.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/6me4x78sc9ff47cxae78a5tkr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/6qku8vf188ft13vp3ae9trxoz.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/6zqwqmnsin97x9ud9kultqn74.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/704yuchmbsq3z10b8ad6rpil2.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/7d1mm9gtelp7vq7wmggxisrx6.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/840fyjzc0i7hd224imhx1mmwh.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/87izbdw6f1ffrhvp4wy75g5gr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/8b8hvn5p2dihbfush28g83aa9.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/8gbwdukscqelemw0yolx6u6et.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/8lew9bafv638l5dn92cg5oodg.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/8zg9aija18c8mt4wrmahsya9o.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/97doqyuuiacfb4tvjcrn9emng.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/9ikc8fo0tvprc1wxxivwpdgap.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/9yyku7ukwjrynvu1vjhf8aht2.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/a1om9ehllhlvfd7jvs289snil.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/a82hkarz3t22jsw03phugz2wu.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/anyulz50qmy0445vo1knztbvh.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/azvvzqa5pnztbpptpdadj0olr.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/azw78vcudgtqru7w7r5dh6awn.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/b1r8lzvjligsabwv1c6ntfx0p.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/b7nwktnrqxguxy98xoanwdrap.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/bfoswa43f6hwx1y6kfludgvxg.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/bo1ok6saob1xhzblhnttvnncz.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/boiczuds0mj59y5r84eqetsxq.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/bsvfripba9icr5qz2u5zg5h0m.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/byl0mlum385rq9aom17jxk91c.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/c019r3gqw4o5fl6dc2fx70hzs.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/c6axg94ukdzxv8ts46hpg1zzi.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/cdok9ezgq8ea7w3inr9y8dlfe.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/csb7wgyppcw96n9tmca2b9s7n.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/d8ay50isk3dltfglpdnalxgca.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/dep-graph.bin delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/dg915w50ryxp9n6z4iv3exinm.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/dkllhp2pu53acvi38uvx6thcg.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/drarw4rwpu9autl9fb3qmz8lb.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/due8pbjt3fvkragiogvie0kuw.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/dztx8djv7fhyi8qdqu4d69ezc.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/e2t7chvfa4rv5tusg6yvguydf.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/ew47g6a1h4hfszxkrknj84720.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/exsklydkt1d886fuzawl5nojy.o delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/query-cache.bin delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu-8rulzb0gbrggdwzi1h1bvo9fh/work-products.bin delete mode 100644 rust/target/debug/incremental/mseries_power-25xehw9tqsm31/s-hn7rel4h2r-1t6eavu.lock delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/03kjwtmc7yax8vn57rhruhhdr.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/0b499gwyqwqux5812dgfaa7z4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/0n60xpl27jsuyditxmnz0l15d.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/165kglrc8usf4pz7y1gtjwnkk.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1az35se847r0zseb307pvn13y.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1cvcvko4cmyho8ywk5bl5000g.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1jjx8vh3pc9i7jxira9q7uwsv.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1nqfhhchy8tlzjvmjhfuwh69e.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1qzazooqx053t0r0yo2wgn688.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1v6enc65pfs1awc4japyjrc55.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1vf5rbu0jm5op0f1sizlyd5t2.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1wyn0qsnlqm7o3m9ha05dvan4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/1x1exoo6hu87j8jfikdwj7p4k.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/2422xm0r1pspvggse9vs1g8ce.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/2gi5ahrmeskpo0sb0xsp9tfnd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/2ideixffa8vi4c6s4gl8jco4r.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/2ifj7pslh4do4vylckqs1cj5u.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/2iz6x1qg2xkfvnhvjikj2wr5o.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/33cnorz13bl4r2un66huehf0l.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/35xnbvctn2h6owv3ik8m4wfgu.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/3bjazqv9jm82aurbw1w9z3epl.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/3lvs8coezqnfbbzikduuq07t0.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/3p7a7ej3sqqpvqm9oxdzv7td2.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/3vyjxflez4in39pn3q7tabs82.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/423fsg8fb8pa67p50luej04h8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/495jl6b5p77dfchp3ys7nant8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/4b5noahpofdhd2834db0fnbep.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/4ho4ky8atc4gsk5qoubnfguf3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/4xw1cxm5qmxkvd6bsn590v8dd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/57hywvwmol0uhlmooyagmpwld.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/5ccmf9v8jzan14y216nx41fq0.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/5l3gl9unmy73kh4bdgre1jyla.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/5m96ryj150z5bivoyfzbduhzv.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/5nlnke7h8yxy3ytbho04ot8k0.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/5tzexitnzwrd8t8056d2zolf5.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/6a6qtsx8d86uk89xc5fjmace4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/6dh1yslmt4dylq6qncge9ijd3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/6rn2lr41hqqgzzbgirmy8069m.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/71pamy1ufxy060ecn6089jtpr.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/72d8z1tnma6ffywh45zehmx6e.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7bchabb32db7c4bdtx08vnpqn.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7bme6aastxavdo7i8t3wza5nk.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7evwnvlpz4oqzn3ehi4o8sv1w.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7ge0nk5heubf0pvup0iru88cc.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7gephzwxek3uyhufcjliwpchf.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7j7pq7ew5nbvfinckzrfkcx6n.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/7jqv6mzxtkmpwnab0nmme41bb.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/87ly0zmi74okrryc60aq49cb2.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/886rreqf7c6smnu3ak3pvhvyc.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8a1otvsszllxbyl0fct25fmxv.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8dk1wf5jwlc1lqogvp79ugb0y.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8i0jsh61u80h4u7sw904ngah3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8osdqq8x4dt3jtwtp9atk55be.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8osdssmye9a5vfjqqttkmcag8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8u6nzvyseaa16qmc4ljw7cxtg.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8ukyb2z2cr9st0zravwsa2v0k.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8vajifhj2usrq79wp8u8fuhlc.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/8ybwa14qu4m11686f1uvxg6on.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/92wv7g2dhz3145i3b1njmsldo.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/947z3ckmt30jhb1lu8rbz00vb.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/951nxsjz4fwjjom2ncmm0wki8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/95xtc6scmc5om75fly1oaaycs.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/96xbwz1fomrvo5f29j310d9ck.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/9ffkt56lxbwdh077vyvv85tj3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/9kzc49jl3emt0kmianievrs7b.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/9yyvwqbhtx67ard2n4zde6c89.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/9z5dmlitqk4wmp1rhtohd6kyd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/ae6ijp4t6dwmwb214lm5f69tt.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/aimmoi4xi8qxw9phr5rzv9xw8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/akh64688i3pzi9uf3rrf6zqn4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/atv1roznsxww59hy2g2p4dde9.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/aul5povrwuv43o43d1600y55s.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/ax2rvo5hal9ywxthk11wi84oq.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/ayk2pkd8v97ecdms3n6q4ut2h.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/b0knxmmh7z68a6l3hclxkn7zq.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/b36hkul2ewldzgtoyja6489s8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/b8uieim5dxvav7flpxejuh2oa.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/b9ozpie53smtlvntpm6216mi5.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/bcvv2wd7k6arxhl1s11g5b7jz.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/bo29o919t1267ycjiqvjouq68.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/byadudatou92ne127evvb76w5.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/c1d81mg33g4fowrs0tgsf9zxf.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/c5sfau9to8l30w2bkar3i9av8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/c80wdb68nctbcm9fnxcvajkh1.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/c8y8vmkgmjmv26ukziobwdcqs.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/cg4rw74vec63x076g7idiz0x4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/cn9xvmdzoajrsuc2694yp0abg.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/cxfdwlj2zft3yv7b7e6hzz072.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/cz0bbj1q57z4jlh8b7fpvy9yd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/d1ilq3rlbkeqdkusa0x7pveyf.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/d23p682apjcg1ij18f6fqr16t.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/d83zdfsm572uwxjdb3r8kh6ju.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dbflvh1tqn8wjf1we36wjaqjb.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dc5srpcjpix9hxy023lo6m8pu.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dcxhwzmrtvcznpcnsfjia6zvo.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dep-graph.bin delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dif1hebc1mr68ctg8sruvuswj.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dje2bc8ogxdmnajgbwa8tqgwx.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/djo2rdncrv2i7afh6r9pkzz1k.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/djsf8zx2ieggisprz0f54yudg.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dko36iog38tm2zivyp2em8v7d.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dunkedkvn3y8hhcujj5towql7.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dvbsroje81o678vk6hui77lff.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/dzh62oz2atej1w0g80fkwifsw.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/eaofh1wym715rc0ivgzkn21k8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/erwusf4rfsdsfsl3205gjt96m.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/ew48dfwocd9jnke4o5ydyu2rd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/exb0lh7xtd0730kdaeelmeykx.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/query-cache.bin delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8-4qbove7mjbrdq6t6xxws7lkj4/work-products.bin delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rehv6d8-0yqxjk8.lock delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/03kjwtmc7yax8vn57rhruhhdr.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/0b499gwyqwqux5812dgfaa7z4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/0n60xpl27jsuyditxmnz0l15d.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/165kglrc8usf4pz7y1gtjwnkk.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1az35se847r0zseb307pvn13y.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1cvcvko4cmyho8ywk5bl5000g.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1jjx8vh3pc9i7jxira9q7uwsv.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1nqfhhchy8tlzjvmjhfuwh69e.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1qzazooqx053t0r0yo2wgn688.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1v6enc65pfs1awc4japyjrc55.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1vf5rbu0jm5op0f1sizlyd5t2.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1wyn0qsnlqm7o3m9ha05dvan4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/1x1exoo6hu87j8jfikdwj7p4k.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/2422xm0r1pspvggse9vs1g8ce.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/2gi5ahrmeskpo0sb0xsp9tfnd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/2ideixffa8vi4c6s4gl8jco4r.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/2ifj7pslh4do4vylckqs1cj5u.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/2iz6x1qg2xkfvnhvjikj2wr5o.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/33cnorz13bl4r2un66huehf0l.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/35xnbvctn2h6owv3ik8m4wfgu.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/3bjazqv9jm82aurbw1w9z3epl.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/3lvs8coezqnfbbzikduuq07t0.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/3p7a7ej3sqqpvqm9oxdzv7td2.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/3vyjxflez4in39pn3q7tabs82.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/423fsg8fb8pa67p50luej04h8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/495jl6b5p77dfchp3ys7nant8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/4b5noahpofdhd2834db0fnbep.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/4ho4ky8atc4gsk5qoubnfguf3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/4xw1cxm5qmxkvd6bsn590v8dd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/57hywvwmol0uhlmooyagmpwld.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/5ccmf9v8jzan14y216nx41fq0.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/5l3gl9unmy73kh4bdgre1jyla.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/5m96ryj150z5bivoyfzbduhzv.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/5nlnke7h8yxy3ytbho04ot8k0.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/5tzexitnzwrd8t8056d2zolf5.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/6a6qtsx8d86uk89xc5fjmace4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/6dh1yslmt4dylq6qncge9ijd3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/6rn2lr41hqqgzzbgirmy8069m.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/71pamy1ufxy060ecn6089jtpr.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/72d8z1tnma6ffywh45zehmx6e.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7bchabb32db7c4bdtx08vnpqn.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7bme6aastxavdo7i8t3wza5nk.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7evwnvlpz4oqzn3ehi4o8sv1w.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7ge0nk5heubf0pvup0iru88cc.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7gephzwxek3uyhufcjliwpchf.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7j7pq7ew5nbvfinckzrfkcx6n.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/7jqv6mzxtkmpwnab0nmme41bb.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/87ly0zmi74okrryc60aq49cb2.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/886rreqf7c6smnu3ak3pvhvyc.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8a1otvsszllxbyl0fct25fmxv.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8dk1wf5jwlc1lqogvp79ugb0y.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8i0jsh61u80h4u7sw904ngah3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8osdqq8x4dt3jtwtp9atk55be.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8osdssmye9a5vfjqqttkmcag8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8u6nzvyseaa16qmc4ljw7cxtg.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8ukyb2z2cr9st0zravwsa2v0k.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8vajifhj2usrq79wp8u8fuhlc.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/8ybwa14qu4m11686f1uvxg6on.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/92wv7g2dhz3145i3b1njmsldo.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/947z3ckmt30jhb1lu8rbz00vb.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/951nxsjz4fwjjom2ncmm0wki8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/95xtc6scmc5om75fly1oaaycs.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/96xbwz1fomrvo5f29j310d9ck.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/9ffkt56lxbwdh077vyvv85tj3.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/9kzc49jl3emt0kmianievrs7b.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/9yyvwqbhtx67ard2n4zde6c89.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/9z5dmlitqk4wmp1rhtohd6kyd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/ae6ijp4t6dwmwb214lm5f69tt.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/aimmoi4xi8qxw9phr5rzv9xw8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/akh64688i3pzi9uf3rrf6zqn4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/atv1roznsxww59hy2g2p4dde9.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/aul5povrwuv43o43d1600y55s.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/ax2rvo5hal9ywxthk11wi84oq.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/ayk2pkd8v97ecdms3n6q4ut2h.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/b0knxmmh7z68a6l3hclxkn7zq.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/b36hkul2ewldzgtoyja6489s8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/b8uieim5dxvav7flpxejuh2oa.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/b9ozpie53smtlvntpm6216mi5.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/bcvv2wd7k6arxhl1s11g5b7jz.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/bo29o919t1267ycjiqvjouq68.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/byadudatou92ne127evvb76w5.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/c1d81mg33g4fowrs0tgsf9zxf.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/c5sfau9to8l30w2bkar3i9av8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/c80wdb68nctbcm9fnxcvajkh1.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/c8y8vmkgmjmv26ukziobwdcqs.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/cg4rw74vec63x076g7idiz0x4.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/cn9xvmdzoajrsuc2694yp0abg.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/cxfdwlj2zft3yv7b7e6hzz072.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/cz0bbj1q57z4jlh8b7fpvy9yd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/d1ilq3rlbkeqdkusa0x7pveyf.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/d23p682apjcg1ij18f6fqr16t.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/d83zdfsm572uwxjdb3r8kh6ju.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dbflvh1tqn8wjf1we36wjaqjb.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dc5srpcjpix9hxy023lo6m8pu.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dcxhwzmrtvcznpcnsfjia6zvo.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dep-graph.bin delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dif1hebc1mr68ctg8sruvuswj.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dje2bc8ogxdmnajgbwa8tqgwx.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/djo2rdncrv2i7afh6r9pkzz1k.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/djsf8zx2ieggisprz0f54yudg.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dko36iog38tm2zivyp2em8v7d.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dunkedkvn3y8hhcujj5towql7.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dvbsroje81o678vk6hui77lff.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/dzh62oz2atej1w0g80fkwifsw.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/eaofh1wym715rc0ivgzkn21k8.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/erwusf4rfsdsfsl3205gjt96m.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/ew48dfwocd9jnke4o5ydyu2rd.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/exb0lh7xtd0730kdaeelmeykx.o delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/query-cache.bin delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3-922xctwezypr9p9om0cf5pnb3/work-products.bin delete mode 100644 rust/target/debug/incremental/mseries_power-2jlot0aa2a2re/s-hn7rel7zuu-0blvnl3.lock delete mode 100755 rust/target/debug/mseries-power delete mode 100644 rust/target/debug/mseries-power.d diff --git a/.gitignore b/.gitignore index d426caf37..efc5db93b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ __pycache__/ mseries/gala-drawers/build/ mkosi.packages/ /mkosi.images/sysupdate/mkosi.extra/efi/m1n1/boot.bin + +# Rust build output +rust/target/ diff --git a/rust/target/.rustc_info.json b/rust/target/.rustc_info.json deleted file mode 100644 index 9ab602296..000000000 --- a/rust/target/.rustc_info.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc_fingerprint":9560274887975835320,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: aarch64-unknown-linux-gnu\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/sdolbec/.rustup/toolchains/1.93.1-aarch64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"neon\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/rust/target/CACHEDIR.TAG b/rust/target/CACHEDIR.TAG deleted file mode 100644 index 20d7c319c..000000000 --- a/rust/target/CACHEDIR.TAG +++ /dev/null @@ -1,3 +0,0 @@ -Signature: 8a477f597d28d172789f06886806bc55 -# This file is a cache directory tag created by cargo. -# For information about cache directory tags see https://bford.info/cachedir/ diff --git a/rust/target/debug/.cargo-artifact-lock b/rust/target/debug/.cargo-artifact-lock deleted file mode 100644 index e69de29bb..000000000 diff --git a/rust/target/debug/.cargo-build-lock b/rust/target/debug/.cargo-build-lock deleted file mode 100644 index e69de29bb..000000000 diff --git a/rust/target/debug/.cargo-lock b/rust/target/debug/.cargo-lock deleted file mode 100644 index e69de29bb..000000000 diff --git a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power b/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power deleted file mode 100644 index a32c02b02..000000000 --- a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power +++ /dev/null @@ -1 +0,0 @@ -b37dfada9b0cbf83 \ No newline at end of file diff --git a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power.json b/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power.json deleted file mode 100644 index 48e8bcfe5..000000000 --- a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/bin-mseries-power.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc":7244470188377863884,"features":"[]","declared_features":"[]","target":3065080704236293189,"profile":7333259681545804295,"path":10094114566185419210,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/mseries-power-294d937b3ff57b29/dep-bin-mseries-power","checksum":false}}],"rustflags":[],"config":9396254390672932401,"compile_kind":0} \ No newline at end of file diff --git a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/dep-bin-mseries-power b/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/dep-bin-mseries-power deleted file mode 100644 index 5c54f74a99114dbe9324f717bdccb4490efa0e4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31 hcmZQ%U|{&q$Ot4ExPiF1C|N%@F*8rEs2C)~002r{1w8-& diff --git a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/invoked.timestamp b/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/invoked.timestamp deleted file mode 100644 index e00328da5..000000000 --- a/rust/target/debug/.fingerprint/mseries-power-294d937b3ff57b29/invoked.timestamp +++ /dev/null @@ -1 +0,0 @@ -This file has an mtime of when this was started. \ No newline at end of file diff --git a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/dep-test-bin-mseries-power b/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/dep-test-bin-mseries-power deleted file mode 100644 index 5c54f74a99114dbe9324f717bdccb4490efa0e4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31 hcmZQ%U|{&q$Ot4ExPiF1C|N%@F*8rEs2C)~002r{1w8-& diff --git a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/invoked.timestamp b/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/invoked.timestamp deleted file mode 100644 index e00328da5..000000000 --- a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/invoked.timestamp +++ /dev/null @@ -1 +0,0 @@ -This file has an mtime of when this was started. \ No newline at end of file diff --git a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power b/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power deleted file mode 100644 index 98ea86abe..000000000 --- a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power +++ /dev/null @@ -1 +0,0 @@ -0de59e50673306df \ No newline at end of file diff --git a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power.json b/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power.json deleted file mode 100644 index c48f8bad9..000000000 --- a/rust/target/debug/.fingerprint/mseries-power-9d7f16b60ec457ca/test-bin-mseries-power.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc":7244470188377863884,"features":"[]","declared_features":"[]","target":3065080704236293189,"profile":6118567749210759884,"path":10094114566185419210,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/mseries-power-9d7f16b60ec457ca/dep-test-bin-mseries-power","checksum":false}}],"rustflags":[],"config":9396254390672932401,"compile_kind":0} \ No newline at end of file diff --git a/rust/target/debug/deps/mseries_power-294d937b3ff57b29 b/rust/target/debug/deps/mseries_power-294d937b3ff57b29 deleted file mode 100755 index 428a28bb7ff6990f2dd016b1c9061d488dcfe7f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6021424 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

    k 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

    *@A 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 JDW< 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

    7I7Z(}7`p93|q7d+Pk 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

    Y{aG3|bK&5Nni!NMLB#MTvfH^02IV(Q>&f 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 jj%4jGlL!d@D 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 z zXR`t=$wL9MHk$Vaj%zqhXey^?@81KSxgkm?I;D4nLU9O3q56xNvH!z~4gi@4 zPCg*rbfmUQRg0@^vRE&#ke&K?mL@kzc0Ajo>A_pfhW`RyEHVWU4DF}vcOceJnc?#w z!?D@D!XT;3RS|SC^=7{in8Zbrk(6#5Zt&AEjg!+%o?D0}o``cwj(Y@dlaf>QSY2&^ zzKTK3Ll8EewxD`p$jR1Fd)d_nJJ6CF3Z|r9ZIG z$IrI%WPl81-jSZ|Y_rr-5W|rYwUs{%JVlR`6}XM?S)C4^^ac5D69pt|*8!p$luO%% zt}Vv1QfvKD3>enkC!9UR_~04Od~oIB&SvNjQ9XLrj5V}r-T8`p<1?_$mRtL(NTg%+m_s_ zyj25gQWQp8v2UlYc7^0X z;+6AMYvyycdBuu)d0)DroRpgPh7Q}h^>EE7AS<}l1;zDpK445B~M z;|J~`q-*IxEcgC@_bC5e*h8a}^OZRE18fpDx{>BKQDKj+-jYz;Hl%^H9fm{-f1Tt_ zp0n#1yw}<3NJTrY3QJ`bOs;;;1+mknS@7w?`XK4;tBc+94U73(BF}A3$26XYQDAo= z8(QaB0{HZ<}Q-A;izD|7O>1J@O>`arNVH4LxZ%vV&B4ia@hv>W_SGJ@*~j67 zt>{ZamhQ0FO%Gb8x&(NmFeP9&!>$YREGqi92Ye2hJ z5Qa}k@Q`z*k5`kEzv9P&1B_bnplEE&H{;Y)DHt{o*`_pcO@ZvF=X2aXIg=(Goi<4Z zke_rTj@{>KE!+PlEUadMZOU+?uJuyMEs;(ROPqrQFbajoA3Y`*D2-Zvx#*H+^0Sx+QCR3ci)LpvongKQtF-k7G zGigSve~{ce@TrdM=Y^u{g{Md+Dgk2K07gVI zQQEEcr;5#I-3(6&hOtdd=VpnJ&oOWRz!))wz_gBtuuXNHcRlwa{$hD(@{t%|2h?op z+C*mK@$%}{E#hTYLyM*8)jH}LT;1SZ{$XMKyjX!*dq39cSJ(V;+q>gp#Mo&tzoPgB zu|~g&74>%i?K|S=F(1mT=dOlkI`PH*+15)v_t{Owoi)_QXrhzEmVtDo?XpKhlZeut zOhAGPyh%id-hWW@J{_89pWPqBi(C?aLg@w8bFn)IT`Q6lnf##lKQT)5^>|nbo!AQ) z;zfSEr1FBKyWP@Z^0k za)j;+Q{WyG!l9V97A2X@5o-2d%HRpd?;(|3hu;7)k;5&DdDql!mJ9h>wit}X4osyGCN#PNw+(V?&dGUzcY_rx?v`BLl^1aOIt9Dw1xK=Qn`3U;NCuL^UTA1B+!yxo znN#*6dx$u`ZY+p$-Gh*V*cJA`=3-5ob)A$dm^*kh^v0bTH?B==mkd%{!2i~$^4tPO z)!}XfpwbMqWUtdigI)d5UpYt)VO9!%#hl6?)p+XjHg_7nGM;Go_!#4r?!yj67fG(Q zEs8uCqew4dWI3~_Sq5ETQOLTt{1x-xGuG%ge%{AhH!ZQE(Xlb^5>}}}AR}r;#+^RR zM>FqOQ9SP~C*9?qbQ{gC1QPUJf@~hZ5gnxOBh?27bQQ7)yp!;1?ZFRS6U4m!67zC) z%Flo}Ggibpt}=(AJmlaoSS*Fz6B5Kg2j8k?*b5~VDyJh#{C4g+!lVl<4s>ZaA5BL+ zau$>MBwHO-8oSl(Z7LXw@{$5>LuMs3?rde_VeaKdy6XL?ASCL&fA;UA`Z6_{&{$>ty6!m}SqJwr4!Lb<|Fs(v^wZD{J&yCF z8_mS1l>8w~3qy#zodDK}tPHz3glJ6n#7lT?;C5ka)pI{8hG$ZCbRtNx!MU+crS^Yk z#@T?Z9cofpAJoofRG8oAyX{DfDjx+v70U4iDz=w|V;Hzih@dF3uZU^`T<(@7-n7^a zqfPMZ^FDNtpOy!yO0?I-knVBQNsXtY@|U!!TVXV!`_Tg{di`-)SNU>K>C1uKZL|ri z3l?~XuM!`1aQ74G1ZBR@c=OzOSsSx%L>O6G6HQ$M5_k}9kfg~We~S)Xxki(vDKz;M ziBX}+S)L|e$t?q2IVG_UT|ma$I8^hu>zwl*KLIc*>hBSy~LrtKXC1BlIy( zV|dYc=_2^2G0!Bb4RiN~ECJD_974K5Q}w5XRE9FRENQ<+Km#?gu8t7x_(# zEDk((bdR!5Lrw=3s0+w&{)MVBwjVAgIK&pt~0l z#ydI&YE&mFrYeykooiX+sS?gN;aU-+5cl#MlD1QixI=KS|DDe?MIhg^)Ng^Cbxl3v zJl+8NVvZYoEiwXGw91o}DTCRZnm!3zl))@MfdpD;70DooFB~G#Ws=NL5fcbeASc|e zh{G?DeC#p;WA|d`sOL^B<0lbA{7RJnqNA?z0xls@TsAl5#_%x8u6cZ26fNkZ_YvsQ z75~=0O7r$>8h^HqoEp251`3CVg=U+8s`kT40SB{1LBb0*Jf%gc%2Js+D1#q}l|wBq z1E$wdq#@+JrWwl8Mcy5}_H;NHah>;TDELOn=FAOFt|eef?tsEuEw{31v6=11U*^v% zq(VE(k6~SdkmofGm_2@WmR@5i&XSXpIu7N{RH*J?l6C2~_Wa~;Q z4GHCTnt2e~2$=wjEhS-3ZW7SPq$I5Hv2}x-izAforEUvot4LdsHx>6<)Hy%#wwc4t ztblQQ>e2XJ*8<*wdO`b}hRqM^g)F99|0WGjGHsHSVM?3yjxn;@?j!i5CHZ{Sj$yTZ zj!%*L08#;hlKyh?eCk?RBHWvht}mjEM(+K;g~Y3AJ%3ebt8CFxtupm8wh{h)?&Vvg zX$*@D5?w|(Rb%%$KCL+lIkVqdW#zw5WZvtrimJQBdMB3{%;U@(!r5V!AJ&#@S)OSN z&8FjU+~e@$Doz;-ikI?%mBqfvz%VQu@6e%UA+{RqNO*d)Z$ht74brUC;&-E)7Xg<) zVYgi=4Fw4Cs&_0b7P3j@z3Z8nEx&{TPl#7w1jC4XhTz8mR7-KmJ}I5eEugA zjHO|&>ZM&`OWPn9NMJ5n&fg?EE-UbNZZU2uMecJrJ7Xu?y7HDZ@)+1gM%Aw9D&ItD zB70ZwYanx(xEkl+Cmp&@Xy{Mc3(`EPn$qqUUl?9P?cC8k!i#L9TN z01*#Vx!2&}s{FEe#(K_HnCgZ#^gI7BoK_{yDutp?yI96IwcYDC${M)67HJA<;VC`v zD(pDPbdMDpjTLm^Tc6>PZpe89A zyZOI{8&gDFu6ySyoxe!qN&33Uq`6fp!AJfg0;rG>SUb+W*e3&ppl~zH)+Yl?V}>%| z3L_fA-Wq!8csSG~Dq2ye{26SV;<$P!6$>_mJxGcN9lyNphU4`n8GH6|HGiI=(~uDB zmSatA(bEw0UgLzy>benEh-w?V_Z!23>y;wikeN^Dn*VnUDdHeAxdW!vT`F`lNPn(G z&2Pb6X&gHf_{1%sa`g#S>9P<^ga?cnqx zJa=;b+j?#=!kHH3qlTGZc^v=Vqfht$$YpCeTvTn ziKbOSImu~NK`9OlDFyzDA}xiF*q)8iNREo~K2}<bF5bT)&B?^T%TyQ&pC?K&Rn9>J#^k&j}vxE$xKw&q~VS{w%Tf#y|=OKTAjk8*DN-mk---v6SR*V%rb<{BtxaB17&XaxNe8ZUjRQahgcGhMyie%))3p4$3*(pBuSNE#=w; z38(A=%g`l7pBS!tQqAMKjPOr=0vc$~kH0nC_PU$G5_<&q)Hd=H8J^GP*qWViJvmjr z+ui)xrpjfe4SP?Y0#p^*?!Ob%_^w-vpqbyrYD|G4=_9-)sMx~I%rCYBJeL(|?Qy~^ zr99WI#wOh0eD<*xZWBAH7}hexf)n|4A5upRA zQ52D&Gp({k%N8_3r@~>11z$;|b2HZ+4O}a))ccyfQJ=g>f8i7JP0Kt39F@+pZ}D>Q1%xXq<94ru0D zxiN@rU0g?<#?Qt~(2}r<5*qGoGp?i<>TSk90fsw6v=e<*lcQV7ok&dIi`(rZ$Mf0w zL|E9$A>=d9yASH7K043`iP#Too>Gr=qls8YIp7h)yAHn$T$kkWhZhDEY2f1(sjQ1V zST%B@ya&D!IZ>Xb;Z@~B$+^4-_XC2464~MHf!4smBiPnY?`&v$6SRqR*WW5eK)dOLfOiF+E(Zs=PXM^*5d$neP#V1F5!6RW zpyv3VY}S_>$kCwrF4pPU9G3*-fSfV|62=m9CG2%7JOJAOF`r$}xN!5T%@N%m7~sE= zk&ELF=*ENs^c^B}E94(9NFw|bneth^7v@yaXY~ga(48$rcH{}k+97srxTwVWIQu&2HTeeYVk4JQiC>`pDT0%I# zDQZe_e&r2S?K2CNvhEraJdL1u7MRM8|0;BNG@t9R70|-t`cwg-D4_|Fh{VPuy#2i=oW}UMNol`AB#DL z6agT~=Xu5=j{k^`iMrg#U-Mv!1l?-HkJACRJ5;EhO2XR@*y;#$gwj!NL$`Wf2Y+F; z^ux^1j(`m*5md!UjzswQ>2OFrUE>%)>?V8_)Pt_R0H7`Z3!;t$nC_>j8Zcqf?w{bj zzilaGyN7+UMYh)IZn74onfEUxC3_DZs590F|haMghvb)spFi_OmL%MV2#(|e#b1MB32oMzw?EP7r3F%<@tuMxw*Qr#lx4z7 zYi*MLFqVqaIlW#xYa@nLuS)Ef=8V1z^HkHE>A2w)xC2c?x>WfK%CT^N^Wx0wSTK_= zXFxA7M@iqoe$wuJK!Rgb9{6Cxp+mUV=gmu^ParmKLgsQTWK^{_t#NT?%6DG)PKRck z-3^Cp{OqQ|as3EE8F+a)DADzuM*@oft}0NTz(cTUj{5_uU5}fToTP`wjp()%HIe1V z-3-y+oq^?f8A7j^PG1Z>r53<32s;^30^?8#=|CudGhSr5cTxQ*@yJm4s1Orixc`Los28cGk>WPy- zeJL}49UXpw6Bj+9euv$wr+&>@yL&FN0ilQH`U-a~B)Epb(@|>^=M2OFwM(qTZM{Am za!J>sL+6&#aPH!cS+>XG%(~i%*AsI>(oAJ=c~W9)Pdukl%YTJ@ftPlkn}oWS^ooq3 zE_z2doIO8&KGmDKXW+hdAe9qUX5#J9xcVENvsv+Oc+1VQa@b&~Pz) z=LYdUo)Ve=!Rq>m*cKhD*klp}Z#5@TOoD_b62%cZ$20jCvN+!W!v8>+djQ;Dh@8C} zzO7thKP`!!QtT3JEF7N}va!@eg}=ip;a!1=MQLt+D{!w>cZF^+D^+zi5u>cMo*rwFwcfT=m>5vMVca*{btmac zW}%Yq{Dc=ZTn}jAG(r#LQCZ~n5QWvmEoS)IcY48#@nsvxfck+GaxrG87JSPtxrjTh z7%tP!B3S6m<}-=pV(b)(YXc!tDfgpe?H#+AdwHJShol)@ICL@P^OvnXiC+!5ncY(f49~Pp z=Rqtt7D+h*O^Z4>$_QRstA#gZhz5O(#-c#_=#6LFX#md1wpf}Qfv}03PAyMzmRo~o zufcQ_Ie9Fc*AMNLY^G-BO_quIm25_>--im_O@ruf`8t+1qpb~a9(oHE-$XUv@ z)V~%3-S!W5Z^Bk`O1xDfZ7?h1<&>~0qJ+uml&~5eW`kLI3@G8Hh!S3_NC~{Df)ZXf zY~w5Tim=^-8&j)+ZNucfC^QX|Wvuu#Qsqq*0ItZ&$h-!EaPN?L3gZvU z7NU*2L@juu(Z)pX)j3+KrL7a)1GUiUm$>=5n8#Bvj?!^A^k$>r)mf09ejhOfZ!w%^ zonejdgip4V-&FmuSB`<9NKBdJH&wR~F~C2=vB=xPM#F~<%uQTgZaXoT`sa{hq$eCu zobdT>CGI-pS`y*}u-9k7{0$Wg1#dN`svYqef%HegFm1vTwCP=Z(n$O+Bk`!KBWH9&m*3OCC8OBl1C%B`lPSDr4bkYu=OobGWO02%Aot*=bVE43EemX$RCf z7-Tqr;U3xyiTV}<7w)mp9Xf|Pp6Xci^5tcjbaD#U8I)u`qs*0i+3T#NFDt*hHoeij zLIUDQW)*gE;I{JbNq(&7M&h}_2Ssi=i3wVqAIrV~2e{`sg$>DYZ*yGJL&85mu{)7H zGu)?4Hs55shiFW4*+XnTlE=JfbB22tRYbUU(aOs&axAuE+y(w@b?{T14GX?;H=|Je z^K{*FDSUk1JWavHRk|MD# zsYRt<=e#I|h!~0acIfn_mB?9SI59tn^p7Ef^R$`c4f7Dk)q+)myM}bQT##-Zlgq{R zhe1Ra{l&<(dCdD)a1y|HC?Yt838t zxDO`-QO*jg=-Zhf=3w70(YK8kFl={evRD0q00=bpHX zGv0Zz`dOz#5_kE&x1b5|o#RK7axo_AO1Z{yDY=ntxFVBY0Wqx@Nc(2ceah9S%m5!J z=c61d?<>6}ZKaRFd68iJ97mSy77DaZ@}_o34a?YQwipF}!pbua)wjBV`iKI;QUaT_ znr3w<%bD@1{oTs>RNFK@wGWM1J{TU%BFDr42hrLa#ZkB^GdTN?Ud_|XvjGxwOM%f# z_c`sChYi1U$o`xeY093a%RLLTHpGtatiwDgrO6(031ph=Td%X5n1-kYXvq+uNcTl6 zsQaaA|8HN*>bU zmI}48JT#^RPv-Yf8cg|mJ>tRo*uZ)eN&rS41lEBJWl!nL$2{zncEILu4L~y1v@}`i zSez4K@$qylK5bb1pduDOim*7kT3GxN0y8XrcC4{D(XiO(=&-oguz0V>;?5)%*H?kX zLKFgD4D~7jHwBH_Y4V9Kv-}ienWsXOQ+x29Z_2HNR`@`9p?JF6fmRrn&P;a}!MjqV zO6G2|spt#bygIs+H_v@~Lr~$V!%Lq1mOGsDZFhLWuy1*U{l4uEcYNC&{&x7cjMB;9 zc85(ze9I&3{%v=-@!Rh3oRQx$O2>TL9p;a!IKmPOW#OJoi9l~6p>S_x&7k!Y_x*nK z#b7dx;@Oeow$x7qPnz|T z95@?xf!!m8O}T|F+?qg4dJV%nQeWK7Lyuvv6FNou*Z!0uz&1vJ?eW&S`elIERtb%5 z=lKk?ead#^3sbiv{jg0tF}Ll4meH0 zkk6~Qlp7Z1Xe5z7U`WFQOtuOPVM*mye!z;?noUg-MckPutix2P}~i}fxv)K)b}hq{d-{4`6CBqIjh zb=;`qZ~*}8nnK2x!0aLzYPn}`4UVXM&?pVt4Kuv>pFWiXeA|t3xXOaIX{3Yul6zv* zpX<$WixTesgRDb|4YopwCjg9KRn7k+Xw>r+OV*w-z1$~QPgkQls-5G8-71%ZHf|Mm zMPaB9PFBP~fte0Cv~l^M_cz2N)%zPBu?Ho7r=UmK5;@Jw1dR6OC|pv7>pmfw4t6$t zO1SJ*N3^|7WMIp6bN^lvYj?gav;)ktys)0W9$=Fx6z4%6e?VjNJlsph#gCNH>r{wh zF|;K2RAB zrdHJ#X~-C}g7`d4hd_yYgzzK);YJh8%v^>kKIETq&9pA$JCpbE-&U z8Kj<^6TNv?%8Csz6AUtT)2jBRUHL41Gtuj}L^KuMQDDm6(;brX&q{Wsk_W7$$bFU}??{#0A4KHk8%t}lsgCtIVKG1uQ4L2( z^Y2Qp7pbx&ofKH&DCypmI7$k;)EG|f>!abp!32+i4^7dD+;S+Nd=F@M>&svl0)FD| zz8(|Ee)aEO%+$n9CFK!$InY=N2lKIfwx`+6*@?`pQWrjs0>2g2zNTrVJY7p7uR@1C zZlm6k8p%Fe59-=ez8gw8{0p^1W_w?Ada{XxFL;Voxi!OG9`&myx{{06#tesgG5Xw! zpT^!Hz`rsGxC#Q^(%ZVYV9!`Zddwfj*MjRDPD9E9j-*l?@0LE#Ss)7 zeE82GLu{G)0e6*pIa$5PYyZ@E){kSI&PTLO#7>D%K?T&TICgI5*qJ;ZkIS^$?-FY? z2{8|y4?@t)983xoaq3r$975BPDH`q%h1V?=Vq$d>r&?+qBEs}Rwv&~hy}a-bWWov0 ziZ$xRU44~m>h>?I>FYJy6Z@W6Vy=5n_EU)fXUJChRt;w31*0@toQpfZwTSd4z^$NJ zduN_uS&x*Nf;<{U6HEq=lroo%Zakb8~8b zks*=s39JR}-G_`r2wB47ZO`+Im_FsrKLzKt!v;^l%#fZ~GmA0ui25hs>sFEaPj$}{ zkcDK`MID_3QslHQU?6ZgjH>{X4OQ@mz{l!`fO7@9ZIsl#j63$=0(HM6btJPT=`9VZ ztXox(B$ok+hC7+5YLEQ1SaD<{d7y8YK@INRQHODJz(0eYunOSxi|>Vb*AGUrwpfvM zIycF6e=z!I`*zA5P_GAGn-IYVww>m>2jCD`gwMhIOaV@rY$pMG6pCrXkqspyv%(2wZT)a>Va zf3+|E5&{!Kh9R*AJc-2bBKr5=ehuFgvXgRON`-z|ERx^C`lUAF6E$6>7!srQ{ecNIrdhA^hd z5A}sMZk&|M(k@sq2h9fvEfrEqj0AGsS*k*pfxr-=81el;7^6?B>zEwqhXt3nT=PWDF*0Jy) z)z>w7>h+Ub0ZR#y&XCXS5Llk%pml7M3=zqPE+l@R5o^v{QO@FQ{7TTMFWT$5kS!Zl=n=unj>rTq7DHpHppfo;^7w6$xP_{MG@w(C^+P4u8xpEE%93Va3FFA()9#V;A- zyvPI`tH2Egd;Zxgj_m$QCUJCL8Bi1z0J3w89eChvk9&NDyJ6`?WuT6{Hit^hH|u zBs(Ot7;Fn<&J4($U19bOmyE>@+gb!ih(fK)6!zg~cn*pn;YAZFw(+(L)A2lO+Ru1- z1(C{hE*S89#Bd^1UZjcF0Y0YedXlo9NICdu^svvQ=IWyam09I4wkP~Z1mZNN*! zS*qj?8l(_q9eFua7!qbSaj-SMRN?%eijhM%Y_clsCdck>bh;l~Q!tYK^k8R;6Y2Ov zHa$3$Wa9}eNqsUIGkkO`ILL9;1n=I=5prhS&4b)4itJSinp{-&rzD)kT%y4wJPI+O z%T!dN7esMT_1Oo}U9$H?W+irLsk%o#6O_x#$#d^TGc7BVU-Vey7n0H)=n%kWly(Y` zPu(`zEZp?#Ad~SBUGfA_=?z_)ysz!Kr$mHvVtMDsa?c9=T79nYVTDMn=mUS381Wo| z7U?qMQORU#awd}}(O?bv4t%hRn8YL-oANo^vBC|5@+J3AfuU(^3=O;$_&~+8Zub_PJD|sFhXs24IEqB< z+mw%Pp>^C&4;K}AyMXie)FUeHhY46oFM`4E0DxLq+j zrLp*7ZWqvU?BXR(U#u*J4N%I=lgE=_br{e!+T_M7_GP7*_c(%~sBxiXy`qFrDsnc+ zOgagXLTG;Z4nzrAD5Hiv*3{MxY+-TN+O|432eMItNuAWiL<#<~6}_MT8@a+6mOmpt z(F5|CeLT$f4`x;(8dH|*R3zA-ZAdQ%gzUvB(q6rHT(Lu$NA20vRwuYGW=MWTh*_E8 z)*=$>gZtoCyv=qbALw?@uW9NXL=HcZls8L5-IG|%$nNPzfH}S7(r8*lPZ5t3p(P>8 z;=Bft3)akD2Sxp|fO<abfBdbW zzJIskU%T5%g#2Aew_y$JB|1(pe8!fp z=lcV1fvAnQq#n(UpL&mH_9xHTOO%d6%v~xS9nAM5=ZPt$AIyu~Al8`%xdBp2`_!N5 z^~p$p208Ak=@R z^WuctvNpQKx~JL@cdxX9(MGHJM8Ts+ly)AEj8k;UCmeC&~Gb&-K6t_&9VEe-9SuOMQ1|hfh{djBN~9qARawWtK{7?6xHb6n%uaKjxtF-Ne1c|hD9&~t;KU?% z>a3soz|Gm(0=PgFaoZewM^-PEP|ejs2_NE(2GUv-ZF8G&@!KK_#|lDZ|UjCddMbpr#@=zwqo{3xip)S!ALTduhC_ld`OH5 zd6w9-up>nv=qdx7`KPNFYUeKw1SAOnC_%yq)x5d1_rImgk43o|Zy6l~)yLvwS|HOs z+e$~U+!Og<$zF#eGc`Py>!yJEBv8c3R0#r}gbL7AA^$4L`6lqx6<3}w5S*2l`Ac}S zR?jOiC_N*c$K6#>0y`=fpcZvDHON*ZF};2K?}&|88Sla03eMaWYFWkXs_DV(qg`V^ zw44oE)xGW6-jfOZ$5_E{gGzJQ!UU0tVzs719shyzVHaVbF97Gcy<}R-j`xWD*qtfB z=gBPB{WkK8r+UblCWddYb=#yVtej8KnBCw-8)w2SDY2W8GRd$rx=P2OTHzc z`%JER@#y2jeOStrJyG3|C;3pl6u6!Al6|MHR5)bC3gDO|w~liRnLTW(rV-r)NfVx1 zU+!oZd8NT6W5aCSP1&i)iAmN^1-Dkx(+0@RE7MB&3L~I*4QS|I-~^i}rmTi3g^{7w zGkf`VQK@#qNqEMH(@e?7o?Ee=?Ui_Ag&Jzw)~f%6%sc!GP1{h^bdmeH&IJW#Xt;<+ z(tPNvHU9EnsvJeDHsF)I_G0rsd;>cHFJ@u_bufG(aiNmVD`Y4 z&x;?M*NAA|QhV%$pst4d9!GmomtJVFj-TRq=imE|1;1*Z>iVI>2E<^!t{OpJR60amuY5xH1!{})}0I5Uy{~P(&;RKnYn+R@_U#RRm zERR&=r_+%pYj2psN7JTIo@Jh%QddFN#$(!A`B_AOR1-7>milyp)o-la@Lv74%0>O^ zw^e?G3T=sdP4C8)7+T`&wQy^{3^jKnbOICyV^NE1%1kEq1`qH`hlr^fCsM}3w2da= z^;2lsr6TD*F>GUV(KVhGBXue=)D4`ZmG@0nou5hKsO^SE7s;e$r&v;7XOf+CRLAYa zZ_vy4Jpm+2n$Zt6s7~NhuTgYAhl;uNaZT_dq?&pt>i>SOP*GFrdjK{8jC;Z6vYLeeFmIi-^G@ z{u;mTGr`pyC_oLpcO~LZN;|3dsFbH?M|DlAcKHC-apfu5n_?a&VTB~kN#q!FpI@s1 z{1S9GMZuurQe8ZC0BMx2U`jQsxE9NxEVlL~zOBQDRDG%0%6$Rw=YJ`y46rp0nRB<6 zA2xQ>Du$i!mV{xyewbk&9+YU;D9p0VSx&%V8ojBvCdX-08?jzirgohr>h1_IhjPj zP2AlLsN=Rl&RWw;u1l_I`m5z8;<$MD+y|9%5B%88ph4D=>;rUjw(CnbmvDwHWb#={$B;}&2X!%QjhXXD4Waw@o_Whqxd(|xv@5RE^ ziJVaCFpWjb=|<#Dy?ulpEkPUF20ib;PTl}y!zG2Lo_C{}6RoesK%f;h2XC)KttguJ zq-~xoeBzf8Nfoa#PZH)0N0}mc!c$Ek;{E?4`*A0gs$VOmfa`6qkcHvjv+%Cb$rfCL zs+jBERAa@@_G)j=mANs+pF2S%R?8Ne243@z*?YAd&jzu)dGU2_bWm1vC%b)3ieo28 zd%(>#A~rHwd}`!fg`8eG9BFIYs^!$)yQqdxfP|n>xjA6WNG<1pln< z7u5(XRr-LO=Voo2jZQ0i4h{IIMO>xS;z_Ot0eJTn;7}7XeTT(qcENR2Xy*n({CeSf zNwqe{0zJ?+yovuMvPG-_U-E`W*;4|Ow)LdAE9(hm0uBQnjzm8F%{z2|Ok6U?ZdHNZ zjE;n`hy29rGTc3RNw`w$jo`{XZc55=BmTimaUZG3_iNa{u9Z^cLi8+6stmP5MM!djG3dalY5y~2AK9zC&LZX>)% zoS5%ecQmMCO4U<7N-`^GYg#fj-5)uH8f{fWphe%6nL1ZUR;``z5RY_;^ap*e#jF?- zknz@&nx8H?5tDjR=oRXY&wy#asS5@n+uL9a6WKR@Cz>@Vz}+}-*G_L2Wv9PZw_me~ z2~Vkx`_VIlAay3IPZb~hJ8`*3h(P=Ber~fPYxqrNer2vu0A(s^t~)M7c&|81 zD0U~A`;LTS>8#L|AgWiO23xUSM5c;l_^$LA^pSSK@0a8F6*Vg|}~iyBz~_0CU{ryTl9)a1!{ zWh3ZGZ`Bz6Zp)Cp?Jc$_)1@!Ky-pqyN0~g->(YU=hTDlCCKxs9FNyc1b3F3!C@sMK z@I$sQ8FS)p2aAJOr?{5@DG!mi^SN4dt8q!?OXP5{%X@s;IR2SFzVw!WvoV^yAv3~$ zGA{%1F^8zLjdWOmG(^%|A#6E1HX0no6S}bNQd>PQiFE^e3NsNS`8r!KOFr(tTbwyY zi)S|xI?C~@WH?2nECvL90#u|9rbdAqdVPtC>QQ1)`|rO~qPKZ&J5EvhgjhK!cua~Er!`|R)Nx|-lw!wo&UyMRbrQzYy zTv`{B!VR3E3m`q7HU^u8l~4wJUaSLyjZ1Q^3jOWT*2diMDlbBh+=t{MhM!Z~YRQ<)UDI=Ln2R;+k zj=N79OUY<^6|JbTt+m9>g0WEMDXr7<7fe`>?NKvv9Cf#`(%nt(EwCo~kDl^v{NCCu3;=^)(Cj_HZ4bV*Mi@6|#2^u=(H=Aqr!2yPO-O!R9aGeH6J24dzX$(LGr zyXmA2IYR~XCe*C-ROlJ*xRYRfDXW$AEu@RhY5^o|Mn|AO_b-NF;?nFDHElD*)IZ)& zu?#h{wxt7Pc3pFBAWfWW>3@>mF!4YR`A#x%>3H1Ba>5c78vu{`KmlG6pPlDp(MNPh z6wMSbwh@WwAKcZyN%}+t?>9@<&#Ry&Kum&}QW;PL1gy75t5_6|ASfevWT_06a_7ja z>k~LBOJ$IP*^&X`2}~P2DDi=_3$c(eHONzk6nzoY{0GTiP23E`QJd=mK=_YX3yNR! z%|oQ5;D>vMhyJ&{wBKSXPxEA(d$%!r^A7;&S|xlxRN52_*0s0afNk*~po&`*6U98y zzqr7*yip&YE64hqs*I|x!x8-nyO-{g_y01$GR4Sx z`$Vj1wT`)as+<>({+<843D=@Phna|JwEB^oiLF5oCZm}O4-!R_bo1{Q9{iV$Lb5bZ zG2SHRN^Bb|H9oaS3vWxM7y|B54)01q| zU^exlv^b5apuhTjoG*GQz=d|O$1<@p zo_N1YpmMg@J~!x|bj?l7aChvL7JS;c5i+RW0c_|>Icqq2bviSk+@rb_LYHJ4qPE%Y zJPGEl;g=LN(@Lau`VHef10~XmW^ixnd1ghLV2S%1qc3A+%msx19Cclq2yw-3ER~}4 z4ty8p6}TCNVW)d$BSJq;5DK4CoK94mrI~xWS)!;Xoz9?|eYwKJBI>ZA0=YmuKu?3o z130TF2;*hG>#3H^QQzOx!W#@!(Nw#76Vnl-mD-~xiY|FpNTIcm09ATIC#@6y9&v{? zc4=>7PkSQT^ApDkpO$fx4yW?x_tdqnTZr$tIgJk<2FEI(?jj$2XzW;ba}(*7TDh-~ zD5Kwlly`JE#ELQ9E3kHcNaJg3NNLkr^hlum(IX+T(W4P4Q+YvoRtA!dK9k1(E}*;VWv@YM|dr%))#l( zwFwol_9?$UFXCuf@4pn7jMrHw8(Zq;wuB+?2_1XsLiq#i&z^jX27F&c-vXjwZ1*3B zLGQ`Sl{6t$<)QMh&{k^S(wwzETsdx&^hKHN?8}DRMovI6t*epE1ar9yfpxJP%%!Wn zNrJ`E-e}76*c$zQ623iauUfdDG4g4AtAGG$lzWmYbrI{Ua=&B7Lv?USA*z+) zZWV5wZPK;Bg#f5~m3*#c3Aai$_qeBrD+k8sSi7UE%JRn2Gfm%!ShJG=+OMVupmAly zI+CGaZ({{+k;(of+=#wZ*(@;!w?I1l1D_&j=YQ{Z2MrCFnPS}r-sVy}!+O7QIJzNC z`H=+wTIL3J7ndwaA>R=iT6lcZMA9x_qFsxl1Sa1hBnlGMM1BRSC+ku8I02B_x~)jb z>P$J&!YB583sz+RJjL%y+OfAPRQ_Xm#QV0%y|}1OqLj>5T+j4z(jfo;(r*L2CKR@| zc7`6(+xg9*x<0M7@^2l;-*k0?5&cd_K*V?f z9}xOdi4D`IRap!A*@Gc=b>X=O6D)^qa#7PYzQM>_Eq$g|MJbEzoeBG@D8gmHns3Y9 zBL1)!kSiXva+9H7YVio2Wm-roSuXG6XJICoHSDXy4UcFwj7_7$%}G`toRs@3lgUz6Y-Xi_J=i-AyCg}MW)*8#6!HG%p2-;&%^ptdhVbcY@4EfHaq zM2HCXii`WKP`QTAg>}~^eT?*^GD{mF+xtX_bPs6QUG$$D&0-JaoQJBvFM7;<>7p+% zwddS?`VBNFVr%&a@MB8EI=Kg#f`rgFyjm2FLp`#FYjZ>NJ57D779m(C|vG9>HHQgGJSc#5Y}!Xkb27Lrk< z0o89b$C(G-$ntO6zQ$l1OQk#&6^n3U4ItnHveZscO|HcyYG9-Y_r8eXU2x zA~Y#?qpe9>_aPFcevd&L4xWjt`Zo- zM|4GffmuTpmG`IJ#dNE>Sw;I>S!LBO)^YH)i&2LtS(>l6t5DcabwCv!Y&s&9gk`Yf z&}S81U4R)c5j!p&JIHnu!jGdP1R|5?%5(_bku1OMk>dMGR|5~GWJPbFPSo@9*qd6Q zf9!odckr=XF+lGKl&8H25j5YuF{lm@jp6yK6EzRfxOCPIB{Lwv>}*06GRyl?4b{`3w5ycp66KNJ)Y)1G9>HnhtKM zr|KQ{q#z7C%F0Ut?Cvgt&*Lv;7vWzEhe2w4Rm4S^N6g$X6Y?fdynP+(-cBP+AL0R? z9UUcq`F^691SVwO`q-cYq&b_ZONdITnRlFC@3EEl%k97YKK(LaEB6qvJ5KvE%uWdo z$YgQ=NC_irA%+JuZLp0#z$Dj-Xqhx;uPF*|&&K2_bBo z#JnO?B!X`fJ*!ala`}X$NlD4&oVOJEA?`z&K~EJa!BjM>v0T2W6X%vISpUtxBpmhP z_!l>NK-`LHA{mQx+{`6j!MIB?g`CCKKFLi{0bza>Rs^SI_W-z90SWvX2MZD-cIUKw z$fJFc4rg-OdGKS!u~Ocx(tU(;s~X%3XQ?Ii;d+qs>sWq+`DU@xhcm8E0monSMau55 zZbT~j=$T8xDdv&|G{*@ODv($-6%Htn(r@JcK>DrlNMYKX z@ZPEkd#A+u)r8dr6q0JfsF7eLx%K&_qxeq8~wtCv%R^rA5E0r2XE+OvS{lcQOek+*U|8UwzAF z==dM64Q}SF+NaBc&RBQwDz^MoH6wfqJE*d!qV@x+v1Q#grih^;4yBRti zA&j0c1V{9HJgd;K{k0)*I^axFiErcbJnhblmCCR=mNkdHNd@`H1oVEn0k7W3hPa&|aEYAZF`XR)_ml+`)_p@h5=b zqK=PZ{g--v4webc|2fvxopo*IW4UXo9Bp@O6I9_esJw(FP2B7hMwhy?Hm$aJD)KLr z@>)G`VN3!v0%YwcUNJ8XDT;ZBuEBp=IX_=QF>8nEJFr0?Q4TUQj(le4!8L2#Cpoy$ zxcYr7X)mMSLfR!U0H$=?JXB^8%_WAa1)S&%R1U<)kp@akgrft@WtDtGP|>@WFJnSt z^4B!2&*TUF>>lLtBYhA81f^G{{H1e0F(wSo18+@t6W5DY&<|pu^?qaf7IQZuXNUK7 zzKU9jHSP~5CcD!omy4vda1&X#MEbe#hDe64n%$L%yPTXh5)6fB2f2qhVLC}kU$fjx za*+s_<^WiNYYDkt#uJR2xwPbF@%zjD3pb7f8iYSm&(;2f+MxEbd1`tS(NhW3ItU&?eJYrpM$mx#KD*Xe zI69rcu~XQeaz~z|12l|@=pphAzg38@?P;+Vr`A5JbeALQoEod_6FbVA64$fgC!xC- zgxF@8XO?qI>A@%f!OoLSKoYBO;`zbsPlOw_zoYlQhe4FmG~`)Na&q56!w)iJG~8sz zourG-cH=q99o#~&UVjVqu%ddO7rPhOacKtXFWj{BN&9@xRf)k1i;@A4R#v4lnXd?9a`hDAS@AY-EZFeTUhR zi+D&CrgYZ>wRAUrqX$t$tEsYRqKQ|FwAvl_Hk^cR0Q_(K4FKi0G$>T~;FjjRYFB)%_NS7jFBk)SH$D9cG`-*jaT*58{S7HWj#3nEjA{vc! zzf>y_RKviva`g-shx{P7od#a4B-VapHE!u%CXJGWE2Sc`Dc}lorvGNJeN#o|K*pmD z(5^G7RJ}5b|Zz`K+YhLC8Vqj-L>C;j^#@Q=Gx!n;rLR3@(w@7H!pC?N7t zj?Mf?SyIbOQJS*?G&dk-2nW*L(#$Pn19d*&8H-w}-

    G^*~4SBQuc=k`*VFBoKKU@KYw;J`@7ji;x4q-;r)phJf_&hwK6r{Z?N8-gm5$+7 zbFAq$QHQ}^^X^!;0!zHm9X~}m@bfGVfcxIS$yZoXAG`qKDL3QVOsy6*4)qU%%SJAY zjtellb9_4aBlzsaRh?Q4M$KqBuPDUygW5(~ok-&PAlI}3k zmIXnYS<1gs#vGn3x(I25+-;hio5N!f)?QqRozSrNHtB>U_TY)$_qrMa@&I~cv~**L za@rTdn25EL=LH`jF=sVOvT1SmFQhi;!@T(<9qWj~hA@k;*F*eXA4Enazt<`W#70-R zF;>F^#mt4=1m}=9ovYv?O+Ser1B#niNf#}-6YSH!GojloZ@76Z3k#-#Uz*Q&wCtVa zP3q3}JpT$_(2bRW?clyt^1BVBlI|2%!!|Z`sxQ(B3yW$R==WEE`$D9<0AsaMGb`CiqanvtEQN z3L;(vp;*c3Fh4A`=x9FXB#S?58-971ya?MP(kXv)0qA~-HM$qEndOY$m>fIXxsV0 zq%GN}tM|dN2+nV=OPaF{vL!KbrQSV9*9Xlx6yI=l$zdn><3Qm(vDnAGKzV?sa*Ae4 zETF1&yc)mJ=;p$cSX~_A<_6>&#zuQUr0B5`Zfssa+D`*Ca{uMuep8V43(OMb0^Fj{ z{b%{!{{|C9ThHY`hS>QVOiBx`^xJTO7Y4HP+aAq6FCpBZ+)aiZWv=bq+g$fY^Wb4h z=%C1eSJf8XeGylf(p~ZjwX@X5u`0c)9W){`@41(PEjB5Q33Zg!4F`=;B?Pi~y$n^BS0d`|kaHskdyJghbs?IrD?&uA+-(+xdw_hbY&%q1#Z6-Sb~W1Q z33|?*qYjcj3OEHNuLDMZZCi}6-m;672qCk{F_{zfm_Jd+xCNXjT`FslbTy6omx`E= zoJ$3ydKJ`a=sNMQcZNPQAXgK_nQ=2zw*iuj@H3EI^~9K%GNZmJqrTAL*pcgO_5*i_ z-gZk(1{VLUYl?Xs?#3Vx>{&9;9+qH3m;Y7q&l#h{y)3FO0dJ!Q-ZGF4z6h-7K!5n?%ZnBv-Ur#An<731U|>{yn4VGsW^xprsTR;~1JQJdxq8AG786tJDX~_4 zV&!LW_rD4e7T`w1mI1ia;nJRAIwYE;ID^^JG8Pa9c*;EZP6xq$n<(nS^2T9+(a*OC zOgK3i9{D8SGoUPo(cNa)Sr`J>7bCPtEP4Gs`Q!VzOW{X)dhBF6_ z(Po?6@L1CQPKS_z~vD&XQjJ!AKm7P;+8drC~nj4tTbx?w$s)U zOb;exNUcj87E8Af^TRgM3#te=7r{xuoZwMnIf%P+Cll71+^lwBw7AVGm)(|%qZxfV%=nJ^(sfx z^esYulOhAZKU4f6{Gj=hPViH5gzF93w4f8rX{a;O#tpoIlPTc^eV#1zan~i7YH+0= z&xYU<#o^{`#N3miklbsqHFaKu8{j_@KH^C9d-%FQzu$s>Qz*8SN|kaz6}cXiN$Ah| zMHe8E_#3JWigtAfa%&(PdSh+{LXD}+r1nZ_X*oWm&c2<5r@DrqnKE2;(Nc!DO4+yX zqw|djFIofM5Q9_H&3)KNxe)MB)LkT-J7QCrUTFR3jW`qCTSe8y>2X}vSZq+##Cfw8L@gc^<7D7mJ4plhAh6rv2(ud}- zS~>yI=58d@rKf}3Czw~h)0Ocl1gIMWKg(l#Al|#1o&QwPR>eKN0Ojh&ZTj>LJ~91w z5MTYSM!B#LYS&oPL=0EYZRSk>Jj`J?Zl2e0FRyg;UBZtEh3Ue!iWLz0fszY11eE+~ zf#Da<&4zxW^lYP`I{<;%Ll^&)4Barm*1K>{hxd2WkwJSGvJlKCu@=3>w26ouOV7BB z)N2xV=hVJWDt29@Ot9KQM+X3y-UxkXaN4H8Dd@K%-WY)EY>F9WctT6DUTT8MAtGo< zVYF-J7T|3S<{;CenP6i5gzs$WaPD>*_yn8XxKWpI!}5TFOHv9aIDE#K5x!*=E)b%l zu$5#X(90---vgr1pi)c}>7rzz+sci+r~sV3iBpj*gjHWh&zUD8S6@Ri?9EoLWD7)q zMWIC}_bpJCb!P(0=7COKBG&BmSo3Nhnea4mfx8}Jf`<_OGqQB2AIfk&26G5`;O7 zpiq$0qk(u>#?z1pcrNz4v^(1pD6m#y`2yHT-Ia|x-xY=wU(7T2%`lgFll_6N#&)F} zISEbNi{;=m8<%lpHM68v%#vS*9hCNJ5dKI!YUI9xZ&Yfcsj}H{NR0H=&lh8wTOk-p{kUwaoE8 z9}N4mBT8G%;ww}q!7u(aD3QXG0{jO-Hw=--!=miQ;yoyra;Ms&@1nOTZ*}ICkJijR z>C>E_XN1%|gIeuDij=ah@$2Ac-{t_)R!&KRF&u?UCxj z*pEt;4Jp>Xw~(jAyig!iRh{ZLS;HR8&X@gT8`_=8xeN%HEv1U z%?Fv;F6<^lR=Ez!!q&ec=mv zsy`clhc@YK@K_YD27+NTeF|P}7iJno_Nluvh6xjUfZn3Bk$L6uE_bSvA%Zs5FBCvp zh2jz-k&P!z0m?GT+fh;I_?p9X@lrG2@7o)%I#2U4Bil4d>2t184Z4(&7nr zhxDn4Xjx6Hq?e{`7fOxub{K+JicZ-koyoit*u>hQMx5i!@2L9tt+ zo9rR3+uhiH55X0P2e$2Z&|5CT@+u#~z87ws8r?V}qZ?;ablv)HH$^!5?w-U^^=gxy zlOIR#$#9fUaqbOxu#}hkEtqH=V^@E4hVK?XY+RyIkgN;*e)ilZ*XM@32dFwTx$fEQ zF7QID71&*sSE|DC+V4DJzK}i|5D1;k?vC5j?5^Ll?+8OPy0S<5-*t93mkcZ-&x46c zgbSWnbSZ(&;a)!3D0>UH0%+N_Ob#aFBEN=*WkoIASK(SVS595b+{+nZMc+hX5^io> z2wHpG9lu+L%IBo=@O<@3xUh#56AML(%dF9ZO!xBfqpqR;p`K;-e{uVZsMMUj(8L=u z&FA}kq+%ntwSi@&9Mb-O0?TKY0+t&U9`?P#@`WNC3@nfHy$4I5oQwadh>IJCP~E2x z%Ky$a6yj5%6vK^&;DmX+k6VlwAZch8QGL>RWw>(QLS)C`*LcjAw5U`IyHZASrDWj@ z?^?N=D^r%-ZVo%Ee);nEuop96pr8y=%IGp$h}*q(8rMz^wzV1!S;{wmRXc$CBNG!X z6%nxU`lw3volPbtpqO7nGeqJ;h=esjX!hSMMj67B3Kf`QPC-f@y{E~BUrPT-O5U~p zV2XG&l|sRpRw%|`IrvI%v@&K)kP_jc z@ca@*lWo`?s(Wp8ba(UH!8hEAM9efu7+FAyvMy%#(b`>FNa6XT%mn@4P9sL5O`Thz;-%+a`$h&JVF|5yS@kQy?}4h;__{SUMMC z+aicPy9Xe)D1z9y`VixthuFJvwmfuG(4O|9K2e9#;w!myC@>yyYPo8vAVz{y1Tzgp z0W2Z~nTd#$+^=oV_l~0U zoSg#ONOm5-RO@1I^9EGB#Rkl_^hsPYdgRs^h&F;pAvi=?q2*bHKKerxJT$|$hM%=u zux8`&r_11R@(3OcxaVVim&tN25~DRFJkuIrz?C=F7$j&GscK5TEb!;Zyup*M=xDdK zLQxS-+z0=_JxyMItyIBTT8SyJKYZBAjio?xX&lX5GKRlfYurkxj%t&$pIwE~j-YE0D)W(b+C}%;uvC zHyGSuTi8j=1N*av*|@;@jf8iU`D`1um&kn90#~EwnVb&ka^;8?E1{%=LWpYMU+(VU z3{}T|;|b*#mUU zShF!3hK99$_?1PqvOOfS(jEi({t7=)b#e6s8I$RM9d1XEoLltwms**^6kKl_aT@Ks zLJ}@baB!3QSIb(Wn3lUKG-11dPVAqu3k8Y_^cRGY!F#p_GB^WX@UmE0DQTyGl2B}9 zspmL8U?C4)30j!rO&@Y81R!N7gd9078ybiV*3>x$bXMtJ!&g@LHw%+(o%g$YOaMEa z@Mt(x;nAUX1As@niYuN`HsEBeXR{;Ve6eHTfL98^G~)yg%3}O0T_29IFlh^1LDEkx zPv%BpP>E|NE}Fy}6BW@^7atAOKDnb!aq_mXfA=xP3MnBUY{tB4S0MXf9 zGY^K%wW}XwRIni=X*{`z>iXSaL;CMPI+t9_0l5Gi7R{Un%~TRF1+KKkG+O+q`P1ba zfc%)+iuxmKtvEeC1Y$9IgIJ|YJP9zPs14`=&Bw_@FD_R(U1}qmWL;{yAD;CpzdYBg zcA zuk1AU?hN8*&gR@`?Y--*9g}uLYo8T;)u-N9Q(B-N)IA>hND-k{JB~Z z=J7>Yw&XK|$-?lcHn81JB&dUD2Nn%zX=`E2P%)Zot`e_WoXn!mt@&I3Bgs`rU7J+A zF`#SbYPd|qlSu%wUf~1%vl5;e&w9A)#|Wb3LmNjb^k5DYF`9;NkciX7QfqyrTLbqf z$N7!MWK_D55NrMZgg9NsJc#aaH1-u>hcwV*JY@w|xd$yLpa#IlyosOnk#21mVnhBR zI=TL}L)=n31W+t+uMt2YJ&39h`1#lUEw?AoUxRykyN=`M&x0JaR1d%qj|~{hWv|8G zL|S!wHJ9EECm#SOJKYc~InCY=hLg6(X=bi2-eYg;cA5J-6;CI#p+J%X2q!tIGgHWI z&&vt_fV}D;`8N$t-hwWzrB{~cUQ^FU2YS=49%xodgxky9r4ZWYIAVwkt#psTr}Ch+ zdkaopZQR2&kq`PHSWqy6m#y3zP)X6eG4&ZVeFV_Sbt;(OQpU|@-K+((>W!q}p_MS5 zHsacAb)o;MA*LhT@);rvfDi}59h~%8M+fDE9d3)sJE8bri|&&%>1f6oVeW0pLMvn^ z{=ifk>u{U8iplmnKS8>Z*$7VJ!_$Z!;>%xyf?dTtUqS>UohHHeuzDO9lp&c z7FB(U_HpytC5o=*j(hYx#!!n@?bU*noF8WLfu9N8aS$HuY0GNiM$n;}y-$@~I%uj~ zq@It!PF|LyjT%=VM%F|zXcPk8%VJC3M6dWCR%kiq)+1_3pj!@CXJ+=-lmG>9aXpo; z%G$NY5#P6)QUqyJdID^ZPy=aT_>5$JSn3*cp%(wbs}*p2iYWk*mJ%by?&(Q}wR1`?P%2FPWk7b(o#KJVc#d=3+?n%n9Tpmwapf|jYTu_1Tv22)9hKbvGm80sDr-^_y z##buy+Z`8@v(1I(A?iI9{1iLK0?Qh%112(hKwa``BEyhgzM zTS!Old{kKRL z$_qD==~mzh-5Hj@b+_$|aWtK~$1%Uz5|&((j`~)gb72#G&N=%0Rp`)Ci;>1zJzKFx zX2dvdvd?s_O=C~4MF=tc&_NWkSAc$Tz3HlTZRMV0d6=! zEk`=!dPA@o#kB}_EWu|}pu9r7bVyHftoC}-Wl2*+S)QebIewODFK-UMddchrb)`=5 zY3|4*r?hG~8gfI@3#sL7U-)exm&-FPw13LjMXor)kTl)_rmOP1ZYjLt*#VWcQ+9VB~*pgcS?w-UHfLx;JHjSzz6e>4u_{l}cU7#ht z!WlT0pDC6u`ZvB(90zZR0l!#m*s&siFW~HYfVol~As(WM$`p%O@RmIrd-V?X%wIHl z6A)(hOosqCpIBfZ8I596JEM~s0ryyN=_O8_#-EjgOFwaw0xr$`Uh|5ynRyZ;X69vB zwo|xNTMD*z#{0S1Va97cmYaBbEPlll7_*hE$zVUB9wYwF9$Dg;pvd z{Dy6X8PJA3!G_h{K>a3hd;q1DXKjEc)gZ+FN>bBD5k=|{ zAmQq}o+4Tt5tz}?j^fGgH!erv^ z6%yNOb;@3*DQ`4TaM-fIt@rteomR6A4$S9E1X#JB)uKiV_c9O>=@!t`-JO2}cp@GR zg3jM|+0%&nla=Rnlu!~3^3iL*ExKcY*_naY&`!;OVrb3};Yb|jthw|L^m~C36wC3- zYn^_Np4m&+^N~D{=A!SP4ItC^dap>g=zRmHsOVTZ{vCHFVMH-32@6COO3}AHSO-uSDQY0$R zeC3~zIf_oPR%a3+ge~!l9Ue3aSl+fB;P(LQC^QXeT<_nOWCc{8pmE z+0{9LZ zTsyO!>t|=6U|`jLe03?N>q8*|CAK};N4F#H_o-O(t{m(>ek$2->y0wlxB2*cPIs5q z)Yes_bYS|cv(D1atZA3p+~Z-BwKC)kk-i9ikjGoOq4UiLCyb8qb%1bSeGqmehBnpE ztL?$QqcV&0;eh&m`iXV?nzwnV@pe9pCUZ-@ZA29J!;xrMYs`j27LJ6;pLBHS0`+io&O_Lx=GcQ*aRtN8+}JqbEkt29 z=k2qj*}SjWketFFz1+FzUB@$WW@3-d%$bKhV#e9?7#Pi?h$MbMC1a2?&xaX=xiZymlYMm6D~HTaix0@=4pR781@uz zo~Bp@le}gT_=))^GVhgRzb?iaM#NpL`B|}(YOL23dTFE)cN<<)W(t29QN0+nSc74# zh`{XWaN7E88q8!7W6I)Df=civM6@EL1G2}*S`+@)#r_Ms(K_CTdE!IJ3{|NiNwdQg zX&tNTVqf-y7cXU+1GM5h;0KHR`tiIiYJ^>;b2f;o;kVE+qfo{#0PQp$ma7S1?5ySC zWhsv!xxvA6o|xe|YgkKXF?0QkjmdATK6vKHm^Qtm5j+8mhmDNles>^&##)|6fc(gV5WWxGHgd=?O|m? zid8*)tpvotTJ3VJ#7Yu;FsN(M#mF@M*nNpk^V!iL*{T%jdu*g6+-*F*nEs!J1DDcS z_8NV5bNFm_?Ps|Hu^JHs6D#Qxi+8iB-7JTkRXtMfwk8SwO-BF9K}U7xC8ytE3SN0( zRr5`iTS)>~H{%kt2qgGxV&7-JlZ@``ok%u&1bo?!GN1u*O$z%15R~qf!Faq;31%}d z%%)GB*=zw#HCBJbE6r{jjuMjI=b28*YvlEO3)(UPs}FHQUZ85&$KIc}IC>^$81@S7!lFh6OBO0g)h^ z)&E$^&~Rd&CX^&f_h7jJq`=)$nDDvXYj8(Yd`1%0NF2=AO0FYXEB#K9Dq+mSR|rz- zW&;Fa<^`N&!<{u?Y>DDsL%H!R|3ElN{ms~btp!J z5+9nvsoZUo!)trVBTK6)x0F`e zoz+0r8+-(sD)UYiK7Vp~q}|hL&pVvqpT1cgP<3e?R3&y^TmBqJY~Q$hq6ySXK)=so z{WL6#>2rm4qEb7k2d)OoK3g``Ci4!ivYOmDRxH>IsRaJRE=g$%-wRtfw$2v58@BM< z2Dgx|n3c9s1Il=1x{qhoxilgFjHAD2`^-Qp@4omAeh&C(ea{1j+B0C_6sG7)uM3wh zJD;Z+K^$68Hm8d}dNDp4m?7!y$85ksF^sx>-Lt)A{}G5DU9jMp1cx^p{@@w&j4wgJm(b z-P!#lr!DLW?F6SiMB{Ci?=-^vmIvs^G0r`OL>;-G(}3b|?w;~Agsj{>L7IG=wGA^j zkf9U~^b5#~Cbs&LJeN;TFH~(hP$le$0-zzIkL8z1HKk;y@4`^*w?at2^N9*8JZAXRRW&FDlfROnc=F~WF*sXcLb zDW1{b69rAI$4y?iu>Ib1mS9`_#N|j(rgY!aW?E(2%;$QW3co# z`1s2sJml?{7`&?}{OwoMi|A@?!%VSV!!F@-Y^C5UyKAZQG}oOngXJSUuuNaEW1JuN z42oirm4QvP{tCA-T$14;uyH3g0hn{4D*?+rfq4cCMK*!ay#4K02LsH2BL4u!!2*a& zeRCC&v}HwI3JZ^N8^pn@7qJy}dhr+^u=Rdo>-+(ZRh|)Reu`(J$tc(n@O}*+)ch3x zLjPsolSl+QklJ_eJz@&qgMzI1aWkp65q~HaSdR1qCW?i|!DU}qVE1pnbmX&_sDISg zm3l+=AG>O4mW8%9f9du`ecWsrYuqzhx|_q_a-N-8rCyr5ccDQg?t{w`UF$qfATcBO z3Q>k(oe4E4V=Yfdmg|6eSlu)emb2(exBNm~KJ{uZit?o^HNq0i!WHgrU{d0i{vE-= zjfM|xO-?y=fa4QIRpscVt4hCBf1wkpZ>&LsK`-xK1TCKg_+!h9LqlornWfo5PbusvM~bC|Q*w_2`|8QAGZ$$T^Y&ZUsPf zXl0@czf0W~HbcjS z0oP(|5Rsm6nM#$`Meba=%d>#rTki6o?UdYA!vtG6|`>Ac_NV zP@K*js^UqWJh3!#HRl=ve=pns6O_|cwrs;QUAZDa2fBx;kc+Vt&z1w-3WgLVn3n|$ zEwJW_IzmM5XlpqV>T-yxn@RKoJC()FpSB>g#ai?OXD4zL>d0nDWREKEVUWoDgeA5# z2z3vy1{u*7?k-5ge$*LaVRG5o&%F>9Hij*aHdzaM>ykPP^ORrl1;UfTfDz-X;mNB3 zPrg9s6?IWTQ8ICmwJean=T|ybK?2$?w$xksP*f6GjisjYE;VN>JuoNPZ6*oLqeIPQ z^+(DQ(giHr`dL8?dX{@9Fh6X~RKj?);KxlnlPl2)VQwwcdpvy21A)}b8GjA2T6LzE zxdp)$xe=K0Sl!3XjX?d?N_qY$h)}D4xzQcSX zp9Xl>MAFvop|H$h@Ln2VEhvxFPF7;w7}IU{r-qKlexKdv_+(3cz}K`#JeJ2GLy5o4 z1+g)#CX@=)yHBb_6&7Pp-ZQ#N+S}PHHv{Cs4by6z{YP8(r#vylzCxy|!@=cJzcHL^up&!H^cTl&@4wdr+Pskw$LHIT zY#DJpFp!Lxf@v@rk>6GBv2yXK$NDL0B@OOt{aZ*R@BzN3jG~dimjem>DvJaL<{#Uh z38CVI%_cChNstnU=g}+gOH~YF9hGg-Cl*wv%ypCnSmfh!f<*i zvhJGu#kA(8JGJH+AbWH78Uk*GY2v{qH<_UC0KPeJ0($hck@+e&Cmg9>m-#yo5s(jc z8EG#!m^yKHgg?$*efL8Zc)AY&i<9}F-0OpEbML|cBY91wJ&{9cz>a$oLv4lq=Jc-ckj)g}2Z(mLpr|JtQz?_!tRwFp^$ z4ZhX`+=8p?Ly}(Kv4d^ENCC9*q7^8aI2)*NcBi7*~X^?*vHHIQ`qn#M6gKGj8phTg0N}Dq+VJ3x3sWNo^Xjo zQqLQ+5hQ+-QA?4rs@5on$9MQ5mD@gqC@zr>A=|7uzC|zir zk&#netA5YV`R$Wm`1eU0tITaH2^sNn2YVH>tU3LOCTu}9Bh&>l#7y#UBbUNPtI48Y znaydH!eYyH+D@{0xG6a=eY~x8|LF-q>5?ucMR6f%2HlLSLcr}TQV3=SsqC%SrS2KT z4fQ=6W>pb~icGW@pMiM_4!fOLE;V=(Q&#vwLLo6koHQRn=o_v_27loKcE*Lc!h)ZBq zjUeQn&?_~AO#LDeHMp59>zdSH4A{Ke8@->GAD{M#W;Anbm@HC={1iqTLZjV!Zg4Fr zE7yYkz6VQ~-sj6gLGK0bZVAY)cLowRXn{*Z)iKozY)=DMnijh`s8BS9V)A)VrRv$1 zSJh^urx7r<&Fs`1m7->MNckS>Ore8^&4!V1{nl!4gmIR_R4*y>1~4m)m?J*+m`$`he%82Q}Q6k z-RsO;=`B9y9IAtzhMC_eB6p*?=G zLU(uv@t(jL(bpj=9cSf~5ZyPzq-%Xzbh0&D$KZYtaqkKcZuqxf*h@+>we-rge3I{~ zr6aL}b4cM!kiyE*?d zUX)kv$d6R4eS(}Xt`|N=_O><)3_WxLzjrDN43+dqwRA?T@pVN6GDf#RKM=DlRGB!M z`vTPQQSHN_w@pgjTw>bU&}8gi;%70|K5hiZFuM?ClBX0aL>;$-a$uowBsZ6U_fIN8 zX*Z+vR=#f8;U3?<0CA#2sk0Y~2wF5R#~ivVC{867K)dxaGo)14(Xk&ZvN(1kHwYK- zY`l(eUGe^DE1@zZ$QT9YRC2uxB~5vWlKT~5GErq;DcL)*p`4ZuP#^k-@MtavyFt*< zV`x2`B2_9;1uk{u-R_#K_zidSJh$ zmwpnLEd8E`{@VZ6BK&W)U(S*X+&q-^(bLe1rSXKDBlBU}VEnrTUoC=W3Is1>B-BDc zhc5^i=t#aMEi4lz~x z`0-vUDfli6Hqd;?6nG6=$NqM5!|9=wdyTU|x;tuJXpi^G3dn?(i)BX-+(5ex8@sm+Lq>CHbOMXM7TpKeP&QhthfxOVH?8db2yjDR z4>i1qo>QG0YP_FYNA|a-?tTDO;NG_{+Pin4(B-Z-iqSm#W*eEcZ+?ok;II81?VFaM z8te`381r;b)@~KE?&wxINSgNUHZ#n$a-U-1(C^!UGCRpe$|S%>qDUk~9C$miSjGb2 zOdU{hmA>+0dpCunbcj1fYkFMLid0PVa7U`tK*@c?pJ;?92EvQ5P?_^Pw_=!^iZxob8Kc6ZbliKF`hUf z28*y)_~z&KR=~qIiR|~Y@H6*SuHFQj`xR<)l;+Ki;^#4Lxu+JKUA@u_xNhfy{q5at zP{O7v8WZ8ITMQ#x&H)1j@HT}H7m#Z4QtFxP-vUK8eMuXA5uM<<=n165DZOb+w-{ko ze`{ikK{$Rf0zZY}r)dV$tkx3f355g5!oq=Bw920ZxP4EI$YlX?^DypCBj>AI!HPg0 zvBtj?v3(6{ua@gX+yOTM`Ff$Tp%PUTBJsp>1rR+H*|Sf0|G81?ruLoYVqd}KK<$+y zWMcFG}0 zES#-v{AxC!Xb z>j{iLu>%j{{d|mjoguY9QYWd))6(|yX#K*K0#WFs%-kZ1UyF!-c>VTc`Mt%{eNXSX zGN^n+enmu*Dk%-_RaIi$jzGI#skHVL?(NILO5W<_bw!;ehPi-x=kgCH9=Y&YU zP!=kceL|MfLs8j{LU65-r12i&Ss!91-J57mEnA?4AAW!$#6lXsCQf8GA{y!1k_Bcu zwV1{gmxnS-Ig^|kP4Y7)X_NbtFQ(*{=ge(!G`G!raBk|<=C&$(Zolmkl0SIb(YYkucUPi1M#X~Nyg8Yhd5(1cYmnnR)eoxoLb*4;!%~G)%7c7V;(jZy zh^!~g9QHxFac^A7IQSSWq*;p_Vokn|wKz2q`|(+Rt46SAp-Rd3t{0(9(?y+$+$aIv z$6DqUvyUqrwz*U%xq74 z1ZT%lh`(1thY!%9W)5)jmXOH7g1KP9YbXp#-E%0ogsaL6qbHUahMboqgwX?qteASP z7eCeR{RpRTf_@lI@5@|NXN9jB`w7xG86MsfA<(D0hd|cZSRx_~aTe2ARBk1D<0IX3 za^XDJq@d`OSaGR)uLr&go#9BILdC-q_nK5Ze->c~Sr1R4zCxPds?$uFb}*?AvBm3Q z4Thx{=746nN6LmbOrg!vQ=KBU*XJz1GZnM2r`cPoa_VKq^d5`X(6GcUK>ebx$ElRG z8c_Yf%-Ax&M;UZpXp-;|4}Z=Uty37^|3sv^n})@#Pf<#yvTp(JpU|rHLE~o+2pZ+V zPkF$)smk{zs&bbYyv*m>rlX{Ox4{~<+JuP(d_y5 zie6~zW}^MmXZH&(Ooz_hP+(QBaewJ?KLtQ)tzwtV*Dc4YdA4 zKXa>5yiObOdtrTrdqn_GCKIXPcq5ut(>2^44^)IRzwR<9VQ2Df4JN2qXFe^|`95df zb&p8vLlsG#ch`?BRP@=!ZN$(-DOV$XaX0I7dKU5Xg!mx_v^(P#HQ$XJ-ALP3yun05 z(wEuTfMIY2D>FLSJChZQh31&?N5+%_2v?Rk2{@KvTv3uP_k5L5^~Jw2UH!>?|HXYo zq!D_(r5Hp)8%$EX*k-?|-L z)xf8lD#NF<8~XH{%JAvAZ1kvZVWSsU7R2@~eZhFUwH$N7IoT#zbS|@4#$~JDi_1km z3fxQQCwSh}-4Yy=zGNu1`-)0Wr}VR?15xzrQD2ODQkcH|-@c0v_cgY$%#HakH30mR znn3vOG^c6fa{(q<(DgB%U3({XvP1RfwW-C@rfO*yomOvYjom7iCT{gn3|BHiy@`p# zyC`V3L)z@x0{msEK^N8^6e}F5f=K1$*R%nTzg+W-R%rXQV=3O1fA2F%Jy&jex29Z>vOV!J)F!$ zCCat3H{8_$Mc}FznD1LS+G{)AA@g@@abhOewO?*!O zFLOQMDO&2j{JUKK&H+g=$l_LXU5YATR6D{T+f^;xQ>@d!HcHkDOJt@&thBePe`NjF zxQ%reYcKv^o=sFf?}qqB#ou!Cs<20en~3{{T$86Gl$=96=NFiFEpIe@zhX2g zgyy;l4;q`>qOV>itJRTi+QnLaiF>K3|Ad-z_Q_p6B!*#z*GK1igY!+;+4XasflL+9 zSjt-@VaY6kx!g~1q`&T&!v4zb=o6PdWrnZsSy@b?Kk*eC%6+*PFAvOyuxa=ifii6_57?Bt{Qnbda<3ypRj`A-;dRf^6+I3S zCK!!R+u8NGQ={5BoOb*XE1Z92OV zvfduS|IF3+6!;IRE5&gP8tjQ(db`99U5+yI8?l49Xa4r?9)a-DQ-`=aN@w&ph-)E! z8*Wrl5woCS6&U>|L08U+o#@7b1&~%Z14c#51!T*lUxkwnx-7u`)M=^gg0K z6#Miwn5b!(XFoJgN;l@YylNtFPZziqb2A^HaEjgM=pO`>;n3?2Eae5X7rMNv_W}g& z^S`1obEBMqKMR(3Z+Ew%2Hg@iLVq(0T|1NI9u5r7k!U@K6#FMq6A(Dw_ZQQnecXz~ zZu-mig8-1f^6qAwBLFBdU=#|mRW6gLy;+hEXNA8ClBY$NPo%UkVZB6|u4rRQ3)_?j zxpbl~ctMQ972N%j1}`VEPPW9y(sHDoS5agMS9;@B6oI8KuGE61LdhV}8A5D;HNKC; z;+MN)J%24P{Ak0^p(g6+%h6FxCX$PX5X)9JMN59Dt$Z8hjY4^}lYYZ=9I_djnT=K6 z&hOOjPf52U^NFVM9P`pNo~@n6iJrC3VS4WM;Gx8xJQQ_EdXdhgA)Z7SA)aIjJc<7H zJjrAn7PFB<1l^?op;!=c9e;99U6*{W?pD{=PX#^$oC}NwfyS`y%!(s2PVRVbkw4o1 z4Hl4wCF&9?oJ@_dJ@XF=xLpfpf zQb5RiG3lKvTrVchhmG7u0{1)NP2V44Ru0!rLn6_qPAs+Ejl#v+4+pDQ4P>k3?- z|Dmf_VC-avR`z#8UisR#bnDH*ovv^Wv=MG(s7|qc-4>=-t}EF_&^xHy61hn;&lekM z_-LNK*K7}7_$sPK#SQ_PA#xJ17TfSV$AO9xML6vF=kMC{_ReyG(jznN^$(;PrEfXh zl0{0NxeYgY0q{a8Bx{&Lxtk$Pzq|%}akpv0y`kx~6ZUg5enMWwzKRtSQHf7E)Y(ju z^=H!5-G_!jz`NC)VEsz81plRGv6LbY@v<{~FRPS5C-Yj~cdy2iY42Ni&($ffFOsuZ z>;L!xH?&hf;D-5K=HSMU&;9{7e!z|TbfXHoam0T{(l&ZhD*cwaBVd8vqO=7kDJ$j$ zv*cz1_Y2F2iRpbkky~yXmqD}1{&tdKuqNv1&Qi-F_9jlaouRIxy`(I@Tq%oDUc=F& z1U{MDW*k?=sTctVbKk45pt;#E!`rwsp$I!uBg1-`5Ap8Ir7@Tb$84qR;cHEw^)4iq zClzj#4Om#_rqI@G2P3fZHfTCP`Cse?{~>&=lD#Lh?w$OY?i;(&fAAXfrR@nR^}Qv` zq?#;9vfu^WZ(6V^2B2+mWmI^(jU-m<73daJ?vekx2mcdtoljI?<0M z*y^664svFK(JOB?x`b^x=zoU|9>E5yxMeA2ZIo`M;rf=lU852cH5WbrCf2)yZOegf zet^m2?G3Q@Al9s(-;c9F52?gi5FrDdSTPB$WShTHC6o=OPu{?`^8Q3O4}@w@5nYOc z6kma@U%_1N8>$0at~NDBQ7=q4t}6-R3s@IiVDIY#632Yp3A%4d+>~ zzw%S1d>-LB1Vl@rsl36=t~rHon3wCCJsjKl98ZtJTS8i##iZBE0nR?|p+BaIJ`?6F z1bw6p>AA&TQXS||iK2mpn8R;ISz0CjDPB$NvNWn{uc2S~e zl4?p1MS8R+YdEPDy<@FUKMe#+ss1Nno>fyA?kGRn{jf{4Z0TM{)X-QHF#U!H>`Z01 zSj%3q%Jnv*RqgB*r9=P7T3(02vV9)NKJ&$)?%_X@X#d7o8!Th5DwCtHW4QMjv8wB0 z`tK=SV|!0yD6%PJdbFAExW`!4H0vU(Bb(C9b_o)u_Ygz)%zr0}8p9=hg#O|Z;CCki z=RR&}yF^i2*tZ)2^Pob4+^X@kP=VFgJ0$g>!aaE&eAXqgLKTb`6V5|=Td72d`#B8N zEqsp-mjjqRVz&|d(Jwh91zfDD$L`WD5@V*7WsWwOO_cX8mGS1YH) zMNv=36qIrYyhFMJlX&xmLW!#*cRz79z&+l1GfMgHxfq28A0Am!|%+bNP|y$qZs>#CbzBr-rBaY5L!^)X8d zSL$Y}qI>WSqPv4}3sQlMp&+e)s%tW3&Ozc6<`ksu+H9&aF-zIn!`$5vF6P!2g)dB-=VfjN7cN&VG_M*6l%Zy~E&vl}^x5m_uq)47yO}_ShcP!9P&GxXH`b~D zd$anFm7E?x@gaMBDq`|LMMO2F*%ppvW*Vo`3)*~s|H8Eq-1(Wx{psy9AHrGe;N}kl zl1mN+y=peoD+(qOE)q_OQ4RiJSYfc=vrWx(G)Db*fb-ONftL@XHyO=x--65=KDa0P z;KO)<&S$Bc(i#-JsSvns`h)kH?AKoQroCMnTYOa4PQM>(5!Jb4Mq}W!z!B_a5{YN9 zC5+5;KPJdbWOEhgN>AKvM(rqn$(^aO)lJYb2G}f}3h9&OZ zYaxku@iolREJ#TMYttfajl3ss%Fi`Q8@E;J_Zeihx1hVs*;OY6O0gVwzvTbF2<~Qf z?0Wqz9qV$BlW zo&^%v?@2Jn(F5(#Qx}96)nZM$V&Ed3EjKUgl>*<#ajxVn>5wnMpd@S{`aSbo%F1Qo z_gw&mQIwrA={IKr^pz*&3SOkfi{+12m~nq7t*_KfDVTEi%LE6U*i?76cq6#TSjn_l z*(tGvBQr=jWmVX=^KGI(_rb_U>+XaVfaoi0Qf8|~^=-%pq8G3uT`23*Ohg1_b;AeZ_1y5x-k^ZssXk17jiVqaaO$j{ZS5Ix>ar z4-#wHRd8*?O$M{_04yrp8o~*T06<}bA>l9!*TdV4{saEJ-EH3%a+>WFOFgfRe3*k4 zJ+NbaJr7SVUhb;=l`*|A9Z#o>9<&xY7z$=z${^oyP=jjAAX4Qf3xn@~IM7N7!-QjB zBQMm>0D*!;;!PD+Y#USn#}yGdNLhx$^@;8nDxohAw~2?)V2-4}T|D~XikfrhXz-Mr z(@a*K4eIG$S%JQ>DeJ_|6!NPS4*cLkfvI6vzmG5nvtH@mr&~xlTfE4uw>XrJH>ZFD z-5ii0T*+$EhC7ZtQZ|lIN#X7dk*GAS{~JWp?JNl(PRT>Nh$`HCq{=2;m^D(;BiAO} zA|lN9iJ#@q*Mpo)Vd*XsKWNbjN$=OCZ~zG-@9jn92C^Xa5#tPQz(0I@4O_mcr?D7D zy^_p!`t^C~SzXp4v#l>i+qxx}b!hAcV`e(otwB1@@+;Oh+9rML*#K=NHwV{#)?Rv_ z;#mAfHFb|6g@3Q@RO+a2;X1VJ=Q)o0Ehm-mJJz(TyNfci%lzF3Af7dvz>ZGUgVnuvda^t=s>kpc?I2h5IZEv%2Bs5*$kz3-0Q%c1URgdz~vx4m|o2t{KS0#LYi`?snDmPdg=Buy^{R4dXppom2Jz8^r zRA%uv;jO(A%jWatBc2AhCmn>Xbe%T`FU?5`*vDQ~`|H47_4uE#if>(5#a;WPi&9of!Q^6Q{bc6VCu67b)Z=-p9*J*3z~=^E1#;3X<{r904#{iejeyGm6u;2T;dY0p&?)jyU$h0*-&%qh z)Zc0@)vW$NdY&q6Lo#zG0lG|HgXpD)cwNlu4`$IjDbV2#b(6okPeKn0-Fp`VVV)F@ zjdQT^$ykB!%xtdfFACc?pt)^&SHJ7_u6TSvh`IJI?0OGSxANlf)mj){O`z1B#Y%cG zgCjw#o32l|bzF-R(6A_ZX`*PJ2pn>)+B9)vC5#WG0Q1*q%}U~@sf)(O5cSZO$fBrD zk!qmm&{T3oNxjho`HY+6Yc!K5fFrK6bd)EE5ryXQkFMLUxX&U0V3w5*=K*pZhh&K( zxVCz@&+uRwQQ&4#V5o?Tx>1* z_1cBTH?h!;x{O-7X@At~rfztZ#w~Fl|4)+pxcC=%sVkQyT*5R&NHa1Ee3`p;ZX^w^ zZOzf`0gOI99~cR4ZY-zY6hNk#>cN$_6}Xsg>|R($O)ZJbfG*TWLBAbes0!Ib^Q|ut z^kC7fudQ|{ce^jG8VK8j7cui%MH7szD)HEbt*7@gau0Tor|_xVFp&!0lwp0YgL{e8 zYg%J3I9St?v3SFdtq3UL6Qy{QLJp8cpVU>Tv7fS6v*LO2;^iLkwFSTWTfpE=usgR9 zGgIo`z-go%gz_g+)2(fpA~_kVl7!x}@sD1Jc9A3_HVu(bl0NEHJ2#UG?0nO}Cme4o z-vM@;JVL3_U!n`})%{1>_v4AMrgh9EVOss-TxX@ls}QOlx0BtE$N(=;`jfuAy#z=J zm|mdVW>SO%Ouf$*Fw+c;mIj_f*963ieA;dBf$~SJAK>f(W^5n@ZS07bV#jCRBzaS% zGB6;u+Pab4jyQi+aWyT)gTtThIUG@KYai0N(NPIGMj7ZV=GN4WM&Bl+9v|bAH5{Q! z#QkuawUDjqUo`OkgrG6A!+^WJ93JwBDXkt4EU}*0ypixa+=Z6mldt&5zaI`y#kEL= z-ZKrvq{UM^jS_eNZuEv->mLsnZ_eqvDU#L)wYP@lZW7eoptroOpiSw(^FT+X4IIM+ zMPQSy{c^Yc*^5rSTYJ%I#UCI*rwcLNa1^qJv&aSw>WaA_fKc3t>(NKUaLnl;9{yFR z4-V6cBP+%k75a#v|D{-QYyX(2(E=B0p@q{}Kq&~I=Odyya=qwW zeGQfMZfTl;qNvp>nZa=FQT^kQgCck1;%m59a~0G->kN{n*p6U;6g)ZW)CmT!^pWPZ zQL@d|3^I`D3=O?GGjWwVpa@3H^%j(AI~i&3ACSa3G}YYKkmEIX?xISAafH5x=*oNH zR@C93_F(V^)Uk~G?_Bf@tjEu^Ownn>6d>ym(>#aMmffr!pN^JER7^RJY4{nldF{kv zOoD&2#}G}=8yfN*b9q3%&2|RdEjP$usHqHTnau95g?kHP*VK(dkZIwTBMJJymCv`S zxMF(-a(}@U&4>hQa}Cb|y%-hd`WA%4%HFY(>Ui3H2}u^4Iz%D(Ye{d(<#J<5uzKup zT_ff0IG&q3cU^ZyJg=>*{ED!y8hlqE^L@0gSc|^Qx`tb$h5MFct-mD_n50|#pxj=v zlub%Ky7>ycd%ws0hZJV>+|3G#hFU6kf*>Ru%hrN74sMyI4<3Ueq*C42(N!n(719hY z05c@a{3N)!5a}eZ3SRQfZ^^shh)az%pF(eK9fd4o`<^K!j~h_LVQ+!9SOO<1PWL4^ zi(IZidh6N^q2h(_TKV5wxz}+@7*eQvtPep=LV%hM90E*ea-H$mnQm_h1-vf}bAa7F zM;0%%P2)HX{>fVq^o$bNKa!Y z|6j6UIlx}nkP9?MDyRBxkjo8j_U zT-9xRXTs2kWU$2uQe0!viRMxwwgqcB?e}rd<8#IvoFSyZClQN31DizwH5Qj6;~I$X z)zAj>k0^W)ra~v2-Ml$ea&{pdL1XN$?f>+!|5LR8oe|A$#`CGI1U!^56#qc-#x`uT z`?Qg+GC4o35;fFeSj{!72e%UN?te$%!fdS4A&;UaB36^WtP3l&Wness9(LoD9N<>M z>R6x438ojOKHxERJCxwIf`VAP(&9&HUhH~Hfq@vvkwJs>y%6RO+!8u-((jR&7jt7v zwiLhyO=4>qsXA2WeATQtNY<;J* zRh{o_Q9CU^ZDuq8X(IiTj zw9~%9o%w7@Ea{diKh!-cF1i&hl71GQ?7oG+*7BaCv=|_8FXO}4+1-bdt)tva>n`0a zi|k@#S|pEA3`G@YU=kNm{UB^G#WtXH|8sNoEhF|@?n$7gP8Xlab|vJlWp zWkN)WdlOz#yYL=Y>~LfM?`QUU@_s=g!KNtF8Y4_ct9y!j3mPHOBTC{W^JUz9pitwF zuCcR6)4erCIN{}pg>d}?XJIWnkEyjKE^}@+0;&=1?r5q)8MWp%}WH%V2;} zKUkqv&7pZ3`Sl)N=&6cZ-70A@Ft`NsMIEQZ5ps@u9zBKFfbOVS3gJl)W19N?Yjh)p z41QQL_h%~BuA6#wFKBMyRSh5zLd4xqg%G@}?(GT-Jk$HE+ z>&&beKMOK>4xDEGS8u0- zT1I%T-I?X?d?qi7d0Qmvj8p!FT0u_Uzjkc?hlaaB&kbJp1yScT(u7>DcUO~8SH+U4ZGJOEzX*V(HpmGV#5nTYqJKmWi+85N|g_|u;)op)EP~jWY z!>dpE$J|N^8`7-U@=M6&>{bO$WO>_0&c9NEH{fk6NDt0NeN(mTTx$fXn2K4A z5=D)gn8c)Gk}oR80!`ALPV6LaS`(Y@B*q~jF-{3w2APymL77!RWOM+b7*udTQB=TL zR0Kp61PgIi-rs-ieQw=Tbqh-<`{}R0@9B=z-Dl5x?X}muxDe%7DK8c|V1PIWR4xOo z(T*2K26*KXS>7tU&u8_9O^wa86PNsTf5|;^oPUs)d|kNYGT!u}5?y+UNwe-KQ5 zmL+ttC*~=(9Af$lNSs-*v|yRYUt;y;m;AGdv#~x^g+twH5-3T&y%_CbC%~tOf24%`#x$|==mxy97j8=>+}&?Ty-`OX^Q`yrc*ZnX^7gEEb+@2sj1!) zFXLWd<;dc~9}sc}Q%lHx#QD!bZ1o1h-oVlb0XIf*U?3bFrBp|hqzEFrB}h{Y9zKG3 zo`YUI3P!KB$mRD@&78x0UTfjIdHVuSINyY<1!hYoLIu5XP+%eSZcO4}rSPSzPHr@_ zovZD74=|^svm4Aobn2yd)-e~mjkpm6)!`g_ z#u`=~sz0~yTw6biv(!qf|3MdTl7X_H8D`XGdJ&gF`DZoqDyb<6sF)PY|0+f5uhpm4 zv5nZieA$mJ#R@$bp52^iIhD_*&@Jj|n)kVK%)$v)E?z-{ssJcb0e-+kq514V-A|2X zZryplU>vy?Zi;;!`N`2bfUjk_(Z&A9^A!zZW-k$(W(nB}*w3&2abeoRo zl+H;RMq|FUb;Ew^dCI+=stTT#ymdIkJ;Rq?QQ-ia$mp)pE0($kkl@lJ->tCdn-%VD z4qxL;fos=?61N+`U<&PB4HlRxE3}kevRoMo%lBi%7y^4lAq;hxqaQ{p8l;-Q;7`qp zaH$@&2h`ps`eYcrArpiw1!Qu|cXKaVp+ooj>1AL*KcNSJ`C=Bn8bOHmStc!hdAtU(W>n$~$mPLe z?n8Hr7qyaX!J9&`wk*6NDqmi3mCpW-tB`K{Ta#$LzQCNqTd6rzs96g%Zi>?tdggAl z*gB?hz`<33=ivYFzu)J&u9hb9y>qKjceistOBdWD!@(C@rNPTjVKC7OuHUqqBLTF_ z1Yy!{-isi@Oox0CDAFR|rrbC-*?M@j!rHm5G^2Kzt7aeB0Jnf7coYy=TF7$)q@982 zjbNqAn8{#nj^c1Vz;@f**Kr*#i`x9y?)9xvjeaQ8W%z9$?e_yL}<4XG|k7jVC&3NTh+12S5feuOCe1J)m!VcWT^(L&k0?8CdLIy_-)GisX&PFZ?$+%R>qkQtFVBq*CX{y-3=^9?XSW9OzjjZwGY2X zaieg8!0nTSBq?2ueHiFiNk3jCN~{1oRs#S31?!3Tb4^%%kw;iQ)RoFIuMr0$rq@W@ z_*XNY8D@~Z0$RD3^T8_1*t`~Q5zalr*DX_srG~R-M4E~S9x~aR)lULd;U?oI zRd9|<;yYAK?lOEr_tT&cXmN_5LF*o+6Tw~3k=j)MVvugp=<|I1#q6XoR)2DSwfinY zU)P2}w?6`AN*yl4np75RU3(FAi2L&(H0M_4VX0NsN3BVka7UKGh9=A4sw3Slu4%qH zq*BT)lsWnu%+dSE=2h9}&4@+j>UE7+AZW=;&9OD(e*iKS&7iDay$;laBoGpK9Prc6 z+X3%}ev6hKE)_MjG{l%fsGw<+26u(v{5 zC8P>?L19?5CC9m~sN%|{)^DeWf+Kz#MoO4r1SncSu)y9Ss9_*Lu?77J+rB+wrlr|o zlB+G&;cgywVc*XViCS&Z9?c6}0SV;6_D1v>Am(2`Xc>i7WGMOb+f-8wsDW*+^!0^A zPKA+lV|gD4;*WUlG#;JnK;oF3xlO;X26r0Z@{mDnU@BpO^7LZ92r*peGL*R$JVjs{ zk+VR%UGz8?l-5!1-j-`k9LLj2`e#Ro_CXhQZa&hdt(_39gv6+Q^86JiLOIoNM5zG6 zUidE^G0rec4gnl=Qn&F;h%>%{HT9vlTXCrC3_qjkJx*9yy)Snh!d9=<51b9|!bb6@ zd+d^31B;ykMcESY5z;&DUh(c6WViBJTXThe*A1V_=J^}$+sQ(rEfJ|yTIx0-SQCPp zf|Nlt4{^Ay{2h;I0&b}fh8fu476~-CMGD6&(6|*cH3=H=5S~!l31F$-3S1%TM9K_P zv9Q$gDze|T;W7R1G0>=fs?v`OI<%v0+^s-|Jer!PR;1i@yaa5!JpAE>vNyiwVoqFu zJqPXC7dUx8$yZtk@|rtMT1uuD4exq8DN&JAkiW~O;cdW4iz0SAH&pIhDqVGnZ$;7~ z>jmp7Q}=tfKqV-utL?903(jnivLw!#JI>@T?w>ssI|<~Z42`D$8FfW<^sjp8OcP3c zeNIwMlzLrPjO@sbx?(hHi$9x>De)?-p9qajh=7`w(1En+;5H+$D&&FM1acFJeFzukHWUdc>HnOT|KtRi z@o)7ia6GE}ECee6YdMl$D#irY;kkUwnbBcAdqrP9BjvUd^@7MM&@Jud?uEPU?=7M; zi-(1Xz6C8qcl?qu6gP$*;6&FfJhQJIDUZP(bc>v!W4_( zzcu^UK!rX~3_X7C#a>nTmWy_ttfr52Sygt!c`-8;Y2=;aKa33)85??l3edNGhy z!*872$(KMZO3Q^g?wnM0;dg5If75pL0jaq?sr+M=p0PbxO;B(=SdA0AhPm3a`nU|G z8n@hddq5dtIOvmzAZ(U40eaf#Ni7%P$Yw~)pDZ5iz~AtCTWu8S;(SFaT3t)$GzbA% zZfyx^+sKTxO_m=AD{OnfM9r0MX~!UJd-Yr|L7LLFF>etC%TamRk^s!LZi*cv$q|rIV+gST3>K;WtpB(}s7IvG zDF@MDJkN_Jd@Xl#^K0@Cl>#Dh?jKNKGPa#jXR_}w5iH))UX(T;n>fkY2HC(58=cVT ziLd~yb5};~2h^R=9brPx#1n#}Oo;fdiKBznBy=INKbQAdu{%rl&<3Loc{1V}Wx~=i zShb5|e4#8B)=S0F);&hp5_78Bz%1+gRAn{C6Y@gyhvH6qCrKSGQ4QG+lW*qAJr=t; z*5t9D6X4dQpjk>lG6jI_2dbC2_jVq~a?0<9I{8ovn-Bk3&Fjhw%7Mn5rjVLKj#XO~ zEXG8?M@K8Rn6)Z*A6)DMkmKegnMUTbFliUJ1#VM=&oCPDX!kB^BT3(;!@ zZtw4^stVYZj_w7_8l|=<864rSZK(UU{2G1ZhRRBTErDA;5(MzejC-V)2c+A_zGeQm zvb|;Pc;u$A<2t1$@eZcLh9eG?@L z;=~LhS@!x%D=*;FVzu2CR2#rzhhnIRbRtZ{5lG(&lHNau>+qeKM&n=yr2z}x-zayt zUlCkd6VArfad42asNFOkfj4X8Qp$0>wHw8<)RzKoUd+g1CTa&i`HG+ly(#YM#!g<9 z`57S-kikRKrReVt2{zUuIk7+ez55u0J`IXy*Cz2Y*h#;AlmcqL%6jStZdje!fiydi z9o-^T(!)>sRJM6pJn4JP5)wP`iKXyy-YnX4Oj!wCIqJrFp5(RyZ@j#{R@TUsYUB2D z15cGysqY`erxD&yu_<$j^&4eOu6Z1Myy<`AGR`1nM=4Ql=D?Il@$P_kE)}pIh?QU@ z^CGf@1DCHc-F3~R5TCbV*`m(s}QE$;`6kuXguka%nx>iuAR% zi}qxvs_{rDxg#gawTOzFJ3RJ0gubr9TJi>H^e?`p4J-Mx#unc8~?h>9d5hAUooYi z-=0#iD4?ED2rgFY|J&YmRnAawdjtXFMXdB&{q_YwMi`r5yBY{{0{01q1ia(qDWhvR zE22hlE9!;#^qdVtc;+!{4b2EY$7ugr?NNJjuSdpkIp_Z!aCyAnD`f$Qm)Ll)+#;~F z9@?WS3{bhTZj7l#QJ$wYl-aoD-^0uO7%6p#X>&?cxKxO9!Noo>EYypM85A5btq(aB zYI+-WKk7w>jcrvAM#aKo*V4_z7*oC^0A~D~#r^~f(~HgD{3-e1^`+=)Z!-Dh#Tj!{ ze>oGW=_i>*O8yb;qC@0X9JR+h-?sTNnA^JAm(1<2x4LtL!Nv$dTjK@TM0b#!<_=6F z=*2w&o$Rj*RG_X(2kC;YMLHcn_i84voj96QEC6E#Xy_l)6gVeAwC3Z z!J;3c%;b{WPmY>smvR7tgo!E$AYaLIQo>tuDF0$}aw&IB!gpd$`AF3n+1>LAg}oQRe+)zYFhq(0Vn!Ef?A_Uf2@^D5kf7=TVBDvN9B zT)iaGtlA+FJ(-ul5KohG955($D=(C$xCf!O?jh>DAVYB|8_Nuc&go&#+>7N%#tXki z52fxIdgy_CU{-nXSR1E&kD~43x-sp1A-LrrC_1`>ooeYx-WBT1N>*a1=h5V2l;v1` zO}zRU7gEzNHg<~Gzh$>yz+T%bGWYo*$@Ckq^kaqft6k|{%1sNuswq~wCaiQioz)Ml zkT~C$%E6D=BpPKsGP(V)#*=#b!h@oxZLb$ICo|%Ih|cta^+TByf64JQZHc%3t_rswDSVX~D1M~Bc9@wbp)rkO+Msb!Q!M}4u>A3v<-eX; z{>;SkcgI9nO>+zf=NNS`0yR${wLR@Xqygkc5a@cn9!GAg&;z$)yI~;wuAD#py~1p% zFG2%VTfif&m*NSi2BGSy*l+rT8EQNlv1Mpn$3t9{7#-n#HVy-|gH}7-m%J(f;;XvSMmU zj_sx<~e2HdYUEOE{w0Cz9ms_pxvek5Sk1#d; zUSz*JkkiEfUTDAD%MrYnRhVxDUEI1*VV?c=IWpPq4sIF+2ECiGT(w)|*X!K3-Gor`{uKJTVkF|DVii>|ZE)8&4 z7L1<>1M3^;xCqu}rLv&IY>RJZRx;>8!pB#y6EFKIbeCkuT} zCL7M`>#{|!beoI3^<3yMV_B&uG$(Iu%c)II?xhR$^3h{6D8r5{7OoDr7aL3HMA8sI zGPT~#uG`710$k4LPK@VHXti4bs|F+wtmGkTtGi8?VWFNQfx7Q&1TvOx9NM!j+2QlW z@65W$1H`D-fK`BqG@mL3rV=uO4Ss1NnA}3%Am;H0yc|XN`I5_Q$CyMU)Q6l{*7DoC z$x8n$RbotSP%X)fX3-fzXEs@~^B?y2ghu5J_MMc?{K^bLDR zxIB-v#66*xN;&mKoVrladEp(vSxoN$SFWdOAkd+L$OKx~fByBrHp<>Rj5b2Jg=htA z$+yDuipi&6{A4Zuv;l#NoX)Ef6)%h0EKz`yZa~mzNTbO%=bAqU^`$lG6E}H?mzN0@ zyLDZFV5*X>gk_P2GR1Qcrhj8scGHN7J?E;Z!tLZ@Qn|Hq^;p4;5TX=te1j-aU2|@v zsHNM8j*1W014)7e3*N(ras^^Jvha0{?{0v?u(HkPc@ku9mzskc(z;#HrP}whH%$CZ z1U!th|B1s`*u>H3gH%f0Doi~qiLz#EZs~BVVL1eAJ&@%b=tQ)Cel2XQt)bjKhtArL zWhN(OGiIwl-;90xsX|t^{W-hzrv*Cp8 zU*2gEeU_|_)-=O(Q*Ao=?0I_RQHb7xyhRRpZz7E`go&GA!4I{7$Jl?mhbhzdg{YY+ zw%l#q(57k!+%K&YH_iH}bR#%}-q;Y=K(Jb0i!7QlJxRhW-sM(yiZ@F(xx@+mn|9)J zn>_$DqL{3|u^fo{V>E)-d47n?FB_2Ckt%%t-aYGO8CYH;G)&PDgxI*!77ZyV<`yjh zGWbRa2@!6kguk<8W@!vXLV4{m&XDdt9A`RS6;)ns7kw-UL8Utsuzx#Zd$TXxL0=-b zZa~nbliY^jOGM$vsK@kM(I?~)GckPM!uRl-?0$eT83comhVaYLZZ$Fvsm7FjQC@HN zGXPYR+re*rzrDFH-z=&|(k1&M&GdcfIZ{UHne*^BRWBmbo7(5O={hcVA2%h>-Sdxh zm1n7Y0kFqj69D!pY%o7XQpCfqphN;uSEy(W%c`QfS8!Hsb>(V_nw zbQ)D?e<$uuOnU{R};gdN+#?Ic4)@< z>?~%qM-P3C?SCzIAVaoTI2kTYYAu%Wnf>QJ-rv>&&1SMAl0eC)PE)< zjajx1GE#Bw`b#dmlDu>gG7W zKy?2di0w0UzaFls7pR}Z+(#w|B-0J_x&TWqMZGv1(oWHUbjT3&Q}UU}4A>ww+(|_% z7(+|J@_0;Ptw1hhYVv$*KXn|Wa6-UU2Ml5r)**{i-`fFND;USY`!1y5?cx@5LiS++ zSXEtRnohlhnrZ+5~JY;IKnkQR3_ZI1ut+FaDr{`YakqLGI z3AxWI{M>w_J78&V$VXtmgE^|VM`4tv=11jc_zAtq+BCkPiShaJ753V<{gjE=tTjn> zIuD_LX2J0m#Z$}F%vugkAiQ;&-!dKzb+HPLI$oT9zr=U-AO*TG_5ftmI@cZemSfLz zVv0P{b4;av11+`uNyuDnt~#_|#D^;|&GwblVOP5A$>yG~u=3>=cTvWvGHN+RebX&~ z1KCg^%a+X+lsKV~taWXu&nZ5Y;G`-oKy$HsP>#~(#=ZPUl6{wC2K<{N-(2xZd zG(D&`3+x_qA$p37o^oI2MgBJr?W>d%Mhp8;W-@#x_SY4dB`9P(mnY>qS6QIvYJKCD z{8so&*94Z{3w?GaCi-1Snc~8`Y-;0cr>4!@-&{Eeo2*q^1Y596l+f$A(kk$St&oLv zLV;a4x86cDf5m#P$;{vG{m+R{o%Tgo^a>OFJp4SGzN%QRUto3qu@yG%$|(P&Jgc|@ zQc@FM$qIJmxgAhQUCxS%di3fQmG*+{KrSE#^G5}XAw}FQWJ1ih2#HkMm&~Tb#Jq3I zKuLi@jR93)C1IX~09pl&<3Q=L%zel9hS#YEBm%vtVDfW3|Cv0ALj%aH5Xi8IM=r1K ziErjYJUaQc2Fka~S+>YK{30K{ye4aPwhq|CEWsXGcY6oN1ed zHMy<6eU&!5)UD>JKt$cY4%dy z5`pp>jGRKirs~>&O+w`g#;%8SA@3qx6}mnDDSQj32y3DTRC%&v%1jiQ@?qOh{ZUJG zb}xpJ9%Q7O*^rVz6Sl?dhDk||{5U4J8fqK58VxlGXUWpDA`*}**3->@t7$malb~%@ zDR4C#O_oMQ=nQ?A7}oFBGeUTMV=4FHdFs2^6;)^TU2A5SF(01aO7YO#2(o)Bpe8~c zaX&!LN+1X2le-ghn^ehhhY3J z8ZBeA0SB*avr>s1xrBw%Xzo9yLJtKvSfOM&${Tq&E-2C??=MDWTM$JJ-YzG*aTbkJ zzZE6Ig9ZM1STvyt?E(B=1I?vCHWb;;ZG@cB@0+<+q7wN{t#4Ml;s8^dFtiin^bzwG z-POA+5y0#-=c+mG1bk9}={S6abkB!%irZr)mLtiOU(jpR=He)m<-q~c|J@SF#>U~ihcp??IwYvjc18LMwKwTX?LS`W_PQAkj`4i9j6xxu#X z=Bs`)VwqPXKeD$M?Z5-DvIuK#$p^puk)!xO3j2d4s1iEY*(Q#>m3#OPL7+p})Rg|u z^s;~|CYr1}P1u51@}XTY07F{E(P@l4cQ;8~Nv`Pr9RdZO1$guTrKqe)fGTH^Q=2oT z+qphKcFN6c#R$)#Bx`VnnsGMJ_JAB5C1-_^FeI#=PDowCG%lrVxIr-jo*c1W$0vxw z+fovRyEb}mmsb80^9+0o1W9;xHVcurX>C_k4 zFjL^{62k~jE=0b<^3$2f1l-udr3x-P@o<8Mny2 z&>I_BY@RhxI_ieXm>SmWVn6Wb7={Q2PO$YK)*I`I7hqdDjV;x*rxByxkMLMn$er;* zau(P^Hu>E@poLV2g*1McItw`%NVTBSf*SxMfawx{MatcS5KomoqfYrJv?cgI5@n7Z zjP;uQ;&0?{@sQI5rY~Q{mVs))Eg04q6t0Fx5m?sitZ)Ihi^gdG-xK`Mss4aoz=T;J zI$$IKecl=MKN6Z!s(iv_1Slj?V&r$vHPG;b^IL-pbS4R|H+#6A5e2v<5zl!6XnZ z5`Asy*-@)SQR%w{GoX~+ZBVb=b|?n|bGLSL$g##xsMDtjcv40pSZ|m?StK{)Ib#Ev zJ&6ADgLOpwadG@{m_E9%N^Y=HEaZy3AXnV3IlUNHLtz-5Aph1Gn#m&cfZOwkFuVq! zpyV9OaA_qE@=SF}p+teFNu3o%XJDfsO+Pql?;+Jq96E(32SFx*Id}*6ma)r(-q2ql zs}LdtjFifX5rY36N*)4yOlmF}&VGZ1oJFhB$A)lLK?9n>Z!fkN%|QJN216n+6)`p-7Dca zI^yVf!bOB>ynaYi2gT9J)@VRoAfoqwM{^=5QyFjH0UU=NcJerY-otAYzs2)fmu0gY z8-e!b#ml){g@4KMc`KibiqG~8exK}y4;_+M&=Ozj%f;IJAos)a&RvB2bZR4v)>>j))sd@~sE`Qu{IzJNkjU(5s@LJ(kYAi#{? z{qc~TOuWdAlknd?3DXi6Q=gD?2+#SB>yEtLXRw1T1CkdLO^J>b6(`m)NK#xaWhsK% z1(?~uomRk|)>x6)Q)(8uz6e9QG4Nz`iwab?kMa^`cu$e{KzsKhT1pBw%og9$-Cf8X z?fUuRP7!hHP&W@5N^7*}XDS@1th%m+bIw?GCBlM+_*T3pO+1c*Ze@iZJjbD$U z@eoT3AiKX1GU1>XWt&zTOBJ2@-))=^pdbo?f~$d@0`~-DtsaqCf|O;lIQZg5*Jvzn zE7UQ!YXBCsm99zv)yxe>n`KtWJjn$V-*BOoqxse>DOd~4&Oks3I0FzWLH}<}wxPsf zt)=dj-$~Rnm<0($m%$;ygmM#(vNiF8_YvKqTaAiFkd+mqHs#Kje;XHsD-PXK8>m+3 zKyR{?0bZ#Al)^mLf@yybp0zuz=|K%-MXn7cIV0Cf78You`xP%WD}F6|^#|LkNw%gF zv?kmcv?dH%5tkE##fEyA&}E7Xr~(UHHB;~NO&D=ly^q^yOUm7rOXzWGc57|@e9PxW z-FBkSMh1Q4cJv#HmnSisgh|_AQ@z2V2@>6lupoLnX8pA>ZwISD{ivr8O6+cBidMt~ z;!bchZLQc1yI8{1oKA%ni@+Tx`Lm)@DTGjwo5-H1{&2J)dd%cmFR-47#7zM$KEQrR z^|Shgp1EH^YVrD3Rh=Rdk%hwfoTvRIw8s<|vC-}VKE7mc7sS?%L#QzJZ4#E#2}S}M>U#FBfh92I2;F8{XgSZ_cSVht*=`hleg61Bqkl@+D&90=%wHzig*|HT zD+a3yxgt=snD>>3B%klIV4SWa=u52>!8U}L70mfZ6}p$~5>nno9fN@GWmZqMQg<_^ zZ51f3AT}(OJklBnX+8nG12}FaF8dfnPYI;;ZSbR|$|h2wj1i4&5?NsUz&|)75T8ys9|}_w4tVn&9R}CZo1-t zzlp}vS9>2RPyKzb1Z7ILTjXX#CK%!I4ib;t%lxeAvL2slFG%Bv&K-;6yb#Qc#r|FE zV5b1N$^bw!rAz5GApnqkDG)ed`7se^3QnyCMy02s;G+lOzLk9! z8vs7{T zNQce5h8$d5gE7B`NlV(&Q1GL>Dw9!!vl#Yw@6&=Y0kTil+?`n}5`Jgbmq+L@f7AEz zJa(t{?dY!L0h6hHjHaoe^SrpPqS9+8Qn8KO2=#spx9?`+7o?cvBve_)x%Hd|LVoNr zWV*1h4|_*bi~daIlvJIHH?|WQ$cf$;d>@pe=@e|)mF`8(U#ReYlx zk~ic!#YBD+d&{qrr%mt4He-Z1pm}C5W9iVeO_mN58U?_5o%w3JR>2KwAx~n#MFZKR zWxjVuYW!`kubXiQj7fi^=RRUvwO+-$H6ekL9dwV5a>J}cGC}}C&E2c;p`wLm*=pLl zH?WsHY6&fyn-s!-s-|IE_f||caa8*L3_6GI=*zCbdJOJ|OIYE?LE3dOCp_)(t&H6r zK);sNmAW-VD=2phi2-VV+wAr-ygv;;+qG!F{Bd~`j{Tlix`ERYA2KSO+8tn#_|#Os z3F=1!ka5=d6=j?i6h1E+?*{l`Dq&=8C#G68b!tgOrfO_C1QzQ$#+{@E&5s1$1S@95M=8+g}dZj&~RryY|f5x-s@0H5)P~t3gbj00BB? zRh4i+J4dQyNDc_ClXZMQ^w6|JmzFZzC@DNWQ|Kphq_Ecr1CKKI0MD&a+lZn36ttqY z?rEl=^7Y)XIPM1G#+wg4?!WZFLIZ-nfmZov3gAl40& zS5Y+Mc*?rCeOQ%MxJQ}bv2G5F6qqeAY;df5!T@pzo1@a1n6c`r)Z&5K-wX%ts0)l$ zwWuRaB-oRn$^T(fb7y&={F?817`VZ(2l7a^SAz`;#9EA)h80!0=SY#@Q4BZ#K0Q`; zl)9l@^>@%V@fFKTgXNRlo$TxnfH~y_QL{ZfojR$zLb+b2qc|2xoV2dj*y&@VueyQZ z`wql}T9?|XW9yB#Ol0|8d?TDAf^74vWFw$~kuM6DukVut-R_#5j4uDlYYYX-OR)$G zD;~=e+?Dt;^g5hd$5)g%JUbeCEPfQoA;JPM|JG->!|p-38^$?nVJh$LK1N=1k{b~I z=8OJ@Vx=FeRwZ-{ZO&@dH72B`ZXsgJcJ2`<7w!4~Q8&C0@OA0vREQiE&fsG>bt&#? zN^n-dI?t^&$UVb-*|+|;Y-l*cJr`ba*y+>SGXGkegaVZr<&;0%)Dx$%g-SDG1I~8pw)5H9(>rx^}N!giX%p% zS8p|tEKZL+Pq>A@FpK{euY}+OZ}I{CoxBCg*_}^FVa5&VuIFzsB1{R&p6L@lPV;fZ z+Q3`LoT8n3n(QWkpD!@*8Vbk77wMi6FmgR^zf3^Q5K>0Rl*c=g>uvjc4WRaWovQz= z%AiJKH5f^&NO`wuOWsAFSZ`z(U*vY5N7n;&pRB)lVDW&N z>Dc6KJON_pFpR|v;1-RORtKJARn~(GRPjlux_D$y^wt|#lVW9V14@^$YPGBJg6U!6 z0?xfPZ~=R8!-aY(b~8IOPYN_r zoj@k{1TW)paJRV2+mRkpzmu*-T%$8qT89V6{W7V}5z}Jxa-5M&cnR`%#ZPRsji5VP zSGb$8cgC3UXm_k9?cV5=HkoC=-+3_G*S7XI4cfYu+>^3*qP9PbN>@gm25O(i%NS4; z5p(vJMeiK7%N-_^{g$HnLm-VUAc^LjVsTwayW?>3^B%Uo28xM9XZJ&f1Q$!Ms&_YCo>jG^Hh z622GUVjAM1jQ>%1t0enW{W;kuW3j#>!2YVmN@1T!H-u#u$sD3e03V*9i+^4=^tLmt zu%#C*=Bhshc|rq~fcgOJv*5@tHU!xMMFR9DXAdpvwqd}`4IBg8W+4a$Av?WDkMgnP z0F#}_kAeQ>z$#fHxe*sdKXxxeXRCyQbf$n~ipUwupzcV2DQAHHm_p>9KQAbRZ;`<2 z*4P@6X_et}Z<#<7DzM$a2WCG3JjBYq*U=lRkv|jTBP5Qz<3$Gk*A_{WOpyjcOud)Q>0Fa6hb2Ny|_o|IdR4Uy>b~9rRDm&3x7_LHsxckvW>!9k>0a=^NkeV{!N`60Pa?YdGn`U@Yy=pJ*)U}(x>M3KITfhqw?JeG z`NVvp-XWh*D`+P`PpX-DyfS^5^agOge$N4M`QHF~FGy7KUaAo@VbBfG7V3LB`yVX) z82T7rIFEDw1wYq8)9WdCf+vqd!Xf}(yxa{y?y1yM+kpZFz1-&w?9(;_iv)EZx5jq05z`CMIkcU>OjfqMH@S|1|UCcxgJgn`7n z#Cs?y#<>f$qQIhy^Ayxg8HNW6iYAcf7b=(pydafTf|vE2;35-lK4{p*^=Aq8Md=Pz zRaqjaLC8c%ZuRGG%I?lAe^e31o#UfsrF(#yYPuAuwC`NpA#EWs#sf!RD*SHLQXZkq z%-bgN4X`G5uj-c}9jhT#b5H+HE$7{`O*z1Ks$0FlNQ^-u=r=ff?e&yS^q~RF`Q%ZW;lJe$E2tJ6*?Qazjd#&HH2yRT~DDF9Td5FdUI>)$L}qRN@a+0<=pUe z)U<@cx(3tK-nfpD)DD#VD)a)5DKv=>cD-eL~7chp*zEA|^RmdtNyG?#+s^j&#x>4vx!|08;_bY6ZRc#GKw?MM*q zCc%9c90uTiExX(hxO@2ISh!qou@AZjh4boN0{5>5dXeP@73^*Sb`9F!0(3K%CwFhT z?|>of4bYPvaMR;us^}MWApe5(wU8ZWPNbye2(085oGvK=_FXemUV&s2!~SyQ z#wMLVzyYFK+5G`b@8DW%oBWN$UjU0rP`ST-wxxWL;c}f%hQhe>HC!0Gm@lxgxh`fB zzGg-SUnAu(@IMTl3aVy9tYt&h1H4-XL(5zi3=R8&U}z%f#$c#v28hp4#C;vK8lT>i zJV|zXA0xN@?9=-oWv6%hqJqr1KbXRQF-?Z(`zM6AAed zEMwRFqgwg9{0>?&+nEN9Zn)*C7_es(m7h$o`l|+TSn|`ddSQ%di3GH~a_> z$2&1UavE>S)Oq1eQQmoYkr#_)grsO#%gv+ZORh;0E6^kF&JLu6xdK9v<(Ok!gXrp8 zx~oL6p+2+jpqp76_g`E!?T?lng64mfQBo{An!u>|)_ z_JN^N<`8>*7}L<`l+9j-(m|`g2dc}Mk^5L3B^1c44ZiSnTekZ|mYVz6EDVt#+bYcl z=_vQ?MKUuea&Li$q>E=qCqb>5aZ=h|_S<-*_)us9WN)mLt~FMYKKACQ3VNINmF{>3 zQ?N6UxN9MJ*=IjV)btAQ?zzrqc`Ut9#m{XW%E{Wcr$H_Z!~|2f_Gw7$(`58E8&c8k z=S`$@H!hkv0iTQD$zrY|ylW9OZw-U4E!3Sww($0n9@Bs+vr!FznNo@o8*xS^Q(e6o z7)0j=wYzQ)nDrKCpmw*y#d=V~bQm!B??SEG5VwiPUNN_151GAC!>P;j+-N;vvn1Sk zCQmQG01=nd0tM{e=guC66=hxN zIarI5{oUR$uB=^Eo9r41#nI^?4p$f0o5_e$a_{@a>%1%yWjP1*GM-J5MW(rp$2ac6 z-}W~XobDE^^NWfL|6<~;C&;m`pcfT{6zstJ(%-HFK=PnE7N)H(Nb{pgxjN)w`38)z z1bci9NY5j?ICX}XASV(v&4JhGUVM*Ts!7OgE=F3s`Ft<#nAEW<&fTQUOUMl+xVc>f z{`DM>F9|XokezZk{lq+W6+=WBo^~-c$z4K*C%5&=V0_(Ii(YVN0KE5*AT^iErvhU1 zm8eSmJ0)%|Y_)RTQV4#10j?uea*t@%l)EsFhms5or$A@ir(!fcwdmqQFnNw13CjRo?L zGDl{3SzbGfQ@o_D-;{BeSb^K#+&_c2U8vvX?s;AjNuO@RNKQihcUZqRaW7kpb8{QF z0S%jp$A%&Xw%(b`BLB7Az4X_bf~CkL9M7!P+6fV2zQ*{dFbBFoCULaM(^{DYoz>co z%k%RuSbN*_X_v^Z#-}e?i|aj0MK0Ws`2^S!+S8)jND%X zpPTgOx(|P@nP$R71OY6V6g(;zyb;&dg4mjc_km#i@tmNWg-a0Op&0|VV#VdP0dlv< z8>p>&5z570_KgtJD#Gd}B3)Ru52AieKEYc#+1+;qKqZ{3&1Fy*DJA(_42>L%OK%ep zWZvyDRGo~(ve>l<0r{pTF8X{fdS@=WIsG4#S_baNI9~#;uV4sE9A_(7tE)W4fWJnu zoplnk*!WK1xRo5lRE358g-}kZ+YS>rSNKZ-q(oPZf|H@3t=omEkyJMTo>8I48#!jJ zg*6`Jl6ACXIa2+gbEDTXQbTck{q-bO%k~k$3w||7IQdA~48_K!x=$BBHIJN8K55l< zXm^rUQLCjxOA?izZSr;5sA67JFdHGajL%U~EkSDB2_sEk=$3;yWt}8lpXPx@l6t|$ zs1umvD~PxAk0@8rwboSN1Lgq#>SLm@@Te?Erbi+i)sYo0jM)WvT?84i5oY|o(`S6o zoa4S^`wtU`NMw1$!^Q5sJiU~#H{IR$a2e|dozYh!ZP%9_i1fE2Bb&13VU{UD%w*_M z8epgkj#R-!A`Qsi#Aolb%zm=QzZq0Yy^0dd?ZJ98ufo<(Iv+Z2DeIpyHxAa%dU^v9 zTFWy-SxsC<%3}8c=Vsm*ixAM%%SH2HUi5vp9)!Nij`rswp^x_ZdtdStZoX6kZ5(0W z+|?~=LS=oDIzLY8n8KyrULUvsL`^~%KK4Qv_dq4DnVSaIQ-hC~iA3u!{fUcSZ|C4^ z8?j+Z$8(|ukMr{!&sK)yj%?M3|85*-*sSsawR>Ybk1rL>2G%Yu`-Lsj9yQb*pUT~h z|G~w1<#In^z*y!1>`Rfu9-PZkcnTm1CBSMJ^x-_>7hnJle20)$ai}kv0FuYp>?&jq zren3Vkt+7bohjBRA)(_swoC-3q9Caccb9=qxmye6DV}E0U+D@3vw!A?CqQ*iGeK(M zR$R=GbN6RvL>t7sZh-Fk5FQWs??Sf)!#ClQrU)~ba;HHl^VE34X*l-~=pY0j6w}p; z{|y67LQdTfPI~})ajGEsdMuDooAP0b)``;TZwQd^ewfx^*3aD2g=ue8om^x;kgh5h z8{I~CkwP;Xr+q>?dl;cTe3lpB!i~K|y2(g9rRH5inBNf2KRMtV zP#dUtirq6rQ?#vo0pofpeWYp%1z~S7+4d4u^+=aHxs*no_BfgC#EW>!r8T6hkLMG#?#GYAq3?J&Wx{sL$V#c%Pag>gT}C#ld0x)K7T zZ{Q~emq~R4$tdbquK^D*>1;EwsERMf zH!=$Z{VG*kK<(FCq9g|XX8!1)mODI8-2NFdNwYt76DP*Y`+iv7oCMX_3UZAEk9I{9 z*{(;BBGiHbO(eP2k1=ACX0j|OOOx5A+vGvN6J}}2A=Emtrzxx#@??-tm4chz^Q5oL~nP7f4$@kjF2#3K@+(7TEqm(+KDzJ1Zh+fyw(FA(xg@`Z9Y84k!K62ldGP1ZIazyj%(?}2~46t;5kZ)N}e2yc>P z#5G*aqj0o&6kh&PHhU#Rv1-rjWZp^5+`98ZL6>T4aR2%G4b@BqHT<-ULxmc(?PNP7X6W4*z&eN}#y+*yra3o=$ngHtKOi?LkSY1F;JiZ^Iy*Pm@4ZI0ao{ zhCzI9opR(tZK3xDgJu{&-V0Ti)drt2)e5^kjV<{*$YkPqdIfN?>RQe3M z^}VH=V@^~=FlWbP!S`5iGgK7Y>xbKk-_n5-&b~#0W?ot3DJ|U#fRHur zmfe;KKrL@72-epII5)t&sETNM4&*o36@&99pp+QUa8{hvLB-_srFxk9Ez_& zV0zufK?e46f+r;$&XAQ3&uKDMc+*0P*OOT7qh>IeeDv!7v(2gNexX!to4`S2O3}U`M8bF=)yJBCC5_-=r9V<|S?b z$>v2L*>>Lv7-s{}JP+O9@$j?gV1P$mmAKKMD4PsX597VgeDRx|oZZ(oyKp@pe06|+c#y}nfZ^;5d`?4+Gj@$ed{NHgW zZ9h(_3xdCjL2v*dxJH1tP#{Pc_>^1hL2y+Jf&&vEcx)XkxIkMxFy9U2IW!2w<%Hir zpuAE-iPyL5vfGz|@lM0^=(hi_Kz^*OjXR?5Mk9*0t3Hr@{wkY$wq5Vz!tG8(=fg&=3=2Vet!u zZLk+`eX}6Sv2F%J0hx^Hv5O9h)TL_^ie);rr+vjmK_-@%>%Oq1U7q%Qk{9ZC;Bd6o#Jkf;9QKz6uIjZ7)Nb- zLX@vGXcM@kkLHp5saC6`dX16f+p<XDWP;^e2_67 z7}^yB7wAzp>%40JP(GjFW+xU0S^XXO=pioX?AD`FmdxvQ_(3F3ToTDPJ_7K&@_Qht zDgG_n(Q7EzZ)6{4;8!aP@nN+1Ee|j3(mp68u2Cu-wa1PE(UeJVbl=VnP=hQcG4t0V z<2-S2;&N?6$JfkE6PRN$u@Z}e8P`ay=7iXk;%IPWF+Na-%0*0Q5_vSxZoz){#i&%`fJs%CpIQPBT9XqVEd*t;eEySe$CoNB(5<{ z-Y(=D+I@_TtFXL8=^^U?)hR(HR0DF`hEbTKMKlzWi=;%LJOgTKcyx-$Kc#FD6o zE@t(ESfQdmJj>yPfF)eB57@4Y(HOh}C8h)jkD%${H+fO5pcQCa723NOT!8M2c^R^2 zwK)a6^vq#(_);oNVmIu=i9MsXXZooW6ILbkb$#Ndyb5z~`mcwv0j#V4TCS=)76pr+ zflkqsF`>Lw5!Nwh63S4z9W=IXH}8d&uVu-#A1d7~=`KLROWGyDGHIy1YJPPw5bwx1-?gL1cclSc&UDfa}gspR}_ z7jdv7p@)o`+Z!sR+)skF;wCJ2WvwW6--%8MS^V~_0M%LlQxTcj7QV;{Qi5AI{l%4a zKKuAkdp;H;3Tu3tone-1z&PSwA{6S^>qL>s6|PVtAnV0+vbbcBhG|bkzcBDUz?4np zmOL_ZXD5&GkZyZ)@{+_J6`n#6AqM7Fn-%}J(tWVTDmPjXtTL+30wuy(^i&0ra!_il3;>o3D z1=-dqjF2y=u3Mnjo(OR!SW=~LrraX{Dl3qO##trgZTU(Ti3VAS&lJ=^;|8_^u#`$N2XguA2?AG$@}{C0 z&h_zAx+0G_quEXFz?$S9ky8)0RF2T6c-vY*oLC;tkx*D_ATPpuCE?q`1*+_)j7iqy zRo)meZd<9x_cB+_(a`{E9n_ryLY4oE3|(CFh(btiPVsiIyEp!%SK_rsE-YTe?Rux~Aa?ZU?)d zhGTGA6{iLbY?8v2vLBtQjYCLpC3gT?nwxuwlynlVF1g6`Ybx+5=&}lYlO=3^=mT9x zp8EhsM{_t%lgli`NOo02x5rr2+e1AH+PLADFx#@8zRb^`wtVXBHWRZ%5klMeSrP9m zZZz@|@T9fbWkqfjc5w8%3l9cAx~IJ9M!Kr)HYS*EJ6o>4DI3SQ9ww$GV4^QJ3hjhO z59fcKxx5hb7M^dYKZq96&)Vh;sb5f|9ajy%eJF8j3r=;zJsOQp-~g=Ny8v{X!r4)a zo&~3}XScv|QO-x{i- zMIT41;i%)ns51YAb_MfgVLz3fRFIli2usm1|A?}v@<;Ipnc(X~u*DlqO&b`aI~i26 zPu71<6TSQLfTCg;K1U$G9CV(*;L?mWZInu2rlom#9?c~;cs3g>@kKE~{(?LPORU7k z&Wks8=)vjgV*6Qf_zl_9o)`Azqrd0ef#sPU$W7^L2mYMdfo#P5K-}_24eZ=PzdI`v zm*-wco|dTA#Lo~X)(d(birrnd#g;xR?FK4JmjU2jDjc|KM$m1NtJ+>o*7MrsVN2E~ zrnrauT@+h3CRO_P@To-nS_fj7Q*bd1g1SA_7{fB-mW$G*9YOYX;I?D=3ratcmz1as{#B-~K zGX5asj4<`9GV7oI#4<0b;VbCKSUPdtMj(JP z6O#GL?RB3$S&+t~imUUFkSnr?xNP|Bs8CPY)SRfDBsHhsuM<~PRNZ`RPW1apgiz2e zn5GaI^kUX*!wr{2$tb)zT!nOg_?OXo$_L! z8S8OeA4*q5t$q~!Y$$wsOW9&ehwvr$0UYqL{NTaI7k*rGj@5rU795WIwGjg2cu=Vg zns_rV8bi&ttN;$OZ57*jB+&03{FB^VR}w+$C@hHHf_7H7Za6{$8|-y*ABDlh^73d? ze5`vt?sR-&9Qm!jgv^IJY|UnbI~MG55?-d6BEKC;l{2C2K|c-wzYq{fJv;)cFF8-= zYe^Gg1avnvTN5Cl+FlI--FihpK*>qIo0%l0FP`53gB2=5l|9eug~=&lhGEBh4sLRO zQjfEBHmL>l7K@BCpK|TxhXp1SKe5=2#L!#eZ>i2nE1s)Od<-}IQK7k0YpvVugZ$_G zinz%lI8@79wfhkt!@k{?wzMghww_b8rOm0gG(P#IC57D!3O$}DBNlk~+u(IJJmB4a ziO1r8uCiVP80l)+X+iAj4vhe-h;Bj2}eLl)x7b}pp#aoCAe0h9d&>F#NQ<$E-FF`^65-bvL#hBGLVDWn)ccJx=m$gLDM zkIQxjK_9C5H4(Pp4pGmqpx?4PJher@Yzc9}$DQ&D7UauF3wk?+?mkDiVr8em zx6Dm!#kk76=x9kZI`O~5dPSXfM~4lFIu~AwM_?dD)jxe1ZX(jR=;-QHRaKh*hbi8J z0~Najhg5*w8!T_NB9adVWgN?rZ!Cuyv4kqG{gI1+uP>!YLJ$zH&vS2Gs8l20gpR}Y z6Dq=YQx$!k_PXq9U-Q|`50gZ-%)Jds9}ndl!%3baRP*E?QU~Dr2Kvof;1b)Kc*S0A4dA$^n`Lpm(@_}Q zD@DNl(EG5uFtq47;0%oCosLVkc;1hEtupr@x6rEGn9r0~W;494^(G`4-bsPNiJBX8 zBZ{U9jYt<#HYESZ+=nFdx+4$i#2EPow@MY0u$6PQE3Tv3i@23v!=RSR^g}$S#dii3 z_cD&I$W6U6{h>)Eo>SOoMb*v7Dm+)VT6zjd^}_#Wf4UhP_H|A^BQ5c*ZSPA<_S2FoHsb=gS@l`l8L1K8WdRE-jR{4A@*qbtcS!8Ix{|sO--@7 zxxBcxx^?wd$0xtKPT4f*v-!H}dvw$73F+_aY)#0E0VGVtSeeu!Q&GQ$TaoV~s1NE*%B7=Lp)+Wh3 zECCH*c~Ij^ZZ@@v6$K0l!Ut$O-igRaVdS5zMTuHt&772^mx zo@`1`2dBdu>@eGg?rswNwj@sPU1{Q9gq(43AMar>4;ujGBOesOV_m{BmhxyB`0nNj zrB1*qs19F_iHUMc8tpDf>wB5|O;!N559Dch5cFGigh>esvf`uJ-I;lCI(vx?znbYc z{8F5rg)O|Yd<`#k^0zG#CD?BRd;bP2OvYnl_d~3vv_Es0r#6YVUDKbP(l(<_6=ON| zMtg6>(%2rTA&J}a)gZnaQ5dX59txprmq1b(T7D=7m5elDh@s`jnTbwo`N2Q}FleHL z+Aa+t^a+G~a;{&AB|#742Vu~QB#8F-tNHe8!j$f7x+$qko6_IHlzM}ZRxb@S*YS!t z!$rRnJl)$p&#o`P(2N^oOaT>J+Q=ll;o)fWl*zN^F2z2d<6_ z8h9-|tv^3T(KjC)tskFEZLhWsV{`LZL&Kcy8IeZECat(6nG!p&__b88?9MguYdQPi zChI4;Ip(=0)?-Zg|M5nN8z$?DWry#zlh9ACO}iyH!HaT3Pfflqu@o%tYjCP zT+Xw*{mO7m3C?L^XVc)EQH-9Cdq#%?f1kW*o{@UBQ@CtN8hmgIF-Gr1-G0b6loW39 z%lXf!qt^N3c-C{u@~rFYV=lwEvGe24cejP(E3N?@r#V&QFW-fCDJ%RZsbHngTm?U8 z<*?&HGyD094Pg(hsEU4y5z$rQm7(yA#ECKygjJBw7jkq67u*vB{69J6q}yugpetxW z2d~4o_-X-fC7DaV97uoJjLg2!gij^z8EpNtUYgpNJ-J_+x@>iLaWV_A=O*{K(RWjp zi4#S4?hzXoC8*IORu|lEM9ymRP|)bRZgE=^2`Vu97#Yg+L!U*JO1B#<(6v|8{;aT( z8&QVwp}m`egS_9{O>xn*Owr3kFE17Qmvgy&Nc&de?!O=%ea&4B-B_ydm=aLmNEojc z>;&XncSV1Cqz|e5$jcWX>`@Tu*F>8=0oh|g4Pgfo)pXAD#uovr~J~g}RxTf7n zeYJ}%=>kiMJm70;H=EFNFYqo(a?+O`t!o8f?+(#C1bJ(z>#G>obj{FO0!x#8+Hc>o zR_eZuTX9KYpPB+o>N1tr)daiLs0Z$Xm4-K0a*B>F&0UQ0Pihl&BzDWa6hO52(LSyC zjK=NU#^0w|S_K!Vog?euH(j~?Nq~P!$)!hO=TZ15eol?LsIl@fi&9j3qj-B|ZGk*u^M627m^dw6pjuH-qyWO~o)_paj44yw~z zW`b;%hFbA&M4bg)SGiYG{)}gJ=Gz z{OPp~W;M90p~0v+4R+Nw$a%uy^9Fj8vAZcMK#biv$Fa<7u_+@vYVo;I@tFaD-roNd zdoac4FG?exW2|GUkm%0QKWAj%ct*?p&J_b!LM&dAmOS`7k_X3Y9MnA1F2`wY)Lhs6 z9)$J@5-}1xRO60ybyR#hg1@+jh1}3kubNy^6{!3QFQ-`sA7obu-mW=d4*r_g6UE7e z;IV^zWotn~;H2X>Ij|dw!DXZ2B1b3w>23sw6=q+TGm6>r&w70i>bjin>kwd~ll~_VU@Qam@*}M!tn6=O*KGW&VyY{|W=-s+^hl?mM=HtxPDS+X zl!NIx#SvbxCi+21b#zKK|Dm)if4cKP{jRIN^VbguNMfHo>CuGt17^~=iP+k*AHWyT z+c++a4b%5ZSeAX}4Vp#X+MrqFs@N=YO1aD;>nVs4B1iDo{KIlbOERG|_u%do^*- zi*fXxBuo8ZJkOVzr-`H1#bV?4W1A!2)8=@7yP9%->ec4=N<6=5l4?$jama2I1(=}E z<-Vu6?|6bkaQu?1kM0&R`L84;=NT+d{iy$$Jtb0ilL)qVSH9bZ!g-ohC;M2B!eK6f znfKwuDJ~&T7}UqjoIj>Ad-xkv2bFLE^iR3(XIgVA$Onn)l5q9s1rnb$nW)BI{+cvz zyBlM9sWcALd}t6<8cC(jwjidG08o)fQO2|+aHy9Es=$swlG}ma@R0XgQcf83LcV)+E^_3HM@H|@jh6|y}wmS z2RF2J+84YFdFQKC`GWhbMunTAQ0mr35%0+>Pf4h<>B@9ZDEOcp&gg{uBE(Bf_DvL3 zsexE)VZ*?SOFU7}uO#(QwK29S#ng`4OA7cxfsRNC%bzcHT2IYAek&?e~ zE4cvbo}Y9C*$>pKnELc~uUrv`*Q8APCN55`O%;Af@$C*ekBk~7ZF(K0V6g{a%)tqn z;|o*Ut6i7@x#j(nGm|v={_6lT9Td8s27Qe0WobOOaZNcl^=fk)9M7#$VsB}>_hE!+ z;%{;snmqg_0;W^Y%x(S)6jINq*_r&;a|o~GMMox1Z2;oE+1$H-kl7NtH~Ef()GT0p zMwcvIHM;lx^tFb*oSuuF^?DNxkPR@?8Jdkwxox##`u3_Z@t#2aPvt<6BziTY=(#!Z zm*sFmoe_T*|Bwn9@psaOW|fEFzk0*ilM^;HD-y8&6ok@!2B7Z~BSAXnj|+JH?=AiR-kilm5JgtCvT)&32vWnGPNHFa}ZSJUm6Qj#y^ zBI?%ob`iiH8@l0j@-`Q_Hwn$i0Il7%Sn)P<)%dW>l>BzoWsO?yJ6f)W zWV!3*6xI~WJ%X*vz1;i#a_>CQW1UmySQj^TojbGE`Fv9y>z6UE&b`hJew{ZTz_A+m zE{oS$?ZNlf#)2H?rV)BVV(}7v?+v`f`ewmgG5#_*{_jW&0{6E?~d-gjgLsMRPm$J z()i&ly8z>dmyO*>6aPzg3i~jx@Fo5J7L-v*j(rilSIpFiUx)c^>E4AU7FUzSz}Mw! zJd-SU8e`!1HI2lpwpZJm-Lb?g>5}tGk)ocGp`p6q1(BTFV8$Lm%;ZL4LWF*n%!)*h zwomI&V_V5QwgzQREB7|IQNC-G;xKwoqnz4?gT0uu|Eoz?3a}O0;Uj?nJN>hQ)#IMr>lFaB^F4 zY{~MEbMvqsW)?gGTZ#Rw_GZh|%Dq|JzL}d7d)jYkX#uz-tYy|XQ2e!8ZFdxfRk7Y} zjNk_*O&2S}@qHWe+S;{7Ws6m~86Dd5d2W)gvYDhY_RADwt@s-DaS~SBR$DqV?UxN5 zOg8$7-LAYZT*b$^S(cOLv3Au3{p7xX_kVepsNIOKWUG~NiNYES$#bc<6()rmb>BhC zL8I zgA}>xb}9>+n?k1CJsJBJ&TpQUibf^ji01n`jI)6e?bL3Iy&4sZ!Q;)Oq~S#`X+Whp zn}@oNY%NF`HpI%#q}R-+OqC3KTk(04>Ep8KD}{gN5iG&tiBp@RbvTmhZ820-C#l}y za+%eO`2ki>hr6rs7VSa|8|ebB&*%cAdLII0OQ!T5R=-n~Fmx_??mPFeoOLfF)UK1X zjxC#x!9sS&-l9@!oLCzb1A!8g#?$9vCT*%eFTFQAshArIE$na$& zqGEsshI3(BxEI(p34`!9Alb}*0%AA z3X}iNU**p0h85Xvv{#&qo=h0as&-C6VQgncAf4~#9z~^tsLme```!<(nsvu{3sLgC zM&JNzAkLbFw{pM2Ou$kYe$%~$&h8X0G!;D@qG{BA@5EY69{h)2Y3XYvNE2$q7FjCVYvU=^*gR zr2mBgQL4P4U-SJC*T^Qfu_-4;OA{X{#?B1AYUBTSRlVK3&`g-@}wCN z&tz2Dv3$|}j_0=_SveBMJ-K(_&Fo_zbdWo+zRnKF!9?%HXDSHBW*z&TO?d2N;LfEW zcp>}PuRm~DPhN8`d;FyrJx*)v(g$QMeN`h5zcHf6jk%Zpk}Z9YA-FLIe%#5GdpYDU zJc66tSg^l33+(Ux9Ae16Sy2C0gCGwNN%&lU`U?}@oex79|Bm3YuyKMhr{wrGEnKBYpYbXRt8?rN}cRklX~fB z+4~}QR}1*geR4zVS8jjtpMK@|8|!7bo3!sGRr!bNB$Au_GPZ;49Y&6?UB!z*?NT?H zAXY|V-%9W+eRQXj6GSxkKV@icdRJ^4_0BQx;6B(1tf3Qncp6cMCiPAWtJ2JEL(m{{ z7Y4FmgvNMK!kzcQid-Mdl!j1TOb?Za<3WHK8}FLSYOGmS*5WV&lA9VaOxEH9;X&9} zi{58J^fYz!*Aw9YHOCo97~?rcZHYq*;86qp&110jsa>lF41O-%<+yBeno4`p~TzmgBQ`60?$P6GmqA`k!;&!7Z z@x8f;K^4-9A$v*dHHOxIISJyoYpojUumxm7CoP>el^P23C2JH1nZ2*%Oi!!#Pa zBs+|u!3@ygaqa#Jwh81sUZRZjF?zg@Qxz)#V+m2ibjz;$>mO*-413NCE+uO?csdi; z@!PgnMX61O+diO^KIc;}87|3WyuODkr4ogb zQpzXCCMplne>+g>fz~}q(K|M+w~0HyQYF+si8Cs%K)mI<=*}dvp8K_pDMd+JkYIJ4 z`GoAF=H(SPE^NfOU|GEK*5IX%a~YWlwYGkEk#1g~oajc@E1-2zij{q)$ot z(c{i)?{iC6;1KKjN=4Y#Er1-XG?GXGMxJcr`YG{gXSZBIs3=CIFkD|$qqNhdZY)7C zWbSY&QmyFs*T7Vr(U|jYrF(@P)eXMjG}K+Ub62+#flx8}w&(|+LmMXc>s{zbJ8kfT z;kLa8j==0kPx6PK&6_rxDmSpf22}NJjc2{a^VLkRw(QZ7p#rBO31Zmyd26Mt%1P9G zpAcdm+ur`rtOU)z&TZoa&59OgHIq2oX|F{K&W88@JIiDmNtB2w9}(DYNEjT#*F- zx-}@kAS0D-0ZRBpZ`#O{=qW|Q-K#3E2Wk2+V6#b`}joVl+?)VMlW0oIPC zd=8{fn|(T878#w&r}SH#6+J55bn$ajv7A7xIDMC6M}$+faG`1`Ri6=mp~=DQmGI$D zMx>-+O_iklX%n!skHhN}=rAZwBhZR5%>m4JjqlW%(N4XHryc0~D>n~gT5dkz#ILMG ztUJNZz*gn~W~rs_R)3*axTiS*x*%>&1eQ%YTQy9JZopRoo4qrKOUdrU$D>>KS(Wb+ z7v}-IUp;Wo;@otxX6fiP>bdM{xL5sWVvL!3#2WnF9@LiKDyQa2N_#e3rWVkF#CR<0 zuA)SZvK?zCuLFTbY@DnhWRRk;K&#${JC{LT=+nx*O_E1zra;B1HSQX~3Q%DA<^Udz z7GwF0(K~jl7*DZeB43+j^9~R_yPd`95>C2ZYMR63dCW(Y5x3u2+JkjL7KJ5U~Ta--OtxSJHhX%M0z0J?>xsCMJ68xfsyf z$$6*D&U71h`EVJIq-uupyb4XAAL|>-`a~r*@U1i*Qo}~bPcz7dQ9b)aJ2xH5B+7MH zGbk6+-Of#h39;#F0kU?tw&}NXCmP?ooqG^DHtJwYcUPvV(G-pq-0*WuTtsL#9~V)A z3OcT?#&pTUN6EYZHO73FJpm%+?s1|~#2$FcU6G_R0%!8&5rUSRroUhHRO+&bO2s$y zA~w`4(r2_`JD_0h!GXphs$76d!i9P;?yha>#iv!fCoJ0*Bm~1h4V7*QUihNuU$cZz zP1$}os=8o-Dokn|`677lC+-mj^1FBklo-YYwW_}q*x0GCPUuT+1F}2u(QDBx(6_|B z1_&e=UW**q-n~X$Dh6|tkX70`i8PGd(X9qN(nB;jQ&qV6M)|!lESe%>KL&2;B$hF6 z3H$S*(qa{+GOtZk;PICR6?jej6s;(8}-B-#~p~5`cE4%?%CNm|?up7_-F#*-XARfwB!kTWP9z_Tl*!)$5cCop) z7V5Zs>?GDfsk_SD`quCl@ROa~86={RzUxwm-Z^&Zt|dyhVy)7JRywM4m*~(ghlewT ziBP_kTS#Xm7g89X6-uw-{HkOXL$-4Rq>~Dzf0lbqRU0=IO_iJh+SDkbyttG~Go~ZYX^XG7?>Rg09HqD zb#Nb+)!??$mb?ZB)3-)?hFxuFG8VCu?oL^ZwU>{e5@TKU8MPIH7z~szq>C#Kw4I8yAEbUXMAO3w_d_b=3wKDC4Uh2E-g*9;Fc zT*fdgwbb2@2*JQraNJVGmAEsAuML_;-%#&WB{5AS$8;CBfn{$>$%qCJ_Zggmax{Qj zq1`hz;q$oqWUC3IO!$pL_$EXleAmUadR=gvLG#yxLSvcum)Y6c!Hz^FtR?$Jc7V)@ z*^C+HRxi+yl%rNjIlSm&i3#qEeN55Q9i-dsMn>9Bs9h)rAi3kSy@{zGmmJYR9lPVw8!6VuC zN1v8TY}Xq}soHHU05;ZGmucLPZah8H_(i%=r3%99xK@PM&sC%1@2lOKbmRG%#*fmC zlQNAf(~XmD%#v9oaAt|y)qn}(3=p5T@;MDSadSte#5=WsXIlM7VsvO)RGE_)3gtyC zFM)OVJ0yk@ky`<8BR8Wu3^o9{JrbL8GVI|_$Mwdi!D{?iRPzgc3oz(c_NfwbTP@>OyI{I4UPtio9!JF%0Zuf!gsr-oUWh5qCZIC!Vxb zdO^P-*Hkp9KN#mO=3P2tmU|8KoWg&WyUb zyI?>xgd+^bPi-+QFfCQ?E=IT6T@o$#yZD zq%=u-z*XoikZ(=ZsjZD^Z9cN$tK54GBweu7MMXgLOX!r1bV-z{B&Pd(xeNnBWCb=e z<5aZW->{T6dQ*VC3?-aOY0o?vZsi-6(mu$&Bn@==T5na=62?Z}$!}yW0Yp{>7D-~z z&Vx8*%Mryv8;a6WZc&CpX?2<1+ssZ!dTync=exU?I4JT^{YZ%FCA_pa=Z%^-p))XX z?gcn{FW;G@9Fy;oZf31yBriaj3i%Edx4CS%|4QH^cs!VSO#XG9C$?_V1HtM1LKN)i6wBN=3^B>H@{sE-yVMw3EC|6h z(kG8UN0-mA@Tm1Ho4+GG0Ou(t_`hUTf6T=0c_J< z2%8b?c<|%sL3hx;zbDM3qA-moxKi&L z{8@`Y_xQsBbcA0jNENGnZ0F;*kA&IY7)@*Zw>qt}2Ly!VhVs)=i>9>-&Nbh(R>h>T zOQNJq6R=1~gPfI6p>ghzRkrLO1V#DfD4xE7Jr||BMP;Q2H7o8p`8}5}h-P^qvoy_q zZt5sMP|q+ssq((H*)gciZc;S6n4C|!6))$LMYC2}G_y$g*ch-MDezi_xYD2(*qdfM zQb`-^B(gIz*p4-ceUQrd=O>i&;yvONM4m>dYTS12OpoR>$4-Wx^aVFp~ zRNd!mg6{HD>Wi-|a$m#YonPouaE>MitOD-h->(F4Np}I!PWCj2K0FOJ-Y$2~EGx2Uxv3qu*TRbT6%=tRXB4zfP9D|gprpV_ZXS_`?$_rh72hC$pWU+3G-X9 zSxyUvl5%$*B9dXp7u3;`I$rK5{FToi#TPUYOb8-mv-23&=S6mt<*m7rz1_rHIGn7# zXRA8j^MMaO3u9oj*mxFg2`2lJdVpY2C3<2}Oy`M8NDthZUIJ4?>e) zQ^U`$auPKfvJTTny(#u(bEX3-L3*moLXL9igCacYBMZ{q(!@Vx{Uxm|vKI>60r2&# z-)%%Kz-0Xsi92&^#qq0S(Vkcnl5U{QUY`g7zw3rKM4n&Y3h@GX?lSxw4Q7;QiDnTQ zZX3KWRBU4wvoQ+43g8j2juh--z{(=H??eQ5RqeMjXPc`AwIlUfRNy5x$J_lJ?KbEo zQ}}%Z19$x*y-!)wTM|t#^)2298q}t@Hkw|J_{N>?>F^sSzU9voI?ao3?cJ4#Z@!{j zse40GbVwBXdJ{yv)V&_$xDBY-(ylrt$HhtPeRdVEO?*G@&MarGY!*a`<}ix2R`z|* zV8=L=^CAv~MBcTD>_pyK97^22p^7O@**C}nw-xq?2f666Z=~%Jm&!D4_K0@?S(ZHl zHOwfpMx2)jyu_3kOE#OuLUzjBEr|w|2%8~7V_eXTlS~ug6eK6Iu^8y;5Z#q5>RxE3 znYrwgOes(s=#mq?Y99GDV%Rg zUizx>FGDaxgLv5T-X5>l{kb8d#Yyw27q|6no#lGIWe~ShFRYj|puKXOoNeyvrS3mf zbfBfX7UAR|irHSkb+8}E2gJA#?n1|&-;y7w)Ca8_)YILGr^Dur(Z@GJqtO}s^>{zfHgzJss*O_#QHAG5b=vP=Cm z3yzz64J3=-8BL=_?6SUw91HEsK1p;tm`f;~R&onQi@HwPJU$N1KHFmol|*n|qT3gBoQr z*@zOn6ggyf62AT*XgYEv^KZrN`Xbc`e%746Nno+KMJU1eR*Y|12trfsai*~(bGPA6 zeXGzd#;0tVEOk>b;;~lMYLEka8`pr3l08AElT4-PjAxTm243rSHoK_O3=LSKqi&f2 zc4B{Vx8R-cyGQxNM0ECtz-mjWfVa}e+p0!%irU0_0(Q;p4|{_J@UgKTy!}+RWCiz-+3Gk}~(vF}#sxFh(uZiu+XQ=l~ z)zyt)gwZlH&%|_kjZz8V!|b_BfW(t-Np5ivn3JbEvFg(en<6j-s65B%WcF#Qzpb*d zeuVDza#bOv4Wxw$1XEF*!9NdQIg~DCZZ@%DV>Q`@$o3|&zWW!DN&>AUd;oPJ;5FUY zG)w2V#mTiywK0uBCar7F@dH3q$Ess?o0TIP%roF38noTAlGC@!U5i0d79T3U zN8Z~)ZF`LG0+jwmJg^CG(!Wv)CEYvFYU#X|w$K*?{C(3rv=!-wupyq@ze1Tcxh8yx z;NQfIQjKTC$Gtzf%}9?mBOM;ig;8klS_3~07pN=cTKwT@#gL8)gw^4j!Ev+gKM&sq zAsxE%WIVxIwUVJ z=%?R1+^JA2Gl{nDHB$0=h0~Ypi*f*;O+^)E>efj2^nEUiWxKQWDKl zJzpW7F-Udsy&uos3*2Q`K{8ceV+Y{e=m6Y=N#Pq<3O45Q7@N{N{JUwpPx zp-x>;XeZq6KDp1H1~NnSq&pfTZMpmJdM}E{-&Bfkwqn~dw-QI+1Ri+cz(=Pb@VW~q zsn;rbM55}*qf$wo1DR&fW7YPkDJ zlLtzMjH9A@lsp!ASg8XQwM^!4mAnGbgk&}MpWz%@A^B?J!T1zHs?{pBn6lTP1 zN*z_sC)_bl>XQ5Q;=swRD)O&4iiw{ed;QDkB3g(>#jc1KM}UH&MYB*lpCj%5Gb0;_ z3rj((>WZfub?z&QYr#iqXzR`*bF1(BB?hf>S4rpIiSl{H^YyfQy>SD#wMrFNG8~+X zq0z-~I)T9QHa#oc8mo`>j(rp?{tc{3 zzh4S5k*q&uH%}UXxSxaf0(_Dpp86tAT991Zit7P&*a`@Om=B%fq?|Ceg5ZkYkPWXX zjw6h%n3ZofczgF~0UMRg$lMALl5Rhb74m)ekPH_#=_HCjm#^r~LXOsMA)BThz_� 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⁡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

    lNJG% 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

    |4Q4_ZmOpwcsZ@-DZB0C*63x zM`S6OpI3vq=C*2p-kaF6FbIXHdLKTC>_3Pv@v>3dy@HZRrTI$9^vwd^X)S}rl%{`4 zE0TzrD-LxDYLKz+O~r$jl2TTnB$7-DHJ}VIM#7asg??LvedapP#FDC zKTd}xo4sC7r}Ar}-crF`me_rgL1c+npN#wK_6s2hAlW$!j?n?~Se70Yz+rsX*| z{*juK_Ybt5Xfmc5K`#+c&y5F4OQ2%82nBv2a4AEs4)mx>aY!QxJ?DxNSU=}N!44te zYg~jP+0vx9XtL@FPCMp4CB@1m<6vh_*|C+P-X6O0Mybb!6gy54?jc=bf_BmLFheS` z)0XMFxsh!9ei5^lm`xy9fgvtq$b|7xbWO$9augwBu8mr2p|y<$Yl;0Z?OcR3hS88m zC~CW*-u@UZ!#80gkE)X(SPjeQ&CXbsX`0C_LHPMpSzQ-o zAiu#tke=WV?C(i64X&iz4(eZ-+qmLYq!YidzEY4@ZGntSjY1%0nsTYGVq}W^>p}u4 z(p=Z=xJtf~1dN5Rw!072cPBPixs;;p#@d>2_nhAn8?#Dtw8AK;EUA9RsAyG4Izmc$ zXH38-5t)pOW%p3O*`5Y7KJCW8s8Y;-3kYMe#pB8=%Jx)D@g$QV?YOD47MzUB z`v0x*4dzlqm%?0#oo2k|(-ubr6Qe_JICQ8T*_J~WS&TwImr@0$NHfPGv~v~v0oGo~ z`Q4f4ocU*-OEu~d6``^+q7Hap8OS3B4(eAiaOn}*CH&U%3bw#b6%|&M%mSif{Qd`9 zLox2A^3STNQL$%gM1V{KRvGrLiv%e?wv6~_yNOa!GR0=nNhUBKE+eIRggGP$=-g0C zRkZ(f^lJ3=o@^#=!zqJFqN`YG+cjcxVZS=<#65+(p>oH!F8V32F$+Z`Ad+9!e6jTQ z#H~FiRn_mJdrigq-7QEuvS52amIc$M+Jfw%2GvU^4?6bh6e-A$t2yI`$E__o z-1O0Ia|W9YoEpD92>8CK2H$GOM%yFp$ajp(ImjkfPUG=a5RNfM$aCN`%GU=0?xqu) zw+CUnghq=(0b7V@@p7-At!Ry=^6%K5uZxxAr1N*Yecx{-e805Rk*JU+}>);+#L z*Fz<*Zb5%;gd0)JYzX+vxGx!GQH==2m+22x|2CF5!NFo~%vm{!gsM>7_b}H)9SW`0 zxz)R;;9VjXO5f^{1G?I4)1imgK227m;=OS9g*O_YDCO3F;kEI|_DgiUI1_X+KrSDwG@1hLI z>A}kt;ydfz7T!ZW^PpsgvKhDAn__z^7pYYwCaevPC^1I4N65C?zk5_cyo>%6$QV#DlkbhJcIUJ?h<$!1lh32QnkB&}XnPwn|hiXU=!etQyL z#2?0!4jvbv=prSN2ua+)XSaR$xgC7EuNiXft2Nj!0g!~i$jji^Cm^D1+$02Yl~&Vx zCnIhD-SD|{1K6-kn|pEkQ87zc3uPum#|BABIcu)aExrUkD>Jv^?Klay)M^p>ebwVin9a4)4i>e&sfnrr2a#!i`${ zdJdZQ#*H&c=czu01wDI9fFWU|DRSETZqw#zyyN1wyLC7Hz<5ZHO&%%-9d?ggYD!CD zs@dkZ?RB;TDRnVJCQ;18%{9L*LG32nmD`gw)#kRu8j@lr62i%k>)On_X=v*?r%Wyl zb*gQD%+d)u)#4!;I?488tlp`%1dbjOosi4C%^b1nCXdI&VM_hlXP2cs@_BQKs&sZb#hSc>SF%4kZFwufOZg6 z5SS#5vLyHLU{|+?-xh&L#;M51D0N_?*FQj2R4ECo&$mWuv?W1pv%8%g4}zcnR(5@}avORjCXHT6;eEqq<4LBq1n=y5>RlPxw!UV~hs+ zgYB{>`{|`{B-`mdKrH?EXdOzqVPd=Kaf@27k@`RTqhpE`XfI@%!ml64x0&7hSNGb8?1m)%} zHtGqm)s)uiHlK2o^D|Yq)v&V!giXjzMol>!L}7@HD7j{9PLih)$ye{WSpop%Nlb)d zj0Tk^GB(S~EfMjokyp*rUamyB%pY0(B;uju4G-nq7pkU{(_V@br@zsfvXqyeD-M$! z69q&ssDFF~cU1581EjdV(|S9aO~ya3N6Ra9r%a(id%g%e)G)F0jQ*lm!uU6mDwTUC zx-r&YqQ1JJp$I|bnZhc{t9I!4RP7)(CqR`}v5tF*v9&b2Ht?6>NqB#EIiJF6?%qu9 zrjvFsy{5*VVQ_VRe>d!pLO-NA@7^0Kw-&F38>Qc6Fot8C-Xt@!$NKW~}jxW;brFFqZygQ%H1rmy_9OdGCjqwl}`@ zFYj6RfFIme`Wyd4aF4IaqV>HmWPyLd9K6f-!Fc7g<@G@IYINBmdkL*Y(|4!`Bhg%k zDNQv(qCs)#o73{L^`fkns*>Zn*O3-bsVimKD%O;MLS`|gA_Yxhb~F5$utxx{hrw;Z zlg)$;LT63EIx~6!4PR4{tQvHf>p3+o2a>ZR5at2}CHNr!k#cI&EsuQ@l;d)9VPN-qU8NU$?E1rfY7vq<+cW)QJ-}fktb;|M$?+W-^*N?yJdaH4TRRZnz#E!+Z6E8Pdsuge%jJAYiiAkQ zb9+9FX0Ed9`6p+%FMsd7@M+cp-qQ8+Wy@st$h$xOr&yjeEcEp~j=td~4F>uGsh=Ml zbM+H?*kHg{Ln6@42Qwl z`K^h>_`8^j$Z(CX>~-RA%=Goo|GV!0k1}OnvMHPKDm0fCC0=h9s|h-surnhNUfoKU zX4#P82&{q_;JKWvVCeCr1@Q24eg}4+T&+jb7ao;%@XyeUeD*0=uAJ73*)`1Z$t9Q> z>J44O?tI7!{=N4*LeTmL6{OP}mLmcn)zNIDT?H*VR|ZTp$j2w1%?fTZlq_%(krn?Ax4Mp{DWaD^AGU`41}2$Z!Ghqa;uJ& zjhN5rhHxN2@%}CNGgyWeK1}^8-d;8Q{P-y1NGQb}@3k*CQLxAGL2j-a-p? zKfDcB%h}ZzD_4lGV;nsoVdkRxRtRov0^P4ee=@rYtXnnx_CfADR#I+NimG8JxyJ z)4<`qw)sG>b2pEB8vTU4U7G|o7WjDGc4PTW^3vj9>RU)(z+#=87TxurV55QkvQDY; zZuu_NT^HrMh=u^BCP}P{y3G9V$+kreZ|j`S1sEShZy)~w13gE5>XKY zg;6{wu~7+6rDWqTbF`q@YS%NkDsJP6Wfvl})A%-$0Usd}j7%rC#6zffGE{+9G@{5B z^Wk1v?59A0&^$luY`;Faqu2B4)tY&h!a>3fbuqwVbTwMw;_xJZ;YdRP10pT=!ao_R z4;JXe^OqmpCI00Zk~h}8CtNaO*Vl33*shlIsee74e|DUw;@xw+dB=-=BcxqYmUx{G z&9@=}OAr4yM&ynbG%0uiH(IRy-FyC#*LmZwy$(9~i}Mk_XXlLN$gyNp+CUHeah14w zH-d9bAD-efoiROUjmO6OX-W9db`iXoU3JH|!T19`lOE^w`u?#vV+-lz1%GHICW)k0 zEpsNBa|Z*$(qKT79b+~jL!=rHiCCIAtPyuqI7C=Z;SAzwa<`cN!$BlXEhhOj+Np)> zW#D5D9gX=+M)oSLNM8`5AA^BT0)v4@0*`-CvO`E{`52a8a2VB>)y-=J7k~H{qKsy! zfQ~4yH72I4@cx057B^ml+QBxqr*^kz5GS|DsFHlA)0|F-DN0i}sHrCv7zF8yd8UH3 zXq=y5FY$^v7{o{{Y(ThLM)Rmj-q{8rDM+#_QBULI!tCrTK9P%|ND-4Xf_lH8|;el*VM+%AMo!4>vVWd4T zi?pZL!ARnS(u2YUiRenN``MuM951c)xGA|o>$z`}TJKnBz3wWh^}G(c6dP>S#t_h2_GR9ZL;gTbQkB7@*LEl|F$RH4@fyLm5(UF!eRNGf3p?aPXs!x)xgwiDl|JY`(M;?{(%e>B0 zT>?XUcCEL&EN_J(@Y*9?Fe@dA=r{C!jvwZAf5uYt1o#%MbhCl3$&*OwZ$pj1 zGTOg@qf7`L4Bt&ynZ^R_h|{2hGGJABr=*PAv2?RWhjd$!q??Spq)o>5aV%e=I0;I0 zQh^dT6(}ho-8g{4VUu=8lT!w{Lw=g%_H;ahZ9C`-%h!J+xlvklZO!uF(_o||Q^<{& z!H`Q~RG^(Kg@GW2_mU(t4(8d;ONJ`+W}qOs=VVk(kMzagn8MOZ?i|Vc@L><9Usv60 z&$w54Af1Rk@?Nc{(Q2>eLmnBX6pORVC9C0~#WvYPwc|4BvWkR78a0)3qJSY*#tboC zAbHMp#(8a{G$a$T#q8=NiFocu7%vfch9&^%qtCWz%9OJtw#tqoU`}*kI4?>@S4<$}ab6}T_QOq#w|U{>cB>IOBVrr%9+oE=eSc_wjN zn&f;u&GJSaAC?e0v3pOh|A`jTT__9k%Fck1e;nVYmc&APJhMuQqOZ_K`93qNN$Rs_ z3go(7NEmf^i9jxl*38)@{e~jGYp;EmJiA1(U^qguR~g970ItdpIRucB|YXm->qv<+c`T z6;av}`6}J>`#V{fWDT+^9|itiev(RO7JM-!9MQ+I_r}>0^wjtx)6V7DqYB*^jAL!T z)plwPxRl;$q}F8E7YOm3cX1|84;g4k5pmFwi2jW-V5()?ke<<9E`j&z6)sp@XQIlF zMFCHoCi6O>Y4F6~UO)9ZSAOV=?3gx2hYSwEs(mk?u{r<`SQ;=!-s39GTUp&rmeA&f8MQEjcvsFPw9+?8cRjm&`;>|MVyAP z4-(KWLYlZO*dL$m15ZY zjY{3=k5@HqOtWi|Jhfd>{31g|ppIuu9dh+K$!Kgdd+u-k5nIW+7Zj%iKM*2nN>0OC zkn~eIH3bchqSO=wxdq@Ndb((Yp3b&RPZ&G+hAY3NsScCDbKePi60ceiJY`9%-IJtM z9y?mIsS5OHv)~V0@|6cmzO4?TZO-s_6$cQOZEea>VzmgyK6e)&;QO;6M6o`ZvJH1H zqg&!=uYZo`Q8R=Wc-m`zn1?eVDWgkR2ozS0`O|%sVbC@=H(`m7OxD`gcc;j3@K7{` zJyGdYhSj=4v4iR86|vgzB!aQ$T*`h91n~;J913{~prKaXwloQ&3i!@)jk287I(ae2jb@3|0V;x`l@z=P6 zP5u$=d(Ad9ECjMVX-4ap=@ccUIL?vckjhm&BxS~;Lf(t(?vyE)6o5$nOj{`J`xrH? z4N&u`2sJdh2W_Ja;_pj@8oXlEzz!GqP{G*Bq8>-|DO2n`+Go`qVcoPpE!d_ol4!ZL zqNcFAvH)`5C;-tb%@oTCy(oHNny}HcXw6Bcp4K>Y=(QdHNRpc+h3>oR;Kr-Tj;Xt~ z)}pqopMvpVfF7&$Oas}}HQC$srz1Y3VV8(=_l&t-bOKEhvEyqD(zEc#ak%nV_lqFQ z7f14&{KFYqi=KI3o5QfcgaPJZtcj2m(U}Q9i>r!-VlEfZ%^tl--ZP{;66iFb!NfBM z45D0v;@l?(xdP2V2sw305Y*2*Q#8O2N1jzdgQNkdj(WM$_R( zH~44}B*DiRG+lJsM$_CtOgrlnOmLI=>}^|#l>j^<^2L}VuaC4KpB1(2W`yq9DCc5*KYe4g_ zNB{kkEWQ#jX=GK2<1V}CF=DWa^igGrPFpt45Sw<|SEJR)$7F!fayhyuvE7q~96AOM zErKa=O)P>c_alk(zc}@_oL&>nTJc+qJ>asV*gzGU$)IMU;*ws-*U>&vTjFSU5d>eo zv_u5U4SI;iJzRw7HUmXr(SC+yVPQxEO&W$ayAZ#{xdZmlhw=PrG+kTlYW^xllSxx- z%djRtm27JP3^xKs#+as){{HoP#_h7Y)PE*~;iwhT)He11NZPTqB0@h-X`avC%*MgH zg2Z6WLU(a+Yt;7$)sEpj`E%TlqPs`YyT@sfQ*7WO^|PtN0TIuK6JyaOLXZ(SC?d_0 zzC1K+3Pg5&Q#MeRHL6T7QA9v#ZdfM%PIx682k~5lDiDlFtA}DxSAnz^NpJMjIy!UN zjjSLSd^WnXhzk0qSW;lRNC((31(c4Lqt8Bq5kyn49?nLen;iV&^gd5kxBhsv7>y^Z zdvvoznD;3CS%WbUsat-w?6w{4w9)xChHYYm4ardEAlCZ7= zXld{e^<bzIYXd z`qO)^gRv+cnh0g8gthd!<@W&g-UHZW&OSxHfglgkE8EF3XwkvvkonF#Q!HW>Q@IR> z6o3@Nwa4mip8L5z!=YVd0mHF{lroTzu!DqxOLcTaubAN+9jKH|al{AA0?73^&z!UK zoyYdhV`iZ!IPX-ZtXs74)&jci(Uobu2R@C>&kE67aug|PZ}%o?Um2rhiK^H27|3`E zCoNFR-jw0jut4*-l`kJUY@;nPXvsGZDK3&nxzKs&4&n%B z>cwmp#0M?S8OoGlrjBpal-raQRX!{cIm^=Aud_!wP-<{{$VbHuo88&|#0~+6m_jh8 zg?_dq9h?^8#VFJAX$@kotbErvZxa43s_9rh^G#DULMn+|T+%@`DV^wHx41xDf8|eu z8R0>`th0E#iIA58A)Ut%!@BzGS%|5!)1X7|nnDTZ9GpV0Xn$29%zJND;u$k{2m%^Yg$dFkh0zrYY$^x(3HH?BHAc|gq*NO zfdv-No5EQEnvz3_#pwxbP%(_y1k%HR;L&1!C4fi*$#BHf#t*P{nyDdD44G)QiP7Rn#(d(gLW0h+9f(I213hLCI!|pvLe*g*2f)@NIJYfE zLZ^wi=nJYWNit`Z{D{PWT)_?{Le&JnWbs@K+bmS8mSb9=iWr$Wbe+tdq|-*DU_nGL zBUwD>EYoG_YqHCd!dX#Srh%KeNOOw~fy}Wxt!LV|-dkB(NTSkU_kzxlhx41?{=Mz) zHVa3lhTyreATXtiXks(+4~$|{^ALLBEuCbYgmJc@X$JnPH$@h|m6!b-nd$)PxLv$bN+p^%S9SPR8@ay^pZc922Xj zqs0P4D1{j(G(`3C82xOQBCgr{*FlD`F)zZdG%}4l$|Gi>$DGzvtmn<_*>aA#hw4vO zo>$3$QCj1Q8x3}@aO)1;pI>>-NB@XEs=I0F5M(qpVef=)?B5`-jLko>9V+x-FM%EX z3{!6f0vICqOl*@^lk8?eq65x23m5#WqzerXQ>dr~9H|jvBxYcg3kOFC3x745Ow~Pz zsy3l`84h7_)-M!iBNk_<4$~`L*wu!!?M#$7;}zC{kUCXoYCz0^hsHh3z=)CEi3QmF zYYKfs6l&rWW3)+lk=C-SfGBUCBBC<$Xm?L;hwDpvF#f4SnKEKf239QDD%yL$Sloxw zXPTAVDjDLZggYYwq!c0_PAH)gO$p6@k<~3~&w|fc$%W1_pJOwza`JbCBm+5%J=GdZ zYfI5(2i~v=g4=d?af@7w6>643pR(x;OVGZ3yzVPgWMxBm8)1**#b||v&(4r3M}<~z z#aqXng#Ka~Tu;9E^Xt5zFWn?aEbJ^W4fc_&Z@`hbLk|xeL-+cZ^Y!d%w7h3>ReA50 z%VfwxS`}2+h3xO{ax}XMPR{6Ws&0QjjK3H3Am$hAgNp;6HN4|Tie<=$ij?;JeJb-w zZ4@(0f-ha}ZO>c8DBSGkhvG2?a^cIR4a6fPp1+vidSc2kEuE!DY)6E!TVA{oSVS|U-6U=EZ+jGQ1o4E&$UA#3=o zp@18@Mw$a|&6Idxc~Ot|u-Qkt06T>cO`ZaR=l9gWn} zM5Xf)OT`5~Iqgq(P~a0Sh^YD4?>?WMu2)6nJ!VtL{bHloA#$T|vIp*s3w`h=Q|D8~ z#;aNJW5>03zY0RuP;%&!QJS-T-&wE7K_$1IuX0*k9Lq;_;tocuQ!7J7qkuEysZXV# zFL;!A%6jO|KV*ga(fmUskww>gTHRp4E2F}HWgKC?ti>AUI>vtRaXp%%LgZEmHdOi{ zx%UZ`I1WGFzI@;JF$aj6m>Da`%O zHBquk-uN`K>n^0OFjz0$BpA;Lj<|Qa0-v8cbaS3y1sN?;)cN6XPcUp;*?*RIW59@N zK!1FMzyJ56{{KivBJ5%RhOM+Q*Z%I9JE>g;k|*+h>a^u=#p(T#P+Uhtj)U{{e+iTr zk^rQZJMktr;`hv0J@LBFjJ0mN2D(>YMbs~z3d&b^`soN!dAWQ7s*%m)^99k<@d2I> zwI1Ds&1J*mZvw4Rz6|(B7ZH35@&;N0WF&P2p+w0IauF}|MVS1I_y!pem`)?&#mUu` zmUCSx=UDhDs+Mq*i@hjXEMcq0GKu48ih_YiWtqJ|oUFuLtH|#$`$s3#2%pb-+wYe5 zy-QgAW0N;2zIZHTWp%vqdH%`M`5oNIFhudas*Yy?5aHEvUrko4sT{v)oC#|}DMbkk z_s#%QzqxW`HbZg#zO1zs24+cOzvhm{F@Tw=wORKDxx&QCJ8-lM1@+2&7AwjS02aa| zg)$aR&T$4C$KzXDk0$v|KDG{UJK^N3N#kNW^i9Eb?k2OzSAYIxauwhJ@OnC02`v(9 zPxtxN$H_G8eR(!SmbUi$WBL0Ixd$wYo#SVL!r*-QhHfg9N#=0j_$s&_!KlIo;$Qjx zD02g;6j=K|4NETu{SUH>p-3O!k5(mCR{g-j>0U*f^uJCgmy#r?_i2d*3P3sUUc7$( z>U=d?twRflQg%-8QHO*@Lq?pWoCOlOO^q2XA7gxIiCEp)9J<|-hQ;5-TvRy{9Ay*G zjgDU9FN#7@XG?)lMTvQWu|wAqE@>2~ehVQa``lBbLI#vGT~{~(I!TL_cSI@tdEz7OGIzoclZ7sOjJCPi+itVv&)zl~QsE}B*euODV2?o` z5RIc*M-7kr7;!#7SfZu|M{C{Tayeg8_>J@!KJd5&HeQ^Z!Mf#dsoRqb2jH^#($?&W zZW@7{_A?ly-1fN9kNBEzpZ-t;v0P-|F6hL~6Er{Q zFV4pwP*1f))sD&Q*Y5h5?f}7oTMP#AEvU>Ux`n1e4IE7Q8mXeN%8N{F)WFXo-(Q5e zO-r~7?r47N{bhh1!*O774YG4i>9gPYqq^|xeGQiLz*IFYz9h zGs}7M0Shx*7JqOx@)f0R`mRNL71PR=uqwCQfe&-+$y9J1847%gG7lAL;W3~o{=Ldsa-eeJD>EvGsjlegMBhfdKveD8)~60l zpzN!F2Ij2>8XV!_q}+$mvkH%5K(K7O2tQh4O|cyv3c& zyR7PjZ4n*cr^8oJIaoBFJGnxT{)KnX<`!;%ZsjwiiLRX1i`g{}Stcka^qgOiL@#5q zSU6vUu>?oFarHqil4H22jJ3&pFp%cOkaV-{b0Kdo!bN#NzI%VEc2pXzI!3L~M9SVN znr||(09JIIW{e1OFAo(1lr!Wn=kuxBBR~c#64?U$uMu#>KZ8Ncfa=mT*l0+xEF4Gy zlmKTHGug~iINL)6+9WW_@0nE58mb$I!#_jMvBqidsAVy~5 z__asI1N6!oIs)*nu`?U@?d;>gfgkHJ(& zl%Nf}9M(CLy@I`onRG(#^3_f&zFgd$?@6tIZw@{_USGA*PV)Fkh|VX^uTIc)0&o56D>il1&*hUV=;*Q=w`rHGsUw%1rcOIySC?V2ASIqhDa zjc0@pm8aDvyO-~eJJ>8&xyilK{Yu*Gj#GWP5}mbIJOqAPSn|K~(NmIk9XHCnsv_^o zu0GO1N%gY}BkeZ>zn3%8l-?W+bd@RlSf`>K=HJ#53Jn@-qQP%iQ#pT-zQ^2%O z>%ekh3Nw~$mb2}1;daRs8QN=B0qZthMEmT8CK=#(J>y~ypva~RlGULbVrB@A(>zp@ za4^G2O5;Y&eH}0qUQAe~PInoMKZ*7}NCdeF^P`OMs^k3Z)D6rYfaoG7z*--Vnbi`{ zRf%Kjus|fU02C^qSmLyM?$%7_E{Ep#dj*)lF5nFklJp3|Qi9EpJF{({Nj2%OR z_9OXHctf~!Fp$@(lfE?{CC67sYZZ&gD%Q*bx{Jxx^%Vz>yKg5~&#&Ar2ae@~{F~)` zy+97>&1^I+qg*o&P=VUc1BK>xF#9B|c{HK9yBp1}hO7cT02$c-;NrCuhz@JpIlSf zx4bC`6-Icz2#5wu@zIxfes++JgZy@$aE`)*uzQJP`hW(S^u2d@g}PLMZv!B%uP41Z zgac-fz6sB{U!yoWrpv`u;&0D+?PvI~2YqxmpCPb?8z90Zrr|m6Q*kFQ9Fy$jxzBJK zOn+1AJb@W&mOudpsI|&#F^vi**mGpGXd4;~ct?W))`Zd?xhEmPuz|rq>;QuWi`n%QKSqBfReW-PGy zO`;+-mx#$);n+YMfQ9inNXtjl;^lh0@_{+d;4E^FY?OmVLf{$+Lit4%XJ);<9cITs zWjB``$b^AdkN=Bz#f6JT0+B+X+(@gkt6;%GNY1jfYCBXb6mLk0!_rId zFV^8LmFgOvp}vZ5Z|{LkLbLQkJ9KBu1Is>)^G)oa6w@9Elv*ct!z1kh9IN+OOFF-p zt>%=nUrjnAV)pWVhr{_W=OCQoBL&r^-zq!R6PuQu$1V|>&F7}Dmk3`1_6B&1L1PgE z3VOW=pKyYcnL8{BPvjc+)i$dE7Cvb#4KrqWeishW6v07@34+UlitVBGu*-X(|0zjJ{^!200#P2!1E^<0K>K$;!a)2>td3WRWNK>kkl$Vc=79U5&>SF8 z3^Q`bTh_EvrFm_vOc{6-19q`xC}QfCzcUUgLXYwK;8u0DkRLmRfcz0wU^rMM=2hV1Qe4&lRJyqyVH!0u45~zH&(G z%*xR-C0e=3Xq2u~ENwCZ%I3c&qZI-wiI4@8jAW2XWDN5*8UbejW;Et5GIb*zY+%l~ zP(C9^^}c@Zh3-Q(H_cGt?s(@|#|gK)vp8|lcd=uojBb{Z2GOlQ68C570_ad6Og#ws zhA{fjpbx#H-dBD)8U|5I0ksG+M2+MQiV)i+ipDPGG7Rf&a#?v;Dm9@kEy!DlfPc1X ztN2UBa2e{z^Rx`@r%kXlm{J>r3?b7H!9$%tLn!p0~$j zMbQJ1U#5;YPrNC~hU;GX{nzU$m(C0wgzi2sQCU1+`oS~|$Xu(rYI2sKPm=djqysUF z$R%lrC}J6rv;Li0Scym`K`P@5Fx)rT1C^cSM-WnUO;sRsJ{(J&k(o$6){+Vt#>P7d zjgl4LIWbI9t2l_FgzlFRg`|F2eO!b#N!Gobf8p`uY-i5DoRfMs+RWYYbUq8ZPx)_t zon6|I@YK4!|H#MEP&bPRHS68d4{roX;%Q|ScK3XBHJOuq0hLQ686>{_+_WT3s8dn{ zeAS_;lxaHSNW(?Qox-?J>O{A~$}kMV)Qkr!qi=H9U9PO!BwALiuqXwcw!Qh6XZgCe ze~sfZnn@zsN3Ms9-kbGmv8GC1h(6?mB}>)T^s)FNom{Uzmo9tf{C+mZFWonO=N#2e zLrNJeWoAiiF{|O+X#VB#4_EgyS$oGVfYqW=dQVL+(f#YkM`GFkM^xygD&6RNOX=QE zkN%hZO2u_(vDpu?nK<;4RP2#pPJS$CzGNi5am1n4RRBTO7)FTeW{L#`;&6e0K!mto zbi}cHRdBZO9dQkCT*HWCr^p~0*^T{n5JvW2nW%_uu(aU6esh9zY2&}8h5v9tlKFi`q6R0tPZKz@mJYQ9 zhLuoY*`TktQ9t&~dcE$i1Xko*Z`dtW5Y&u7NX12+zFn<`h!|pZ-cB<&5^{Co7xTZ`~@k*S1<>ghedndeIBl4XD@DHjYzv)iU$@i%{pf3VGb`XlGgQs>Cpaoyj?WJ6QzWZdAhI ztvy#zz@IO$gOf1>pDzv1YCUk=YeO39+e|46VQy*>#Q9;rmrBb4;AY2XdE6WMBN6eoG?(B<{~T@z9o(gOiw=MC%>C- z@a!fIGK7G5CC3cKi>h(HA)*ExbU+948|1CXEoxJT2xPW^p-KnDv2=~253!NgyV!_5 zh+#Dc&_L~Y87!uwabQjmlV7G!t4`(I(X4ptgb1@Z9V9=`(E5Fb>9}ZU10WC?x6jn4 zuf&H&qp>p%rqg4jRXxSOl0@l;*-$^clK$oT`Z_?b5boj^u#vEITCXOv;LjyxJ4vy4 z!Zsn~&18N8IWu3caJ)Yp4m-3wI)JVg-CPfQ!51878|3qm_=4Nnizo5DFG6Z2t^Q=; zMiQr>84$t>q8X67i+Am#MAqJPBoS7(iMfJ@B5W!i%I3w0qU$zlGNx!!{l7#adK>53+Fg(lRFdeB1HdIZsG{(5Ut~J;!AxkgMqdd^|NR# zgF&M2raECoKL!WMnxMP0*$tI1Q39%I9Rc(Eaw!O1j5P$+{Ei8Sw;TAJvdA2aHxyY6@=hB9)$P^*sxqkQ%rSC{6F^HsT4+0#R?6hA3it}W z(zXqF!3r6;ml|=jCCD{Ko@Low2o@&DQL_Z;oNb*Tp*oPepP<(Q#^><1(9))uoO>-B zLSIcN85Ct>&nC^>*jU3N@XV>s9c_Xu76a6Wnzp>+U@=wc(a5 z;p|YB{MwYxkoaENv}B#5^DtZ4!VIrv`brp(hn*w*G#H#jAZTFsd)mC<%G0pG0MK^r zYz6b~{Q5eC`D{bH{`EJ8_dRS~VSQh;-e32vm{wmkgyns2^oErVe#aurB?XSS;yWJOIAuuoJ<`m5Vuc3W+84stq;yK`vjnXZenAt4|= zCngl{&lDI^%YD4=b;KCmgpEK78o9Srt?>`%7=?KXdzpmXhA2+Ojee3jrcV1LqC6Fk z$)G}~Jhmq+l&84kP)IN4D1qWUDIwa5XY#%)kMoIL&N5HP(a6uNuON z@-8HAvUbd7Sg70=EmPU7M(rx&*Fu%?Do14uQG^FW(m<#Dc@h}HNZx`hmszLsAKAA~ zLaur?dsPT-lnF5$BEg4ybQoYLzc~OX9xVOkKu<`I{fRYdmkuo++2y9ph}18VnAT${ zmE!tRiP;QgJ?md6QLB|HmA=g|&PmHpSaHI}9t47+|@ivhMl%euO?Md(X?9FT( zyt^L`PcS)m>d*1c0J!M8$JetTu*!cu`tP4)k+7&4isp&hEp;?_qbrs4__;twiTvPb z9~O|ZHJNs+Ul!U(m~tf#*fX5?$c=!+sZPCt*c4U!E#C)h<)eJ2u5Nh<8LB{nCCSQDO6;c{hAI>n%DR|F;O z)ICFyi1OgMyP*>2suT@@u8$(_vOn61yGVwR@_vi zPEunqkb{?Gk+h+FI;SFCqQB^4V+Dhj zmXd9WP_$0O!MOgI#e5S>ZTm;6YPsHOt+GV`Q3%^a>&x<>?5su~0%SoP(ZBh${2j9Ol=nh8zmN~!yxKp}y$ zN;1k0#U7Ad&=VgY^LJ`U73Ox1R%@DIe2NY?GPhW;P(*4{p$WcE_x(sKOdZlm z#U-+Q-4?QZ#X~bmmaNs}LQUMSpnx4vb9ga#P3=JIHy~)rbO)PhpBnjw2f$)ZHc_YJ zk%nyiJ{MjuZ-fnZ5voZJX55!wGt4_j_HT`k+6W98i>}1XntVS)KaG5QYjJ8(BAe}T zQSrPG9W=;1&6>CDN<^LKnRG1Xt31(rduuzDBh+RWD!tC^lXZj`$1RHV3ILE(K@H$1 zQzBnQv-lMBP|DHZu`!7mF@LGfY~44XXTg==3BzX4{5L%^@rCz9&OTe@Bm0Bp-@ucig+N~xZR$7{XNrs@4wivZm9?C2ikEU3OycU*6-0V0zVbN~Y7%zy& z8YM!-MRr+iKUTmkfvi5TGS;8RY0+pu?Md@f?qD;2ydn5WJ`N{4r7EVEF6ZYlxd_Vc z$~BM#Elu0vE`q|H_M7o|y_f`;!HU$sDs=8SQ-9c-%#dUR9$eD=C3-0|vymdDBDQ=d zahxGmik0tupB(Z+;Fvc`x)HCpvQr?`gvz*kJyP<}S345E=!{{Ag|V#d)0i`w$yE)j z3rel#!ADSPKFoLIgJWDL@#GPeuq8gti*A2BFK8L9hJLt@Won}jZc)!AjH>WKtKh}E%(a^@+UyzgwB%4dUAa6x~nZWU8$ z_4#p8*VgK`f#(oec+SY@sfxosA!kawK+qUxpmER8;9t}v(#Q=<39mm!W3)nJ$tq0| zVOmyz4{6qfz;?8oDoAI7n-H;Si^+4|BkqW)c_gF}vd&Wr5m$H>%hdx~g_m5-Dt#Xl4sqS4ic9FcrGrG#WO z*{MPss^^eYkH@|KlxvhHPggU>G9wJZfWFEQ0WxM7E|lLx(WZ4c?UHBE66`7o78;7|10TBT5y1((3AR#7_)w4)+qzW<0fQ`*K)WN%u_v zy>kFS4+-ev^xboz%2>uJyp{3^qIs^Mp?cN^x=D4&Or$LyS^iPmbcEPf?)uvPT>p=`*J`V!~+$Q%ge zMC@<-uVWhObz$MYF6wi6))Bh>!0|8wJ0Fnkt^thT#Z8pK8PfZdTi>4k& zFoaZphaLjIx`C7l|2DcfDTS(2y3-1&27WO|;=3TQ8uxIyyDA(zvIo`mWy}|#ZG^$C zH2YFsodXpWEcvx2{iFA7%+SplU_6`FxziXLubH8pmLz~{5@#u0KUHq}^vyhv=c*MNj9OAnwO~E`RR1zk>UbnBt0z9uY2Z z9M0tVonr@{^ARe_b3Xe=`txc+Gnb>YHkLYVw6_8O@zMVk772DS80fng476S3Ecd7X ziBSAUZ4_%{-WV*)p@xj2cn&&d!iP>oYEluE=vk9=97WK1b*OPhS* zxiGxqN#e~g`?h9=>;M`FW0SXgLE&Db%+`?^SuEtP`%6lPr9U!{GBw9u0<4`6 zut}3#Pz*2{;Bdg=T{kx*a1C;ea`*%lDhU=bL?shmf7-5Ozu<`sMa!n*jT-QWt-ZI8 z-hHU{9isQNrd_+`xME*LwUKLDDYn?~5%>nde`jq=8+cMHZ9qX}lQ_TK6+JY#rSk^> zGaUdbid>U=sWqp5yDA0_B^$-51Qmr|1J5tb}woBo)L!a=3fMz>~-l{z#9=qo~yKiXE+IlDg#{ht~M7S4Oqx_}UevupQbWvm* z!A%xo1Mhif)%&LZ;+U%9kcNWL3d)F9?;GpC5Z2$1%fz|c!+gZQ{ss|`U^<2&O9z|s zpy^NbG`~c`F(L>eueN$-pUQ}d4cK-I+=Fa1bk4b*OfiFX7Wx=bfjluEy=^|wC}y$b z%*ja{Vy8jPNSApr8&7dC|0#Gc^I!7sTvOjznJs#}n!d@WWv5(WUkB>VrC+K1cMa+t z1slYdERg4{_~M_{1Y<;$Ck+$jCIu`pfShCQ7(=Bj6FK{aaF|ZaM4lCLfHOa>36s14 z>P7g90cALA;v!~o*d$uWcFtG9!Z|_b{LiB$+TLO$&^2b6s8&%I(TIo)PiePOG9ET+ zbe5H6G{K!T_+qsjO;%x_DyOM~aG&bU>4k0Ld6H!%uZU_U!ye-`U|nRPWmvvXn0{b5y7$~N*;93RQDgtNbuMIkB>>KgeCOnZOVMbIbd;!3F0vr z3^GTh@8-=PmI_X*qPm?#&T$SJEH)e!xhUsMJ-tj0jpugADC6{*MBcK17M@nC6h$qi zCVrKhcp5r3L7GjykitgKua$5-0N)+uCjhgmMHwcmfc6MwJ*A&SE%$HPuq+X?c=Q^F zO5{j|^T|rg7T1-t1T`iykypc`RwS~knal}e`KvOP>)0i4L8+dD`VOvKj0@PvzD27e zY8zi%!bL~AXTf#x4^^?;zKik*xh3TsacsJ6O1AL{+Nw#9r4ABAR`-W264tMu;#`2F zBy}01bTaS4=g&^V5G6P*AMc$LE-Y0>B=TfG1ykA~>UffeyRD%t(NS&HWJb4LA3YSz zqQgJhVdZRx<0}e?*(#u%D@Ahl!vRF$Rzy#%Huc0Z5Kb00dF8Qr-^qo@f8?yCxG=UN z4@Mlo0uwc}1}1biVZtFx!fBQXHGXq^@2^Vy<~c7#hd@pREkwAsV#+DWaX02(cMv^kgo3Nz#6#78p zC2qM;i2+P(BJ$Si(gTKXOWu@t*nd)_;l(&+me@ zQNc=7mWk|!y+U+uX}6fu_XMUwd$ztiMr(?% z!E#Q$H2#f$5y9`RXP=j&h2cTs3wbn$i#eJ=S94zrx;zzu{{%v>zaD#OF2= z)yNSoCkWfc0NgZ`e;}k-6R_`?3Y~v%NQGMhqEuiz?zbz2Lq_Z++knX~&a#^#x;V40 z-D|trvJ1xZwtKHSTwG1&GXz`~^ly&vE@<7_l}2y{A2ihpvwrX5DnP2-`nOpy z0ZMiqg?*62lCT9DZU|f!RMa+j=Y4~#g1Uet9~P|BMT6+iEJAWhNoFlbo0Uj(RI}G` zS^7Xn;<%2mR!icl$j(*Dr)*`{al)Gm_Lc=5T0D|Z)O0`N=HN-?v>TqM(_j#-XP_eC zGU**h`9fCq{U23&RBd~xanCI+4F8zY88 z6bj7{gStyuJ0LRAPs6(P_FL&#T2>`TR&25?zrsMWgK2(ScFWXS*qmCh<=(=TY0GZc zKTx2ja|{4*83vKrJEq)}yexxFdJ&hU7j!I+Qw<(W%i^-TvkHJRuIxjNVa3AE+yMFE zIhiVFx6!t+BxMe&4{}+ejvUPx(Gk0G02lK;)m;WDx6f~ zt*S)wfE`aA<$A=8*)@GE@bI0_m>v|4zdN8kk-yEYw8c4*P85FwC$wpzP^4AO5&ho! zSv788*@G@tsRR_+5IWI2_#it%SH{0;0JirDd`jx?z5BO~1Vs1f1POfW?V|-K`e|+u zO{L3pmF{%&n+qRgSLqr&6rTj9bb55yPnJ1}*>A1av>*|5I!z=xdMcWq z=KKM+xvok_k^ND8j%BNTtWb}-{pQzYDN>%1|6O`y?~W`UWasQ%f~T-=V3Yl{Yi}&+ zj;MpoN@ka#BW%-Mw##z+P{`|VuNil3{8kqCB^1RW60V9@)NRzf$6hX({+9Nh#*Os% zu1!lxS)m~Qup>kzn}g%rfF^3t^o&ZfF>2V`TqE$ZvJknlbei8lzi_$vN*gyQg{Iqt zmj%Sn0ubM5H}Trs{5R4J#JKzJv|W~34Tl638gf^q2AQj9jgV=LZ7R5bEcFuYh&E`` z&49~v)$yF)1X~|s_wK4#C}DHyRqd(9M4)aX{^V&e;D?x|O!C@qPjAs#$`Ob=_{QG5 z#$vOrl{aWiBsjgW;Vm@_xh@{o=v{6V(vemOoL}ZICk1JG}Hftf%6#Mdj-si@uZW; z<22tZRQEdeJ9ICo{b1&gTOKzdXBj61y6N)vVl?IJyW>wQe>v43>Ggc~I=cIL^6o0h1%m|dg3XgO5*b|f8x|jE>AOz5m z&v-taj03m3THa$sd@%mtFV>g%$iJMgXIG=;JwXpfKeeJYBu~*?pIe&BJqpK@iTv$* zUV{N+*Vhxd0`Y84Q+E2?3&tO<{Pm2NAW(+v!k6ZUYUUWyHbl<-yECCbbD!Gf2YRm{ zgjir<700Y?PiXr*+}v@WFXwk>K{&ZukEYb=k+t%%e>|SgFk^k?v|h}v5x<#SP6Hg# zwRtZ5QvTjMmxJQf2akXFr)5CnkB@BM{7Xr73C8S+eH*0hI^iN1`}}yC$L`O9a1~sU zjoS0ue+1)O+9PCv(?7=#X;(C@{WbsUR&L*jQ=SjsA6<nXrTb+JomfY z@Q8obp?lsA1+injH1L$aJ->DOtGOTxA9Lx0<#-eZt>N$_T5%RtN6~kW*L{_QN}C%D z)N%#`?gfVh$=Oi+w8239idKV5V@pMTEBMC&`()RYWhDuJ!Ruz|JDnu-1*l`NyT0S< zVBZAbA9DfT+X&!XyLu5`GozWeN?X$?O0ikSsAYQSF%yMgmgyAYS8x+yWdu=tt07;J z(_7z4X=l0nE+jMqdGT)ll8>%I`v@A_N#9hpozDvbl-|gfQ0o&B6QKGEVhWDDWX}nG z;(S46rU7pf>XI|)Zo6fi$os}B2Ls3;W@8!bPR6{IzOoQr9^j;wsi)~oQVBPNa=!>& z(Z|uC<}|buJHS353_3?m6bREf^e7q1vLNyRq zVWyMhFPK&mJc9wjC%YnAj3ij5Wodx9&>%@j^c(-#2s51`6u;1lTwsRj-_E!sKd$0XMIu1Wa^&x;c|QJ~0QFBT#qQ0lU*(@vMwHj}d; z;oE5eKC^SXMU!0u8MVp6B*=OYk+R8J^pG}6l(`ocyGQVsE~7BjJ>$&6FOi160Y>_` z_wN=vjN&auphWqINR%p4Uwu2uE7Jx7eMRX{zOca@M_dsbcq zZaT=!hguB|wP{0QN@tOeKDb;7xufGL$VHR0EW}e4OnnP}u+TPyUmHrCZWyfmDb9W7 zGoJ<>14SIn4OXv$*_#;+=62eX*?1Y;VMHzcl5bjI_dKL%eWaTYC|Vz_DHCszg?1OY zNImRmeYCkEfPZ7rdVSvpjd2LkdToh9D?GGleYAlr<3!#AOLlm`R;My5X0a zal@|9MI&_SF(UdSB+v`j+foOLwEp#UMY5yq_fRB&HzF)(dsb&PxeK@~1gQ=8NUgkk zhYw^ZvFp2&c{f-t=gaPp{vJ}YJ$>o@=JS<_i$4A3xsFc{f8)o;^WFG}6-t173uzKoJ+|Kfj*+z_koH_x#tR|NiL_|0sRAG;{Ky z71hx2=CM)N?-RVpf}^Qf!sZe8FcV7*Z@yZGUwnhG4t?}T<8cs%vNq+k-aYQ-SCi|B zA~GVHOr`;qOge2kdGaSSe=!}61E8G#JPrH5U9>;HMdHN2aq@RFxtmTpRO!^c`aD`* zcc0PU{99DGI4;t8C!?9qXZ(&c_hiL8sv%i!DMysxUVLw70OxoHz?4{!uBY! zpvU}d%$vl+2LpckV8D-0exok|)DEv`2zlULw&i<2Pv@7T>99MYOc(?7_;a=C?LiGSMbfu0xEL4V z8%y~9FoCIvKFcBVMVKlYHGrH5cCQd;ugJpSC2FD>DK%-^xh^Db=eCf#e@;=iRV>_0 z*TG{+am1H{0kP!%fUja36g@W}K=3Scu*o|}urwrP*FSkOozBNpLg2Kn0^TSr1q01- z)ZX|^yHuy2r_CP5eQz3N|V#GaBL`Sz9 z4EU}JbtR4g39=RF8X29PaWI`at>0(i=sI|c*QNH51&P=aQ<})Or27sA@qM?q3!!_P z#nDTNBi>N6_^F{bE7KsA&4OHHC0d>`Bx_V?zvgz?teV@(=Bp?#0ugJgoj|Ap`U_*3nq5%DK0?B`-_<(|tj1+0Z*R8b;!$=nTM@ph%Q{T(pkZfLCBHw*-t> zivhAE5;plfTjCiO3`O*O8NGFe#y4LJ~Uo+~@>kQHyG0>ff9VD!kE@iY${0SBQO} zp1?Cf-Re@A{Ui?BU4&(vu{3C;E2bW8b;UdnP>CEf5&6uJXC5Lgx;(ueDNjzq#pv_w zc0T`ba!#*)!K)(a5?wMFMAr!5mk)9&lCQ9=JiTpUcGF55*3%nrKtZBD^HiCAbC_=HGK`rA5XNWTlp$Q{PS6cLm zzE}Ds^|+tEtTKA>zw5bgD_tkY@oDUx3V9&>4oj|A!E}24I zk@3^>J&fV;oY%hNOlC+863=VFs`!JC4C&4)j3&N=vf{-=sY~kP#Ip3Bb)rUiFyJEL z!2nglIp2!@0(~@9(bkjAF{pDSTL!=c#jMezl}AUF(L){ZG$ORK7_DxP-{LRq5M3)g z_mWO#*;G=U;yMH*&-vgFJ5(kOFn=rrGYdh~XQR&_7h4YosyJJMk)Ask$Or%UNS(u97( zmpif!GP*4Ls3Ubpd!>qeRxdXLAi*GvGb(3Cv58!8suAI7=+OPL>tT4|di3KbjGSB} z4FOyg+{smxyUgj`jzaD}I0gIrVX&xn`7v1Z&Un4~{KM2f@$7ck8_yRwn$GU>NPGd9 zOBGC@$f%O22B;<4IZj8R0b)n-m+}3=WiE8PzG5`zc(D#|(_4kMu?Sx=#2I`^=^CO> zBy0HW9vPF2M9#r@FC^yD$4NS_eif~ty_hc`FyT*9iiPOl7&8}|ScQ|XL4s9f50w)L zIb3TLZsh5UF@Y9|1g&(uSk5W`fOAPypVBV!VTNBOtMk=pwGNL1y!7WEB=tNu0#)6I zsorvRC4jQj}`AP?pr^yY}6`>WJ)qXYldQTJl&PKBv zm9mA-f3x{j&=W%hy3g(*u?dML?O-w_Jm*(R*P6`0ser=__5bWvsJF`e(pCpsik3(8 ze}%u*_yT^kgoIK1=Y6Ma*6?xz81*bL>Te9Am;TT_jsiNQ6lX9+Vy#T@rdD(u<59^s zG{+*20%b1}lvWv3k?AV~N+Q)q0?;Tz=&YafvLqXora(bjzl>&AQzpW%eq!>+SHC{l zxs>weIWPSVQ8X&~`1SDW20e^D7Y(Q>TwY9kVr|90dks=O3$BZQ*a$U8C@SU}+I?I$ zRs4H`kSa}5d|pbLSm$>3V*EO-B!cn2K8Bq$#_+v{VZ3Gxzd&1`7oIbL@An!1pYi`h z0)~k%;$!#aAL!{!c0hPXx9#|mbo4I#V%|h^-UM%wk9k2v#`%oJWbyZ;vl4XtO#nul zx>I7yN<_T;qkwUx`i6-uy5)CZa_1%O=!kwp4(EAPz(^M<^$|Z5_ZuCeH^eXeZ^2cNsE111PB!i&0BbZgtAh49A)>u@7Z_4&i!H#WvEExq& zIJox-*iMH%|0&!yKY%aBZbb5<9l>9Ll!|ETkSvsK499IK;y!|@!E%N&BwYhT6NF=w zF|h)cuXhZwSv826i7;WfZ00CGV;=vIwOpL;^<)`RPsfPbeg%^!KieVjtTE)f;O@5UZed4*(Z#NNxS<-qYki&mG2501>zs>qg|4 z*PjdB$iYjlG54AVW2UMLo-ydB9jZkmVw=Xo>BdNS404&)OEwLpISwx$ssMGZFD^Hu}%PA}mwK(&yJC=rdvFKYrNGJP^3c$(ToLz@ie z(HcWS^$O|IG(*1Km?7`LkW$8`58K)}SL|IbN0^D|xuJg~UOohV4XP>p$f|GBDzXND zdYDwn_7LJf;$<#Ko6{c=6&Ig6WJ5q4j<~G}*Crh#%Hr{yQPy~Jok%VOK8~Zgd`*FuXT8g2X{LkELh(35exRAP@RvQ9IaR zcagi>)Rl%9IjaY2k2eW!tr+x|SW}ljr0z#% z2c6INkxb;z*3&6qJbR6QtC|L^v?!-t_}qjkp~tg1H^w{=mrQZaHN%rVbYsw>19+D} zJ$W=`)wnWtxba*^$Rm$7ex$k)8HL&f}8Ns z05DF!Vu*466@jc+Z|cygDcOH2IP_!v{NPVT6`;y&R-H#tY`V)oc9M+UWS%)HtJ^JGwY&Yv3Q@tQIJ!uypHoqmDV`yI*- z+7JJWA7_-E_?XTZhQr=^_IZg>$LzMkDK#eB?kRi|^iWsChP%o_zh^Qs1wut}fGi8(U(vXFYSh+#vK%X| zTd7S59WlDU__AwhBB4F&|DbBNc6$4|*HIN7VjF{jC~r`jUG$s7K1y`Ti5NKXO}K{;7`nvV|%JQm9?F@-XRvj#a~5SH4uDj@`)8 zvPCooipjXbkM~asNu^1Azn`j{N|+9sa7E5_InYpnj{)g=_QWBg8(h7eTBJ%C-HA>Y zB{~Ki)hpQ98#d)pLb!`MsY+OEL;^q4`LaaG)x*bUzGs$5HQiqgS0zGdgq>5dBI8l5 zxg5+u5*rqW?U~m_pJd~?ylAk75mQ&em-p~lsYg+Z2YS7rkIvbR#m6T;MGPJHZghVc zpl1%9zBtY)z5AWtXg_Q$6RuAY3p(K!)PqHs;@a+Q1NyvursM`r?uSnMCeS(q83-cr zz3e9j08uBU7(f_>69Y*8sBYGq0>%D}J~;-}njk5A^(jI?N@O{TX)<>*O))6aFdS4m zZ}5GHjiX!opEPQ&dN z;qD%po?>e{;Ve_nYe49x)xCQ~to020FbagFEnq(5hEKEkF=P8ykpN5q19nxzvAvZV z?~gnVvRXNpA~5rK<8$Y>6B7g%Iva>$XJyZOME;H|r*|*=_2jd->e_;BQK7v@dtBrj z{XLS&N*0J4P?ZRqQ@Qxw7$^$VWTWPXS8*pk=9l6Eg)iw@3ZwV(mrHOuPvhPsH`>l~l+ap`s{ z^DE)Jk&ygH#pvWwZCK=QAdE=BCrbYJkN1p}|9%E83aaYsa!w*L#<5NHWhxSh88D57 zq?5(jQb}5T(PctCuyVTf7P3sb~>e^wI0%)+oW;(4<+_=$4C zRNIMMTsD$B|3#sahAOc&VwZRSqO?u?0pY^?20) zhr3IiEONSO7^p+f4w34n7@!Xq_tN1t3I(N>GUgw#wU6naJkD7WIh_dMxAA1v02hk;DZ@aJHzbM0@ zaye^>XKbO-$U^|BN$VlkZU?d-^$PjdirLbbNB^Za2ny7^}LUaGF$^~x#P^u~R1dASTe zO-3|YVcT795>04E{NTYrUx2hNcbG*`3rxFdCQ}HlNSBxncZ^2TW&!K+onPuel??3% z*u$kfmWqR+LU4jtCZ<$T`8OCMVH2osuMb0Az!~K2 zHaaL20!@VhcGB*kF0gjqsGtyR)eJMKo_0MMwzlb$+YvH79@Y@SYoZ@OAqoWLKK$bS zI!u9Fm&32ZFUDY$%xIn|bXX*VaqSi@_)5{|@TG z>%wfGmE5J^#{oq{r+ri4P$oNWX#lBlE^{qa3n6Y$o`+FCU5pe}d}WO5Yx$;8@`Efm zC>;}yn#Cf&(lj93G@7|49uA{yxdhCmB-vzAah2)J^}y-0Dpdy;ZyUSjIKw8}t}>vx z=C;dC4mGfRRHH>>9HJuJLbyauNH&acfEBZKR8yHj%?5bxy7sjYQ%Y8Dt5CHLY{^tI zL?O0QVp<5Hh?eWbepxtdsUEa0Xm?u4SsuI@QSl7~aBIumgDaS+6)l7xp-a99Z2`l6CUX4|X|tPA}e*Y#}?#L`qXlD*~>?X(E54ptyl zHmjyTswCb?&$vtZ8@?kgcJ;9nHY5|E#PiPLXQ`D< z7uT<5&p+I}x_jz(sF^Y_!|UjrV=+!QT%r5Xoxhle6DpOGPjGT~)?SRJyfv{mr}ZK{ zoy^wZn_1wvlhD7TH~xGU@ZF4SU9LJb-GY9ANP^`j&dGFyql|DhTCV)bY!xhLqiGA! zJQ|Ny<6BI~_>7qvxChyO#+#fhMzhKI0}ezSSHI@?YVMB_#1s$rdcM0{nV&Wp;@czn z^T*-pYA{%?1_Q&w1_RY82!}79{?Yo+kB=tdYgrJTUP(&m)yBaO8{OhYw5^dSLC=%w?n$}zT!7cC77Z0y+ zFQ*N0hdC%w_Gm(Vs`NuEt@^mc)S^w429 z{QERCZ`x^1A%DO4)5(XR%P-qEvvvEj*Et@ImgC!A$LTBt+xVl^YU!^``6Jq6699?t z4IxO+84Pgu!2q`(4CMWR*2Mh~WUitX<&9=6ffTIEJe1;PWwP#vm zX3CBr!o+l@ZLNLS`x-3g!Ifdz;){k56t6$~q=XPN5wp{!(K~|3@%K`SV-_m{@oD8( ze9IHy|MpkK2f@%UVHKX0{cG_lRj`ql#uc*HRB|+DiRtAo{ouE zP+MKucmhQ_8gtixENvZaCKaPw??E8O`$~Y#u*X9Zh4HGlAp{fnuxeq5ekp)=Lu2*C z#l)n<6XGIrMp#^oD~pSXE-n-(vh+h@y)#$BftCX=Q#4>RP+@eeh%gk<)!Q_vKvwA(!%o2z(H6O^EoOY9ZAWufBIfEs z5Kcn|8EhmYt6i3b$YmNLWY%Gb%=6is**I7aPbjp7_}e(h5zNsMZDjr@S@ z5?Q+iUoe-qj{wHUbsz&!K7FRdjgcC3Gu&-U%{-ppEk?@#SC4NI>I~jYbM#O8$B$VR zNjXPKy@nE+4xL{5zS&(LrGN4`dTK?TB=I64RY{-qIS7lm?`CobCCViQ4}NJ^XjsSN zeMudm%+vm~W(PPdS^e>RJzMp!f-&r`FgSYk=IN7H{@W+7Up)1nzWM$2JEHTauU`tw zDvNc?4V~l6jas4KLVS(+=C9^%S<-Hia_=4g+;gUWvU2OU7oOYlqc1yS9Hexf;;(n_ zFOi>y|6&>AjWg1OK}jbFh-%Scf`BSoIi{gWvWBd}(QJj_e@gN=I1Tv0ILne76dJz~ zAJTFM16uFnAK2sn@zLdcKK&PRWBKG3cHwF0O7k;K6EarbMPsHB$4z^F{}sJ{DaU;q zVqIwf^=VYt=gI0;2w;fjk#8Z5h}K$b7R>5OdBA7giafQjfm%^;3Z`_z^22Um(5bh7 z;Y#;`92=ZTbK`U{B1f8up5??Ne44_(;9d5lvcE4x53B_#f=w&at09?5vQIct^@T~A zWC?H@9#`1$vAmPv83KG&Zbs)oh8yA2Eco&q{tlf@v&b-Yp6g!DzXVrpZ~o;Odp?+b zG@9MFpUn7}8OCX(Hc*8HKgd|1Nl#GC)cJmc0dDwMGstjHw2_?MM{ZB#3*ol#aA3$$ zzcu;K&(rzkXevsCo`O}Y9;>#b1h4I9@-z9VdFQDu5)GA4R1*R<@g_QJjU<7|xYHOS zaSpuJ7M*k0+Jc>#b~(sljm*v5zjVxpgZN zJ%!mDh$o2b4bohM)$#o2m+$^+y*z(+?RS~%)125%aw38)Dk7mc(ZPY{=jCYO&zF8M z4TCyc7}BVhqvU%6yuL%4B-`!3^8t2uj05*&=js1v?_GG?IFfc@ z{giy~0*mZ>_5(D$lgB53Y14@}k?lxwqU_?ufFg$$EsA6~q-^D2|ND8Ws=MdX-NPAP zWI5Ok*5YmYQe9PD_qtfG27?AsCfO8Ad*K3Q%9|~Q<6{32{X%SZ8OP3!mOHqQ8Z@E} zo)eiU>!Ok^`=FV1xpm>b8-@7ip7l9-)SicJp~ln4L+l2&BHtzNCB^U&>9C9Il0n70 zIg1;to!Y`=do}zN-bW)~5_Iy``S8;((a5`b3Vm3_ggTwKF>nVKBG^*NPJqbL<#04z zlf$!r5uW5f6A_669b|xBPmf@P0>z7zQl}RFy=UQdv*`K(R{WFL@mT7zfu!fEh1KV( zXCTpkodZn)T!fckNzNO`5kcm_R>y(WhvP9V1An$oeic)MjY}HPAc68x75oWRTCUl2 zfMr{iv*5{v zs{-!>EwQ;G|Au1H6pIPxn0(eer*EbRE(O&EZGNY>_!>^LZoG^}p(mi!d?(@0f+tr@ z;!adysFqm^oecTZsz$h)3)4DK!cu7>SBaVnfDGJ_<{yZd3%h^?+W;mfSR>HzeZBWtGDOV@7og z+<*Ol0Q;g(&%R~TmlMZ|@GtAZ+L2zWtvsk<>HVOym1@4gmIx24?dFO%Q!gT{efCkA z?5eH)4Pg<{-eo1)t2W6iOp+l$T=yvg+!Z{<@?<%2pxQ>h$3_yN-oJ-&EuE6KP&En)vR4sKygBxn-%Z&S(E84+8n-sP+$)ee)mzR;wTPAZv7z;hE* z$uU5{Rw`Pjk9#CW6iA(e`Mt8CfFztx0cBhF!&SJ5m6=`MRJXBrZWeb@IG(PQ!sCRc zi|2URp~Iv$XicUw*n61xn2q+|GU=rp5oRIqjlK;}==W zdN>}ghe2nuTHJ+XU&n>AX3a05=k2L_L7Ve4!x;=CVSIy#>2Yg0jN?z^jVoT-o~$@S zHa#vydD6f-#6?l1)0)?zY;@9T53?87R`d+jH=xZL&ePeO%BiX&?Hah(Ad+mq!$1Aa zH^(2LCRhVh$UQdn9-cf(wSTj%3I^-5 z!c%F5&5QML&3Si@BeCjSs8J<9#K{m?gzA7h)-_XUL}fyUO}=e)-)A zrr@m@5zAuB?tMVcO6{ZL+Q+4L$!r})oDp+*2efUV$WH;4ZDmpj&gU1-oKt+GqUN2j zKxW{-Hl6=bOkS_9(Rp9Lf`K7d!BJz&I~pvY3q#L`%2ieLt9o9bHJOIl75P zx0mNP{n{s`ius5%=;-t!`U?`7ujA9fse(9CGRRkmLPU%CdbOB^RDdN2UdX#wc=Hje ze+@PX$~K_&D7Xn`X_W5u0MT9#XiyB+0^|gCQrMpSLPi(wpkOiLKolm+D-8*hGayh{ z4EWi%f8BI&j+>5-Bo=051H*dM&C@N^r9qrSyYMc%qAsA)Ip`%)-vFFI14C zav)BAlHzy7`P-qo3N$2O;3?jDccl-sGD5IE6TAse%n-OWl4E2)#j|6Vw&1p*ZG3nyl=|7Z13JhbT7A957jSrqq2K*EVrw1!fldUml9CaEr${Mfn2FH8eMWsP@U{83$HEmIMdi! z={z)>DaACuN=~hJm}R6Hh~*WwEn>fsn6s#%rInH>LPsB^j-C$}<_xUUh)@X%p)|v5 zb!p~Ms2{uoqP#O`GXmfHB+@#PAqZ_C9iaeZMhA4#X7RO}fu9EIpBHVu%iI}x3~}nd z#fokAHdP`wjpNiC6PBRwy=c229{ehTIDVSOqJE7cy!6eBFlMJ9PWX6-5ND$S#QoqF zxb#XhGKAw=sD~fmM~Y+B`yqZKDp6ly9^u|%JML+GDkUeC`jHb|Gavp(V%AVEMaBC| z%qPl|QEO{YMvZDJdnf?-*vhd-2VW*^t3xrOznVyou2D0JH-X_))<{c3WLm4hR^#uK z4Mne72bS%?T5@DrZ~-N3g{}mlTO#~vmhcr7<9OuuKtY3T+bq639tFcRew{Wneg%Uq z_@!Kc0x`>9lt(VId=?If6s4v(6%7|HGqlh}E6~fnVm9u6zhRT^$WlpL3 zvy^Y4ShyXk*lo5|Ql-)R2lhTGy?L$5o_uTtJkpXl4O{kkHLuQUztw^#_v?cEOVlRjxCQi8waHi>(%twU@&O z${$ZAw)lT45X!`p=<)!%P^4J{m(JtT@GS|*vE>>Ssq^fwVrpVOf<_r?6Q~-fUI{f{ z#h>}IvD2)Xn1zb#&sUU^F8|px3fV6g(|9p&f|Ev{n`iVdKSCD>lEW!k>jri*J%$Aa z&c=mJKbkaO^1uAL4~G@MkV_#(31;fpLNoPxJXNp9bLn(8Ox5dQE_TW}gM=ZGJ3m*n z@8qVgf+Cdw5}fiXzo$+Q{GcnzDg`I}rvCB0qf_)ys<-a}xR?8TXoeV8IS&71nckBB(p247ONGzgxL zMo}Cy6%Np?kA882{(RcCM*J8BG!J%j!WiKlgOtpl6`G(l`%UlHU@2~ZLv-<(mFWB~nCpfB|&b638lnra@`sj%@$L)CoP&31boyeba^W*ySzUy}izd-p zeS&&K<=e<&asCM!(k%o3Iwe*&$ks66vM7{4%EeXkSWZ6?wW^|;n5toP?@`C3&aAAF z>pp+4D$3IaEes@W$~6Aqf^$Cw`U-bzuQ}_;PjChgOe>xZgCkW`5_%Y*Rl5(-!&eGS zzebYC<#|8&jOV0;UC^gYPbwOZT6}0)t2EJ!5e9L`sdCfobIJDPDST9Q>w#&_S)khf z5X(-go+{)?3Y;09;li0mzKMhG1ih-_n`kVRLs^+SY)yN|1dgj$tfEH;O-W-=5m+kQ zg`9&CkLDV?E+j}@glPklb0Ae_8l1>Up-!_;Gzi*BqLor<9bDP^P3lHE4&#%G2*mlM zzCl9K(wHK}`eN!fAPhdwYIbM5x<DjoM(=DT90Dt55A{YhgcKVfSOt8^}76zE) z7cT2{!2mp#TMIN^qWV9-B9NAq5Lp{{!_K{K&x(JvRy?4JOo__J_$5lTxu?&k?1dk5pKootK(EE|bW!7| zcxsrw;5LU;t989W58Su!FWg&0Pl7%)L!JxN03|#8!Al~ zy&j(hj7b@wllY657t$;dYN z5JJ;%)|$fwdmlBU;fH7i6oPcszCgAs-YSH$RNxfHp;;&tDa)Y_iHWuAJSLK3h)@cD zY`f@_If+6MDJ3p5tRB@jbD9HaKX^e_7m@s*4EzL>%XS+RD59iu*1IA^;PJp`?m}A? z+`=HU&iBDUt3Uk!u~tQzZV9HzhGkZ7|IBn8P|pf#)kLks!x+i_imEf8eXS&(asK)3 z9^0{DK*4*7c+cuv^*btMJhk|FDh7j8Yq%k1>&ItHySR*7(Pz|;#|;DxS6+Tyu47-g zmB9+Q5fC3hMcpWGAL(_@TiyH|moe<@7yOK79oBKvui0+WWHrCbufxfmDu{k8cb;HW z-tWnuei@;)9xUOaS%;)xZYC4tHcuy7WehiQG(LqwwGKDiT(M8)%CrU8EgLK=$-+{t z_(wcsCyIqrsW>0*clzb;Jq+&;195)pygpNw1Te4o$-wJV5xTm}>f{Z}KsOY}TY`ME?h^OD+ zE#{7Fd~4+h$*)Z_p}xt?2&q%@lew$}iUlapUCfa~KnO_KTMIjH@%|@yOOKJx>@i=- zsJ7Y%9@LSmJ{ouNfTQ(Mv1b4jxekDslduY7|87U>;>lMJR`1QezZ8ZQD*=m@lisQ*ehnmgo4I=WW z3kJBIq#3-b+js>D`Z##i_TW`h>A_@fB%I>BKB_epq!eVjKKiveD*6#dA}Rb2u8fg; z^ihDUtpNg9}RWruPX4TADaqdp4Bi~tCh z;TqY!Q$VSL1?@d!j8j{6^X+6;$cr+&@|+s>#D!(BO1KZ%SGN~i`Qm#>;iW+SIE?43 zK~{@frE#)=U40D)$wJu{ex~S;e|i<85?)C&9%z<+>I`!u#=v~TZN}R&MMpDI!(m}& z+{$6`8xcZfRXz_Rf^OI70}-$1!@Ed2IqP=}0v$hYvqs(K5UTDlCEw$7~_;k16f( zkVxLPAY2B#CVl-X{@raHO!*fm3Y`558m;ppda90rX}v7JRd*p!aItC*=wB*MPXFN> z3cKpk^0{bpP@|Rvm}olQ3}>(V4@_|Q8EQOn%Yxt(x$S`e^m?Z1tlQv0*Yd?|2JZrL z(BhzZxq85W(ds^uxd%a=s|BpoEd8I+?1UR(s1M|>vx_YtE5M*YYKC4fH-pa2p}T}P z-RtqpX#%v*hk_>2<(-MHxw?~vFch)GhBzO=W}i@Cb;vk&belSX^MY>c9H~B;f2-1_ z86w=49rdh{^1!Vf#4G_=!u3Ql2mO7PD#)FO0jzYs;mX4YiQW)XBqFUOfYc*9KWA?1Hh=* zU#>7ZXi_B3GFshD3H+m(@ zkD{Hdnn>B1vPnM>+(wSsTt-ce`R^j#!u{kw@2Y@O3`s*keBao<9S~Hf~KGjQP6sN z7sV*ef@TVcxX`H-+n%j8a@3a}fea{~>PjFf+|jMDK{V1>ETcIW;Q(E6Z-B--U_4V9 z9cK~YXxi-37$IG@DNgC8HT_6qv{+I$WVs1yZJq&+;o*GD5r8Sa!=s$V)C!OOE1p-$ zv2U?p$zqQi#6fwCE0#RnW7HOMg){6h$TcyhAV`fPBR5i?3_h=A@K@tA^yX6QB!_L` zN~en;xCKv-z#IXjCa%N{Wm!N}lYweY`aDD@w+LOn<{u(R*7NZcy}$H6Mmx;`Vx&o4 zES6KWlv9h8ol1M>(c~`Zyg-5TXiTU>o1%te)Y}-GY%UB>OX*8;I#DHuz;?iLfi-cS_~1ud%25~i5J3HHImsfHS55DSzE*M);_^@iPu zLyoWpx#2qWiXw6nWGhCu_J?SWjs>Hj%|eL|<{9CnH5AIM+qGnBsSCu1BitIg%CQ@V zN#?>Yz!7EuRI8BwDWuoLsau^~`? z_d!je0N-Hy!h$-S^#gWVGG6hZN8Kz*v5KYbim4h{R%DjjObAs7tWGjcL%gZ7JH{+Pn$ZE@VSoLi|A4J!J-_Qcuo-_Z)ZIrK9vlx>_+)qmTn` zJ0!CqlB#Jt)&nV%8p6BeepK8J(4#YFlaaEMQ8?Cv6p-G+PY!c+BYOyiXZtk>4Acp9 z(`xvX8dOE3FM)vzBxsSTi`a|)R~N0#bSmIZ!HMGS;iA=8uAuZ)ykz7lg0^MAbl+yQ z3l!^JK3wQljz9`==A{otFx;|V)?o^4T|?F$-PovnB422eb^K>Y!xl zPm7-!oRjff2kKps!yZ>9cddcrz>t}i$+J> zzt&}$OaEB-6UUcrn*|&nxs+-WQ-{doj7y3T!5xRHU(6ODM)3YRR3(;Hywmn%e=wTx z2Gvz7Q5Je!r}&rIHt%~Im+hi^`#4N9&aF=vlcZs_S8+qhTK3&X46LQ9oL@hzBQh)^ zuK9vG7ZYq;7aI~=+ApUu(iHV+OdV)C@efezff95(C$6>RRy79bI^$ zTX46+suPe66WX|Ar`oxB&gNBnYdmMF<2SIg`5g7$CfMj7p_pM?C;^T_ZT)_DBCn|Y z4Of`T^lHz2rkMO$46UhXw`eCy#@?U?kIQr!E-};eGcA`~v-A!0&jB@MNPqwAuZ|@s zGUW77IU3i^;zN}dEW4P3Rgts!V@Xtyy878?w`>?jZ`xM!3kKtlkk`v)%k)9{anAESpR7pCXYstnfrK-Hyb$QHy% z#0*p(ftN!wO&a`6d56&yihM_8(rmLr!`vG>ECNufg#Y4P~?gm8DNWQ@5MALXB z++G*8)qi;ar8cQcLI%*Z_E_t!)w$@)YWPX~pu8ArH%UA2e6{12Jd~dWEieeF2A5nj zLF&<9X(0m~W5yldA>8`{8s( zG!M`G3aA)ieq|F^%r}4YxQ$pgos$=b^a{3m2MDx)*&7!VjQM=vS1Q-iF+4z%Gm%vu z{fzU+ToScE1P^C`^n~~(1UNt0h^*c47s~(({E&WYb(1*O& z>v?|aLT_m-A#SjLl*Dgn7q4+tH;4_8{}4-1e0XJ;h0f@-LP*;q83F(d@o*RCcigyrKI`kz5q#Zwj@N5 zAI}i^>%TUg|Iq|}AU@%Q*mt@;U&plPtNmIMeCRwXV58f2Ur-s`*~*#;K~yB`@uP4S%%|#xQHUOb?#GN zy?z~SAUSyFxqQ)oK+r6F_+j!x)g2*!RY{9k>Qk-S<`!H%Z+5zSoH(WN7Mq2DB)jeP z)OMe3VkBDQ0ui}N1S+|PMZ6-Fu-7A&FRzka8o>)}F9!gN4|=cI1MbsoB6s7;&Wd(# zxOAu;6>f#8P)12(iV~p^!;})44vO^T``&_u{ouVsy`?boQO<$c{+D9}+jx8WpitiP ziLbUkhrt%B#Ggkyy!|c@s%XM@LHVGHbM`CnZC{7USi&m0@7I$KC*usd(>Pq*Qk?MK zR~rldREyCJ$+hP-1H|km5y;G6LK4r3*ES>8P25z-sl&5CJ4e68HCXK&pa6l_Ucl9J z5e-)e&V^SBA9eJ*;FRv&@NT#s-7p59`88A;m_>Avr-*MrM}JLb-}!Jh8n6m!Yw0}f z75AKTvcuf4q7jZN%2>{6p^+4@4>PYd<2G5vhybXXL{_?c)NH4&HBG>GbPFUTxoJP- zXKh68e;gP@45Z8jH6+BXSBqIVK{7xPypVUV@P^8sq4ugJ4-gj0T`Y>|2#A3DEtRW< z!-KB38cx^H`RJ$CMLnkoAKI9G8^hPL+7qd53`YqjO+{P)J-um!=_Z@zlrvTB>{v(5 z&4>kmcDzi-CMS^| zh*R^Hqfjx`CCs#RKNi_2PJ81!osB4|Y*qEWwS|eWEMg7_px<#=< zS7=8(ppuht*i6?>Hc){5A^}*`otzG&zvsWHTwv$V#A)>94r-eK&j&y=k#*S7%5?H}`{0sNOw6ZaZjOY_65HV>Jq3qTnr<+0a7LfK=Z_MEbH`7Y2YQPPz|q*TPWfVIuzN2J>`zU4xLr7wpH9Wz2}n>@ z-@OlCynX-XGQ40i-4D^tXhQP7z&bL_KLn?v#Rk>k7V~J=SQO1+0>!s$WxbpTDI}Bk ztM9{ebP^SkaB>z2=#*Yj?i z=_xUvz6B`kyt59KPgvhX^N(rU6~g$oFLJ=)it2HR?xUP9nocVk`NBxZB_oefS?dvQ zcgy^wE~MQJmH}HVQN?vM!(#3u59hmzfAC%~!1%ODv>_U>9`GPmOft9#?{Er>JbeeX z7M5T%F(nVm9alf&SA5v$%A^+>IF?9>)19AC_c6vZ%J2&8^@q_Ax9|c>hdx>nPG${^ zeodZGun`|@Rn}-u#eUaC$DvK%81)A?*HV_7N74L+vH8_^C$aX6T9p~-T837NIi~g#(va_OHNP7SCjor zevRFSX*3(7s1XHrvs|I6*aMj+5|&X~5cJXlqodbIFJKaK5Ju84kMXq=cH`yn)BFbQ zP=nJ8dP$v8V^9N?FeDX{?n_dRQQr+U!T8tQryNz!ws%xyflCM5nVEXMWG0AIz8~f0 zv#v=0ld0kPu%*n24;*r(aI+X6BI$!QTgU)Q8oVGSfcv}nwv$_wa-rD>Vb5hlUbSVD z1U1LLQtSe0;%}E^2URFx$7gevN0PEwVnwipD@+l2oMM_sGw2}A7yK{3?gtB$=%DvH z1lwf#WpgA9P*Zqe67jQP+nVWUrPxCf2~zXLl0GatVF$qM<*Qp@!xxMPXo5Mrg0A#Z zNB|W;ZpAGR+|*_&nB-WeDB^ME)KbVGNB}5^qMM zD2^8^QF2ucAjdhfz}Qxg1*ge#bX3UaI=wI>?Xp8*hSxSTq=*T%dxRKzL$pODg*T5? z((MrNv$$gBm;_SnDO`6$7cdV4##bINzUbFr={rpzx=mVV-!ns0af^~2$El4A``O=e zlkWcmcl$U0O9lDeLAIdifeXkyfAdh4QU4%=BfO7GUO_WVkPeRND?R-38Zt7PNN zVA+yG*w8}yDAC%I6KK!@41R*;M^P}>u6u=wi)4Fx!wVr5Pt7OK+G=B3VXRM%ht!Rg z7pa>c1gQsI1IT{kUX^Mlu1rEW#+1fd%!0=>BHg!@nKeS8`Y6~&YYBL zn7q`dIO1SKW4rNOKWI~v$00-y45;ED<4cA${z*Bfk$FrK-eU__YCg9HC`J({nr8$I zZJpag-6m0#I9AG%hj$kg5oWaD#IDDMDY0g_QF5^SG^r>~lLcYju)l7!M2dk<0 z%Ixlz4;?fHJ7n5E>Y*{%C2}Moraa22wuD3%0LaN}7qP#Jea85tPOjGEWbJ&MF|4@sX}B#mF! zF2lwJ8maYb=OO8FuThZk^1L4)v}n*EmrFlDi6?rIXO>!3zT|o*V3XgDALdFu=iqr= z%>&NZ`PPS(j<0a~kywMteb!82Mt zOuG!Mv>3Gw(>(YQIOM9Jm5Eb>zuHF3ll)IK$v)C4nhQlCPf|mK!ak@7yiXieWO@`} z+85}c6fnsd%dFT&1KryTh5yWw8wWgp++n#pAHAzGhr8|YANl0Td;kHR3bSji| zU##}lULQ~It-p5&vt-wP(Qo`i>+bd6C{8Q8{zpDkH-pQXxBsMSZW#lin7`$Wh@&6N z=zU{TJx#}MD_92yj3oVYE$sod@?a!GsYn@7hECEYoNJaVJDAot}2XB1K89-t1n zKz;M10O*0AmN*|1^ZbBI1zZ&!HW)8j)TCPicTh4WoNs0nhJ|kr>CbR@LLEo0=htVR zL1M!b9!XzPQnT*DXX_c7YHUDKrp@7DcxPX%4j5wUCj&lGET10Fq0?~yjx|b(-*gep z7ohOq6ox*PuH{&6@})lRN1_WuhZ*Wr5~vB*_L$}JF>TEkH`CeJxFAq}-l?Uw$I)bn z7RK$D>aTh)imi|+J?NQr$KcY-kJm`m$;6N{V;qLv<#0Y7-BRD%fM24g)n>j9+K33I zM6xhO#Sd;hdwM-UI=X41u6%BzXf{JW4*q@itxB@EUMyy&yrUVu*GmnL>L2;x9pR)j zP=Uq;ZCS4%07vV0Au6F)t3T!4=O9*Co(2y(WaR=tcEAV@u{nu}C&O8Stg zo;1Mdyp1n7t$Bdr8#AntOyV$N(tqfeqI=Y;H3csro#hq<2O|2a$ioWalWDIf9XxtH zJ-+hAE)d2irH`>oa0*KC_{%BRPX1&u4n~?}JzGJBqE-3TA)teJoyebV?2}>%WDhV2 zAe8_YpaA)&)o>XuR$(-Ysg6?t1kapnWEf_LIVdbmt=+Az!rfqyLLMjXv~k)Ynk(&v zENToVnpCC>8=106o~~0>TRY2;h29#44pBV0>l7({lnUc>_Y*fIxHqT6jeQ82rq7^8 zW89!J3_+TBjq+}}F5C2jsU1l6DFL%&pO?l*bnrmFluBFOQnA0iTrDWvoJ;nr4sU30 zx?V1)QV_p5d5DrxiFpJqJj0@l1Q$-m9sL2bSRb?~Bgj7nKYA4ha%0=Nm8u0X1EYYF z%3+0<3t^rr(otH@Kmt=k^*xe%Y9utc)`N7-<2G)dnD3rV>ovJdX_pRvVS}o*weO5>n% zMmMX)VqNvA*-mra-A<;Hg`NuuxxahFY{;w7(dcrAm|Y}VX1KpF5D4*k*^(_N7%X4! zM&evCJ`2Yb&_P4sXS!imaQb>a!P!pHWuzZ`M2JRE&j{2kL?QSdt+~KAK^ovfKUDYEA>wO5c6JzRfwWxBO993b;uPd`Dn^Y7>(s^z+6Dwc* zrTm%(JJ)zI&;svq!zy3QrNXK_&?$Eba~2vePw;^OTONrVg^1dB!`o;a-K!%y^N@5R zuN%!-6C6P(Z*OrbJA?LQMj8HdBm|tOmjwd1Rz&pntT`Z9)T8;m$Te`Y;I&~+9+5lY zOh)PD#5sEh%$m5=3s}mJVZfKh@*Q{9Qx%Gh(3z1yt@0#F^c1E-S4(;np~%T@e}5h#CF3{3#CW7-iartTy!S5iYu z{KY&58bIGIk%>EB4^*rKe;k~e&*K4Wpt>~&lwCnI*^T*G=4i~PvDuC$%B`!k``2{P z5ig8}qZ^J2B)EIdEZGcKC?*J!BOE)zzxqHKTF|L=)_{UWKbV9m5a59<3;+dOdzpf! zsY5jz&VWIz1C2V&1LD`VRKH4~Bv=A5ek*`Ldd-5MJHVSud72{6--mhlLlkjAx=DbR zdLh&#Q{eog1?O7Rz!?S$^k9eDi7-Si0qTNiT&-}k#ub!sgSfH-zz=01YKI1%VEi^#hz_;y#5S=zD{Q53T7v+Pfu@{hg;euU zg{Y)e{ z%GxHf>r}43?a5pcMvG}wUv^`FQBgmZ3X$?>AU;Qw&hzItA3I?1OcKrEYBhWa+Lx;b zN=)U})*{HkCbpb9YeC8v&(I3$e}9v7gW`dDJ!>Eq8$dC{!5lUsSpY9du9-wp)r+E{ z)6%g3E41YW=^22pxX!VuS>&l2rL|xTiuvf0!-~-YV!=z0d8q5FXCC_Mnd*Y203aLG zLXrI(;cjb%+H{M%CYLAe!a^wTlUJ;02$0-h;$~aT>3keXvv#b#%VEY%4D})Ep3}c{ zgdaJH9xcR9`7mG8698bBA0>~uy+SoPR_T}sUhQ8ww8j=2_gs1O7yC*3N<*VP0E zK|p0yr$cgKj&Vt#=dXFI^e|Nk@PL!nfdVua{@8PXca+~*J-0Jl8LCd=AZU^)8Ui99uL<2-N0rr?baZ_XyT7~9$$QCPJ-vGZj(bYL~ zK=mEA=n9DlQWAT#o2tC2gR7B%N7(LrsL=??b}>x(g6qn-ty*2Va4tawVh4F-DUJkb|1^ckWc4tpBLtX?LHHxgVOy+*dax*zs$`h+ z-fQn5rYO2wt{-Rv`t`wfvFq|`D5HLjJl7c)Ga0VYE&OJ@LVVSHg33LrpF0>|#5a3I zSXDpi0O3fh7|}qRISP`02r1Sk?w;YF0opx13@9g;WeD;t=-1_d=opv_*T~347;+Vp zkn6f9bhILJYmot`&*1RemXRF{fXs|d&P#5-q*P^jP08<)tjsjLjY#%^Ox5z=$A%HO z@7AU?TvmES~_P|0@N z5Y@%15UZJIRhAU^-HG8K!p1bKjj&b@@QdMy~=U&ue+$(W7Mg;})lCUde!AwsDRkjhRFCNpiP76ap*l zW2~h2m0!QD`%xgVa2-g)F8oVWLeL(8kP*;eDwUUAujU7MLyZiUPPiyB8G2{@gE z;nf{?p_sil<02i{6g;F;Z9^%2pYR}Avzt*Cap&J4%(hxhK(hpr_utebZbh+)!eyK8JT2Xd6X^Y0YO9Q`WMlM zJBnPetD8_*Os0GYOqt~*knGS!x?yJuoM+J*cQqBk8BKQN#S}@X2BtdY^f=79;W)Va+j=R&3jWf%7?Ia z7vL6l8hf~t38`ME5M()glj_>`14y#w-7cqR*gi(<^}GcX*qozVb5lamj?88+9g;+L z0)eeMDjcK(@llh%Bye(!donZeJV|^$x#kE?>9W7oajyRuIQMHsl&h(vrn2>b4+&ox z%LhnRXI96Dl_>!n{8mr``1CQr|GEVD$&e}$WJ=Q|Jhvp5Vg@#Pgw5#VsYXa!Ii3s) zo1r>jJJ3)cIfKW?0>u2=uk}Mq4jJ@oXe3;z6z~&giAMgpk4C*74Bxxm4q&+pJg@$! zDtW|ip@r^lOFe)*tYW5c+i$m?E@opD6p0I>UmIpgz5_ioq#(xg{?BZeBXqOH>}ry& za+mh&o2}%{QGaO2RBPnoHT!jV>|T(%zqK-3^A~n*&=Z58A#rYq1K8-E4z>3om6z`$ zthVtIwlIp_gn79BT!n8!^TjDk$d~G=i%xBa*BvJ;rN-wK zfp2v==G{!!Ua=!9J4uamk&&q6e6L6Mzq~3H+PZCZ`E4nhWcrrq&05XGE9=9zLBA6S zU2Gt`{oK(5*^~w1iSxYT0 z*Fd;evukrNgpvraszGB;Ld~lZi-c`FZCteZ>3?_%H;J_x*gWlXOjybutvmkI%*61H zk_@8pvx?nt3h#M=N98||M6Y>Nx$YiM9Idh<{eP}}UF!$3q!k<`NkkrNk-G4=i`56V zP394GcV)iz;SscXbRpiVnsfP>tiEwLMe|Q0VX~ori-9~-R2O)GXz_Hk+P>;2CkenT zj()(V=3p4D(64<6+&iaAelvHHr-n4Q+Ha&d|GWoFtnViD5Wv4(_)uR5yW&mQ+ zgl#&>!AT?OoP65|ZKPV0?S2SLVo4FX_8>YmCBu6V7zPS&yTj-)5EZHU+XV_3Ci}ts zaLD=}`xvFVnZlSm3CltmFpK-CB7<3pNjG!H+(Hf+pr1Z0qV z{ZCoa@7IuvQ10q#N>&0r;UZgvGNdhxLyJoLzl1R3Q&lH6>Gz=+WZ3+F4DE|}vx-8L z#WD3ycD(ie!$|gIBrMUD?yj{fxtlyRsh@`oXv#+9T6%b0NfPx7(d3gW*CcH3ZE*QR zy@dlVzfvjsy?255rR8umMPeP|`}FGr?q*Bwm-M38dl%^{^c10-Dm!0A)AZvN381df zj{TK0Ll(O!Wr}tQf{w@&T3#E}MG>=a_l0tOI381EqDuaDOxHy*SCoB5?qN_Lp$Zl% zsjR}g#eMW#=6(^^UX6dB&f?bRmjhHizC$+rAK~KjbR1F6B z9HKbl$9nkV$6B*<8oCM$X6AYJuhfv7+(cu-=7>Riz4V~}`-1D0=mDUV zPDRx5eY9E!rySBZo_?6F*>4K{x*JG|w7)OcKHp0ZC)qQj#M&)+5_HUl} zDjrcyTKAbFtFeAPU7vGd4VbGgZdS8FJ-%6)Q35lHWT!Zel5!(y6nhYgj=hSh=4s*d zH?WnV8ef09em)uyw&7bXL*mKD>){h2{!nW6^mdDKlC2G*4yP=Qvpd$2@?MGIECQ zz2^HYoGVtA#Yk3VSu)2mF9Uy(<_+lvAKJX{%27$G^#i^Xm^?;6O|m8Z=rn(5J5-{2 z`DwVCoTGHewl)cL+~{x;t>VQT^<&l#%`rX7ryK;SVS*ah`q<2(>!8*FEl~_t5fZsa zt>fxSjoP3=@3`JMy?O>x&)}#{3{@8#A7Q~asReVfro+Kvof=vgo{wvje8S)(&q^)q zzNhn}vfS%|>#k?;3Rl2`y`K5`vkm4UpchRP@JwLsW)AygP6R>pPk3DBxxTBOcF;|- zlGgc298(+;?cxN~(;Yz_R|GZ9Qx(*)?=+Lsc&650Isq(xWKg608_N4I3a0DWxnw6F zR?n(}8l0pmsApwBo#9ZP9i*^&Rsz(~4bzEmQ<4l_mvT^(M)On5{4`V~e~{mv3)gz5nqA)vXf76k#%5ftz@>!DwqsH5|Gq!XuKiPVwDjSPs0&YA8R}m>x8Ub^@SUxCeYXc~6WEdcm)%FZdUgBUr9* zQ{{=cP!r~$LQVUEgKqUw_>}9iSqh(YCrrIVG=sw752%Qn{z^hYB{H?9tjmQhBcsM%CJ`U5LIen1_kI~2}0#e zwHh?vm!?#0Rqlh)>DPOc(h(Odwq&)~Lo8It*D??g&KP^31}hUS8Tah_;{>0UUd}d2 zMS@uLtU_*%DJ3waEOu?1)GqPB!UGCCOwLrD)KXHzx-9O2{h?o!RqsK1Y09hz78*1$ z9Z6~k0+NE1A(99x_13{A^=gjG{IZK80V2JsvdB}!nvNqwWY?|ah)NAMxJ;%CWrIMa zNy69_2hGdX!?_rbrH-@+=2URj*IdIvi>lWKC2A@DotlG-OsD72M}I6tgh?+N#~~V@ zeSkzxsF?Y{gN$O=a40z`YpE&6x+`@3y#ZZfIop5AszOKyLKO_#R5kkBMo<>mTPQ-L z!BcL>)(~B)w>w{;ED(;W$iSM&TIFUkupOXE-hs!4U1&Ha$fkz&0nc=_Xu|dkPcdkD zJwd1RF-(3flpD@|s9Q^9LsDJD4Yy7Z+`{@3&Cq#$`5D|#%fD>#i}n&=D2q4Y8Yb9O2^Y(qmVf~15`?8AR&s1QsE_E%kP!0^;D_`O=}gcGm|W$}S99pbm+0X<;PO_U zZY5l)71r)&hr)KL*}D3hK0wkVJO@ZhOtM}Q#NwZ2&k(!vdj$TsUd^fT2=A%O3#Yy$ z4%T44dq(wRS5SNowL99C_gGTJR+Eym5!aFBL!wX`VO6R$KQII z&=A3I(x>in5P>~VKL{fw@5&bcO5Tq{LX3=VK7F!zz4#oB)$P$=P|$7wY9iK_>^~X6cA`~74Tj|aRrk@gJI%#0RVU+Y%^`k>U6H*(@D3-Fo=%l@u`V8|@5|)guwZaCqGqcx4z2P#m z$e`KJh~Y_0zu<{`j*f2*s}(1afudD62Zaqak#vPR^Mzyi(`sj2`g9fH+hvVmCqPyC zErn*4I7jh08Ugr~U~#W!a4d@eM&hvodkn=mE}N8v#dKKtBA}@)ZrLikxYf4T!vTOV z+^d-4F!Tz?m1bUYoFVADDFTYB6w&AD$$IX%+EBK4GhEc`oRKgRzH5m*N|aZ#9RbT zb!WFKoEYRy-SQOL`Zss?8SCA z2WwYeXl@5D8gZmq&>AjrMUKf}jXMQeGFkUD6!ttN-me^V2F+6Y#X-J+$2PZy+%fXQ zhV7FLSb@vF(02xx(4OA77ijw+ErY{BbqP2~@{#C1LWUd^80$*igNlz7kZ4o15&4Bf z7-3mQ9UtRKgM>OnJ^}C`+x|c;6R&f3u)1legJ;3~4lo{wg^}4Q-rPmZ!PINV5;4_jUtAnLd$z0H?xqQRW1K*ZUuV8pD4Ei3yMN`37yYL7U@O{W^lw z>7!>lHl1&EbxbDUjzaC!XX=kChMM>+N!BCsf>A2R47V}4N5KMfowzW-RyO#8ujeph zADsAD8Yg}^08Ypl1vr7U>Bv@NuwVErfeqmY!u(#3@PvurPmdl&_~A`^(wYHc9U*(L zB|WIKjT%O5xFm+N5fYpl$ZofVYSdd!IwYt_iWi7Zs{Z7OSl~0jxT?eb^kbXo1E|Ae-*>MB!B^#Qqe| zvQ_Le!(1PybYJqj0lm>Bqr2of45?6sV>RYG#mCa4oj3+#dHw)L zRbt&i(@;Cs4o2o!e(o0~4)t-?-T{VrL9V_kvDmA)?L`xGSQNGGJOZM$!&JM)xywt$ zo}8U_pT#&wttx#HsKWSk0o8}|hvw)e8r@=fTSJvv66eKHMePwH*WG|eH5k93=&r$gzumRg>JoC%)$wRn1bMiynBT=6wzOU=|r!r5Z2XdC1Ybq&ZnK( z_l&rs*glpDU(;b-YGb`#Ze!LssVfQIi}WTbn1Q!Uo8*AHZ~vwp7DqW-I>&>ZUq3(* zKMc6#H1>xdaj63#orJ`RI-2ryTY7Kt{dXL4=m*QJJ>XFIC*tf`4BeaVsJump&H2o9 z8t;bd8)W;hhpTnWe#7(rCsdJwXD;;_xxk?QBnEe-9+aM-Mz+XhxJ1M2>F3l8$%oD< zR6v1k(s7in2&5C%W zF#*He4U3+b2++|p2Z*W`-oml~waGZCBf&AuFQa$_9T0*l^qXsBx8B0sFBtC@Okvjv z0#0>-Ft(0ju8`wvztdVGw0JW9SaWLYsZpN&D_x94p8DV4)c!}bk)j-SlND5QA(_S7 zT#YUbL~ck3mP_U*`&I33_;4MmNDU?Y2WRxzdk)?xxs*YRboa=mAIKL)P+@nQXMt+m zvdeHoVe8e|M$x2E6iYyS3#OO5#XJbu9kAG}mz(tgBFVSU;SU^F%QJ!(FHkv(WP!HK z#mB!bm~HE#n0p`Y+MpQ74WBo(OfsOdnHhnkj)S9ks7x$qKJG!}TtqtYue8%!)X@AD zuO#yJXtP=&iaJ1GppFZYPxu-`;84((Xd(GDoDc6Z@;>XZl#O7*0MR^gP5K-%5>_I& zt0WIp(^>>A;zw#ybZea~MCNy3d)7$Bnt|&^m4K@{Lah)Qh;p#C7Z6VnA)VikIE7>> zkhx_$y`Hr>3vuK@7Wa`Dp;0coN0Z1xJA%C`w?}JCeaYrbQp#DtA!P7>E{$|X(jGCJ^eZ0(f@hx)Ng`q{?bJ-v zzo$h{d_jnO;Ab=<5(vAgaTe-mwwOnus$4YzPqWXDnUVFo0^+W`l%Q=5?^uQiK)x*4 zeCTR~Vk6m69Ybb+KwA>I?kXm)gj=YViBbt86f4FITyc3z`PRQ}P^(-7hc*b@45Z(* zVcCCVI}FY1z}5B>+o4QQw~pB6f-QrRG27|L<0`y49eM6oQso9AtaQd`Z-WY;*MBFy zJs^N&z>JPA0%xBrBF2bnTiO*E-<_m{2xgXvBSmVdu6s4oOeuxsVxx`U@KPav}N_ve0-Q#H-0VymOgfq~;S=XR#9*lg#Sm+9{ro zxr`hT{cBHO0o6M3ayq9vQ@6M#LP&|5C5C75-!*b0ewdG-XHQW>FgS(y56elsnGveg z!X^hAFxJsK88KoIeNWh4j8C`UFtq}{8Hlb19IVFa!U%P$CVWzo-z`;?~6pADDFHYZ25*D;-5-jJ&M8nXPL3FT<@&<}2Pd8Mbi(AIR0vb?AVhLy@o ztt?iTx03yPln*9~GcJWDL}8m?9f#JKFUBen5NvTAq4EeljYDkR4Od7?ZNB}4I*Ug+ zNKAut60~=ShLg7|9VjvgPLsN6Y^xIy=|eSa+`h5bcOag}+*so_dWd;P^x~kXkSVvp z)JRv)7{#pR6nniakcq@efJr6bmO>Yp3Wlo@6??|vAy<&`z)jCp3 zRf|-sENWBaTVASAf!zaFV5fyT6$DjUsx$X!D{TD$x8#iX)Cx0PAK%G6b5yQBw^5Zu zhe>dDB?2`ud|p4a=wOr1PvV+o2-7HwFBMY zW~#lPlVbdKPC@iCunVSkPDbeyNLITitTRg;P`jlB*44q9-nWv#Xnhvj_n=dz0t3s+ zZd^(m6I|8;BA)=~5WHi10~@AbHGofzGIUZq6l}^BV^G_6*Z^=ZD~p8JT9BI2Wsta| z5%Ok-?~LUDjLC21q+KE10LEnl8r;-A-LykuF=?j9VH*RfzL2T}F`aWhBw742pKiZS z^hyt**};aRY=i2L%2rZ5`D+3&8M&095ULpiSBH2BS(MT|jT}-BkBkzuURoF~Nmb}w zU=O1ENTnn{9pZfb$IEIt`cJaJ>Y^ear4U{DEz{A_wJ3JVzwV3b}VGEI+3R z6gm*M;y`)}hu|QP>y-6Cn5k0?Hi4mXOt1)Klq!9AU(+!O+VyL=wG`W_aG4l+1*oY7 z!{gi`D9bs8o-idQFvt675|FYTIz= zBN8>HGCU3;Rvh>uT2ejCjHwH0@e-~Q1R9hLrp09)N@ zv3L`z6EE0WVi5l#EbmUSJR)AaW-EAbs!nf0m%=-PX?S}G96Qhv(E;oK_OVk2X&Y-l z_y9-MY>EmD0t0QiLPBTRu+{`e)lSc1G0#`WkEh1wYCrxmK=}z&$tK7=EzajA2g)ckCYK-_IZbXW&^)K+0w1YvD1~F5UJz4#O<^?`sh<~P zITEJ2Jb|YyxcN@0z*~?n?Zv)Qwmk>wZQEI!cXo-=J;&S|sf#LHV9Oc}JV};59I`teIDTyl2j?&tc)N~Jy?;o!vA|42dUwgNI_cXTu_iuv0$PZuXT}P30`mR$4Pqz2beQpHtEFlQCt zYWPlV;tQ}5%Kq(~Dz?AFQ9jW_d#BF~qJ~t2G{;8$xHBanpYRFk#itMHEI!*&8+i)4 zeR3XT|Ity9C5*S^9A~-A4!lV6P0L5!#m$Bzb>eSb4bIEeKyYQ*n zT{KtuTGhS;&VMd5r#_cQHg$fD3Sb%q(kl7xmTuSp+030D?GRUP1vB?<=bu;`|4awD zn>N4r3d}gkDIZ3i|H@4{X-eMToU;`Ho`yN+&^WNaNr#*O(wx*`Io;-|ID`*I8`$C7 zNvDMAWH9x0HCxe?=3O~QkuU&k*HwIFtARp?>K)>iGI5h%++X$e z1!q{ugB(cA%v^5A>#u%AT`K6m88_y zPmGa%9F_5zHYSCrQhWb(bKvc$0%xu1Nfa-4HV}Q0z@$XbtDoZHLpUiax!z1?V-%2$ z!zKK;bA+c%=eMy!QHLOsxNw68d+}e-M>7b(_Xq$H^{sg1LtK^KQ!;DG(Dm8Q=k!J5 zG|_NU3#Ws6Lqij?i07<0V14_~4gq*$uIe5b@sFG@HB5mtXmh1c$}F5J7Ztl3?bvl> za#%k-1Qd-uLdloeWZhkzOiv-?EkRszGI@iHRmFwMF3e2ab^(j>2xpY{r6u;GxUDJG zzIKy~Dh6dDj`E3U7%Ej~zY6+Vy!op1M4g_6>O|>Ox+irm&M<&KVW#RZrYO%oWva@{ z?QU3=A-4U8Pc2h5odQ&W;bl{W+PO?28c6?lu!M+Q<`FfO6j!bjElC4zaFNYqA$Ej- zFP$wT{{Eq3Mvg26e0F3__iHujjJ+MRzFHK7Q$o(DWRMqy*S1&DayCR5H$924F}rpn zYSKPV(a&4^*rbge-RJA!ZN$vvD&+F}XK}2{bP^ZBE19aI-2 zw5U!0NpV5Cixe0&IjCMk)K29qcv6x@zDf!)(zq+*QT8Rdj`dR{J33Ix1{C{)Oj+ff z^2yAqvN}Ri{D2U-0zDf-DIn@qba9o1{#~Xaj#+X_;I}9SzsJRZ^?z^E>CtGBq z^GK|2WV1RyYm4$?Nk8CdO4CM-~Z+G-$!IeMY#;Qx=d9sg$&j{e=w&nuIje`WU^=S{g*uxTo2djLd)uk zxJGAFG*<`qJRSHS1MiRRT+@=jT~QE$O%}?(EQ|8mCHQAT_hTOgFg;UJdGL;hVAv# zxM*|CeV*4w!g0ztB)gk>orm6224J=;OG%^q`Ku4T=IyQF_c{r7*SIcrnCDgV_=E4r z1Nmeg`v)Z`C!T%~42!3KM0}Kc4cPsFuG~DJpal))f>Rz}QRloqnM_-w@P08J*N5Y= zy_*w!sv^emQ?wW7-=+vdCaXfivR62%(zM(KOsK_pxNf6kA|_pFj-o+hudNW$L9pgt zhX`gubM|b5Z~6$XGIIY>4+Y|84a`4BT{5Y4x`N+pl;!bX&wZEGfi=a8f%pu6%;Ww8tqGwyr$#DV1qniB@1cHLpD6DM>o9$hh%CHDDVb zy7x9YF57b4Cc4w&pmL|h!vR%^pz(40SsodGU0&2wEhi1uZO7Y?yT>xzcdYXeDxevD4$`;DKVvRZ6wgQLcPM0i^)$Ws{4+H0jIj( zN60-pc%4V-RmO|`FVSGo5~bI1*7Ai${FEysUat(dQ08Nm2(^>FVCxb)SjjkoI__R1 zf3?FhuQ<8TKf7pleRfS=LGvXoQC>S1Dy#H^jt}>@H}^t6{#EU?9;V6uk6~xB znUB`f#k}=u{&qf!F3+L6q7cIf#~3c~fn(w3yJwU6w|}+W1^zAl?VJDqyKnx=-&cH) z@QCPj8?GY?N*ci7*Pf_aaRUp~29%p|9s$UG-3$c#=e$_!2wf(7JpHT^1pcnJzcb{% zJDE)<(dc0WZmwHaOxKzb5P0%#k(L-0yXHBtGqjd9{WGR(?jyxR2+TYqw3YWHfI5U^ z;|-FI=W1pN<@3(1mVk-T#7$ORo09*9Y%DAfE>lkzY+Fqobd+h89);m~ z#a(`|oItJT4L-l%kK14_Bm6=zPB^x`JTl~}vBmXMU#~z)=={zP|M(GMa0B!SI>~(e zENBTwEIVw6K%Hz?&c}9|!c5-9Js7V;yn1(2J6%jB^d#rJZsugvja-&;b$>XiD&s4v zIArYi@?e3c6`U|sn9AmFHnZ8ypwhZgVMl1zad}ILod(+Mt!NGeOuoj;8^yKp2|9bw zm#y_`I9eE!#*4e>qfr#c zZx$~W5n9qSh8M^`sb%IDh^A(=sLpUaT;gwB|6{tWwF~BgH{cH!x2BJkXv7_I(Bui@ z^?I~ZTEF1qRa`f6yMtmPy~)G#Z`Ee#SN{y#G!yz1+zeFbg*H8g zj#K?#5iGh02UNe)0cCtzv~FEEz}gh1O7?{d!$ULfO~J#H+7yzDoSXVJy(!P~`M%oi z?6)B&;lj@sH?9i_-xLFl2T$7DpU34&b={@KD)?^J}%ybvlr|FoA)vj-zKGj7+o?f)o3)$U%iF?LXcN2Y^#w74fU1f;E2S{ zlSnL?PAH?&lzO&7AIaBa1f_)^W}B#7gCMm~qY~&v*tE~Po=58drA_8@lr|v&HC~Kv zqcvZ)yz$lrf6!JxVMt!Zzk`iAw%=NDg?r&!Eh%@e=cfmpKya_1dpTM{Qiz6kG62-% z+H}#3R;$ITIiP=u&(VLXVM&)d%JW4N)LyL?cbI)T-VA52`(hBp&uA0(XkZtdzMfAY zQBSXDQ9pPCNj#|Y9ggMmGkp~0$$XFkJ(de+w*eS@xeTCQ&n#H;%;^HX|7fMX9xvGU zxxO2Y57Z#QB7d6VBArJo1HB``_2}Lq40Zi)=@JIxGXA`u|(sO)M*)3$9y`C)8hg81L3E5zlAfj?X0o&!e3PEeQT*7`F zt`}POq?9S#-nH5|nhZBH;7Bw~f6pW4F{?I-8?UCbh;!Ug+de<4ws{eMr}26{_vqHi z%Plg2<&=g`XC@bIYsbY?hS-UFVYzj9eno7P;I|FzJFX8IX(%L7WN%7@3D}!Nm{3KC zU_Hv?cn6AMxW0+zA4@IThb!pwvFm;Yafsls8Y-&lrRV?}5u|N#Kv}O9jU?!lN4Oj! z1^+%61_~Q5amYS+HFgIV5FmD@Bqo4xqi+rzNYUd^j0|0o&gHj`P&d3&k*g_0S>)9& zWq*86?=L?}Mv{vv>-W*9{!281o^24VAG8P`3z<;x{T`4~78TV)G9S2KLlxHuJaULLWu_^`vtJ(<4kx^T>xLu<*y|bG`#KMi`3Oj^f(paOSxjNamZCrIF3@ZzE1xjq`}}JJAstZv zT{eX*XkM-!a9c;K`$+WkymnnyXejpyvZE@?-{J!(caK%UdW(3e7 zvoAP(vsk~MPe+L8qk>TorqyHm{>Uo^a+6a&flS!z=^2$4hG}r0sd@hX4Lp4QN7xa{ zUuT}ulqT|l(~w2JN#nF_B4mFS|NQcMD)$UCQuEau*6+*npOWCq{ILhM>*44Y)~Hbw zuHvqF(|omp2W&7{w%%^m%gy?F@KgA@e-XkV9CW~qI_&jD@^C*uGO>R1>|1zqp=Ow2 zFlOo<)Cn3D_xSi>{*7EPSbwjlz0UXnt+@0svbO(oy;#hQgaDu*c8;i_ zznLhuZh1Gf?XAJKIUxtK+)478qoctp z&JnF3@7%hg7T+%4-n@GMlp~?8 zJZQ+e8q}0Obhf}*1_6Sa;HVtI^A#hVEne5*%v5{ht=SYx>!qzh=z$QYSN*E&qU+1W zvj0nXf)kS*NgumGE(3EP3N&F&MLbXlJT}R+;f!_ z3j@PBEmU3|Vy$v279O%p2@8#K;$|S~Npw=i2M6<}!C30o!+fTLae0i$PEdMR1Z~#I zTvI}HWCef;4S2!W5A}Eg3RlGEMx!(WkwdT~+JkMQtTXEu!di&h(2txa)NI^b)f$a< zLNdi{&e&V#5)Ip{;iphh1iBs~e(e*e;x(Ds+(eiqVd#7jqcyH_xJ8-rL`~d2Sxe^9 z2Mq&kIrs^ua~-5B1skn@vy#4jH1t{1cNk&fmpIP5B` z&8Ikr42!UZ4SEbA9`WR5biMgN;SX|%XK~BN4(pSY*?v_7 z$soT&e}K1^o%+ZSqdrCm!d0_8%ULU|{UJO%FE4F@h==5a$DS2lcVVhGWNmv&j;gYV^T;y+B{%Fa-o`VW$eb!d&~u`>dOk1W z?S)McRXj#q#$>TI*T+}=Mh~E52?^qNBW^)3AC49g_@_TD7Pp(_4~tmcA9QD1sK7Cs z#bK7hl#6eC1o)~O-#H5hPjLzbQM{d*VKLjwSh{_0{ z*K46n@nHt7Pt)}c^u^_HG+jTS=#IVRll)IqTyoJldu{|l&iH%J&TNU)eOj0q`8SMI zHhz}c5?ic~NF+f$1GH^GEdf!56?D!nbkBnXxIy_%3FUDT&2q6)qKbPIkb$_n1f9T` ziqXflFY=R>+)v8CXWwNS&H3uf7!!@iup`ntRy|)poJRVMwiIP_2d!)sAv_(=j@uzK z_2G7c!S>n&d2+~e=F%ATksN~a(5@W^A+99P{F*~(4ZJfXO2y{pZ$4#| zu{i=AKluJK8ile#!o$6Afd*hUi7^Mf>Hay+O#in#7&FW4*CymIHOnpqPFA_#q(s3X zUrIv)I6od%5}Tow7V>+K2M%&@i7jcdW(!U1b)Id?_k?N0J~&jt;crSTv=Eq&!W$81 zAo7@FANs4kH*(B#DB6H<_{X?n(DRMsoE%V8-#lp`7A&7_*KAig_BAc65HTa*RhVA9y@WVQuMw8jpRg86ToEYmdp3JKnr+g zBm_}ishyyPd=j|vc7|Gd0S>UKh0(~U$@)E@OQm3M!8aCW1;5K4dY$11%0@nbiygFRYVl1SSunhL8Dh*F2v9cOgOQz+|Pe0SrODY*!$zR(Z`EAdu zRG6N@Ob62|EsSW-?*ARl7V%~kLAs(=i7d*MNg4KWzT#}sYB&Ni_;kqhQtwf+)bMp> zn^uJ6mh@9Z7_nCV^`xJA&(e^kgKcu=fKl4nKp@uPr<9Fa^<?TS`3=HJx)=272fP|k2-qLtiJDoNJr_%=DTN$9{;R!)G)=FEc|=Upfy8E1 zSX1<{9y6X!6mg3;GAp(TrTw^jZmfs55w}aB7YE#5q4<{U&(vW((wUl~8hxaGh#e%L z-Zg#v3)jCsB_ztye^7-THeAv{?oyJPa(wTP1j_YSj6Q5war`7PO%d;NQW*KECmtUL zlL}!Nw!9<31g9u_xj;M&l9&1@8j`%*nJh>WA!C*M60oQj%cr=o{4u(iiXH28N>x|I z)AHkXAT@rCh8#p(m8~FXo#2Be6XKDey7w+6Ds><`&m~w~Zu_#LJ$(th=$)4fy84j5 zx-*1D@ZK4Z%>C$sh0Yx*W;y%pN_B+IveV;j2!0>Fp7rtPwK0osQnwzZ#E^w_JE-@A zX;MvFve22`!jTQSG->L!SM$Q0be+v3+)x!{PEyfd_dka(iKPYCp(<0pF?s46HDOB{ zRy8hQPDkoG6ay-6(zw8l-mjw+Gd==ZBG-Zy0)>&O687H~WT@-(bj_{x-%Cy`LRzv) zORqL6m0M@$gnAmWzTf;Wl_ambZ_#d&DmZF8>^zCnYyK zRB_@ZdSL|OJRn2kP?rEtTF2$_RK|(*I8CWs;TU6wg}`J4-;=hY@39)sKszj{EtN73 zuqNL~)fWZ98(`Y`tg??+pT47thwO?g`RApuqiv5hDiRAos6IGeY}O6bIJgC-m%9^v zn=?a0F5zK+!l;A(59y08?<`AxNK976Bhdu*%85qW%hiI)hclrlYGF~RHCo)=E#~bP z_=B9pM)Wxv6>W(0{(!5LFDBvraCUL$a`UHt@a$ikdW}m$Q}CXw(6Y{|l=a#00YD}G z8n%Ao*WJltMbm5|_eAFFWEv7-smh5<`l5yyEDrB7o{TCj?lOHxa-pS;;zU z{bMPLg2+Ire&SdX6-9Xl>`@rLEfXYz;|i}C$2D@dKCBj-rRp6-`?1zW=B@zk4wer{ z1Dr2VH+hD-gh&Q=V!7jY$#Rv4+A&F!LESeq~HH-c$Ql~?hVhexPvFBt*YjLVjSf^MYrwgb$1Ql0~| zB75Y5Ytv??6P9FQoPJn#WjheFxo21uhHt!v5-sa{ky_sX%~sY6$2ro+MzP?PB}HkK z;D=3ashB84ctzY*9f7@ADs0poQOzu@6Q##r2pkao$^kd%F*HarZ_fq;hJ#bENpw+p=YmQwmQ7xY; zJx$Q!1Z_%|YqS!YZ$48%c~&~vdy*PO9RI#Si4GJVIPlm_8aus-j9|6z(mO|5HA7@M zjANARQ;m-V*#zPCInqvc*rE%#wh-Bpu!V@6dl;~(4foAw@Bw@foN`Gp-Ot{z68A#O zjM!a9(Mpe}<;(5EH7LgvR94UAaxt5NDUtaG*lcG!Tn|5>${cU8;$D1 z<}}39PeTo{V}dpi_B1>5`Ekylz8L z9^v&UM?!c#Jg8D5ylah>A)gvoBfM)|1$$x>mCaJwz)o5r$_KYu4$ZS^RANQp3KI6n zFs2F17P(+MU_-8iOFJhZi-vu*OR3j=$M@<2TRpO}B{w6oEEdh>bUYb1-%ZD_#$;?o zos~kTD_cw07q09cnj`Z#y&ex#*`d1olEr6wj+TAoGA*|{!&O}pro$b&`|7AurG6SY zDYyotFqKxQ1@6Yn2FD#+kOKKtIJ!aI783Pn3_NchdqH2&<%wpX{2IxC$^*#f0Pc-0 z)YRy*6hxHolde~mE_*$5xO|@Jv`F)fT*UIP$fRbunmi{^U5D|sR?1=+*S%U&{hO9nFqIyT%(0*mq20zIQOVED657==Jh;)}f4y*pYGcyj1mm0#d` z@xjZ(0W3TPRIH{Xxx;{cuL|_f^N7DjH#h@kxds?(!AnzEE5(-zID>?EQCvI9cc+?@ z?S3>aW+6zcyT|0Obb>=tzYD2W;S!Z{&`I^qRF807)V{LXwNojzjl#z8!`-YFbF||e z6ZjdAk#y{zk3onSIM)a%W}K??m#j4>IO{zt(|&%_+0{3B$3PpHeQKtz*sHrW>0T@k zsYEiHhFt@7-Vp%tIPKWMopTkW<`Knj%+&>KHphj9*N-Ay_2*Qm3pj(AzMLn%2!Mqia}0}My!nIe(k}9fpA3-)g<660aE#0_ zRTx%D@kRMlEmW$F7b`K|wqEcXYO>0^LTP>WVCGvP@VFklhS0CZgfEV&dxktj$w#CN zr#wP^7Ss+%4{;*^`6sy@fY_W-na$Y5=@BOZ&@;|`InYLVa6NTm1mCaXu3BH7!*D=R z040bj@eRRG$tNqY4m#)hQ*xo)~HRmLh2}b zAn~TDj6TJpG_7{-cn&FO@FS{-P*Xgds`LwcO=Vb_flX;qZD7yu)GUVYU3L~@Nwl-- z$H0ePrMrNh!n>#xgiv%DK2=E?E9L+q4 zA)x@NERuS9z%QepQ36Kp(^y^ZF|ygb6Jt7b=%LmlT)0qBYtMZuWx(F5)vJUc(htx_ zoR-er*s)n8nd{ApD9-+-OgEd%hN*A6=Iat}oX3Zx$qLUQXyd$T%2jV9RB%3?5}8b7 zd8tz$kl94R4JHvAP{6%L2-Um)I_#qUl|W-aVb8|okvo3Wh-HL!K~FdR$vwjDetb^} zh<_t0kg|t9D0jjHo0GLN{W^h`iNREXlb49491ObLzh{8}M9Km+Z=UV4e3XA@aOAga zh(t*Q78|B84pYufM1l63#T<^$*`;7oA)=dxPiU7r7;pt#DvY7M91h{Zg+$e6`UlMf zyx^x?!g1Df-Vgc+L?x>b2j`$48TFz9W!8QV8|W0JvJ>zDEW9WkxGFAhit7QFA)#eK zXDLO&HgY0924SGzDv2A;KYUb^(asAe*{b8@WasVd#vP|r z1`s5>DYvg<1fOSr`+-$Ky8&W@KCOn!ey|KrB20!5 z^|e7&54?ApPgx+GMRN|L)%{5CE=vq83Z75^be*c5sp{|)j|QzTp587{I^z{AP&YRe zGLZUw?;E738m-O)3EG4y$GjV9d6v5c#H=%}?^R2Dw?z;o%>ks)Rw89OxXZOckjB|% zBRvr_A?ckL6b6O2yRGCMR&oRs?+55xWDtJuQ#TPhihA086et_ zh*&00DwC5a*zMhu-p)Y9JJAJG0Zz;+Xy3h_+OxSVo~vRpV>2&r)JLTgQ^ugQV!1k^ zim`HrIlBjuSb+i7cIY%9$fE(U6gRoQ0rfpN6IQj8U<)M_Cs_iA#2x6aJZ**T#@rDC zc4rtP_~lRN$(ZnJYRZz@Yi;A-ETk0LlC?P~6&sOAoVE$MO`R1xOdD;@5+RY8C1!uA zU4bNnU>$xDq+Ee8=;lZB1Qe*AMBRQ~uis9`kw!d1c<2daH)4c0`#>*=iM z&Zv1iozJE$5~9B&wesLt9K`~1tZR9LVHntBondI>zBQ4ok(2M!5K{wgl`gkMtf0>8 z$s5Y5C#Uwq);T@9BLZaXGJ!iMTQq#r85>aAIwN&S4zkWLakHF_0V)MuYKDsj5yKT`h>6T} z@c)p0a{>42dxEKD^>{l6eeBnUVf`G^_j6d@UP4$n!+-tSZ{Z_as50r2$w`9+OiAE^{*NRO=)&eC zsq}h!yS*Ov%71vn&QU%7bK;}SA9-(tK&2XI%Jf2EJfCh5Tftz#Z#h`8(ZOSYi}S38 zgKv)jp8A^629V-k5nk!XeBk?=qRN&zFVPIsL?T&^Z(o1I|3K9k@e}pOAqtGTDRiXM zhu;+IXFiE{qfoph{9F5GjB;+qE*KnSF*sP4ps;p548weVKDlxV*X!tvmf-rx;5r04 z#%u&rF=nTb_uHJD_Xho2A*RMlqC)haM}DAj)tX9sQHmZ2@^ z=M!j4+qu5~-zV9S!}(2J{`|v95smwTd+@6R_gNX-1;=2C%_SGa^xi|*{{8zu9z`5j z0In=1v@<~S9%_krct+v*=-q|^=qhYq2oUxT7389%81rpymlp= zMBp~0!6)JGCW;47vatTW1k{!&4k4?f$y37Z>M#f-><+4?O=X7;-t>PBLBl?Zu-*>_ z=|g;wtdA&J3y&0{khSCG=*m)q{V-iwmuWzwWUcLb^yT~t#)+~FE1nr5(If0?o~JfH zh?=?wAdg^lkhwjetEO3c0j9Kj*IIZweCUckNM_LeB3lZrd_o(Eq-HUkeP&4O3#uf< zx>hFg#B0mej>CgaHpA}KXmu&Ml-|L;T)^kFVc3ZZ0>&%{G=KvE{^F`iIEwMnUA*XjrZ@|f4PXyC9>xlruVX-pzd{ZK zTt1e_h##agD75n&S)PHagaSvX@k~~Bq9d0ARvul{$?GXcjE5L{#5-c@M6$@{ed2HA zT+a*f&^DaRXn8-Uv3MrfW3Budc!6=V+2T{|hTqW!?!|FzvL(SV<_L`m95wH36SyB9 zEQ^<7Q_2`$c_AH4&#{|S>`uQHdwl+4QmEzLDk2*CW+_aA)5nKsCDwor8PbL@lXu9L zMlZO<_E~={`iU;coWq^n6m7aH6uI_`$^>4|UML@i#tnWSP8$)~F8vB)_w zojTTjg9Xtrcf*yLW$;Bgc>kM&bABMduHub{AGI(u@?PY}ltIqt%D$c>B5yP(2>l23 zw7D8Crla7D|3PuE8??lELm!=^q}uS7!ZBK5``I)`1?oF`)V&3d@TsyU@F-xuGyo5V zv?FUU#!Olo(>u@F+|>vb&nyWaB2xp?+6#O1%jO2@blb%TDuYENA$OWOZf6s%4q{~A z2$L|6BIV(Fd#i|=zIja^Y#yk|K1717XK(2!fJbtQ&9i^0xy}_#9PR>y zQ8URLEd1>CmWQlbe1?eM zrhe_=jp}-L9=`{cxu-70Lr^9qc)f}iuesAf+*sl@l0$Wo^a`^DuuX8+2Gop0lAW1X zb3gCgN{zu{Wd=CujiHG`LZ%Bzi33mOeveC(ooI2KF2^wfR>KgnNX_MEgz>gt(_d5- z!6@9#_(s*WreyLPG$`a>K}(mP>C3lfu($*k?DjHm-xkR-iZ35r686^b!xs-hp}-h` zgFKoj?;fmgjoJ(4F}tJL9%X4&NdJLguT2$hXhFet1>i+uN`9`#E(alL9`J&aRAjnK z7U3UNI_nM>qv>omT;BmmRF0_qM1Meq!bs7WnP@4GxMREebCi3+OA=upPpJ^95^Ie> zL@g@vD(s6iT;1&7Wik-ZRi*6cmsHU}W z7oT|_+m2pxA-FDSI2#T*!~JvH;d;h|CsJFo}Yh-u`YuM=`(vKa0!CtFwrr?7=Lx3(J zwBBE-T5Bb2D>w%g?ClB5FUHAvMV!=Sjo-x!2Bf>Ar0xRtBsG-}*%a5|E=o7pAxaea zr$pQ4hPHvFR-k1ba32NNDQuI`#${0`ChVCt!S#Br0NHUyg6;e9pE)xX&-BRC(8ZLA zh>R5>b;w9T9;1MsH2kcR>6s5&hAE>hg^zP7aec9!lfsX(6eiO^-8@~Ez+EPuX`^2D z*Jw>;iE#xf+wyn3yJG}Vc8Fv^&k~8>Y+FqH$SRRTSBZ-91l{M$^?bMi4hCY3K-GuO zC*}&XDTAA$Sb8HNlsHgfL{wRNo@gJImHItKmVH4F8K zdDCPv*ZauWvyVShu~qA?^m=?*RG2(*m*bw1##u>(cW9VTY}plzoyG9e^me$3g2t=i z*SlBI@aFI6siDhvBE`C~SdJrA#i;pWKBs0ELP-jQ=!6>d?~sx($LR=J1&wB8PN8@$ zMj#+n`qwGLJ{aqT_Il}s7CwWe)9I`~P+9{oURn8E8`XYPMIVtErz&Hw0lN^{psx3H zF;=Lbhpy&>(vqzac9K(7X)47puA$+K~{K3 z9F!hk9wCgwbka(Lp6Dzv6ZK0bD)EG-bP}ac5}S9qlMa+(H4inlsD?Bf^$APMS1#t; zNrj;UJv1U3cNu%M%h=q`topj8qP%aHP+FT<=n_jMXMF9&V#9%%FCi!J{S{w-qWD=r z_JP^bHhDxNWbOhA9ZKS!B0NgQ#iCBKzr^xCldol$DH2&Qi{#D}+Ld#h;VF2*%2-D` z5Z$42m+_@&rS|}Me5-S1(4-08z^XQlW3nFXGLKGk&6`QzDh^@dKk8JHP~Bao6N{BR zkv$gzTxvSIzEG2{GW+d(NbaK&g(iF9{Ky~KpYsC_w#~m!dEO>eXEz%F+42gPO91np zP!cR)-NW>{AdLuZE1Uu9PUdH({5_0YZqPq5Fum>dHzUIZ`RKCV;k!BxtE`d!FQglM=ur&UB$>fo7QGR85u25k5x48 zUeZ4luLDDcFda7jDE_0AbPBga&j2;D*g0hw6v*LmP+&|CWZcGizI|S&P^3D>%%eQH z3x#>`8j;xCN9Ij}rnje1kA+~%*EkIhKcd=?3FVOzAB^H@j4 zX333-5aioW?c0Icx_vuIFIpnXK3LRVpq^?kppeqp?hc&=ctMb`9zp*4pbYM&-g%|& z0pH0+YG>00`jO5ddT>96aJ1YmNOlCix}Wd0(|0jhVtk~x=*aG)BSeEr);FHfA9_W* zrhoU?C1g=Q!A8(ym&OclXWNJouQDKVn_?h-(TA!p($TfJHrxbbfU@RRZGOJ9LL<|i zIC{O*CE*?%yF!8-gVjKxX-@VQo9l0P%@|M`gMp&ZP^)N)&f(d7j-jTo z(9#q{helMFG;(*iOLk}3{5FG@3H>*XY(2E$ph`MC+AA-{;b)YT4R2<{TS-7VW~i-R z>C?D#@v(c}e))C&d>uAs)9VqNywJjwyHd}lAEKbGUL&SIjL|wi8aKDpL7Ba`vRemA z6++xhxGv4AyG|z4s@8?!ESVNRqIt`3oO2I|86A7{tD2sx6E)HMaI>GjN% zk9Xc!+0Fvq8eGpu-IOc!LB|@7lWBI}h$ZjmUx$2~F+D>SH7(P4<4OE{+=QTsgbU@| zshjLGv97!OFtm(C89U=aR^>`^c!FUpO=1Iv!)A$+C%RnpiKgO771T~NMUp_nMyTXM zWH|tlDi#bH)SRMuv)x3<+oRj`8t65fslkCHz3TGHa^JcbW(e|vdKKvI z+1nS-FRxy|c{6y%n4uLw86XQG$C=>wT?CDIvs^)!3#mB&kXo9N8P6*_HLjP-*>z(x zi54>|3bN+{UciKqI8U+*KvfzqAI6RJ8!{M!8bRq3A`nImn`Pc=DaEi%TL*mVquG4+`MU z6-XomJ4@b^TLAKiRDMHUWMf|9u2Tz%(4(g@Sy~D>zZFK86is2{Ngh9tR5GrrLaXrc zwp=5;(-DKV?dyn<-YI+&w42P%5RUi_+F`UHkYFttDa5p9_mdhgl>M+vjy&BEBk4eb zDqLv*H#c{yC`PJ#(h);S&`5nZG~9bdV1AXsA0JfDLP%+-@Fdi)cd;PW3loP`_FxPsTwL))Z^l7=#5f8xa;|7q5nPTBIi33Ka!+3~kRslE-k%R0lNs3R>%cN78K1jFI*MDtg6tBK4%Gq(rtdd&_VGX$Z{9 zHXVw~rs=lYU<3qfP$-j?;Z#1e^2hwdJ=G)+TKdmFk@KuFmp#qHtU?26ydR=1oMjYCy6VZYVl%7?>O3QANlE z|2-QtsIx!9h?-7Ekaq(?$yd=xx*`&qlKzvZlzZ%-1^~*`E-~zL^k95}LDB6;&_Gs_ z6(%bZnn9O)H_|Hml$&7QRf5k>cU!T-cd_(4&PNq6IHi5>2Py#zLIeD`q5(*_(*H5E zAaD$L_G@sdiLohZ=ja?xmLzD(3PV#FVb!e|Kr8&e3!q7JEY0CWsg_LgqPvTIveZ-^mt4V4*^BhHz`lypF(=t&T2E56 zPYNJDO=6EsJM2LVOqe2V1AWZp>~)%w>4*=e0v5RyupdCn>Z_%SbU4Kc+8$P_#^E7TmX3yaXVK9vsMZzz2SXd7 zkga2xFNl(LH|x8Q8?e={)_1LI+~d*2LxSXs$A6~USo%_CwHSfwI}>T=1*$4fD(l1Q z5@b!bdK@I}jE1Y>Xu7%k7T9SfH;g6;+;iv@P6$RYl|DyTN^z8iPN{pkUB}DyWYDJg zRo?;!jLO6MU-UxZ3zg5D#njCsn zzg=wGqj`Kg8L!)L{85cl&+61S#eqS+9tMN`P)4&jg@^P~{jN6h+-c#jPq=U+d7pX> zr7hA6gTu0*OV!F$RJdx0Wm0)l@Ufm6EI_|Qbv4yNDWdmLSqg(a>_LCT@!t-$#u0Fh#s^BHdxqb>XrB8wy?m-;Ev`;32#Q4-r85HuWm7-Ollq;nw zk~fRL{CV5?uOI%;-+rK=8_juiva)CEyp2?QP1zH4chi&i+HU`LeXnOF%L zV73YZQ0;P?XmS^pegHQ)S89byN#*@F6DsV#nLrKIA`$Zm-4B_M3FOa(lYBz56vNIP zk06<=yLlaOm(m+Ta43roTrZ-n*7|dTI54u6V2TYV{91fzA_TgtM#Xh1`axJxzPBB! zQ9?>2lc3c6q#fe6+Qu&zS33~Or&OX(Z2mVPBuzpzU_-kU&KXvYKnmf-Bp z#LgNED3Aj~(uF!VsE$QQ(|o=--)^p;L=6TIL8dmXlTaEtwfjd1XznKnIc94ZHF9Dy8zhB z?&3`}zryQ5OWMuB0iqhuiRw~1OsX_k5nE3Am6%TL;DTfv$3d5G z=g{~kVQhqdRRMM?Ah151>?MEcN}!XKdn5_lxTo%Q$~xO->+IFiYo7(PggMZ?Tt#eG zDQFXVQcnoGsj+@06Xuu+Ew13$Gq09Di>CLm7`cMZIJy~bXPYpArCwAc^jv_w&>0Ae zh6V0SrD%&AcEOhU{PShhG~r@GD7^diY=)4NBFOYCni^yYjj^5@!zl}UBt8(>BwfP3 zq5S}{&USxj7IJbdh!D_r7h+_oq;9|~fsPm~F$QNZmh3$M+erlSG9jmNvz(35GL{V| zfJm@Sx8hEEK_gb3J3eE*`%!}0+XJ9oKeNG|Zr^fdb&Bo>ahP<7ZZ1+Cq9I<9j?jOO zN_14e_6+~^8>nVTKv1@JIwuWM#7-EHstSBsNGES_uQwbIS9k%x0>U`>Pe?<##I>lq zG|tNmt{Utr$6i&64&NcPu*frSF6yQC!NyzcVmKOY;c5^Ovq6o~z}wv2h*atY0l=#O znNnj&=qmwmQtIx6+x{E=C-hlY-}K5;ehm}O9ZSO;*&3z^+{P8+PBS^z@&H;@s~>QE zYPOLaQF9<%roZ)>R$ftGMi9Z(g)B1hE`A9mqTl#v-!u3qRVQ`?OVX{XL(=72r7+r~ z$b2t2VJ@GI*8fR2LSG0vSK*U(TOUFs(=F4aoi~QTW|g04?a~(cwb>| z&&U~qb6U4e^P)tv(=tuI@DKd81izgKSH~e-qkO^@j|2(LXr^%?CwjJ~->w zk)UHMCpOc|h_inL3T8Byiu5bdXUvt)Q=$&jc2F67f87%GHZA2ceMM|-2bT$TO+{8o z*m9$8qc?@sT4Jl){iy$I2pxSZ`cS_H^O=nw51Pw&qqz{l`}9r-U9Ob^C{dH#d5l8(&dTk04d{6h6(!F?f$^A7S72aRv_;V?;PJ%-E7P27VTk&U1kjzge z^ZlsU(d_+lu4c<;>Rgo--FLDe;QUaaA{j&Tu)QJh2oL!pSH*Apijxor#7B``GlagE zsFJ{OmC0&KuV0e$$5uqMjos-Hg84k0&I&~~>7wryhjWuXJLsJjQVE^Jhj9S(gH;~* zYw!1lf3V0q08}G@MWKyDP@53{ow3b38J@0tfX)DR`^+I`&La5Ny_~9e;|{+As4WF< zkjVg|JAiPJJ#{MFtDH-KtBdr0TjSZ$`Lgc?ASxE*h2wBc?U|IF+#*MUb`KGZGPJlj z=GA*??c~FCqNJ)Oh0%`tL5qwPRVYyKA`krND!4EBNuj*}ss;`bGTsM~UbAIU>u4KK zV0io&h5L3(C~UOX1GiwOxH>lVkE&k$UnZlOoJy50%h~;17QQMT5=bGzPUTk8<0(Yl z!iq!i;eBO%_%#04&*g|XsXIK!&(8=yf9)mq^qCwBe5^Y|tsxMj-QMt<0%y z%7MS!tUG!%j3dSe-@@^ns*bbq0aFfc8?@BP*>T}P)D;MSWE%@kgJRVjx7|%cu}wYwA?(I&0a5}xB~+-cm7c4$b;qGcHQ}=4t&92 zWUGFqj3Jeozr^}&O2EJ=ElvPE&iRnj7`5w_LZI+T!)6-)MX4(*es zS)@Xlm~y;u+5=bfyO5bS3`g-|=;_vX><9}}JHRLyY z^%3Es)z>KqDT~dze27NtQ?mgi?Gw$~`xjitlo!jg3m!w}(Rw+GV!V*r!Xq5@ase5F z3SI2F$eEd|08XfIEaTdNcST!Lh)8Lq2_ka8%bYu5$0&Is*vN0Bur9w}D8aO~#L;fn zO_n`?G)?6DP*n~o;Kf!?v@3~YofH0ET~cQ=Nk=dl<#h~s!as`D;b51$5Y(yc9Z~Dy zdOf_8ds};oY4F(-`iK#ohIB~?1)WfrUFneYYfa?TwYYR^~kI#?!A+I6;b{=SrII9{)F|O#h(s{Xv)f z=S231B)j^}q;{sNm9TKUT~Lkg$d{b&``IqDn#AZG<`khN-?WC!bWj9<6THQ)h92jO zpOEei2v^>j%KRsIxm={k)CMUAd?@2u%7l?8)PrPOTYnCOxCIr)Fds=CDvj; zvR1lB-<5@*k?Y4pV5LHLRDxXAP>*1tZ)&JN918)VeC}tIJta_)CV6kxcs{$UgU${{ zbx=FAi|^K`e&8``F^>!ouH=)nleH6vOGIR8DU0I2#1ko3pAw2Y&$g(1fi&+y9o4VF zs&GU*NOYFpgxfpixz`rZ{YU2J~WXJ7|+;mfp-tjzq}|1@uO;)I3z=4|H;Nei|u;l5k9W_gFD) z$tcNyIYoZgzUN$f;+*quCFeXi^PHfqrP_2iBkJs#v~{vOXlyw7$CoPQmI$i`+hI*J z>Gb*(baGZ@_Ktk4%ys*{_!M~I8uxr@RtVhG%$saP)zYj!ilAF^zC}q_hWS27rF#qB zQU!uY%Q)A~k#8neh}li0e`>^Jq0r|G@f>;dQR(4iZqivsrs#;)Wo!S)TceYPWtX8C z<}Z&n&@Xc#a7{YTqRaBH{-)EU*!jaQ1@G0jZ#81TI76z2L^7$kLcVLRAi|?ad%MzM zu&bsS!JHCTSK=%vyHTAQw=(^JcPphGQJs1uX-9~en(reBk2%7oTM72BMJ|hMxUc?L7;o(*bNK~c(EG|$oCZnXPkG3 z9>`zO-+n+fKIu$TL%yW82=BF4c>+`Sj8n&2yBzJz717##sST#;?`;>G>8u|hU4t&|yO_!Z zdn8gT)UHw~eQi*NGy#u%QcFLmQ3N7_iKb(;YWHDAy2YQ74Y zIFF}{BYBt7tz6w8DzNK;YQpR}QQUkzNr&9YPd2(KW6o*a5i6i|_qnLl%aFa?yev?3 zxRk&!B;uN*mdH&l0ZV#xeFAR8_qNqgG(PjF0JBEGr0siYE-*ZYM3HMhj-n4i2QepL zf+6*RQBJ}#hhzUOipMxl`5fOMFI^@to7io%Bsrv^Xi=U@zW+$zTvpYY)d2_kF=AF3Cn|OOI=r3E!y*X&E5qyZ& ziwM0((a=FU1_Wm}$UR(!^Jp_vAYq|f+C@Pk+Cb`DzXDkpK-zwk{MeU6pnj>ytCzEM z5wv$RrDkP_`&!OdK!9~PN6fBX{BFx{Js+QA%yut3#)t|iVGc^;E?f67uqJqCy-bw{zt`Mfu)>X!O-fnchqi<6zG=l4a zx)OosW5;mgrLHOBfi#gN_PaITqNPm~G}2Bu*}W_|OHAldo-L?N5G*r`!svQ9zCjW( zNp>g?inGO^uB^Z+q~eIIC5r$TL&Y$A4xQtR=}GAa6QW&fO&}=uB`Ol}2XZ$HP)kmN z0DhH$IJyNCo$y(KuX&j=&iQ=J{o1?o%CE-SR$y{Hj(2%?d_cUCBfJ=Sh)WWF4?el$ z?Cab5kt-uG64Wo5Xep9G!J5Swtc@iFP92XE*)DS2xHhP+im%oh2Nz$3Kj1hA=ey6r-A}YzJ0j6*Lx38msIMZk6ZB5_>YvfwG2;lSkgf734_-Mrhqw@SJX!pA+ z_8n2|;p;F>3y6ffpISyM96o8)Vl5Vo79&;C)YjQ=Sv#OEw*w-#jJF{WDYyVrPFz5j zb1kV5r)%wcWWK8s5CGeRmqOUfzf_Cqc%uL`CqkT*$pVF!WE_1##ZdmTAE3X*dbqfa z>`f02--NzSm^a;xoxtkrUsQNXHBJDxTBifNNmq6S11^1K5Lx4}&q|eP@17d0{A4)R z!DFZLba;hP2ttapCQ2soU}FuSUW=vhD}P6W;aa`!6e`Xi(f9Zcy|PKaWxIFNjnq<99aw zbPPwzs+bR9k+*`jPKXog3x~bnQFXFE=>MMNfSOUNknWRl7}c2?n-SIE{*O6X!JdbU zwR_K$?AbMmT`#8$D$`t1hTS5_$fvPIC<1hkaWoFP&-mSd-ss?xF2%Tn*Tc~V&lNY{ zmC+X*jt&Va-jPk^j=vXb9=)cdBHVGp7ryUj z$~_J_;Dg6WetLlzsjcSG$dUrDP+cQ79@#06*Gt_8f2C!`8zKpkwO?lh6&3 z>Y&N$IE0>F3o>%zz zExBLTm-tvP$30!r$XOywl#4;FCYK*!b=f}o|R*Qo8I z1Xha5LQJT6rH>M2FDB9GgS$3UDKqi1JF%sJi5otw&zc>))>&)tkh{TuWbPueyhtQ) zSjAz;*L$rD9L1aT1@lT5Y4td{ON6K}8kt+CuT{nNZAMyj?DSRWok1o|BlwEeI#sUd zMIXBO!FRNQIMLJG(FBmC^l8v1KDir8GVU;7X`>r1*_p}c8&lxmF_+`u z>p)@Irx4%z4b*SeVoDB%gZNOT#7{Y2AMXO-27EI~=TTZ_ev}5b*dw&Wyu0{)n&ayS zb8T783g*7BMZ{;3P^WCM*(Yd=@H`x#W1L8Qp~4g?-%!51%6l;S6&;sH>$DyG9ww(R zgI2#dP#F9+-9gaEnup%(;Oay-8){jUhTHe9Y$WPqa*liJ5kpIlSZPunY2jukg&>i$ zG%nnFEyBhOa9b`~q|5-YL1^uat-VH?Z<&0o^^_MmvDY$dxX z-kRfh2$;ACijDI@OKQdsH=!!}>>7F=Wq+a>JbZDE6+#?x;0}8(sG}zQ~(7eXv)8# z{yCW;-)l9$)C|c#q-s%C3S0Z92$^b^ljVBzbh;jGr4)Z%{l@fu~-Mv-dxcNl z(g!QC_;&P&LWMDA{j(l3P-C9=>t93qJUIKj#v;fQS)-ZkO0s}i{VR#!o? zUN3`eE+s1_^{F{ZEQzuZ{t>~RHQ-*)!A<3T)?sO;gg3yHYwnz!vr3DA<82xxJkHf`URQgAuua|I(8RFzOi)pul0^(v#db6oL3l3^)5vmJp=NgU;>NtVuQg=N6mA6mAP(#g_eW-qptWW#3h+s`u%z>$W134!FP>DsGf7oySR7LFT2fus7 zrj9E$Ei+ORDu=pKQ)@v|6AhV2O|6e%AD`z-O%1#JVi$6rj!f;he`KktiIR*UKgd1r zx9cP#K|>~oG&`l6 z;FyXB`GR9d?oGUqSgs1<1+h?(Er@RjJ46k~j^~4pgZ5h6HARws z`ct8fQzF(paOfE)n7UnAC6Vx~C!6ijh zhtj;jFW7ThIrZmz=O{u>AH13sGr1lZfG_|=&Y8%Y3L(IW{iv|Um#vv(b&>#2rj!zV z@Ld=c>+esgmDF#IRzfdHD}mHMC@N-UTX~FP_dv=I_BecZqRa35k#I^jIau3(=G&P#kVrR4UNREJBkU zVvlPsL{4kYEvINk#s+&<6y3`iGF-cHKVTuDzrPQe-4`9t`oRs?R!tHWg!npmZD&j- z>QZ6j&LUwh$Vzkgz^^Vvc~uN@k}(eq`K-ecaylnYeQLxxddce2S-TSkz{}p)obs|aKAOu;FXggRj_m97b$3Ng*ct((6zl?noqj&`U8P|Ah5?JunC35t*$^@ z>DH&y({&jUf1%SPp{rsZfAFMALf3amVV%7EmXl!CL>*QSFEY&w3XuoESk-Py5IU; zQ)|eGS$bV5xjq%8ev)X;Z=iu`(l3z!Nz}cq4nL%GJ*0EJSYFN^g2k!V>mj9p3#Bkh z7T=ZgDLvmUR-^QZ9R8UfhOSD(9cUdfp&vAgCrI?ORF@MSfr)t*co8yGT+vZj3Mr!{>+0w;Vs{uRxWo%od-=iLZP`# z0Ix0D8L?kG_cFf)#q9MUg6DkwkegXGH}hMhkFY?YeWu@C)mfO+(sgGGqiZF2<$j}U z0@acOVG?j;bC3;tvqSlyII`eS-^ppjAy@;_Krf7^Qn4RX)nY9-g8`a4oMC6u)#BB1 z^g*Ogj~6WyztQAC8cf4+7$yM1nP?d|sC6BlR&p)Z*jGL4Q}bdW9js%z9DLcd;%F1z zjMXFXaovoY%g^Yk(tb^UpN|LqYiWIY9>2rnp7l0H(QHPR2-@E-i+FewJ(W-N7K_7S zg0nV7r`({;dPCRoZ*HcoWgNzvb@O$6xmjy3 zR6CQxTEAM~wXV_sa5U*(AkvUuJ^nLS3C9?{o($6K@hBR%TD-*UKI1# zZYHq+#eV}ye93i+HK{HH{V?UlckQFRtUhim zE${JPasY*4Rhv2&i1?116;nh)Opx>fr;a{n2&|mW#z;`Yx{%EMEV|y_zM96Hc7#4l z(t0-y#;dqvY2k9wW<+i z9NdcU;`St(tpJS%J;)cSYo2bbqUgi!7f4%6a4pb&q`g=!zDDb1`={mNcl^82c!7Q~ z=^zp7+PIfb-y%_IFmA7>28J#yBR}2-EwKoJQvY6r1GFxx&(aQ?eEx(N3Ayp`wfbta zZc`au{QG)~{iCyR)Jj*J ziK+iyYPd3O;QFt^&T1J?i7bk@g5xd8ch3-&o0Y|+phkW9kQj%izU#Of-_5U=v+38U zc{`oYra^Z#T*vrkyC9Mr&Y>pJ^+T27E|jHsfSY$G)CHuX4n$N0tW9lQfyoL@OJrC4%JX(OjQVmr7Lk6m|;mQ>`lf4p0OG~H@*Jtlj z7JU^Pe#*G4sfgP*YGNqJPOYJWPxpt}HB_|BVm4|)?SR?I_K8RqREVPLP_Rm73tQx% zKjBXH3g~=Rx)9lZf!`Z4YX4j1d#7+wQ)RDM;6=t(8V%iAuL&9!l}*(EB>nazM3f@h zv#5r^p6aMZZ9E?7W#$;JG;I`^s2_Pz3?%j*X_F>2xlQN^4t%^|H?6SQzuQDcdM*`9 z4I@0+;RP=k@k8bO)B`;gC@0yjo?5{ zigEivmx8q{Xa!dVQTzM zH5D7)51PupmUli!k{JJ^brs+ek<9!YXwg%e!+12EUQPm-8K(1)-@%swmaHH4s5qqh zN8TqS=Q{V1GwEu&T#Ta8z~e@fp4umH<{WIiIfKPRAi`l5z8%7CHwn7aSOF1@GGWeJ z!Re%)d&Vww$Js#czPq5I{T<u)Wg;V zztJAFCc9mm(R#gH2lY4Pc|(nx0lgix=g3RMHy081ewy6}eODR9k_xPL*e>Lz;2+^5 zwYtaKRznM??CIsb^m?hiz)y}bk8N%KzaxoI{1Z9I^c!v^amMlyneaP|Wmy+!?IuR# z2m+rP=q?zERmy|InQ)h&At7>~P>X1}j>e40@8avcDULM5c`QL{B*JGmQb$w(Hp3BQ z$#gjjed)h(zPK7?sH3QWXcGi6;@%K%My_;(!tt9Ycs^{tPJXZva((;S((s! z*#-S|ha||=SHl^DE@*(pk3NJ;tnFsD{9H%QCz`ykm#c6xT#RSYT8Nem<=zuH6Noz}~gm35UqYZx38}5NsyvXgCh1i<@Q89Y<8$D;#atYs9+n zhu8?m-6S~WMwJDDsuhlf<~{G%wm!eRm9vfUu0aHbLJ&&9=3bye@mfv3%5o7ZV@{I~lkf+zruEn8FiR0I97=K;ETWh~ zLZT0!Sc5U|P6s9>UH(DflfGp(jQFHZ4<^Lm3Gwh1-Sg0&#$q|4Gjk3)S3_1AEI^re7iXbVqua1O-CEAqT$W!HAJ;&y%^3A8b%-2Uj900qN3ag zg%p40+H#{v$24066)nbzqEbxU7=LBO$VAz0Oiv8<5Fbk0%BOt!D`Bk&t~=alMSqz2 zlMF@suny&Im^zfV_i`v)j>$a~w5r6R1RX;Fs3rX90|MJfqSn+5n;`y{P^pGXruuOI z4-&F+#t4U^Q_MkH!a%^R5Etvtb=2$RJ60^d*W;V3W5(JLiOoVB>G-G%O7YMn7byL) z7CVZ9**(`jg=(k@nnAVS)2R0Qy`b9Xu#>v@pVK&o(xXfizvlxDw-VQg!T|mcC2bGX4MZ!>X<31S6 z2bZy+R`D?2gfmdKFdE;2v7iE6`lI{jFj~}!b_29w;m!1|ZQcuTyVkAkG6V&lPZd1r z-Ti~oC?dWamzEDS&G~~upGBs@9}@B;>?x>w-KdrUr538hBqk<1$W&R*_A-4(Uf#9tT&|dz#pG2N7|KT^3n;9W?xVn(D z^{sueH_fyp>~BIq=N59{^#Js8(4Iv%8&Qm{gr?(Sx zEc(rNe=dnWtuM=Dx69&}%*rIiGck8)Ja~RTuVyJY-uymzd5vnvvstZQC1;YT>EKcoMeA{ z)N^nQJfwWD_ogH%%4~GP6T`UcfpL2Tb$cBmArlrg>WlNCAD}Ir^XV~wDI~$?pF|q& zV#=1vps^a>&6aTL4Y{6yG1+=N)|M)QSOck%kf(*T;F5#!kwE0}pIYmK8x>2{o>EdCUWP&U`EorUZe;K`vVjI_D$OW-07eXVGR^*E zS@7oax|E~OK!eb$jEXuKTzJEOSX#~TuGPX#qb66x%@!~p zjNElHKawwUR#Oph6&K-|T|-Eb!Zha6-3H(B!h+7NLB-1UpPQb}-!~y5;N^N~o7c-P z(HOXbGA#A}@(V41N-E0QH8DWg$7j!eDS*y|qq*Oc%fGRi&LbU<0?acB)h5v9g zQ&R%6$Vb#iaT<=rLP`Hu;C1+;+LA5MOO5o7L9TgBm2uc`N6;k1t z7AzXU3VPKhxT+xB<-hcgrs#8~0V0uTR2*NW~}9jZqCFoIuPXnhv36RJdatG)>nADIyr~Gs)ZzT4Fl}vG|^J zsgxHEZL#D>-^IBJ(oNoJC;Et>s_Eu#LbsSUeYuK8PY4KukCT`0gK*G}@ktncbfNi3 zee;-7PU-)Fh#>EB4lFa}lL>~WpHhU=Xuj$q%x^P=2mE8rJDZ@4IQ=~5kQx1wc7(CJ znX!4+LD3AkAfrV#se*re!K1^!ldSooXyEAM<9}{D|NGY;@b`ay2vDv^yUKkV;r#l` zNe4*4hs4L=0l)s9;jdvIZyCW{V%>=E79%<3`jhpj1glgWAkm{mgs{=!7$vq4o8aX@ zO$1LaqUKZn%dh*8%J_wpBm|Y2B29%gE8tRiq37^*fwUG)^!gZ6 zoues34uq)?R0oBp!o^k>I)swB_zCZJI>ZJ=RQBO@FCYMxaGO%Ryio9A3JDjo50?z2 zg)@;qh$rF3s*-akuAMr&%F+k!JV_rc3l!s*Fx!k;hV#@KSkNex99U@bGg7q&c-{|3 zzcVlog@O{Pm`kh~m*s9V|9&Hx>3H5HjV>lDO8MW2XO1^uq>77jym@kge9V7uS;n^$ zhlpqP?&8wD(>BCWFSiKuMnLZvRnNN5_}zfsXrkO@V>ngF>T8M@(z_~X>19Fn(~-TM zwxdqt!!e2Bt_#T5rbMHsD{wy~i{eEpldPS#2{r17gp3;b8jSSoK}^kPKQ3k1i<<4W z+l0nR6u-L&(l9{NsirV#2&po{Ioz^ZvP)6?9!4;o(Cy;DLS*+X5v*CYqf3@7Se2z^ z3yfEoG0z9>adb08idHE3J+MdUNwWl;o?*D!ARYAIg^!#egm{rc&JPgd1R>l|329*# zzwI=M{&5)?x@HezIV1&JBqSfQ)|N;g8rRpu&on+tMpH78Z> z1ofsBK~v<_HzLWEIwPq*o>G}obh?5fLKApc7?tx(S$|03_GfKr69XaiQ#7uJ{<7G_ zog35+jwdD^i_8aS{qOI~V#>I-Vc5!!=pRyzAwv*=FW zy8Kivr>(G+0U|(gr3x%Rx%v(GOsq~q@M4+T;FpuBGQ$vJCB_nM(9n`|?lKO^)Uf@I38NP_sM+m-4M-K5YGi2^ z?K6GzeOhx74$wV=G58)Zp~<`AMgaV(;ONwrzTcD6d(rjPiyYWC^t-t3y!o}jz1!d3 zyrqkvY@*ffUS~8!x;E^L+#NzJc|}<}`71biO?`FdW3Bor{`CD~sp8V0O9`eDmHH!N ztGy?MvEOSG279RrY1m!n2v?R_C*!lsN`@?)bd6XtXruB$yqrgH5Is^a<#5SIKuyDx z{6d5^*GmWk78{=md9NpJCL2xsH66k7RYwyyU(DfF)l4?KC10-qUJ3aNQ%+`zVuP)d z9REoKFJ32;r z*MMPy8%q{Nm?7NZP}F#mMvW)OM~%9m#w!ao@LHn=Bv|3opnl5{Rdk=tP+x&E&ohaV z54eHM)?QEHhH6|}|Ci8@9cpIR*U_i?MZ}eMT}goHn5|za9|nq}=DM<*(Z}%m4pt_4l zs&D!?JwtU`a#r`deTBiI>1COagO9?kyh@?%o~mL|quv|qo8@e*8g=lM=)Vlzvcy46 z`gE+rXgHcQMj%#TcaUohA6Imk?3+;43wi*&=+`^pmUcnEg?qa5dJP>OK?~6Lr3QAt zHbPVh;d?lOoPa8??ea=jO7Pc9czKTO}!|J5yRyy6z-bntjd zRb{!dY`sklQ>8}%1IK?YcWX^xLk~0_;B&7Wv)YVwq~lsj%3f;L4VIDLn>8uS44{L- zE2x9MRA~8O9|ldrr{V5F7P2#?@^3v^unbsC!$5%x7UrM9|Eax+;yI570DH>HjDdPU z-W!4_Io*nWbCd-pKc-I#-Ai0o5Q#mQ?QYFdo`71|H%!x~-#qjQolQ#VQ%%t){MJvO zf~Pt3secZda-Ps2bf3=BAo+!&L3o3o5D7ZBC=eo;`pDHu+NAjKG=88!Ym^Vk)IHmm zbNe8?8-Q8Dd!RsYE-Ysu6%otr48A9eQWWT$N{MMYqs5irOCC8OM|}UvB11+xfr5~w z3>Tms7g{>W=n#-iwCsb1Ib{^A(NGi&zfDsxRz_@Ex$lLsIhM;LAH)T>J=M2Yv+iA5 z(3Um7sF2mC(;~k{cU0Wd5|I}5D{4y2MAaR|HLRfmhKhnr-#mO5Y^*!luOT&>G?Cwk z3-{5qiWoFrl76tosGnfcw3L%$Tp_(hs#>&zHNd@jdW(pIC!@rF97AGlM%$DHQL15I zn~ya5cnrMzMev7`Uy9LgkwUw^M!R2tb?}J;>wXcjupg{c_$3Fbmumk;gV(!h?|+Lh z1axZzd?2Qtuxs$Kk1|a`jU(w5(2`qLFS=!D|7HvZ{W`)j~$2gkGD6S0+GEI&r-aR#S<%!}h}VY>kHpP;^TIMZ|M&S>cop97F|87Zbe&CU9?39b`L zM<@<9!n^0f!A45Q><4H;s}-j|o&9S+aGA{1lt1r5X)5F3>H^t_R-K-kLr-fW%Hl!e zcn!!2+0|Hyj(ufX1d2I|off}MYY~dvBf_p2^+!I`?+5c#02`?Nhs0un#+NL(=vzcz z(*G4Y*Y850K5<68VqO7$!7&6jKKr$KA$Jd^R5+g=;Kx*fzl%h2On%OCoPPHzbs<@SI`3x=c4r1MRizwQPl zTB_$OF4^Q@p2roR=**Yn=?zN8?>13qIHIiaT_09t_bb3WDL3`hnzfifh-KaO?pUf~nO_)0x~^IM4_nt191w0=O|J!iJA9<>~e* z0aPd6nVIC2i74uTt0$tU2Sk&tn253?7NrwW=Bs$5Ag#+ixTtI2CY45I(dfbA6DJH; zOCouvXp4jFlyLHz@Df1|R+ARKE$uf;=+NjNfZw>wdxTGws>l(6cE^@?@r>!Uce*$G zO_wg7aP%wZ>t=KwOHpSZjM3exjcn?#(Rv9X4cwEGsei}2*`SVUP9AvS)5p_#O|VSZ zi!UTQDMdsNTw7#N3&?}Rxv!z>40MCCRTOIL{lbo5q2eS(IrB$cW`Ig! z$Wh8Jk&g8|CQ5n~U<8fyEChx0C)w>Q`<2O6c418yv9;uCW6kS3^ljtwZeGr&qlj?X zgC3GJ9n_!Fmamq-ERdD83FOldW+_yWzz2#1d}NmDnHTp;%_}p$j#1C$7&8V`A;g`K zkqT24SFPD$Z#iqFmTGOpHi5RFa7{d&j}h6m*bGr?jX;8Q(%m0CbGP$ojbu0$l6F$z zD#Xe)dE_S7fPSyni^g{a8-roOvqD=}*plq1Wq&}3YP&IrkTYq6$WgcmgULUoltm9Z zl5wYrG{4)Zc|m{Qn^GJIWIZ@e5O>TEkYNcxc)@HovV#Cnwg|`oLi!wG1MdgET5Lqu zBpT1M;_&_uI!harYJ?ojrXiZvW$i5dUeA+-57!%E4?Jd6=-Yw0E_W#N?{r#vK_#Xq zg${_uyWjJbqcliea)ut9X1FQIfcNMW-tyqpQ!$x5GRAmE6no_)EokTF41Z zkHCd*m?C%2X@b{_*`4n~&=M5L)3|7!(rixaEDrQk%U9rh^yR&XH1P5fk9$EMT~6}k znu0noGUnxkB%=4eb(2iQwYP}&mE*aL#{{eW7AswTj-^fs@rw*8GyWK3B=1P1OA_Er zA;-hTozQVq#Hxn$+(KB)W!gp0{TTiC2vo!_rkow zR&vSJpQU=&uD1@2f-90M3eT-Xs}4IuI6*$KVlb( zD2&*i#n?j1{&@6=po&6d^YNog`on^Mj-1yuzimnWI?~PMdSqAx z3gB}oDA!UH#VevyJuID0h_SoBpmp8!cEQy^nVd92XBqYay!V>(`UHJ2e6ME)RXAt% z)$7T|PzVEi(#zGXK6k+ZuNdmXj%)RtDlIh`sN(txPmLf7kf8)rh*xhki9}Orvu$;p zHXO5C@+@^Qi2_7gcex zLv<#pU9Vbtla~&AA!0KI+{=8x{e!7^E{QF{i0aIkr>I!~QlIE7z9Lv-g&)yh6j|X4 zmU<0z`XmHo!zPWiqburScrI!uR7v(j35GFB{0PJ{wJr7XHRC6M^6HUoNy|49fp07- zky&8?b~@5;C2_M(4H`*?b@Z&_lam$DUBJo7mNa}^d}n;Cd`7wgVgKjU>HIXJu9k>N zLrYUrDsV>t>cs05sBRX2VW8xU^A(9x*kGw6ta42{M!$m1a}Rgg(4 zZ7I_|0XhfBdtB;t1*y_&ZIzDN&y&Cx+-Gs5ih%WBqtV36-`%=aDp4Cx^?G=WIl1~w z{UhB_6V^^(>-_w#{`pO`9{nDL&D-gGHf_zK>!8+Q{@#jHf(XoU#H@oFY6eb+i;%19 z1_6o&;+?TV!fMx*AD~D{P$zO#_30knfgkTbJxV%B|@J1Y)?_#o7 zdN4@QO5sfEHgZ9A-*$ONiX8G;;10tB0oK(eEsY~mg4*--a*k^~9iznai~gO^OQ17^ z4D_bo1ZQZH4k%AiP!UF`!62wB1GqfmqUWR3_m8Ko#$SJ6@KdDq5Y`^1aK6usC|L+I z-d1P3xQ_$Wvpi01z{U}5C&3Z@GlL^k)+xb}G=4hP0pk?)l!xQF063#?yMg~Bt5RdTq&nU-11*iWPi+e+TE>!0XmO_R8I16ozCc||blEY*< zjsrB99ifF@xZT`zsAyZE{F-`n6a?0@{M@@-yL1V3pkB0}jE5_*Yk?J-=D4t;4Z#@7 zCE&O|k@6YBpyNSgE)x;rJ0-c7fRAoFWozbI)~WEEJuBY-OpE0QAZHbAF}=B8{hOnT zS2c{}eC zhRE^An(W$##-vOM9&^_YbjqO^r{WX6Nea^CQCx@l?ZbxlSapTtdZAW-v|CaUtT@$D zux2g_>yS)jbiWE555~Dy(znz0s=@yNaexU(E?gKeD(rx;$0gew1G1TrlE4Vx9^I~& z+g058xLt0B;iupXc0r|$7vo>6XcU;>)i``Z^4CIa1Q%|(=G8zoq}L=4qH@lo=fC{) z^yizYtw5Xo~`c^2Z2>;Yqt@wlgxv?Imc+>fA!QnN|ddy413xRIt~H;$m7?2uXf1t}Sl_V#K`=2_{DoG_Tfo z&!MuSIm&F6OlA_t1pskSsStDsAjUGIb5Ko#n54;>i9xur^ zv$?C^jz+b{dJAVrGzk%1uD=MW{}Nx5oyH2#E^+~H!X!iwA4I8JBUD;XbNqR@zNtkM z&p>QK^IZO17nxtPC*r7(JLCLFLESdxxBVY? z<*p}K%0=m(vnoFKR_HDl=>3f%uH01tbr4?TJ!#v*&Jf9uv$C5n1SpKA-kYC!j_xEK z7usp>DpUnztDxh;AULBqWwbOzcoru@!sZnOh}x3Y$kPpZpZaxV#zDZwV~BG7%JmYH z$Fz@e0S8HX$K-{ycC4=SX}UHjpBr@$?DfpiXpT?)lC4cSUdZoi50(u_fbJ|89@_s$ z5kSQnA5e4z`4OGW7u(eXjv^2XLhnhsqo{jk;i!W8X^<2g5J4J{wjfAfDWz1QQ# z>%#B^L@ZCWsxb@g^~_3rc2Qm@l>${JkAn%`sh4Qs3YI)YwRs(I2yyxo3X&X3&geWL zPE#Ky$}i3^HL>T3zzqXY3ljEtIhR1BE|`M6KNF9Vy^S05NTv01u^pC{MkJCHOSe%78!h-62u&DL+m9 zq&0c^`RV2#^`Fmw4ZFkfcwM8uM`WGaw4Z%mA_Ho}zoaYaMnACJS{C3?L+M20z0O8B zPQ3;{ooYdi{q zEpIwfRrXZ(ZP zG5mU`7fK;(gVyyD2C>ighRLOH?CRyk2LlqJ+PQ>*09o4wkTw|WHW=f6@a|sWNIWMG zjNDePNxkG$8h8#-tx6Bc{6J1ufD3=Sv~M0lY$G*<&NwQV$NMOp$q{w(|2C!34Q6iUY?;NYL_uYXHgXNJOls9R{ub za0a6u4AzwPh!|KsQWgfj21HcjXHaX-qxCJyr2sr1Fitp{OlPn~qHPY1)gOmVws*HA z5)l=)P(CvE5dn#}$&V6Wa=ueGb@h*mdOQh)-V2Cw0R=Ixz8cQNHP(nX086+;z#!G3 z4g&GNfivA82IGoddI%$2?g7TnMPO9gAN{5~AJw6fqK@=9_HS zAzJ)=xrRfli7rvGVs%uNv{+;v7D%kQI)A)or*2kAOG>D;skRO*b&b_Fo?P%clM0Um zL_s{FFCPDyNk1xm*d-_}@1~1LjxJDK+<$O#B2o8Ld`S9`7Di}ea$d1(IXGt3hg6rsZx{$%(%%#z+ zPv`=!t^4L!@i?n_yk4%LRBjbb$##6@%ZGu*;zvf-Jds&N|8?O^|GbzimmjE-K3@JE zFJm}d`@sxN?DcHQC7O{egsF@FhUOBMAJ38p15^GHlA!kbfCIEnM%!}XmQlvs_;}}Yf^!faS0D@Q zUcr#x#HFZxf(E)VqmzRBmyg1aZ=^M$^BS15E1lMxu!?%4EaA*7iz z`6E(SR6P23+RVSrxtNl9BpFr}=yL9CDf2s;J1{8HMTqxZp6>Qp6y9ZxC*_Q}cj#L# z$$JyN-Gc5FpN}ZaArd549w&sp`bG$ST3K&Dj6U_6c1P*cuINZOeMG_d_%e+}G0NoT zDo9g4M%f{kjd1v(O!Fo4-6cgf@*GPp&tv2gqIMziUo55{U|I{71thOnR<@9Zg3j3N zKp|KE~H&#Yh-x78C z$1n*qeWtD^bZDv)Wt;)6=L^cL*$!5%*W1l%yOAb!(6n_%DMS!TBP~%?l_Ag6@p27& ze;MB`eh>=FWvwkinV0aGO5n;G{y!w({qX;)AImymYQRG93pKF%M}^7ce7rLXmO!-d zQnwX8983d?#J*imK?f3RW2#!?u~7E(a2I~)@;+ZQ7n%Pf+j^Bn%ALbRj)*{4yX>SN zbm_)XBRk8ip52aAJlr`XChrm*VY8gN*LkuYy;v+%v91DXK(c?EoPQ}43*mTW-ex#> z3RqqV&zm8kkRlW*sY9Ld~5GnllKSU`cSJ=ZGV>1a`%DWWW&o`UiM58<8KqaAu z(+%we4T_D&O(*jEo)lH66a7KCqmz-D!XRq-3 z?V3u@=Ixzq@>u_LNuAGhE@?k_>Dh5Q#VMIaTnsXexHqjVAv5Z(M=6_ioC}0NUG#F2 zM5osJGiB`t&5O^JhfB!!<94_n)5Cq3$c+`+v)#odwJ0kVa5%3g8DZE$R8I4*aQu@=6s-9R^uCL6; z)j=(mCACOctya3Sf=G}Jx_*sk^oL$`=@lO|#h>C?NRTO$uzdP+$b_s8;;Yd=Cp@V& zyuMyXpMv@m{&za0svR+9Dbl{Dj!jez0U8_6L*UZ-HrZ>#fbf|DykbxquU(CF**fahz(1`YbpZDPZh()<|t zlc^scZyD{UsPVa7mrbvJwcES|fZZ_71(?NgQa)<$0Uv#NofQ6Xwnj0km#>ir*?pl| z4xpK}37zSh$eBU^?+zvvMcVAT7$06|l3fhqc2VefyZDSUK#l%#emy_|dFw>eNRBKS zBoAvmClNwD3hc+TCs?LQn~|KH_Nyss(yZ^wS7{zJ`8Mj&V=R)y$;NPf8@ER2ZMMhF%ogABtUR^s%mF_Wn%u_9Tne=_W5wN0=i)*sf+O!JD|Pnf~YYH z7o4aBL4Ak@+lXAf86qTm{ABXJ5u&e{kIkii_!vdb|L2Fs|Niv{?yppI*twI<}CMt;jQ2=LjGeqolXzfE*#D+bHx@3zG+a5o4$O4g@ zL)*$K1qp8`Gw@9jgo3q$vwHI@UxrueHK3lrpF&rxH(=;Esmra%~pu8r0TU!Ls=pR8X!3 z*)Vqy$BJfUceq)&obHj-NAsWkPFtV?LR5c%CBhDGb-A2ZT(%M;^;6DzPq9)9)?kiE zu?Z!+W?ga3=BOyA-Fm!a@xDvOy22!yQGHzW1DOLH;YgLSrs}zikc%~t;t(>ZU`1Qp zD)HZm>`W&T)6wE&sH2MMqi)jFArvA5FK+^W-VJNmUT;-mv(3k+_RiZ>%jc9rolRK0 zY+j+{5~}P&i!dG!{tB`!E~!OSSn6sOLX?-r)H`v@P}b)CEU-gUJ%M~w0e+rE(vv4# zDANz(Hw4Q%#F;v|yLJXA9^~eGr(P)vw4(JEie@h$-$CPR#qg8RN3Y<%*slJ*j5k#m zkCC|iwi*epO&$vFA2ZZRcJo^9Y3Jf&_q_e`>-_mTgtl-!k`s9L;-Yv1|BZ5eKhW>B z5P~O8vd&rYI&4+$n3QK*JgtwwxMoln;|;1wWq5EeIN@h$Yr9^ zZj0zL`nW|oZ7Dl@4hDPKzY1$-GyF*HE&dtSoR^ zWRCT`Oi-`=RN$}YmdQS?RqfTz1|;vM_%-#`v^Rzu`hk0L6%ndM>)WKmUCYL1caIfS zDD%rC0VZKBEi-EHuVX1tS(bP_6d|<>-I}MUQM^EfBR={<^tifv&ka=tK_EN;Lyi`~ zSo=o-dI$!_T)nO$meB5~Z?xLp@leDW_%DngYz(7_nrx(1F`_iSe?&1Mf+!E6l02Va zlTjR3z!`2be~yY|z!kxmkAIhy(I~@9Q1Z$mDhXcX&EoyB zAW=OBi7-XVNxZ+U^&ob2rKCx>sjoO4gXXj7dV5P%=3d|2#1W~bZ^NL&pFTm^-`gmb zFTkfrk||_oXr?2W#RX_`p>NuuW zm}(!vWl_9dNo8;|L8IENOcAw3mCj9Kz_ybY&Xo6gnzo$u5D1J6NGGHvU1z2cZfvS4 z3UZ07?Fep(Aovii7ZHql%hjFKR1eN>*foI`%|o@=#MJI-{O&QZ(_@?_!UNqkE4Lrg91XF ztAJWtw%M>Kfwa2+JSFr*NJA9e(MJmr##TsyGQ_9x&+$~!cLfMjrjq91jiWst#EBh# z_)DViewS+LX;8-~7VnYJx+@Q&Q{gawN)AQgd)9Q2{b~)PZuHg-r!hd~b^C ziBYOVXH}S0ptL4Pbu_iL-YLbY9F?LJqwie&2FM|IsFt*%6@a?RvAe$%7H1}I=( zpvD%`uYQ2q-&_N{iXvM_2b)aRIVBlZ(q!?w{FJ*wbBT?2Cr}?NVkh}HTns(Idj5Fo zY>`_*DFIy$D4rs{oG*&^cED zc6a`u2MZFN9*y!<;ye^esl*dei6;&P+}nv?LGsM^(iUUVgJxV9{^2s=9cfZEB{>hT zEG5}s)e)8CX#r(YmXdb2^_sT(D)?Imc< z%LT0^3tG$L3R*>omMduG0^mX3+Ed=z6RCD0V)H^GwiF_^JR%YMxGxc_gkK52e4u3& zX_*Pk?M$NKiZy@z;s717Sx&mzOuE{~C0(@w^vWe&dcO~psv4!*7iIGG!8~+H>4R;d zM(yM2gSGPDrhI+ShcypcV4)VcC*l~6R_Gwj!y!xeGZ=J4-LKr4wG-V*IGENOo=CLv zgLicP6rHt}>&Oc5ITWF)=y}aT5y_y`@$|gC-Bdn`l;G2YM0B`BbR{CuZq8A(o-A{Y z=Fu(+2-?9})NoiMd7G*^M~m8D7CJ`_);&0iIEx0M%$9jgB!X`j=v(9&HPo~FaXTG- zAlW~8b%5~SE*Jj0Ec|zmEBrV15&q5AAEf^srT+w1l2?UTlP~}0VN6Q??~452b%@;l zCe^b7j42C|^dioK0#Ho>NH8XORfId$RDe7*vQz+rL`PHs%2{Ww3gAVeL}q)?0R$;d z!o>_uRnYJ)S4cJu;NGMeDYA;1#Sgr` z(+K*(sG=fJ6lPPX2qcL2pb30Gng9x7)F5p83bwV^!_Ofy+Q?H4B{jGhe*P8jsM*45 z&_;@UwEh&;g7Fd=Q~WwaPj*!$is&-Z$E4g*8Z73r}^wQPsrwdis`I zSl4RJ{W5^V=mvrwVs|u@ksiGIAD}TN|mRvF5pA;q39W^;-jj`oZ<4& z^~oX<=cqQ8RGNlvPlMV|1x+N*&O-VyyjiYw)f&DjUH+7MN^oGvb=aXNsJ8@--yd-m zEt-E2oK2AjH66~TUs2bD>dL8K5SFx%Cq7=zC5f_o8*SwMnf%C;4oPh|qV;;YZVu=# z>PehovPb`SG+V|fQvWAp8LOgPl>2HHi9mBxhbCE$8-C8u>g3G7{-B0J)p1!Dl^kDe z(R;wy6>!FBGw{v{KG4J8wRWq#(ResKah8%_|jo9m{HUSE>))%rI0GRfqmg5Y! zFA%#1kre=dpX*H<9c+e+?J94#YbxUiAbAk!|)V`O*tKbQxAbZL$t;9?hBDyto%U}3!JtfO1}6RoYQCp*X+F|T@KFyoSo z;RNLyD-WhfFcX3H=ytu_qBZBo?Q$~=KLrG4MG(-j!Ha?xQf0`0a~(}@?i3HW{OQwg zy9fm1GO6q)%t&RoiB!hnx_t!s!hHs|0&xcMoRHRn`h|W^f&>4|BX)5`6wtLBszJg( z5(M>@%{IlJi+il~Cm~65kg?KD)dm@n5m2U2{I@kT%<-$LgGcoP$LcP`0e|l z9ir)ht{asLt0ZEW*7PP}A~a<+z4KB{Pi_3W!Iy+2i@HEqkW$u8yx_f+l-<|GNFJfB zhEask(yD7ZTD30k=GV)ak4vGWvf)zs3r^4?M=APdOEz}gLh&EQdq?~~){+g#o6=p; zBMPK|%)+o=6i&dLQe3q-QuSv2%;*r*QQLpM={{es=Pc;cr?~Y<(odmDb91+f;-KBX z+n~_m`t5WKnT9%%`OpO>vIJdr-w9OkJ;9z`bponbIj& z&|tMVnuJ5H;=bdux0R4!@73%K#`5*NiyQ0?seFwHmwX5blxshm#;e)zj?hm7uK&BA z8v<9+WXD0$EG^p!Nv-QvrdOq`UHhOWjAQWrq0dK9e?$LJlVU1m>bpF!qv7&=dK!;6 zm*-zDpVT{WG__R+m{D-XeNK3=!ruk-#CRqxKMH?Y)7@o;*9c-O{ApbcI*!=;ROz=c zqfl$y!r$uch1@K!d~ERuRY>?NDVfeu+7QNchEP9s(Q4j?dJ(ue|%C-I-t9G!4j+fXay=OoMYOpfI z!g_)>Y_`jg`$vY$n0nq6j)P7a_04F-gLJP(D~u8i=P(;k$<~nKPWO5o=BN;k8dQ4S z>+#L-LnNrO&*K5jzF3YUP||&#uaUPm+)U>Y3>*6lP^Sfah@us^;-_e@5x61xA!}%= zMk)u5A%I+5lbs5uxzA3>hR^QvLl)AeK@J5;eY2VEv4h4ciazY|Tx)OqZ|{%045uzuxb`D8-3=N+!u7l;zh>L9B6GitSC>F&v=-uv zdc9u$|LnbsZyQIlFYG_XxEEMt&&k=<4BtHd7N2R;i8p>ElD+os#bQ8_Ba1dg(i~E@ za&i9m`>U$%c~;MZq+};Cf>;za(_LL%T~%Gr$!t1_s*k!dLiY;&1n2a20JI0!?Mg~f z2I;+D&6hzk(ve2#?gdK%1}yjMeOHtpWiMFzaQ$9~2P399%s$YdNHdsFvaY?i+h_Td z475L*^^x{j9DEYf>pjS&9icbE9`6kfIdsG@J_Hw=)zv;T*XK6HlR6+YRnBa@S6#F; zU55x8gMKzRqtA~g+lh~6s&J!T!37j7cUNdXxm-fD22vP3DqB&-xbx+0jN4ybhntJV z>QfEKdgr_OVishXLYZ?fK(4=pC+4&V2uC1T7g@yLpH&5c(IPtsNb~6r0vDTw^qE z5&@ap%lwhnuu0^c4Krw=-fVj_ncfIVK0}<@PvMPtR>-AXwOlzMNjpm(h9YF>kX?aK zae&L-qi~$f-fwpiE0$w)F0ACDeEfjckVNRz0Rb^D3*P!RT~iqm{a5%Dt-~qG(eBRq zwne`kI$CR)X8k(l(8lm#@1TKY__=dI|1waW=k(EQ(*=^_WS}loQ(`<`ck_wfObysQ z{qzjqc$|ZL)Db&!zhtg?93XxF6WK9%+zrJ|kSKt)yv~*|IxW*ot)CbDlCF6CaXZm^lD@Pa zj)5L>4DyoJb(S-$M_(8qO$eWrmlnRLv)kpPbZM*e1GxZl#9{HSGh|(n4!&$NY0=UQ z`(iP9mm|$X=jcwRWFpRD;Lt&PD$O4{YM+GpdNMcmO2n869Th7xFz4y8Vga`Yq76Hb zNy%Ur2%-n8`o5MGrJZQ7=4}_c z(t&rpxb7=%;g#r(dk)Yh6NeB&Jc0`dUI=GG#Eh>ak#}v1B0qc-1syI}GAGk;O4;=@ zikkDqN1byR+`~3m{D_*m$O;`z5X3#j zoe->E6nK5Vwf^%XC_4Yi|NN*1RsCObq zl6|GhR$mvlnv)09xiruoZH7{Irr|I(({Px~#0gAbK1ZeOTf&J#99g6YaU2sX)S#`% zu-kN3;#1xJ8J{RL1u9U)^46ZCns4G7hoB_56pSk(qC!43rt~h50ftPD@M8YC*_3dB zgElru{-FGqX79o$4#(qsz1PGo_Fxj~g7e))vxz%_C^N8ogv-eKra`v#uw`Foq$St6 zx~~tG6fs0;c3``o;T2dcj03Xf2$(-5Cq3R_z0z-7Ye006qm>B&jkIpTs*ER^3y&kyh+Fwmu(3=*FtVcEdkymtavx-T9*I6u*d;8Xc$2O- zocq=NDGn9khrPC#04dR&o)GaTBaI4s2Ujz88qPz+6-Jv}*X%5Z2Y?{DbuTzBnh zvUIFHHBn5hxe1u!hC@TM$6C2-8JV>~y+B9Wz;v=6^KWO1`82dp$ODMg1cK&pVTSUI zyT%+Pqh^m#E;+S(yk_YcdZg2esp6z@XS`Joo+Ea-&cKtD+)Ew(+nYNheDb#iSdhV! za49BU=^@1qmj!7}+~BniIh7E-do_jT-{EoqwiFK__N zBDAK48jI;u&+f#T<;aQ6V4S^z&EOgkvdMDQ0L^f60kRpK#`V{qkB4zACkX;)I!a>v zy%v`d2&R+u7S$X|Cw2~E@`U$$xmTRpq4TxZ^ELTCP*C^eXM8+m^LhaTE%l=023~}^ z0iIqiy|csh=AFQ4uoQz1YI;#*kzh56r}X6ORFqS6oRRavQzjU_Xmv{0=II1oQ<{gC z2HmEGqCG8?>bV?``^dKago-1fqQ}@j*>XF0YdFMf51h^G!9gj|c{nsblFB?}$&|qP zhOl-xwzV}{eSXYZA`wS1OM8WcVXK97o-E;p+Kt!Um%HtHXH;&~ugYrTJK1L{Y?ZHZ z!f2uwt|l9Fo^PMwU;=Ue%bbyE{KeM9e=Xi=DA?-91)C zpk-nCjiYuDujUA(x(}u1aC#(k9#c&q71pUG&_RPF!@pOXJ+j<)xm*T8-53$I>YK6(AqqtT0Cyc_ETV2Z__-5}KADuHx_ z1pZ{xzxxjaB{FEJk~O&q?~`wEv%LSm9{sQT7fbusKjQ|(JTLFF$BV0_ZY(Oq@b8FIMi$vSrpdl)dZj(l1A>^>g+?tFlMMxt1JI6%b+_O4Ku^KN%vxDx)rSw;hv7Ct3c<0*9T2!k z+lc|DCYB|H6Z<*Q;Hl02Ckg{13X-jgV%PCu#58U%!6u!%WueR;Rti2f>EzfO5BASz zR7DW=zdcTRTP+ti!L>_93Z_I!%F7ZYnTn8h zfMDJoaJ~Z7h?WJ)_MA}?1C_vuJe>B+aD$zum=&alntkF?33o+*3EUNN#ZRhoC>bZj zFAxMY+FiWCXcQ8z<35g_8k}({Pq>B<&|KDP`4yUG96Ij5Ji*c=~O$7>9n2} z>JbZ}k}#wMq4DyX>L%pLG7nSbAn?9X(%!-2qrKIy~n5Q-?Y?-T7){RjHW zJAnGj$K_2&N)kmW9uMAQ5^Nd#tybB=NY&k_OCx2I?Lobz!x8AmLwTyO$}tu@IHz#s z<5ESw49ijAWCBP^yVM?&nii0qA$8iU>7mKm(@IM^QJN}C{45tQvSa|45lSFV>@~22 z`_&i`mPi`WT6iO1*kQKE@i)$txyqSHZt)&w2 z4<8Sj0Zvw&rF-aE;Fw;}Zg66@c2Y4SnMa+$qH$F^sPNzhTboNf<3XKyK>f|~9Wg&^ zx%Wjy3X?VpYmRA9qjzK(?tc1X>1uH63Zc9&i>|Kir#CX<=*hPBStC~yAM z^i!LTQTYi@05bho(35sJh>_xeuyz4$r4u;K=xykDtdfoIl3}*2qzR3_h8UnHC)UZG69++KTYaX4unqit+4fiIz?4 zO5tSmbC5y;?tk#)Aq8kST(9Qn?PpX;Z39cQ%_q|%+7pKrozul10B z&bi%clv-T<4VRII!^}wKv6D4-%&sBNak9>1WHYe63atIPZF?^8(V|zYh=oG#oTu0L&zbxPuoDUgz z_S zzMqfUsXae9)jB}q(>cPQ(WE+DH$?#Vk~m<1762%nGox&qY|$P^-K?xT=~q@Ki0F!P zuUvb>{c%NB85etSYH!}j?1nH#HxE|fl3yieK&{kPR>iHHm=9l~WDIEvpJcCQYTuj( zEXPPwu&8k;3J&fM4(daHlWH~yT}jZRNrM=b4|d(Q&7Bpnl7cLf7$jm5aAg&OCXypp z8iZl2kd#M2e@#64Y1!8lahXH}4JBnnrZH_O@_rjpF&F3t0w=>{iYg_zVSe1rt@BlV z%hC0o&$;$O3->V`)ZGRJthVvxxB5v`ge@Jj9bXS!W3^E|T`rKWClH1GE<)R;(usOB zOw~jE%hgp_xw<~`Cf21-u?X|}6!1EMH9ih}b%Q8L8PY9-(-&;dF9f$a*JWO6&%%od ziegEJw)!vjx%b)1G=$Kwi)b5eHAe>>h)Fd_D|vcAw|+A7LFP z?^t=$CK=;Ij(w_=G^5jiPBlHxH0N`Un|c9`_d0k|px?q!Ie^|xKEfa@|mkigMU)afm zoy`TQ7$oHytauWQ^cYBk=+bo|%sH%jn4D4I6490du;ULUqZ_4~>VtE!eQO8M;kHi~ zO7oXL4#Tq@Jg&)8{)Sq{sQi_#DWw@?;?~RGVG82{K4>j>fdam!EPm-*KEgOMHI^E@ zi!kxERF5p3q6^jm?_c%4A>1+!wJ#m9mo79~`efF(SNZV6gssm!_yFH`Kcs_N}uy*|tim;LBwm?LLn3e_F6+$d-=MvLWI^iIB(T7^_Aro_1 z_f3j_jo%Ee$|$%1ukTH+agCguQ*B#huA!VZeBj~e1(C9V- zzvbpkE70DLV^TlQ-qAI9c`<)Sie&I3|@b74tFuU3Y7qI zWj}t0KUF>tS+c#9kO~l1TFY>uxr_Yf+3HH#&UsKdqD!ziB>2&{=-qo$&bVl&GDFZ+ zQC>~~t4&_cX#JDL{5%TKqnyG|h&!z>XPZ7%5~n-mL1R;Xi^qBfhRvjN$o6T_#%vvz zg(UU5Zk)DzT*9B{!Ji3-6By3+Mtp#Nc#=Se)?{0A_~d?q%c`XkS3}r~#36OhcL{cN)qa z_3fPqgi4p3;0jCKNS7`*g*q=wsrctl=`G6uR||ITRd~%*K9l z9h<5MdyNG1(V9$o?{-Ms=O%6G)1}cJbHe$mgV5B~h8cm^hC;(kXR}*xQz|tN%33_Qr#^K=ht}MZM52 z!Cx1-!?)jfZ3)27j!4zuwc*Sz`12Wj1?`|ODZ5W+lngltKVMFEUdvZp^-7z?N|uxrTHH2BRo`Tt$}J!BLr6BnZQEEPH~%rHq1W=&p44 z>2Lx&gWKVP;6B%I<`|b?y*tMN56(f5GgQ_X+XOx`SHJMVU|)R0{4!ajn#c*|z1zo-}o23SqvD00FdAjOwhWx+IF}C41G$* zEkM$mEKw_m7F2XtQz|Wpw0p)sc;eQ>jeh zoJyzCnp6A~uFC3J5X`8N{hiLKBmIre`r_WCH`Fkd2a+yYRf6THAos!hQ##Vp&NC2dV&=Y^d_EW3<9@8r#Dhr$!!i74qoZxxAOJ*?UU1yiS);%Bla z|3LiK?ox3Q)I4ufbYuH5{X?J1mSSg$t&V1n2oo;+EfNZb+)$1=PjHxrfc=l zhwjS#D>)S!+{A7zkYp&!0Z3oaYsKT|IOEzJ=IogtUf-B5$jQWw^GL0LT=UG$;sw4J z3F``^ClwW!(rueWivU>U;#p}wZpUfX2GJU%IJqQypf-{P1p#PM-iz1J6aD4{*XcJ;Y=+GpCz zxW0jxDOVwT+T!XhtkBS=GLqMmD2hvCG(qxlfPlBO_WYv%W07K0Yr{y10GU28RY<*0ti?a?;oWB%U4HO85`70+G1;ZL1j%yoqa^U zw;9Rr=cWdrkd=1Z$ZC-=f>N~}v|2ZYxDxs~`s9drN~r~DfuJG!=+ldCt7x1;7upSg z6ehAh9|QM1eRk2VWm`$ebMrrNA+RivuGf$V%tfYweNyV#2^D}y&`f6)$t(7WJ{Ke; zCUOGf=PFpwXIW*!jRiZzt$@=>1L86z5-nuz>L?T1iZAQWz#rr4g(Scq&tW{D+{6#a z($s_BGtZ{(^UyVlckr~w{~X?oC7iFg9MBEbN0pMv&0;l~orb1nc3~L5`%6Rd~ zyxO)C;brY_{)xC^QMP&pKbHw=6~^3ojBj2qKf&6%@2%0L=+fnYuNEFWDiW?MHw`Gj zh6o?d&_2$c=@4HpU?JgfOFA}8uh2%=u=R#2Rt{uX$ZO7eh=0=xCBjiegIEL4^;V z2QDiu_`2PR1`&1PzO>G@w~pD80`_mFGZc>&gm<Uhitz%U`xi&yEY8MwDDu(Vr;-VPYhTc(8CmMr#X#rr3G_N+q-~rfmAwUnt<<-lw|%zCZzfVpOb(>o&Ags(S3*S4 z1`DBTc2tEvaU<-5u`-;|Ov#0q=E`1(>D{6s38#Ut5aJHd?ou&QZ+Y_6P3EqqI!ZDTiDQ|L96)j8R0CZ?H%|46KS59KSuUE#pHcyOh*D#5`f07D3C zAPs|LQgan3p)lpldq1!qqC)xV)QOa&^hTenZU7IbBKZ12t|rK5lmr2^-5}uzqsQo! zbHz6m)Glx&zy3*W6CY8v1NDu+8PzfJ{;8@~R&neG1K6siDTE7G)2sF1XT*QZCyVh% zKm2GYh1`5ajkKs))!lV{JWFl7#mQy-utD<*gEyjn2roBp<}(QN`h+T;P6Y|zLu5z> z$$;af&B-$QUOecQCQI|2Tbd%zV_eg)JF>k(%Ob{G`z3!md^>pD-Z@y)oJ*=gFrW_u z|Mmuo{V9?K7cPxNWl*vJjr^_5gG>>20RbCpTcpj+mvoWg^|>T?_2|i=z(9!vx5tCD zgV-olsPFnVI_?Dh4-^lQDfgP<5JK=S)u6#bs>;fiEsMk!hs_vqCFJ!G*~!gMySD2z zem*j-6SPLVizALNUNx4w48QfJIVsoW){ggITZ$!MQl*j*X`XO!HoZEXzq{NX=fVss z+(*j8hl?^fQ%L!85npZ&BOKiI>|Jz8f|isVj=KgRXPS#V3RNshuS7SnDpDgV)(1^a zI+P#pB)L>H8k=fJIz<2J!^`*7!9Al}6&J^<*8kb{|NDOX-?5HVHY_W0+O>hgt0;o% zGOy4nlTy<A;X2&RQ`1o?X?Lid0E1RDj-~^dkC#u0B8bKf9LyZ$}A>`e*=l zVbMf4sUBj6ehoLP-iy`pU-)@b6EM+YF~D$WMi>sw5LCY;(ON4pC1g`nq#Q+SVpF9? z@LH)FhyLzu6b0VE)JNou4q-_aGFP(2um;X z4(8J%O&7eg-Z>F?=si%8yjXk2;wfOh;&p!BgctMAsar?p#Eu>cF8OLhc~HY)?uA1k z=*oCG3^?aKU^>h5yc5>OSon_}l*70(Rsvg*QqY#%1&$A*x$v{!1QwZQ;GsO&6;+TW!M&;lo5SquI)R$v znb)L8{}LqbZvigTrD37enL`RRDJ7jpUWL`V9=dAG9-Gt~0#KJfo}OLiMs|p&KjGF` z@5;!DL1{I|H6r9=Ej+zyf~OID47n9q7sGd~RkI|KJj&tFIxNcSYuZGkazRrAjWw>p z^Bm|7)Xga%q89Gp{m-0w*MKd*)^HRlTCue?R+(qxE}*<_EubiGtO6)93&a^?ktbaS zR7aO#2~Z!!gw|i61i==vrw_|Df8g1yZHNry4)8^jWKZ32Xh#_i?J)7u%76*$OlB}n zMc^IQpX zbwAQjS=-^KstMXJR+}rXeY4N-pE0~ZRNKG;dkIKzubvLZ!d6&(&UNbb+4O>Wynjeo zU7RmE^1_s+;oxtwZPKCvd~Z!8U1E(XEo=KW@+k?vNcYJeSNm0Qa1t-D*;!v7l`8dN z;hmsST-Vv|ygaTe+MQ?Py7cL-9fKjr8Z#VpOD-=CaFn1VM;y;B%_cLa5UOodX*?HV zR3vZ=nM#foI--VXN!oO=50G(a!+4zRN~H^JTpM5ormcFK3x}%y&fLaywYG-Ww40$X z3@s_Hh)}W-wb5mRw-5!i4(XzcIK?yi+!B-4NxKjrj zdM_8FB0B60+8h@AI$=}?5td*(FCULNy+UZ!lji)LUK|i$g)qDUWk@1C^vTTgY-pY>Tn8a#oP1$Ls#`Z(07OF`r> znLZrqr#hmxC&v&=wI_24B?7Fd_~GePnB)nkoTaKJn5sZUGCO3JVpiR)OryqpN|mGI zu7?qruL5x^OOly#IeE7+6HjE6ty@4@K6cDCMJ{%1K(S*KDbh%?%i!9%w6aN+?CHFp zQt|E{pLns1ZJ4Tn7L>ZKs(#u8dQ+RO z*>EbgSYE=rmCk2zA+pM9K!D%IOseRefOh^^b#2?QwjdV4;J`5@bTPaeg!jB%^w95?tuy zT4GC;qms#eZ-yfGjG0?(I=883?$u3jMuoSFH(0O`xd_BOgq|ug%SQ7`7y?geX-;vL`kx%S8&d<4_l`=SDlTs8g({kX-<51 zsQW_1GZV?C(or1Sjy=kFWs3%*;{M+&7?o^xiHS@7VX-=&EUYScy8zo7pvX70CN2eH zv4H`+_@OTcvRKCyAh}G(7lezEq`<}B*Mh5{tQr%KFjiLN-ca2m4r43!zC}@17 zYD8i9p?!vbyc68iPyjVRQ%i$AeelF+-dJvDwU<$bWaL$P`Yo>^H>_9CoD%OcP*NQK&;iXy3yf-Q;NGZz+Po#*iFG1{J> zod~Wm<^()dfw#8>BcPMC3js^r4aLPn!LmigAv<^jQ-(6bbAzbovecFXw$T}P?am*e1!hIs>5*rp15@1LBuUaH z1nScsYnut{+R=5Xev&k$!!&xm$F`d)zPCch3mwj5;Et*;;K(*QeT3)2`KQxM)e}&` z7o!zyxMwP3wKiPOOXc#(MyeAOhJ98`Z%x!5f&M9Y=3G*_4N{7xok5Ydj(sml!9?^MX3ZnkClNcvx~TfEUAINieCj zLoYDR7y?oZM9E%j>pFDCLM2oO{!(|T_OS}W-O5z4gR&fN`I2hwz*baq>{M2BsWJEf z1!Cg1RG{g4ZK?DVqwRdYSS+TfSu9pRGc-q$4T8R6*S*(9s{s^oC!1JwIlm%3kPhoJ ze9PC6%^AM6w62)DBv2wb7A%))or|1hZRD-2_+}!~@MDcqSb=~vn&j?MIGe;Dc@?gN zhRWev=piq7@ZQee!IvFTl^mH-U0Qu}*ImzDDAMf&z?c*h6B~N*nFxP}+Tfp&!t`;A)Kf&@MjQR}(JSy!TsW6-qZ4%>qwZ59RsrZ!4P35Ov ztv-ZYy!>G7@f2O*J9OoqT~B0<@Ti$zRH}5dH^F;;mvZtnE+;P6Tn|k#Hc=R$0qGLP z-vBYPYq+k!Ee6l=iNihQh-eGOwwQ|bJ#uYgZ=`B7zEAUA9WZ4#kBc%6+ia)9p!)s!nrEmp4= zs7A{d+x(jGd+7Z6T8A}&*q{z9I@U=)VL5G8Jy-HbHyC$hBdMNaIfexiRvC(nTX+_2 z98h!{^$*38U+1O@VtKIZe}BJk9k(>J%JI`q0IsQo77j=BNy)tG%z3|CkK!B~ib-i< zQq;@3=Ull+OmVG8D15}ql*UeT=gMhODDRenRwGuiaZFMW@id@gQ_4`|AgKC!(-iX- zS844KnK+OFDT4Es`eu$dHk4uZ`>a>doKpGC(L34%yo}O8V^?O|RuVo6>T7QK04D*W zoqPW|=A{-dY75!OD>K9DiY=o;AV%;h!$-O7BK1i0Iofo%r2 zxj)8?g_tQb%SBBhRJLB#Tx7fgnb5Q6d{7F+nGWo#^aMh!F20dP0Dsl;s}wvd$gjBWOSXT`Nr!EW1t|0q`y4%c0k+$>B36b z0@eDH|8&6){Y(j#z)`{>Y%tBGnZ_|mI$F;2UarHXIJ+B#-%3@myy3*Ep-t^&`e^PT zVcDnx!>X@E(}bl&%WRK>Qes1@W751i!_8K1Z_-S#C^$!gg!3l0GquqRA zlD_LsvA`4xpMi;+anhxGL9^m3`iSlg*C2H?#kx{~&1_=fLgU!Y`mE;8CM|e4mg;gX zk46IFb~2l6Cyo`W^r{beFb2I7u2mCI-B7PL{tR>U89v%;P%8-^B>*&vBQA{kspJl$ z@GV=-%gt@#@`OgGUa82^S9Y{Q7&K|4)u`*qOCbF^cRO>rrQNN}m(ihbXNI&2eJe8* zr0BYe3X;oING0kV2euv=+|p!jI=Gz)GlXzUQ=}>3cBZ%^QZUotI&YYrYcQB<@B^?a*uUyQ#)8KkFH$^04USHHy3_>=JBbY-7 zhn?+-l&Bgh-Iu%Vx|uzaMy-}0+35O%_=laaxC4ltw4JBKb7T8Yndr}a;;wTuwkoDr z1FR8{%?d!9QkTZ&=>n)WT)#~o_|uWs3dZeJfWFLMc&ke;$c2e*?@C|FIpLCtj<_$l zv+d?mJ1P})`+)1Ee{V`xyL!72RLys~?y<6?7gpWERjh|rPq0WYxpY3t90R%ZtJgvL zDZ((e6U0c(g1PnbP=Rd?()?ip4yV_YpNlK@9$ZGI(I!gACX10sXnmi1lkJYSM0C0jsc(`d25K zO8zVIiYBYe+HuHB=1wm9r<27r&*4twZV`SKJTe(=wzq&~GZ0vxBx%NzRUaOddea4w?w0a&}DHz%RAbUHMYY zpY0lsywn2iLrQd~yG^v(bRY4LU(TpQCGS+)C0CDfsv?KdkxJDuN?M?h>57U92KS^t zs#I{|h@cM$enShz@ZEeF@E2$XIJsG@CbI{Bg3w6syDPpizOs0|S$HvlbTu_H!RvYt zXOlI=44+2;P{f-c?p*S&*m~%taugKeW`&*;7#<{j)cDKcurU8{XaGn(D_e)wXS{b0 z{&cn5-si?MUt+PfX+#w#wNbAHPA9DJuJ*vxvp`{q;qV{fl*6OUcjkvFb#c_4Xu9^> z-x8NNr0rz;S6bCC-<%OT4By-HNMNerkTg}Elbdis`K5sKW8we6g(f?}%cNRvEP)Q; zMD;*2{-Qg@y_xfFaqnE{?cv^~>R;F zI|;766INVQ*-N2+{fpglN}-Y6Crc`a{z}~ZVZe3{v0sb_QwXBVQD)cw`{aMVzyBSSGca#l>8X1&{xuEc{(0bfqu^^? zu~)g`>*ut^MQxEJDjRex1!&5A+T`Q)$c7Z4??-_Cef;X+$?F%7UOj#J;>_Y^^i!bj z{^A#!hW-lG*F^V2`5mW8U{)Z3l&$FhQoS*3Fj}7Gp+N6b3x-Ay^OpbpeO6^fDFy&| zgF2Lvl_#bSMv@Y%4OKs>#VM#PV2XW-Zz#7eIcP~S;A8l>t8%XJpv2+-^GKj|rjuP1DlqZ)cMT0$pswdL4IcBC zJBMhC@?F7vreMhYr6EMx@h1LVR2!T-9AB3@Ovaw#??TFzk&?u;Z3(;-uQs}JECE5S z8Nxa~nC7$r9s-V)U|C_!sUR!sVkA42XbYIPWcj23#4ugIH8FVFl}oCvSgSqc>S!9Y zMyt<{2{^@6n&t!a$jI}8?$u=TA>6djZkE(F+Peu*p=vopcS&&*&@*-h_6zWfVUVrI zBz^+VJ7cEDjArx}VTUPhqEKz>ueNyK`Ra2BDGclRH7Xl{Ei?P}dWL_~jf&VR&+t3J zQU%)sv8-^p{{teX1_%BFBd1yz9DtlE3#xC;f|pbe#K{XYDxLpiX_R?%{bj>wuBOO% z+$L2X9a&kWe{ivkb#f$Hl2VQzwgAbWQyRPSSIZl2V&@`t*EE1sHd%{r>&@` zt*EE1sHd%{udS%Bt*Ec9sNcHxR}y#Azn%};Cnx>(tQVelC;ng%p1030!t;LEY7b7l z^XW;)3wx7k`>%`n`DU`Y`77%drXs>$qs^55@Rv>W*Z)8Bm%p^TQ*Scu&ibvX7q)wY z|C+wr{pAEVucy)swrH(cZ#RP(YOV(FY}HQptt`HWq5es0vb-%4zGBEoYf5DmF}~H0%iLjMYHYM^Ur4wTYY$HdO?6fUJ$=t-+T(9 z?PLQS(dKBz+Y$KI{GVhFg)L^J}v+2 zuiY&dH#!_{)|F2qo1A3Ymf&vxrz8SMJWdgy%XK-m>x zrEt~qP`t3t5A^#04zKBDfYoBGGQ#JBN3zfl2CvCF@ebk7vJQNSGH@l3@8-*G|8qas zj=>3ZC@W_7czURP`%YXkFVSs{4e3p2Te|JsX7sKXGw&!{(WWTEqYy~%dK!MTFx6UI z3AK5H>rIFa3^(fydXVnAZ9DQ?n--yOsU^JL`FwjdiKHrLZ#3UN=U<#CPlrlljKw0H zZcUjl0KpYv0M06aV|8;6=Zq32e+0{Tis;;_>6Y+3I%B0yhb%d#{o?y1SCez)q8nv# zJ_^pt;!su<<+~uWA|MQ_=i%N)Cmh+`U_`3zV1}ecmh5C2XMI8pp{Y+V(Fc(1X941Q z1dk7mqp3c*T$XNNb6IZ2UK`~S=O`BYE}%M>(vqSUBTemof*{{VDP0;U(1t~s$(kS{ zE#t*%N%b~w#;tdulLsEUpS?|ZwYm-iv#oYZ_eX%|;nqT=K_nv3F?qO%Bc|B?&m>CD z!jA(6DBjq&5v8{&yQ(Gd>v$%Er z-GJ_cmY+a5o}Yt^(Bh&uovbHQd^*K-$1yJz=cMUC;a}~NIG5Yaqt~9XM?nR zk0n^!{%)pw&1m0qI>(fCPcs;>3+pVCIb;MmTjAz9Yn`7Uj`@Le%Nkz z+TF>-o1C1-wk6zSBA=I43>oS{H>CIoYz(n=$Q?v??WSU35)(&PV3O1fm6vFjV+OWNajbD;vT(!@j7L8Z4h|eP z6|moAJfW}R4(5U)$i_yO<>yhS0=?RnLmB@ROt9TsnAc`@tN!`kLEJ4A_h`M`%vx8I z&)QCwA&XwcYww|Z7Z=4#Xe2!N-YcR4_u)PSd9cJ8B|(gC9Sf7%qs`@cMw>$bDEit% zw4`}c4G}c*t}M>?IA$Hv#sG4FGRXm?k0AX(8cTX2J*KV0MW_&mld=h$Fq}_G7!Ff< z*_Hph+0D`<$wJ1hHn82&kNf{=z7%sOLT6y_EPjaz;Y(+ZFdUj6HfN-aX26NqQdz^u z(O})1^twmJs%D@vAuwd+_+m?T8oO)kX**e)M*H`2X7nR0(wOdz5?U~wHMOG`)|j%T z8@l~ivBr#j{zxl4hJn7Yw3LzFEbV==G&;vP$*R4ccm|!;BP`BwEGteWs@FTL{)I&r z`3@>R+web?M)N}-VU-m`+SsE|SY?q)Q(Q(}WnI0(&_SDfj4|5X}C5i()2R)WT-9SvZu{j>OML}~|oA3*PNx3u^kZPcR zq51G)p3n?f(Y%}>r0;;%3T9QWZqP>&Gs+W=pxLck=)htB)DPO!tE@WOqmd5`($ z>+OZt#|(rFY~g^L%dcv!+9#QlRemZ-M{_qZ}BOJctJKsIZiQ2Ev=AN!kJyrC*MX==)0(G zCpha`lo|Z1TtTCOE?SsC)tipHu<7B0-M=$8O}wt63Gd876t4p(Q6TVf^lPAP_?Y^9 z2yl)a`AuzTa>FaASd^Tz_4+6XBV>V;v(4%quHN^AI+G)=@R;;D#U|1rDV$c~4nQ>1 zL44%(^NX-s?fi-^L#Y64&x?x2;iQ71cFIL>y~Gi9>}8|VB?vKwa5u@vOC|0`xIz2{ z&c7w~^D26VP9R|Ac-*;;qgp)fqdl-1$s6&2dJWZB9{Ev_#wxg(t%CIRayH($=Mi<= zR!Eu7!TbJfejQB1xzual#}Ec9JDV!4HRQQ5i5K~(H5dtP#Ac)701nFxS%&CcSS?|N zyBxkzb#fy8(Zq`O>b6_RrcwE8&0N|oXRk*O()(uLny$k)#zN(eCj3yBukfTc%+W0 z><=x=H4#DVZv+rWPy=*Q05N!*95pc8GPawnuHwsC_s5LDkW#X?Qw@2Q4MZJM)qf_ccrPPu4HW-gj=)aNQ&e0>{W~gWv6)PML z1+fq4oK(tI#x3-IdHB1;T$-ioS~mbYnOrMG7m!N^x#5RHx@gEJ%Sn(8i9@8ElRxVD zGWLd~a#}&_E@yxCDA5+gr?3f?nbY_$_t9w6S;kpp*8wtNNlUFh;~;i#?=#(cReh#M z2+nh!S`091Uzt;j!&4%FM*iRV*dn@j)Ou-8+<4UbM{ti;Zg}`DE30%6jX{O)=s9zM zWvR-^`Nj#T=E-kg(kA8<37m3k*YP>G_r8md{rCy3mli1_PDIw#|8DVPsSsE zNry#leU765QS{p|Pdkp`@BhAr7@fL?Ac_$|;}s}tVyh$pqFB7rIw%>EzLfEnBYRc5 zEk%Yp2(}UL7BocA2_2|N*%Sy9uvXJ8N2=?w_{F`JlySGHBT&^f2%rW6o?qV#P1h$_ z-~oTcP9MIk;tTa&NkHOnC3oRkSk^sC-`^*{l39XuoK?uF!z=9&M~?`WUeIB{<$Hen zpQz=@fVy8#y^<_AQ>7y=y9eK1Qf5!bJ+7r(gpaFQU=e@5|6hNj#ZYMu`qfAj_6`>A zXx$YjqS7WHe^XlRoo>gZpKa|zVAVJ4P64j_*74c{nv}8m{e7w(l7dLki_msHnZG*s*K!4{F+EdB;8`aMTr%sOZE7zDJ}RQW(Nn6d*@}!VG07TvlrV zgj;GlI5m#%yJWW>$4=}s4khy0AqWyu={3|lEc^6&f)dBOd8S|X~;LP3A=rZ~75 zP0%yXzPxNrW+=o~^gG-O0ycR{p~K)3k%S8*p-32LO{l24cp9L(qNz`{$QQ_Pu}g6$ zE?Y#vZN}~m;cyD$Am-3Ph5YaW;o0;re@+m+M`UYN@h#>HjM^Xq4d(@S` zu^@twV*GU_P;*R<7&&*P<6kD@^DyA9*&^6gl1vib}l*QCR-;r0hi&h6T?7=q}AbaDi*@{M8%?1HFc$w6P^gKk}=P= ztedHWkXDr%g~h8n6m3c@CrIQ=S{6lvL*zSIDaX!NkfL?adLFSA>mN4Cc`Z4VPqxSm z6lYpA6h~Qb>?n@i%ZQeTm+wJSE}RfSptZt(-}nD*6_18Q!Q+oh2_rj!x}%0=@n97M zk{XTqpi5*NA@s@fpP;Qs8TY0&jk^w+f296LWvwhsWl8gBEzuVzk)jor@92Uf%MhXhBTju?<-# zot$&FXhVj{IYT+*+SCZAcm>T9&ZLxZ^4?=691bYZ2xhH81ID4QWEdNwOH{^I3_#5@Hn2zzn9WPX zDZ_$mts)Xpgg_UGuQbkP%={}jt_Q)>QLAS(>f$XU%fLo(kBeXi9m8rUE+!ZKW7sLb zkvaf~s|V?|e+|Z?_T%6lX{!d&U%AXposJ_*UVCoru;Q@tyS7_{s1Y{A(6s7k5VP}} zJgyF)%`tu(RSN+elRbn$E-$1^2;foLnu*1DUkrj4VcQZ{I`xB+FzG7s!2(Zm1Vvn8#JxAS}z4NSA(t) z%k(C%uR5<0UsAf~S*RR*TNus;D2gU&pLH(8D57j^dfV8cCudLgt)M9uL!~QRX7(;< zfMGFRLxVAz2Wgw++soX@-NY!D|B~BI1+a22bzf+|r*@c$&Bhjbs<9qAX0(`8ehX}= zx_(tmUdgjVv@+B5D|6OLl2a*zQ!Lkse&&ZHx7p=@qY<& z^Rn{-y%cJ>bx|?rzV`|}x90Bq_`a8)hd2v0Wk!|cz z!;F(2mL_6BR*C>YD^T+(C!^Mn5*Tih$r7APqM!s)ZR)buQ0)B)>huFZ|?q z)I*Czi}J7gnv;(qOrnXCFgm*uS4go_uzrOag0PnTt)}UyOHlD}NXX$^aXRoYA=)C& z2;+?P)CE&+d|yxe<{#cPo><-Un_OU055D_z?&&mlAPsCqL}E?!Z(S+? z(M@chCqCNBadhTc_`X+V%xdL~SqM&2)dB=yRop#M++0((jC%)4oR~dQcXdGLkSx$4 zH203tFc7pPN2aJ{tAYoR6$MY&MDVmf?k#w_FSCNDy>knmaV&SpY}Wndw<1i0xmvT9 zxs!e4xS5u_4w?9kP@8EF~5gDqS zE=8!JooS9|d~_>8l4jIF;}3h)|0X)?1yGpo!Og2pd{aWi{CD?t%8n4C=lznq!r=@3 z4p%H*mN%FmHAD}+-HKjS3DGMphW3Z2MmGOT_KxZSCVFO)1ZPqw(A~E}n~m0UFN*AF5SP@0&JY>#LP@WlIOB zDOPcNlWOQRZCIvnlq%Av6LEE^zHyF-{jo!FDAw-F6LDBd#GyvSp^JzSJ*yKj$oO_E z0si6XKTZg6ra%^zum(Dq=zg$KK&2(|=2ijsJVZ!UN=u57=T_826nHmPvY=A?ZavE# zBYg2S=-=&2=P|-T@;_(w(r+H~4FV0mdL2t_C!FhDEHxoI9SEDsUYv3@O3{^^=0bB= z%$*FWz3FU>;^nP}dk9q@zzjbHUi%4xoD;_gS0=;vXbbK69RJL9$#gHwnWiL+Rg06@C_d%kT=a)AOpj=b^oBM)|cGp4>#Cc*WxFE4FZ zTRj)u{u$%Nm0YNjhH9v=gW4V~$Vy*b5WrEz==QPPEN`8i#s1*Qw2NCyrsZU+->Q2g zkCtZPjdz(U8ecrC(_L`!+=GwiTvSCqYAkzTqrcUh$7HvmOCaa*x0(l-tmAi>2gsZ9 zEvpOl6U>1_nYF6Ox@=ZgLLQveRmq^!>QlipOvGu2TZHQSH8Ht`it8 z+7dOcqk9{9DKTOUUiQjM;q}k}io%%Iph*+|!Wkp+dTrFweiEAjmVM9cc>+EF0f9Ujlb)nYDc@j*03$->R(=62b8yOTY1(rj9&2gobav*bp zb-zUJc2rQ4kTsoEicoVv4QFuY+}bKw*#08t+Q z-vWrNv~>HWMoIutY_a+lDv?Ti%Tw18LnREtzy5B0Uv(HjeuIu124ME+o2Qy@BDgGl zK~Hslr<*bQYELyQH@e>|RiW@J@_H`5F0bbTNe#JLXB_I`E_EGkHy!Swes<}3aOC!b zDT>*Ch>YgL)gp^-Wm~yAuAJOKv2FAT55h%w6)v|?Qa+wK3+SmVG>GR#Yg8+j*mbf5 zAg`EfVbf=-UVer@kGTi=>7_XUiYAt~_Fj(1nUQg3GOC<=MKDH6E$*Jc6tGK~v#S7G zGpzXC99Dwe62ALza~;ksf@(L7@p@Dp!MqNHO{P?LiYsGezGU|<*JMZa+{I!w9S)_$ zyCH;Vk5&)L)G;oa7PT%LYY)&)sQH9Jhk?nN;h(bF6M21MlWkJ0nGOixDKps~*gySF z;>!7O9A^Iq7Rvgrqq`jQ0GveKRIqpXs&TD;do9!hlK^F}pAIz%n1h~J%T9^2A=$@K z3#T`l{~89;_dZp@N+GV?UVXEH8R&>X;Gn2f#rBNB34bc9 z)jslU12<>T_x{OW5=7MvM!yC+DRI^H(v+^;nf^Tzjft-Mk|9cm(9b^&w?Z>(on75F z{V`Q0_m4wDHrh(}Zgu~`dR|Xhrhpu zK!OF_m4s*gf1_4}@;EIoU$K->lWxQC2Oyb3e5qiV_2(~>$;6UM4<*5$%CeY7NL&^Q z^Pe*_IDPB7OYNRZ)f88_sO zgGyB*_&DR#p0u*2tFhNMm&G@N&PEN9WHl|$VOuQ1=~l@!g%V(TIgPycxx)0L_H#4F zvAo-f*XG`fuc;SDZEc{qK|iG5Q`1L%YRxI6UQ_4NBBgjxS$GDP8E0d!Nk} z7B%AQDa!eWIL7AsWOZNd9H&fTT3a260u#BeYagh3tSc2wV>S}1HjJVqkTq3xZS;?z zn(@F)s`&S7G8*(*sljN z$Qtv$&`rIC2%!>%#hQ*wZt5wvhqCSMp0mo$2y=~EH-_P~&)qS;FQkSIwXia!B!Ayl z=ybvHZyL#PkD8H(9(DML;0CZ7$v119beZixg_Vuk&BoV}xMa==Yg7MsL9nl_1B6uvsjL2246jZ*gGuh0ym#v4h*=fkV zxg~Bw4+BX?Pww>H#JegE#arbY?0qM_V6j?K4`%+6C>8nOlI<9^p&yz2{9+_YYCVfs z<`)(eJBv#ser%ifU&1zznokwoZx5tZSw5O9@cIq&D9Q+G`A$W^YKQ`tGq~__%EB(8 zN;M?|HVI)IX&o@vg0Pr%02jWx&&8C{`un^r+h=N%KJwSWJ^b8LkH5Q}$(}Ric)K-h zxwtD910CN*3jyO>LSag7|72`L^P)P(?`he#jQpRtSjpXNiCEJPI5NVb3C9(iB1a;7 z`H8m%eRxUlbr^Gib=5=EwCQdVB`Omi64}H#xCiAFgCg%VrYN=gI#jC16k(8WF+~9v z7P6=)SdU_}!5=!NC`WEEcIIM=EdEYvl!-o2gUG#}HwFrGIk1moJ~u3nnJ1xt^UShitL(BGAHf#6;qTP zh_5K7$RfT#Oy6RPP(4Bd0*cnz|Fi4=_kI80bYL%*-5@0QVgiz&Jl3zTP;?VW1f$G z{1)Xr9172VikoL#=jx6$689b0dHuZ848MkFT+i^c)L7??$2a_MF-0}}uNJd~bMCqU z|B+*g+{ogXUP-`Ft_TuQ)0r=4;b*TqU!sQv-(-T#WceC8-fx2+F?u35vEwh0CPObh*ilgi892Kf2X0!BqdD{x=dxfS)wUEX6`{(;GM#mceqhJD*;L(+`f;KD4Cgk^iG9SXJn|B|N zdz_y^wGN`<-oks}lkGeGAcEh1W&T@Jg%PuN$;SJTWfl?9{fDnZ0D23_@f_wR6&)AoTsCTGg|#Y<%$Dl-2o&~$<(eTre{ug zBf620Ccp#tnfpH9ZJA}G(OOQWsWpAb`%?@~-bbkA0J3t;;%J-z6w5V2PMK`0lxsCv zd_TwHNjs=!^E|jhy|Op{32R#%=OS9^{A#@@lNl;4%V4Og*vSp&BpS#b$F3#E%5n{_ zMXF>lovbI*`S!+~%KGp7o%Jrl`>Rcf&7AJ{Mj4G?o#zN zZoWq~D|y|tl|`^fn0h1?ufxqY_;fkP#b**t=W~ANZA9EqB?6Yfa(?+V3kNGG{rDQS zE_j>%Hn7lVF^ zg&M(nlHU4Ugn0epx|Z^lR_F4{U$}YE}y19kAm=Oy}fbZ%FR@n zz`bjnh1p~~hxX@ni0V@O!NOdO1xKw_$9$yq?x(qxUfK3gr6~u)4P*)JoL%xMa=u%g zvz$LwzYwkBz@G=Pitc%&o8>J>3Bh!XI*APP7YG#r*@JC^M26>^4*|$!;xD${#cK6ox90Qcx>;mr^n4h7w(~7&s$kxG zkMPfPRLiB-L|}k2khz){?lwG<@XSu`McQb*3`%!j?2`noy#svnRbY5WO)*K&d$oFH z7gj9eNSPqRVd@dyr#p`UWQa}Wuq?!m_&PV-w$6@vjQIHE&p=Gxv2FU+qR)7pCcZ4U z&ZEb56;TxLt?N*b02okVXO_K22K8QqP&CdiSDWpl`DVJCZ+9>X}O$x5RL;e%- za<+o-0>6Sj>lO||LIKfKq3Aczh+NsgWCx=@s~IYlBjI&3xnWxOPUc`>!F;(4H?*Jq z!0Y_HTHIW%HtWmzl(<`LI1a0fm(%e$*S(Gj2?3Ku!tvihg=|bYI2r`+(NAc5W0UV# zbD$J-cxn zLB6*|F4L({CvzIB?$Oo=tFZ*{5yjirT$GtaEPOjPEMNK5G(2NDrY>4%LjQ{TTyh>z zqRe?5P$77aGu*IwtO#K-cJV^yOa4AS_u92 zW@PDMU*=BOveo-Erx&u-G2l05=V zX`3@MoAcW^KVxXoiZHiEsN53w+>e+0?*|O`XMDvF?sn$fkI^Sn?$76LSkTu0Nzl61 zmf5*@S-lD}i^(dEHmuT~y(ifPJa;cZN}PE=7D}8Iu1rV0eF-O3G1Y=wtGx*)vuTMD z+<|3!Hx+6z<#Du|2Iem}r5Dsg;^*)vrDM>hV-N8k(e5h5g<-x$C$Whh#*u%B%SIbx za@2KRDRIqZ24rS_1jcME+L^GE$W?Zsx0!qj&=JnOe>(YuK5S3Piqhv6UntE*?q%^f z5f~b#tkc!3zf0ZYxSSP9HtOp}TY+JWXHep(U|Zt?OTf!^MRO#-RWwt38}%vxG4plL zR|tXl)O)ph`3d1Or4tIO_4ToTH2tC9vFWTwR9^I&tiw6U{CYwbP5< zmG)F6wB&E5E&MMQia`AEFcxhBcp7)8aKz5|d#6(j!M39h4m!pdw13^eKb;~h(HpR1 z9|1)?nDOsZ#!Pe-;Hl9y`a@(M;W!Q?){S7AiGmaM$_P`vDxb1rr&PuxR#BXVLT?!* z1dM*f%yQzf^AREmpIg@(!Ls|)DFux(iBni&62g*^F||6}3vaX9A%b=m?A8G|&~z7VS68_ZjmC89`RmDyhT~9=hP39l z$e5Pd=0^0^kLk@Z4&}%EJ62aVXxAy0YrhRcUS?h<;-X29L%sRI;jroJ-v7{!{Y@cb zB4Kl)N{P5vazIX~?P|I!g*_T?BO&Ypd8L~CBDYQ2R)DyT6eWeJzg!`bcehydUaXcc z@Du#N{8?zrJ9$W5PsfnA8(UQyO=UBNu-2>1P4^s@@9Cw39g=%ZqkjUS^9j{WLk0`2 zKibo9v1mWSKdy8Ge%!d~>0uPW!oR6J128VfaiQtruvwWAVaKT%1s2C5Xak*_SIwAD zphB9{qT(X8S#d|^S7x(1R>hLTV&bmeV?##IpqD;l&M$l9wRcN)(LgSQizqx4>hZ{X z72L~?`QSTaJeP*J0@QFQV0?d{Q$FAcDtU-u%^cGzxmGx-DOX~JpzGU1RVwIEBIRZDNOnFad{gtD3f1B{ zdIG_IuIK`!TlhZ<%E#z6kf*=@faBg)%q1pmpl0SDXX+Rxp75^>y&19K=f#30X3J=8 zM~Sasr%SIg(ajRt;bb#qSGiL=G^*jsp|`YY7}`Ummpt55XonwJJ1jXgy;xfFUSu`Q ztfZ!SV_DtLZ%PeW-FyHmtwnSFScMUU7aEKx1Zy?I=TU=k+={BJba=cyF)*}OFirRX zjdaLTWz}=t^W$OTU1#A~t+4)R71loAfmy?;L;%zWHzHZdX~@wSSjI!CO4EdwRh|!D zc3|YqO1T`42%a4IpPX#2HLz^Xht{8yvm!yEwG4+=Q5z18rdF5PWe!F+f3j+^yUY-G zdN_&Y)l?oq08k$Z)GoY5SMzE2A^&jck$miHD~ik#)zM3JMiz{f#1tPp56K^MO2y$& zdc(`*;!wtH?Hycs`A{yKVv-V&Qn?r;-_(&DP`9XAcSbNUn`gU%ITXmQ%G`J|Lio#3 zrMb=NmjNb{YDxUnT?gvIeHRx3+1g2kGNWG!Oq9H);C9YepTn7m%+_f28CtIIks(1$ z)MRpjA^!pfHj?M+AF}7ksF>*lVu{hOIlI?WXBLfwO6}Agr=1;#z|Vdo(t-#Vc4sN# z;~v|Zr3h1y)`F>;D#58$3D_$}vr*1Mi7Ii*d3#V|+UzeEj$Dinlf5HYz&FO@HhULN zDe-%|dhn+VW8hsL4)JK&ZizPSj_QMTA5N04)E~HH>C%6M^m}rGp#A{;u_B+C$5vJ@ zBcI8bUh5GJ^=c)x4Upf)Bg4KXGi$rV+Xy3v_BZRZ>0$)e zkmQxRTbgy^5WTBM701HO#=cJaKF zv&-`?AQX|BM@}yos`2!_(ako5i`ODiT`nb)bO7E803y+C@EU1yJhLZhJz`PiA>5e} z)?Wlwk(_1ZG?C>N40U8r6Ss|4@}S`ApySQqrtDA?C6Pl9Ir`L8-gvdDmlmoJD5=ra z*@Y%z(`(ViBSsLX843e)@GpxN>qcv>S8H;j;RmnBFjbYe)*RC72``5abdB8Rh&VcC zD12EupFIl-FJeXI?6ObHN4%^c6MMht8b2u6`|Jay6OpF{3)7Ei>*8YGnUeRASy1m0 zNEZRnh>NGW&q2!(ks)8b>J8Ej;z1C!j6pCqMr7MoLk{2q+K`5cqyXSQ1$i<_GXOmB zJV+Mm;Sg+Bvd+|TN2l_iu|oP+U%(in+%tT|=C{h)hC>-GwgB0$Go%~;6(~n6I^hLj z>RC|KXT+A|;ck_}r&BwrOT(?`68rLjTrC%Kvd9R>B2v*9fBsmRNL6S^r0CVL_r}#5PQKZ>(*Q_zlOC{DO+@twuX0Sts@N!6xp>Zi= zc5S38k&lJrEZ{BF#<2{g!2Z9ia2@1?>rW6Z!cS-m@&fj(Y6p#!Z6pG`77C1#bPvMM zlWB_9n-f8PsQq^^3sOAJ4;AOnO8Y+N@xxF}^k_d<%7MD0w`oK>a_Oj`I$gzyeQ#yO z82GNBu^R{B_iYV@t{PuoKN%tD|NMwj_Mqz|)^Vp3iZ*PfBrc$%K3Su9 zkUn%L0c)y{=NN?Hv-$?;H>cN;#B|Xd|w2MUT^ex%xq#KSNn9 z)WZlVZo?Zq=HJHj3q_Js82o@s37k)+9~^|m2i}P;bAge%1U?eHGj$1I{y=_16eF`q zeFDtO5wOmw84Dc>Wkg!OS$;Ovw$Ad&EVIdk$p46)#Wio%;gTyho7b8-MlXW6RH=6o zeVDJMvswffi`6I8 zIgsC;;1}fZjas5^zAiBp7YLvisPw#p!osTEk7JavQ{ZjmZ;6^W4Y{bSJH^E%tgo&*|+REJwr z(>Q~)@ii3*%XCepJG@iLZ))brpC-20TQ&ts^UHRGGsXN z;s2wP{K3c{hZi>!rw6s59e8Ry0X!Va4N-y}I~)NipDk0< zd@K36eAMsB#V)izK>W)X(ZuDG^XIELwhu8Ik2o#5Z-c|iAV>m;eGZFt9+7woGy0e+ zDN`&aepq|bYB*#B!->EU{0-4!2!hyBRoZ*eC*15e;nnIoycew(s4#-YF=k;@f=WSV z>*9+1NRamrH}6oGWVwyJei&`%S4g~?rJEa&K~o(8Apu-Ar8FXd;(48~#|gz^owjMk zm*cW>ES2y)ynOEmdPx{6yh35V+@{$eXdeh=iZUq0KF`K>2lhK}D3y7@8Oi~6gNu`E zlw639!?E!xu$TvUa<%o^+^ZlS2^FSzZKlLf(eIcaQQsiIl!q}63RB-mMJA!$gJh8J zX!}9SL~k9A&l{t51%Ou_qW9!asgj#`Z8+rj{&tVz$!6ilo%FJ)9|)PmFw(*DY-fj0 z0hOtZqas09Z=;2SbJ=(~B;TZ?b5%5?C0?%X?&&41ehYs+0@>5`$#?x&ZOw$Tet(~% z)otj&#sQw$P(ILc9Hj~z@5AHpeD{t%-J^g-zCzl=Ao2H3lEDn!U=Y8RE-&;p?y^;U zo6v`BB?KqZtyNBYT=7nd2sqe88kt4)VZb3cXzNFj9QWd1N<*9wcwh|J+F{E=+8jpj zgoDyJHpuIZDM0NE`B_g*JVp!6&?30h?N zmI5vg?RJr+tXud`j0xAyQH}ry_{SxPD8`O~aYA9e<%R5kOudY^P4=m8c0G2c)Y~Xp zDgyOr<9^spHnU5XB2!z~!p6-1Bam%{alAb9GrO$~=nNgec;{mHNt6`{D-Y?*9Hd;9 zfGChVFlA1@p$|C@J;b@tV?-}z>CWAXZ4gf7jGA9Llr#DhMz);96*;Rlzd}gihb2;g z0^$Vb*!oXhYF^WjwrcjN9S;G+br!(ISd1`HNsoUSY?JFcE1 zx(}iT65WSD1Q_@ZM3j_FuP%y1yutg`h2<8aN<9w^VlELXcC+SAHYkqSeoZ3r#p=iT zyUWphOKhJDKFDdYJ$rZgvW zY+CxVXN&I9rHiyyElB2QAu3l12PB$GBJ((uk9Iwo6BFr5&`o8(w` zavMY#4zdktG>3*}b9i{x=1Heww@#;}5cIl!X&Hb^?b{pbqa>@=#t|HVoCqn`^6(-{ zC6uOX?$LRy(E1zb1K|GM)tb6=bzkxi(h_Rr5Tl$1?KN6 zidkyXHP5n%=ryIoW`awBLR@g;`h=X{L+=#%aB_8p%>aCg(iJ&hJK#l9ULAwyPL($d z*$9r9e-k=%kU6PG zy~lGHS|>MbL@A-}M1ICLVB9_~06y+F_Pjhr(bUI|$;9^DVLG8li1~M~CYul8rhSI; zHT2DspnV3D()d{*S>yDK9h=W0zX;KVvxMViRwt*^^JJ|;h}Q62S14j&vmE_!%knKO zbV8poTjbS3m=;zCAajd5%`c7{{3L}?d2KIhtPMd3h7Y)II|?JA(bXf#WM1eHZh1{3 zW<|vpOy-|_dVqausF-NS-7KfY9C1AQ!SEOaMAdCzj)L=Ki8>8U{4w_LyyCvZtqv?V zK#VzEOirT7c87E%&O|?pKcgZgdk=}K(YV4USs*EI-KS7R3i&x5{en#H$NWF1nd)?w z9Nt#kSJLCGN0J|s(2P<+;N7S&@|sTHTgchzbazFAJ~t z9ae!IvDzW^uHwYg7-0pN?3%?+X*;ek8to{aPvZcKg_o@(3-pBAx^b=#V*#i(e>S!I zWCo(W2ZRRX)a(m0W#ou#wSHywII|HTLxMyGkgfpDQZvj?3?4rl9}KFcP(_s9wG}A^ zqO_6ylad%?B9H`#8b`_!Ir=^tD#x+FdRnhzDhD@2TEWQsY-n0&v<~&ilSkhuOI-Cq z_>Ga_JyZArC&UsYvm%FpNt>l)6Dhe%(}Dw_pT zjHGr|lz3`ZwGC^1XIN_y55+q@yjig6*0EqDCJJs{m%GKow<33vjD^AgMAf`B(37iQ zY`0-WNpUn=I|SQA_#xcav$tfnSehBH8_uXx7Ma5kW*B~!@**sAem2!bjKvO}G)#37 zYd?_YR{DYy1EMSey`I@p+Q_LSuX0uvyH@jJy zjwm6;SDO&X`~PXa6dNgwO)&cwzl2%%&-UycX`!?T^*qG*va!pBwHWJHF@TwOLPEZ@ zQ<*UK*rdm&#{o!M8waE3a2QM%OY^4lXHHx>4!&bp7t`yAV7UKdon?RX4hCoDxcuvE#s%YYz&25#ZS zDvEASz~0Eb2yi(>vYsYBMT$29h8Qq%;u4??pyPFc79r#eu6d|v$=P{b&M0)+-15*D zjD2i#dvX0Jnr+XXe?EKI>Z7=7F9?DSg4d^La)3{>MLL7P9NW0iu0|2hi*XIe^hz)CQK@4xyD$Q*ytQCT+Y6O<(0g;D+loeb-OWcP^pK4PZ{aylDJ#$IjeO zb(U-}d5;T5rHd~%UArVsFv`!-gpf%8fFY2Cvd<8{m%~F`(VA_!me;Amp?|)+xCl4l zY=94xPJq}k`HBg%JK{eNsN!05IlquPc+ZW?Qdn2@xE#sPVbZ-|fDt*+f3pVvTA+|) zfcbc`KK{hvkptv@AiNZZL6GT`s7gFvx~gyUw|neom*bb-iv|z~NkE`Bo?Y1ar}@kr zgh+B-iuk2C=Fs!OC{TGz>a3Z`s^5q!v(?^ckJn#M5FQ?YR|Uut z-B3x;l6Tyhz9lbNPU86{L71xGT48!nV=l->?v7|8_z6c zuVXeXpkWKM@`Fsbr|?v;0ziqyFTzhxks6D>1o$PuUpZ4YL0>~NBLWsi_51_0+L4DF zdM*g?*ThyPwii5;vF7uO7pmdWu8X6cQgM_B$fUfSBo|hKggsaDhJ-xQ8L%b$Qyki* z(WQr$e6vQwy6oEeutXX~N_L(WPpXyc1eKxY&N*&bA(_#Da%3O%UgI7^A(FG7!kZZJ z+T>b5t!)KXJVM1wYzejsfx7N-6LdilW*-sT()NRV&tv!tH~Jz)yz(5PFs!~Q$vSM# zEuR?$@%9LH9!96(ML4G1T;$4_gYHdhPe2>s*o3rQwk(0$PD+kP1ynk#%%f;cHho>= z;$|F|Pa#9G!W!~rK^zMq2o!3fiS&KQaNADIZA*qvR+d_YYj|iu8DrG{WA9y@+cvVi zQU58f+N!C{t?Z5fKBaY~uIbPdCsC5IJQI7SwonuaiIgc)AxPVrTl2r)-#Mqd(G7Hi z07*TZ_{+Crkp%j2`n>u)#b_j3YKz6_7t;COr;~~)Jv$-Qzz%wo^kTG`1IN_A>a*IxFhGo20fp=GAEs}dhk!J# zGh;*0KG-9mD^WXb37&*JAi*cYY@nycH1F;@p6MM)KYogGV( zv>E@3$}uU3n{1?WMkSr|wxn|wq9te>R2y2X-wvwc2UvUT)Ea&m@fHCoG7TZMBpVZK zLExOp!T94vG|oxkK*nPPGCt4HN@M{_kB$)1nBZ26j|!WE)5%`W^lhL;TF8(K)||oe z(B=Dp6tKZnP#JpPX0!UMuRQxsVey`o(^PeF-~e$+1x9&wC$lqdAZns;=~)*Mun;pP zjKn0Qt2`f@S%lheE0lfBSfvm*d@#U?-pyzYulb_)i~OtXwpj{$(liyPt)$lZ&nD+f zdeJ+?EHRiUWT{$E)-<1X62o8xqsn(8zrwc;;y}Jj25fXpvIRoL zh{{}uE_1tJ`ejjo{oH+-gpzxRij#d-Jl$dEh$N1pNt%sUGp+*wIl!#-v)>gOPi?aF zce9<|&1(4p4&vX>hN!$tGMmtb{=(5Te>p#HA074ElU{n>9mU7T>3RG7B0cY?t@iOz zbUr@nL}_m{ZvSOIJ70}fw|}9CtiNQdF@5nrtL!iTFMsnt7ss7`e0?#1nanWEqEYZk?~E zUW7QJxT}>46yxO%^GL&Oz|g})RA)eH$6d@mp#&$yu56t{_yDCozCkMco&7_0w6@{qxiR{^sddT&z$&f1~o<`ca4S-H6vur~rc>Gobjl zfI(|gbm2-lo35^h)D^c5_VveeQf@CKl4xi2D-4rKNt_bW>q{yxwSxp{$la@9o3vr_ zk<-7=5J30!Q|`MdIZw55DQZtr=vmjZ1vdf|b-ni6@sTdznxL^Mg$X67Gx$WK%@al& zhNuh~It)do8Z#^Lj)!|E6$EQ9v~omszX5}z`%M1=eqSoM(|Nf#-K^h0X9bEN>{wA9 ztR+N!lfRu=?*VC}*hbnYvXFK<%!WfWke$yt?H-I&|NZPC2n9_R@0>Wn<+|-kTIxc6 zI9!|~xVB)3V&t$dl4Zzc;zaWA*z#C!kTBmyUn5YJ!?(L_9pB9C)z4E3Xsj_t@#)Ed zc=SYYe`o&OZ9f!$<_IW1=zuMgB0m&ExBq!pu-o{Mk0R;M$5EukLHcr=IJhl|gNs&N ziN>w?qJYK~{r@OS)Y3;NsM3$dlv=_6d5o5YK^mcPi>u}BrEXjt2#qiHMB`Tc7||H$ zkRzZdA-my&J&HN63T)7_Gve^G>2kG(Xx^^UbyS!|rryw6o&HQC+=oi%(dGzxnhYwL zfZlph$QSo^5Y3@DsHFtlmGbZ~x*1X4$* z*oC;mOtjvf@1BY7zh|meCD&Bc>fg@Mbl{lhlB(Xn4}KQ9qFQ(TI_F*5dl<>yX(>`|-SQ zSE-EKmnnCl$?y-15XoY>Iftug{2pyD&_7vpF^Y%t>Js))TBNH&EnKqFE_F$F1g`Be zpN&(FnMD4!#8XPWf0&+XOmjBX6T$uj-9bi>q<#JcCH_5iHq}JDrACUO54XbXq&{BV zJyY@G)!us#vy}RKzk9~w**gqTF^s8b>2W<ls47Q2`YeG`To)*O0)%=285SVvmuEL+|nn z%_r~NP?DV@@f(o1XEv)<5aq3<8CJSexqO|MRMF;l8#B=zGU_lA)8nRO7v45YkBjWS zfvvdCJO^c1`P7;(8V*bqv^>XyK#bMs7KJ^I6U`N+WkH!M{#9|s@+52xM+kdWY2P<6 zmUGGBub&=^t^AZ*v3b^HqS20Da!vy5-7Z^kP~8Zy%9mIjf^{-)U# zuGX%Ls!H$ZG9ehaR^=VXDdgKBZue320hyl@CfF?MP{E#r4BW}h5=Kyc7zlzb6p^C( zDC$(D_^kI_snKV!=^9l&Qxa1bwJc$|%A|$kdjvP+gaUDoWL^jWzn4%I%qOgGC8})x zcqn1wEV|oN#vqD@JSNBXsv=z!@&u3|bJv+~i0_{u%KRC2!53SwTmd-W{AileQi#mOM10P#Wm z_ats%2`dXWFP4k-Y_Um)!ym4CeN-PquR&)|q52WD6O@mje*%xF?VX&c{i(-vdxBSR zk8xR7@h;NtP}?Lw`EO9_ReqA{p1L>(`70T2GBCESt5?wR%Coe|Out?lS^vQJK#y9p zYIxA>s9>uYQHYy>hI;UjCiwT~DHDw$JPI@R5}rk_LPk`2%Xypt3u?<$gnc+b1^w7v%IW@(*J^mf&4J0!Y_gc^<0=8Ouvi zhbkZ>D6Vo19b-O6MIzo@Z+4S_nXLM+Za%aTintO&dG@$NS`AADhslyn};&Pd+9sGrVQ{RNtU~qtumrO4euIoLEe(U1@#yJ7D7SU z+-fwLePW-i+^A?wsxMg&+)$8Y*f`>Gi=$1^1ej5%_K4vFobtMGRN=Y2XGZ-CRfGYZ zTi|4hD=!@o;ia;|)q{=MNpQUe!-|tL4bVX-X;b5XQHV%RqJ+ptB-{CPvJ{)Aazi^F zvKrb9gA-xuK|FDhdZ_y|4&E-{S??S?6oTrXrPJ8$BuY%d;8bX99>zd@mCecJ{KZlw zt2lrGUTI4;2dE369U&wKX;!I^*b4^(1@riU_rQ2smlr`%2~Yr)uVQMd$<@qAIkV&r z?^+IdR4|_=`R4VMTB))=IX09`nxiAKkkwh@&A>3FNPhJvq8kog`ZM?#_;wv`CMt%e zz9e0I01=7q@HV3=p4D3bTbE`{*Q(kePMQo)-mY+j1qj|-jHk~w;x#6=P@CbS%%vP# zkWC&Pp|t}Ju=bFK^OorSS}mBbz<+y=(sL2(2yyRVvJOJ z&~+&rNYcqlwC9`3%GYYHz@{)&hOF{LLbRZKY_rNouxbm~cbqhys9XRx=L z?yzm4yYaA3^&;2PbU_=|Cs5`c$PH3VuLKX_A$k~fBi$VyKJ?vD#?(;FyI$42+gvs0 zf$HD@@bYI~&@r#s4 zd7EuQSGw@qa;Hc7oob!TDVNXJSGZ~0**RMRO!)VFyA@UmAz>}sthf+?`Y(i_ z{_oR1VskCRv~N?qO~VyKdz4F&82xxxSb*(^sW6?chiLc>>mSEhLQ6FtC^}Y8yydUnLkg~fTyvDP_glPn#IL@5h;gctYq)1F>m9VBT4 zK#e^+fzRzpooxeNzX|Ys3#$z}W6)OJmG8zgvDT)1OhMDI8bjzigch+`;{@+52Islfw)*Ms3_N5TyJY- zk41Au_XmB%p%I0X#Ya3QmpBz8Ao4xt)}V!7^r%}5W9z6>7+0*uSY#qiX3;r8rwJW3Gc~PTq~ETRg;NgA42y%_RT1#_iSunpa&p(EXIH z(Je990j(*c&VPt}N3#|M;~tFWI0PapR@a?D{HIgXB$8ik+?DNSSBcy~_;$!^MdZN@ zD~7a_i6H2G#@YhaDOF-AwP78h`+&Ccg=(nc+ouqZdr&l7ezU)UZqOIWy)M)V_)qzA~> zCc%TWRq}YVplG0Exgtq#K3K?Hldl<*m57`hWJ4UzR<%}w*&5rAxRjH*osrYxO02=a z&I>&Y;c(W5iRTMC2&Y|pIIUcyci>k=;}C%8v<##yaluK51OJU`|BU<|NWk06@xI*81BEZvlYdZM=cOT_~SqCZ@XB;3gT z`dNga8v!K6#k^Xe<&uVm=X})QWcni~Md3x7Lw<=l>sx-F4E3psG~?x9UM&=4L8`ie zdPVX5ObV=)G>yQ4>Uk7!D;1m89I6S0+x|OUEi+{3k8Y;vb&3od#E7Zl(`@(kKtaz_ zz*1C)a0bzM%Es_o$Q2WE6-LR=!+YE=n(+XtN>hTWW6Kp|OX(XcvXOx$idF5c#{H@h z^}fea&$Q{-;|o1NeZWOM08XWFI>f2^Oq4K)oQ>-Uz!U9uHeX&wN0jZ3MkVxDC705n z~N^yNP6@_C)-Ah^o2w(z3D^`F9`WpmwwHH*+(EcU&hmWzMl z(|Xud+lG%#x628kyq;Xe0>??4UIG#6!glfN&-h&-3EIQH=D!H!7{*#? zzCgY&vis<-AwO`vEPR3I2QY!<6d7*FvjjwNx3I4OueE3_O z{-%P5pVX=3II0n6T0o{sA#Fh*=(k+!cemK5r(XN?WCrsz z6_=3&R3Se=?EE(wJgi*k@0O!MmEC*an$_s%?M%!IpTp)o`^VOXbI}rSxj}d=Y^E6DB zOB5l`hN`HMB-DZqZCjfUl*`M!T~0n=AgYQ^a*E5Br6k2l->7<{Lu!S7@UNjg)8Kx^ zWD2{RtDUnVnC&!_wHOtN;Kc@}HlraNBQWWj6b;HT0RufATAvxs9c@7o@1azMTu)%m zDyH(kU9sL@i>ev^+7w0CFY_D>jsHYb#*VM6ga1?&W6Z! zr?ILicE5v|%s!ymgw$7mcNN$s>6opbR}-X*eIi?5h3-L5;;Ad8YLMW`I~kM^%%!x} zzh^h1a<=}-e=&8P`*m@8@Sj>zaW)o0J?S-g1ku-efZVL1DB}<*HI#8}fnaC7t*qx1 zSf6L;o#0ks?Zn{|`K^L>^3Lo;C>$Q(ptLnt4B>=T+&AjzHe*D-nsjw1%hGWOy~l>$ z<%PS}9$Ij*)YcpRs`(jR^64HcIs=AirY)$q1FC{XS`VXTl$C1|4vsO@7mJ58ML7fs zr_uLRLQQ0aNjE2Cl=6+$+-rhm7;4YRi-8$^y&9olL^D0nEQ48{m)RAFHE?m}fIW`P z)3?YbqL5Jf?tqUDY}`o;H*~Y#VDRIPU@vROLa%DVy>2-yw=+ZyC~I@I+|KD(`)dh7 zJSa_e93p*Sj7WndN{umz;$!`9mFjy6?=DpN>~KdMfj3_~{e!6`K-n6jG5b85{BRZZ zV5COyk@@ObtZq|5<38QbG0&Dl+geX783JFT&4d#U9pams+a60?C<;WQl|7Yr%GTs= z3o}XkyuziO^xhHbyA5i2v5L@Ioh?ca$oQDysQlrOQ}}@3{XzxO0ht; zzbUc@iQJrvEAbJZM1veySY>oZ=Ta3Pce%N_h@_nw5-&MO=s^^0+vNH4~=9u@QKWa<3}Pl=jPb9m!3_8V@i# zP;4w3O^s)>-2-QiWR#E8GY9G)_ax<2l)a<$sS<$dQkA1Q`vUF7S#_YZvnZvVw1RxP73TZ3$!}hB){!K@-c_9Q`vge2T7coLmCvY`sDi zv^csz&xveGi4cg1n4&k$8u_lcc_VY%WL@U@mY59XWLB#s61WTB>RGhmzKs+14(>l| zCXcZ0h>7ie^y+OxQm%>cq?c>iEP`>F2CSM_qJ4_Q#+5Zh?S&OloXjty$@9tR22SX+ zHH^wkUN9Vn+i4L;+p;wDLN?{N`W!9_twIQtj<@R`?@y%EuI}4kr5%I@$+?sv5-wRnUbmPL`mEjp8n44{eW%00-_aPi#Q`^6xU= z1bgln;{ZcH_1lv%ob4F}u;f^=Xz5`nxN|W}=agGJ%FB#Ot+fUx2~{Y*J5tRNT`w_ye}KbvAi!a#p{&J_ z(kSfL55p2Z_H1+v#tJl`9~HS`(T_{dAx{Y?mzk`nkKWb0uBcriG_HqD+)`EkV@6pW zqaOB_RN<^u^A)F7g>3=f#@k9GntRmOEK-B<7;N+MXiLG$tIt`oAl(1RSHkpP$Orvj zNCVVBiMqK=2+@fxK^yA@R=5XPkX8TV?)3wAY72cmkhJ%hYY7JmRLzCOc7>t3T1QY} zD8$`(bOs9=Zrn|s)j7Ju>uN+{K0pdv9(faFH;vT?>lAA+p$38EhvPj6oOjp{88EzS z6`V8Rf;mscq30r-w(GYnX-Ultn@z!VhlaG4V5*$D>U8yzpfE2JvSX2S5P)|#F8kNP zHZXHp!8YEop=%BlQei_l2GR9*4;#YPwfT~rId0q#e$~;xd@c?4QX_Ck_M!rZnxPaQ zb+w$y-Qjx#0a>)TO?5UwKt^ftE6v~MT8bwEDCaL#s&aMgPeZi{9qhFhUkJ~Ds?7gU=>Xb}SZn3><)@O7>ecoszYJyAB@`_=zg??r zOg|&t(c(k_lpYiP5gX6mY;NZ19a`u+J6Px?;?r|fj&&BBf`z`uF33EU@AVEWsM`U2 zI0LG{GiX3%d)|)lbIhJ}|EoO?nb>j&JC<1TK6r`e&X8GKB@bY&&XKpmdRiMwF1G4r z5_UqMZaKJ0&n=gMjPAp8%QNYEm?eEGNI5fQVu>B&Gju%6 zX8WRfCBx40Ju0N|Bgib`nITJ_qh{DI70d>#)c>$ay~xZB6XCZVCL$OYG|m@CGK6OD zTj=c2V5eA?GT8eJg|qZ7UEjSO1ZyzzrVPQYTQE`;+Y~Kn*P|B1yaSrpayXg;A3#v# zhv6X!&E0o_zFwmxB?6iyF}A$Bi>cNWc`5|94_CNJaaE^wi&Jv3O|(k|KD_Jj*37Z$ zJB12iwJT%?^}yh2Le|U4hqnJ4#hk)n zJ4r*CZtcM-R91C9L@V25J2~1KR3suS1XRmawONE~OMSi03>Nm4V$HsJa+a6LPG22stN z4SUHUk`QynO>xlAh^~6@V>h>OkPO(qS!2M9h%2HS3X}3U936230|!E{N02v z_Pj(R$`AU}-JxORcn6xH)%p*w5E}(&EYp3(7Vd^*w!pLXK0U|{gn);kDrjXFQ1R+= zI;lMbY`7ZQ?zIKPw#y(LbPwI!g(Yyy>Bzi?xFOQ{@Fos?LTZH%g1efL9)R2Dw0~gu z=rt-NW~mXzzy?Zb_W_hruGUTzbRea)s&p)!$Tp=ktgvk}IE5AO?jEM6kn-){XW0bB zmRY|zu`wghGKpZbMN3R;UUTy~4oW6_D9_F3DA%a(3B{}#<<@h2AOx$plSUNwz74;4 zH6a0SgAvhgx|dbyM4YR=N6oXM-~Gl>7gsG{4SUN~lFqYKt#e>3r8w5;wqi-lG1beq z?bJdf`}SA?N8jDv%R=vlR#BP+otg(fP(-o02>vvt{jZYvAJjrZH6yfck%3+Q!zI(e zQ(r&5SbSAyus=Wj?{DnN*n#mlpa2V&|2+6XW~3kr&=%3Dlzg?6*t1-rOflbr-w_aY z9}~uI`fo7ieB1s7vXH47Soh`P=fyaEgJ1e+ACQCmAKl^bn0gf_OZ<-4o|q5M4ht`2 zAk~6?wMoC3e6Voq@Y2CcT!)Yi(mg?$OpIKpD;ChFQN7xIbwkT32m=o46r!#nvGSe{ z;Y36=kBES#62P(y(PF(omDuV!jDL4#rGaL8baM+;W>I>7%$=s-PHiuDvUVc5jm}=I zUuB;zby+{l%SA3^JwWg8M-!7vub6zDepd@u>jY^jY5> zs@PjL7a>q7k(WjyyNd`(<2pboG|Vxtc>3oa2C2n8YvgVTsr@3?tV_3obWYg%bsJLi z^jncyI2L#w_S_)AT@}g{*$0}6(A!sKszquZ!h;a;3T18JV@t@~qAY zRaXZ6Bi4YP(@L1h2f=(NC&2bKTG?i^f2Rgs?h`pn!*E``OD#b5v%sc9PR>WKGDk)9D(vFd+-V&QBOp-h`Knv&A;MDCQEB8eyQ=9HqhR0N zlBo=~fF@NAiHj|vJgfA#FoP1T&_jEef}RQ)@#FYnj?0W{u99TwZ>tvkqMyXr4i>xFGm`fa78D7J^PrKU_Wsd#<-A3AT$C$FbhP-W;TupY{Z8$>@O zux-zQ>cq+8-;(Z2{&aIQgq|ZC=%WS}YcGTNJRwDdwoh7>foF{L%+U%BO90#CGDUwH z5dL_Sp%%#z&-XT?Y-7}a?Tp6b%{2HHk6S)h+ZWvc-00klROD5r*<;x9(Zv0ao{*Xwr>9zr0;_vpEW?`nvVF4S7igDsH5>!~3I;BqZu7!KlA zVo|H2i@0YIM(dx3Fa|#*L>m`pHFitMg|P0UZlO7o1a{{< zk}-Vbvi4@kj~+%A_Q&YiX}CQP-cRlX=^2B;zkV<5N{;iLVmjgDbcArTlXBVArf^W; zdw0yHqPO5^5Y)f3qf<;tUxB48K#c>msKyYDmN`yFtC2a)v?L{ajcc$gLmf|GLB#0Ru;`s-AAN+E>Ll8+ssjf$d?&!>&~}aGaR`S0nLlBskoaLw64M5^E(L4Wi^h~y@Nz(NMbG_t)(qs zamBW|w9^+t5jL(?d^e%;aV2mo{mfC*9)W)CDusVs70kMcR#9_F2}22xf)X|kD`Dx@ z8%X5HcBuX?F#xlgC}C+$5&+hM0XaA;+@Xk|xK~l1*U#tjs6-Cz3mz_|{2{QN^_2Sq z9ap4>dv;8weNJdsL)5P`+PfJ|s533U_%?gTHcj93=)2P)TS5jl%4yZ)3_?ptbnS`f z+9KKtz0T5QTT>Hi8%;L&a#ItxuZC%|VcLMh2o%iyt35Se987nUGU~c*HbOz^;EL0WX|=|XWiD5?&ssSok>w|q!Y ziGTzhj+-C;nBP$|>`s@N2~d~us0aqwg>v2F6CCi;nR{obQ6`CX&)4bj)!=QSHVo^# zvteJ!q??+Z)}H+dO0=;agt2#~@9v@Rc?QkPAXoI%`llt9LTB53_*)zNM(O_P6-C?ClYu zvn_w;*^al&)Mhr!Qqom$W%+9WU2HZULY4B#Uw zxVtNg;~;v0OCZI!=#^9gUthl}Q^eWFI{CnvAWU->q) zcZeb(3&;?16ycbC*r8{aaN3g-*{0f5)v{F`wNvgC>g53>{`JWKn$VN-^P$E_1Vys6 zsFM@o!hD+~i+=ak_b!=DeLKZjfAVI!R=?m$b*+BE!n>cr*94J(~`5yoJls9||FZoK5Lb*w7J1Cc@|wL)|kg*|Bh` zgl@W8FquT{Hy@Y3j&9G=(F)C-sRG;R69?2}XiPQxwD*8i5}F4Ig+ADT$Ro4hkwW~N zR3EoXl2KfzfB^fQYirxhyRfsn_m)`PX(s2dVkfNgj($wa z2hP_L5d0X~VQYj^m?%?@X6Tw)5X{vT?r5RVqDoKh041RyT7h1a=VWRir8|W{)5{!! zb&7@Mlj+qWfv9t~UTM^){;WS-wfnw0#ui&P5GcI*_0wLw|5jrJdw)P5o-`@bp>?#| zp@!cjxRjbnf% zO>UO61sHbT{SGt=zd-)DQlYR1H0SZY;U!86(DDjc)bVUns;E``v!W{*o(`1bpALB# z%|okNn}r8agezC?c(v|7UtJ z7Hm{dQf@0mpHHu1;V&YnX@=k#1fVKoSdgtVYg=ZeQ+@WYO&>Mc|4%ck9oK*ntu?TQ z;@=6NRRVH%lqDP0WEfZ&z(Xi{asL91HKm^&yI-?pHJZ#m!LqiPjr)_O{NWag-FrA; z1^(@T+SA;m7=9)?zL+nOHOmot=LlJxj8@#+D4@%cTuP(@MUvK?hwbs$A(0Z%xRG=7GDd)p!DI3gQ1NXE`FbiGg z-B~@_f|W;(;%IUc9}l~m1zmceB|VY9o)IPrqX?2|dz{Yaam)k>KNqo>TOcnQ0Dep- zjVQFlB{%fG@(q34P~u)loPHsaE=bTtkToyq}rhCC`6jJQfC z4Jf~~p3gsyZZr9PGVE|GQo<AbW> z7OQ<6pO41xv-v2S8d#Wb1Q6Q*dNfTxC9(@MME+Iv7{`pC@8N2ST7s&RS#~oY-NO7% zuyVc=!?{8%0<-6INNZudI@YRBCXy3OQA5%DVuz=S6v50E7fXgIF0de=SNLcy@Bt}p zZKW^EUO?D=ME@01SRKK98p3ii11+OA3ov-iX7lA`B(3O#t@<{MSE!>FnvJXze*Sn9 zp~o3{!Fj_C-fYvG3cd)Npf>|E+BQZs9Z}ZRCAv%QwB$b8SXeSe^6sMI}i8z`uOV*>?dGY2U35upYqNNfn#pdcm|z)o$8 z&N)hd({1PExrKX)`b|Fnr5HT7Jl|Qu&VwrgIOQd>mv!h#Ko1ZOY349(JgV)&eU3^= z)=M!C>rY6Owjz`v=c-4Ey81=c6eM$p)8D$o3Z#8}6e3B2Eu%hSztyd7wvgm6#fC$X zC*b;5mhE4|?H%knUaBNxYzt9Pt?Su>;%?av8-5Z`VQ&z?VoZ*L#n;5{%M6W39D%1V zrkllkKt}Kj-_?$UU{gS7{jO?%rF)c+&4U9#t5h~wSt zYu;93+Zj=PSARUSM0=RR>;7{CgiHg==U31}Z5aKFfn=LIf}v=IBZXuoX~-iwjV((< zdY7eO&#yC@<^1>QKjd`Y>B;I3AyQg`&;ct`xP2T`99- zy>6<&F`5-6oGy?j5Y*vjZCvTV$JR4SeO_MhE43%>@PuqH%1!8UIj%rUWzFew5Kbbk z5-&!xdHA-z&tS46w({*T3|y!OU6wtb98U>>Sp$wXVQZoCxOA+v;^RQa11UB?I9!5(r@>* zW3+JVP}4+V-wbB*8zXKVV}w+&4ldPPDDRDoLP^9 z7NYoibbFpc7eyd4ie#M9r@!-usR`&P7{>7$RbI$nqvR3u8c5Fc+T5bwh1Z`KWIEUm z_!bdBbYZ!&90MR+gSP=l!!i^h6D^giA>WU7HozmmvR>W%x5G4#*h66&K4e%9$iMVv zsZ?`q0-&I350-UCtJUcCxn46&AKSKv%SvKB?hOKp7Qrp*hQkNk;@3}O7{>K&UHpy0 z!tRw(GxLL82!u~|&mL+cA^u?8Uy*Au+1!MAHQqot<~2-R>K&Y=ddVw-MBoL`Hax%K zj($vFUGr?pX*P5hBD-z&+e&QwZ!?s9@gWf1-@#{4q92#|bOl=pe{$lh{#g;==m`D_8FEhEy&-8w`}5wQ0&^ zsNxjK?2UX}1Y;GJObaWW@-FkiRIMistkc?bdB#qJ7Vw?u*c4$o{CVwuDx`D!0QaD9 z60{DGp&yTfh)35J>v*1-(_lqok1$n5SjUNfL4Lv-O6r42gd@&DNs{J^=5QxDwC$p` z8xx?|+I(eV`Eg9XwDfnZ2l%-_n!SqNILEZ*=u@UsjCbFEv&>3k$G7>(tLn6lK+FiO zN0KR5Ry-W(;=)d*UxWG4e;h_1y*4QRfU-N_vwzXX|MzL@ z|F-CC)}UN{P!C#C;J!gYATvJ!4I#?1@Y4jV2_TQnwn-+M5Bc`x ze!9;>sag&n_hA?-C@G{YIY-?2G5&Fb7-ujpr5;uDRSUEyC;V=Gu3Aam*1)o+229m@ zS1rn6srwuY*9F#{OmH+oDpaQJH!@l6t+JUT8~W~mh|RxEx(M!l&$n*R-Qk()Iug)0 z9I)_2Uq78i-9I;>RhiyHrfhTCJ?|`sQ#~|QaN4fY?*@`ONSrS@ETb%tOI#F=mJqpG zv)^saHjeMfB{h}_f82`W>*s40FAWVB>ztWahwvS+U~v*?2Q6Nk#v-R%4w;1 z{7v9ohb)W8#ja7BWM7>G#TYD^l9R=5I*uljBk1KYaZqSI^{GfAS}Cdhp&K9tJRVcl z|9Ul=t(7~X_Y-9X^8N3{Y&G7@*5hGxln-j7VlCcnEe}<@ut0NhKw&@9AHm3oiua_m z7~a2aLW$i3oe6fk_?v9E^DTYAo-~-Sz%p3gp&r$pf`tm6KxaUDx@l|HdAT^;6s3CY65`B>q%a0$ zWnC;1RF23HHY~~&ba3~GyoW;v43MJCJ)gI8(1X7vC`KK^>UQu7YH@o9s}W9tdjZ$a za2Bj|>DuS>Cmgb(14h-86J9$C77>3WC5Qxl`i^xVM?n@&SZqfEy19ZfqhWv0-3V=jQ0u zYUBR^>?Ub%OUb*KPXC4i&=K3lLVC*!$My>ewL?kOd&JW{T2jyeXIs$1ics-&V}P^m zcfo6sWj8W*O|cuQ3&-1~J?++D40JXA@dv$|wZ$^+CID3&jj zbd&Aoku5+7_#GPFGlLHDE;-20JAZ5kIWi-ob~k!F4sz>Mf>+hbyMfPFwVx9J7n6>G zlc05g@q}o^@`6K^F^R?nq`0##IeGbAD!Rill z-ac2Eo~nvn`b5gkt}+AaJ8+fh&8WLT9kdEbz`JGsaU0CPb=> zW+`+1!ZZ)g_4B1Z3a)P*_1@R69G!h!KR<{@_M=^ueP=O;=K2=Yf}2rsFb`_zL1W(^ zP`&{XaKXTV9SjQc%8sE0JIuFF6eF(uZJbto{CMm<_Mto?u5Z9}0IqM)uE_QEBnRR8 zW|k7yFHZB|Tt8pxW8nH4JcrTs&E7m*U(X~sBI=qQT@8!|!`eRb{|JXb~ zLhf9v^Jo_!RPP_ z?&L8rcq(8K{jtMy$Y}F;Y&Q9UJR<(CK(s%5*SjmScbVW2++EL5;_mJg56#@oG9LqT z&yRO+-mdrM;qB%Cf(-GHtle73fmwSFEo`J&2tn(7wx$k@Qg-Nvs~!m;WT4=GBvyw0 z>Y{%Gin=(DK7(l;L4=4UiI2X1+Uen6Zb`-~@WhcuN64^2O{Hu$Mw9B*a*h6P=zDju zzEw4l_h0(NES()qbXSUw{Y&r2(jzRpjp(Jg!{$KgycYnVDMp1tSTTxQaZ<2!FHC>q z9Fqvmv&Zz!8GV7TMoDiX%_VxO$%Ou_`IZMzTe`~ez{q{krMY~ou@{1PCmbikc)Yy6 zna$DM>ly{dRBA(BuS*StY!lf8P~taglC5_H_i>+_cHp46zhX@|busPrazQN%2d(6I zfX3u!RH`yU7Z>V1H%MrgnokjOui&3J!ZdJb0?*d{ISpo#OI=fHFxJM z7du3wV+#wS7G)_Q-d%Rd(nO~XdPCT&kJSSW>X|?ZZ>L$PSqDydYE?1U{YCfcoxr#) z$|BdB=IYg`r$|7H(DKGM2J{TA)>U3BHt} z=r1Fof+Q7U_YfwX&8-)bss9C)JTtv&9UXb=4EDMGDiZu8OHKD1J?1?3mq;Q*-XAJ$ zPtf~?6906)LHb}7e&v*x6;(=91rzB_RWgxcKKjjhn@zJeu#u916IIZDSU$i$4j}&> zZ~`hJOOe5?uLI+SJsw|XJ|$` zyT~h1dE}Hb;!HaivsFeKi4tp-3Ia6-kaAPJd%SW#SXtcH4p>Q*Qg~ zfj(&o*P!hW33b;L&?ih*Zx2ji=m9AN>7}+oFyziFovZHBummS9_ z`i-uw%a6GEES9+LtkPsX9W8cVRTP`!K!YPW5mk|l)>(0~rgU=QOpY(;d~xrldb?@1 zPL?<8s4bWK$3OF^W`G#yx{-kzn`F} z!{t&?Qg6b$b-k?;CUs?m=!lq2%-AjnzB9%|EgEW-=I22dODWP{xi;_CsWlQWsIrxv z-kx^|1wGSP3kAB>h|-z2`J${5qoZF$okO+nC<3Qv)lN_GP9I7InG`XN2S*D*jA0KR zU#8R>@Um=kDWjZ(hys&U74_m1Dgf)9s>0w|T=80d9qJN>mh#(LGjd&^6%~Hm7(49X zbz)jWb{Ul?=t4n=jS51LVtDv^ql-jHKSf9&G*((I=Op7kM$rrT^b#Ll4dQRv-U0{_ z+)U~9LF+n!L4ng@nbi({Ny_GgD3&=<+7dFZO?7du2l0zUy*Ft6BZ*=%LZG$NHI6t% zJ1hQIZrss7?N)$;=q*~S(M~3?R1JsMv<5TMo1}~Np)k81;#yM7-wisGWYBuU%cs9N zG=XA{k7y-BI(k|pgHbUZ6+cW1J2@fK$TP(Ge)ILyuT1eW`6+K7DS$pXaRFUYb!Z%W z7`6@Az&|;$fUj0l>mls@FEHxu)bxd2Rx}d+!Y)rva@o=l8u!C4 z9|5;yo>7BY>Tcnk#O|D}l-&aJ#NCdyx!ezSJv5Fm(+0h)xk_cjT(kZ#h)%(-Y$_LZ zR&zyZ(_PIKkAy4YQLraVmHyu1iqX#jt~fYNpz(fhm>|{@VuGg$t(JMN1Eeeux)y1dy1<+ zG(!9zxW3l^VB2MT#Jv!zjhf}_HZjdFcT^O%4j3W$wybMW-zR^yk1dythvZt1;f5+k zRF9RC)g^f6>0}J~yQVr#hTYuWs4r~%(*RN8ey-E^>SR<{TQLN1ia0SbNg*u<&ky3Y z@CJ&Z!&Q%&3@(ngdc{>$Z92PmvcDVr(F*%^y$jIzVmnGFu&uK3Se?J$$ZH#pnE7a% zYQeJq;38O21uWrAH1F_-D=A6Z$;tl_5&rjU*%UYIqfrI8Nq1*2S*EquP!&}6G zkx20fNaZo6Ub`7N_IrurJ*Jr7cliIT_J&_)h9pC2aVjo3(_y0fa`E$GoW6Mtv&Av@ zm002pgq7riO;_^UWG<>@c;`6)22W?kenbQ~qSJoOp)|8qzViFnkSKuPsPa zAXLUy+D;uq`Xc?bPR>W;_he2nA1dVvLrP;+Q$rVmCtjx?M5}^(K)_Sj+{mj`som9y z_@;dXs8=SeDL?7Na<_2!$f~tV?Zwq+HwtQ*(*QvHciXCaC_7)atDR%0oxduzbF9p3 z#QpR&<%^z#881-*e;+(TIH7*syq-e4{Z|L-`&_3 z$r7fokbIT%a=Nk$21*7Pg%d^U<6OgITs4z(q=&wL1=scf`Rw$6!=J`@Kl*pl8PHG2 zHv#pVC)NF=K&TCQLE&u?an6^}32>$L{J>Xi`TkHGMT6K2UF)iBx>hkT5&D1?n-GFw z6d^$C@l& z15%-_yj^aJg`{~&EZ?#uGYPy5+a}TLkyd=Nr9w_|^jqR?%U&3|!I|u0h54BBvbjOayr}yG`AL@xBx)s+c$q01-wqhL-T}`D4)v=m9H&V3py2qFXrY}P53SDx z5m19gFq%}>CdRCx>8<3I0^Ly$O(Tkfr;x5C3w4DxL0BMpCQDaQhwCpOqW^kE2IE`l zq73&cve~FILzW`#f&%`f+I%3ml6zDC<#MPUji*|GRp0TV1}W#Fu2|P zc9E`6PB1nWfeR+t$)UTSoVb&EuAX_v>3UUs24F~XYbbx6_p@VG9Oye9YU$m6*7EXg20E30y8Q1KW2?MokXE%&J{Z}_1 z+RuSTT(ki=)=386R^u2jjJj3Y_de0P7%SAcNLNt<>)YDiICI3sC6MP>4+3FAV-TPQ zYZ(L>+(Un){A%!QC&3T_?h7Mxf(24CQH^&G{#+rhfq^;v3nUu`Ka))jE@yE#0~Mu( z-8~RkByMTI+v3z*Xt?L7*~CJWEV1?Q)=+LNjD0nl%uvM!Y1vX;VJ+mRo5eOdiJ-^{ z&$W**DK5Q?xx0(c;ED~4FV4=g`cBw0Rj9#T_%ICGNc-)an5BJ@7XzdRW4WfDPkr?5 zX~7dbprgE;Pd$prIY|+Jkw?nrORD@3q!x=?CYrX0Jv@v&z7BZwOT?A{yBTOS&S)U zA>*Qp_6yJcA5{L*`L)ZF6Mh9*x%n${Z9UBpwB^^Y?A6Gjb)U2Dt`~D0T1PyUa2l;t2?BO;?t>&qmjr#Lh zgu;`8`!b|5Rj5}qC@8YSV{@^V0V_{n8TiFrh8PoYF=1mubfnD$$NovUr96{H6v?u_ zP0u%%a_;!Pmw4k~UE2lI1jYJ-$2+-Xb>>K4GqeX zl2&qZV(rwiXA)8aTR9%*89hp_(LYtatyDExpyv?X&>YwnM(cIDh#f`-#Z?I+qY6w3 zNE*jxOw(Ru_QOHwJ8HcYu)LrIbyaGx2MgQirgb{HQHPI|F_=|&oX!g@S_*Q4|5J<= zqLA7j*B5>Kg^CI7U-?h|x^;2A?l4Z%Ur|r$17f5^NOE$-1{4~AK;eL1r-7KrsF3YW z#XxC}Q}FU&A4K>!>SxR?;zcu$w_IsxnNYX(3m@E`3`F9 zV<7<1f3?w5$x1qvD)lch!RI8f0%=VN(4VjU>RoTBYR*X}6eGMtb=@>huL1v9qP zt!qKbPt);x)aA^a<8>s~p*evv0~HpZEWPAi!}*C3fXArd_pzhkVrjX8`xQ~7Da*76wTbD>142s*!A2nJ@t6bU znG=HEXbFz$Ys8fiR78u~d->#j3%~SLPtK(W?dw7tgzU~16!lPfA;dsD6wgNe0 zl5jnE-j(412~zQV&Dc_3j9(3pH;a!eM1U;$&Pcd+n$g|TH+5N==zN?_)>ED|#2yJu z2t{3dNLSE&>5~*dqk4u2A*=nas?(_62pN+Y33}J*db*rsOaRp7*o11z1 zn~RHCPdzKDLG+K&k9d*q?2M+kJv!JX=ap4RXL??{g{SQ#!AB@o zaObUICtgE2%GidZ>R^Hc(FG`QdaqL~3xNdR&a#{N=$3}Yb&uEZHmi7#UW47rdQGic zxKd!Y{`qD$pQN}Xv6>X|1?X(O+Kkt6bb)A)Y>K+0izNLt9c@s1^Mlx(OlJYlu&@C2 zXG^h!!P9U<+AQ@Fc(vsC6F6!6P&(OACM)!ULd;`lQ#h3;S@LnVo|3)9=Chuu>EegZ z0=PM^78Dx=c>E%tkdKQwH#q!HaHV6MAP=K4(tOr2hXk2L$Yo!xL*h(*>o5%9H8+ds zB_-3LQ1L|&O|c4AGeymdT4y)&8E}+#Hkyxz5$g-dY%!j1GMq}TM1EIy$hI0%jWAZ1 zSb(jTttFo^h+x|SODf`%ro+}{%9?q_QE42ZN3|jjQ2|kFUjfj0fJB1;JO~{4sf-gC zAOHa>Z)Eq?4Y^~vtR`~YxU#h1k0>9$<5%Td(ISQWtLT4Uv@W79+g;QT&+wVFE@g5J zuxHI#^f0Qff@X@%%EDI$V|+ZYfeQ|QX3dRqy^q0A0Qz{ ziv)@@m>SCcg5IWTc2+kEKUsU+f94iP372PezSc96jMl|fy*#D}Sk)_n83$_aj1M8KZa1T+c1ERKkA-fKyl@Gj4Hrw6jfzC&{HW2)Ojd&vME!FgMHP$@xeg&{bO-(qXdP59W%O0tW;JBeZuXek1DpK_3+a=5_dW3Z70+yOO4nH zMnQT{s==(-kpnpzkTaq9zFxUV&GWXM(NQt0P;pjLl@%yz%~Rb3sl%auPx#oR z7YD;8pv#Nhm;`tO?@$Y#KhkvYPc5&|+mOP{S~jyX=e{Kbi|<{BO>~rgLJ{;NLz(`l zH>AJM@TWGI(W(?01f@MGP70^Ux9o zse-R~7zDoRvdNRSlMW9MwY2-@VzhxLH)QqXhjW8CKkP_l58<(HX0n#HyKB5FnvwXyye!*KdIIyFaz$+gZUBM|U36OX5YfS5tB_KNqlmFSgw*5|#AXG% zyYiSsZR0YLiVhS&q5UTOjpaNM#XCeYb=)?57RGtfh+XNsDa(G(8mx#3SSHF56Xf|J zn?>vS{Nw006IMDI9vkui8?|VKdr1V( zHXNoISSNja3WB>Jb0&7|zUq?5M-<(L$OYG?N@WsiwYhUsYjW(6;OI zPhmzPOlD;$;^fn-{-!{OpLyEyCag+NM(gzxmgRn`+CnI3RF>CHIn*#wEtwS~3dSqg zs}?C_aTJYCg_ybc<>Cz;23K8C69i@YE)#TKF5n8_Ir@CYh@QhdY)$b7nFP5~gO z@DUGY>*tHf^DIL;cSJvsEe#<8_{c|ym;jU(g0AG*X$!td?TzH&@*Gi7uA~kojk;|j z<*V>aV(;Do=duJ&v|-J`*$=JS9?${jQh`%2_a)xx1 zbu?#Dy?_I|n_lysSqpVPdCnKLo)xtA!nZ zZH^tYdf2hk$N}sit7rf_W+CjTe8-$zjvbZHJ13W)cI7)}j+)OqbL7}jZw?zfAmS9F zMD71PQ5wT5@(yyGoK`!2sXF>7Lb~D}@^2k6xcFRBJnJw-cTDZZN4N@o(s2s=&DxqL=3 zYZMVF$9#lnEXX%CkkY1$xnjsD)oD%MK}tSC<*N%egKv*ho!Q%dA%8M1csBqaUG)Yu z7=VY05CYM-hn=>)9=%U#PWt!2knBV7UsVw z1(j8}y7XdHLRoq~fb}SQ zuXq#L4SWm_hcJ!0h%Og1it13VUJ9FK!X+nC4pdsD7ut)hdn0>h2iJj))t@iNu!*B$ z1q1y@^9i3})<-aU3z;YMFE6X25{4M^=}FQ$aDZpzHpR=jPUQATDyr;lReqfO|(er2%6r(G$XmPGXBv z@M#<+cNy*{CmQf3lnIecSVUPr5qWZt%Vz8Uz6)37X28ZP42;;XBs*OL_Nd=!p&Fav z1?IJom-Tvm1{h7czv>ZHnHu{f4=^g3&ml5DfYFc8E=Ey^_SObzo)~ZU9Ok>tb!VMs zYe}H#4rZ7APc6njR^?XF-Aj7<oKuYWM-+gNPl(X| zXf*#C4>2zSL8dF1y>$GT^CcB7`Ll_Gl=Vxxy6ltET z*hs~h6bSprWwTf}N@|!CkMlr1E&HezZpX(u2cl1D`%&PvExihH4#gwG<1WkqW#vZBz_c+l?UvSJXwtmOI7EL1~{0XkkF8x)p8B`qL|3jK6)9*q! zdjw39Sw#&VN$h|aBr<-1Y|M=)kWkR8&~8VEAKL1*VVWjgqFlABFN8dwXcjq$PQj&Y z^5y`!sb+?_v20VETj~-jmm5oQ{@!AS(a!;9i2fhYZ@)LZ59KI#;5Dy;7`=X~)$ z!TE6E(L39pN^RBk3u{alAMU)X;THu`t|XLilJl&hpaFk4IXR3RdK9ct8xLyo#+)mn zuY#8=dNs+CDp}@=E(6qBSoD2}WwjD91Qygk^Wt|w7FKN34fRcPS`4<0)3&F$;6nk# z{|(`Ggn`#)+yTaSK&;WiG{h9?du`zOau;>&48P~QsK$#2yNSwe(PM;U{YYIQ4c)tz zI8A@u+?=Q{W}<^dD46eMH@>W8kYxmYNG@_v=HkRusST+$czzJCMX$l%bfx2qD=vsY z8lz9KFV>-xI+x5`gJ7nk&85m}^Jt{5q6e6`nOxrCk3saE z&;iWe@KZs+VFx(%KOU+-hz0i^?3S+E=)pBVwxa>r6sr9$@Qf3N;NqkhE-!#3PR=gq z*kQIJ-?PJzYTC1o>yfGIb?X%^LM3j$RHnX=1BnQtJO>iJHYp2|%B-_Zl0GO#<##Su zq&!abqRAi565HOFo6<;=IdcotkWT@qc{XiL3w1y$fnr}uF`yteo^WDS3=p%3ceJ2! z9Ah(Fjd9#-=n-`Xfd>tY<0ieHY$fJ{Lim6mFfSe5)P{waNn^vJ=I|7!IVo(-X`ZKC z#KwlzuytPXsw@-2&Bzm1?NTBwwZ0ahm>O!NVb`bzmS zgZkwd`sJ@mzZ@&WSur~3&RM_hxpUMKeR+^0{GtIK!p#8l4zY0?tHXS#cs()*<(frY zyKuVQS`i^=f#NjqsC|za#MHDz+e-QY8;cex;}99OLTn}V)b*ek3QVgfrB_+6)I@9C zYpp1uz)1W$z@NsumbwKxav${l-*~r?i+w}{0n=RP*WeSfq!{HBUW5V^v{sniY1;cyiwzm_o_V2zp`#EURL_#0&B zLNNsFO1aw+T?;K%N0yL##C51YKDMBhgp(87Wc(%I$t~p9L~Z<{&`3?f+}b1>+?|*S z$zP?eg6!443(VhB)H z{@aYQF+9-o-RiHT@~9-uxTX9mUB?tm3{tlkdE#Vnmq zBK$-B2-x6)i1bVU8fmcClUiT5apg=(Gq^Gi3|UZntd|Er(2y{cw) z@=N6?K5lze&FXozqRk#bNvG@(u#SD5Bi{4WUQBS_hZiPK@Z{q;&Fa3Zx@G7L<5ukC z7gNhL|NJ;9PkwQyFitVMm>X{d2J{r!=su@L?5rS?(xwQ0TagW_MKH(0Jp@D`AJs~# zm&;&a2!BDBQN5)4oksR2)ra>O$f&P)!KAvIboz>iojiAw8(;B)8NJTK&gdqKzRu|S z;kQ{B%8K%c;}w~OaPWR#=O&^FQV{>^qico8AQp3)EyBo})k4rNy4 zk(F{t1N4*y(6#avgHi@zD;(rW9Ryk&AVs$uXXU?$>{{STE_?O&kxLvD_*SFIH1>~w2aWB}UjYOKdaKCv`V_Uv2i{*>SPcYw%yo~D^JK!^%9 z#pdVG3(GK5SCqq33&5aG8TT!0jFFA-iW;~D$Od0Wo2^IR^K7FLQQh?kH2`K16!rN> z37g~!`!@T5@x+TMF6C?b7TWg~S;rZo9%^L^U!2{CinHULdYOazrHq@~=ou?=>ZS*p zkXKTbCcPjOcL8F%$mnn*^N;|NrD@s%p$=es4|6{TmXDChY;7|?-nH26nCOBV)jw2% zX(u*OQ&cJ%@SJ}^Qq~uNYPoVJuR#I+LgtMmTqPEZedLe?*<@{l=-<**2L$DTazSd= zhO$PSAn8bzvl`%c8IYxHK_E2Qs4&gYCgl(Umkg#vOowJTCAsMnI10l^7JGDaN6VxD z7+AgnGOwD^%bMTSn8nq?yh>>$p~+0t#?H4Nsp75#M7awISxX6Y@#*OHJQeL_QoS?b zRnd{gBAJ2sc|L-cWDNu=>#Q2s#Qsd@tAYUYXe9&0HH0~Do$dHzZ4M!)G${6DY4zmz zY%3VTGzN2iRVgi3^Z2ap96)gsmuE5#Oh`>R|FX3LLD)W7jym1gMm)_u+Y zY?S;UYjtOc+KVf=*$I>}zGh-$ zzfo)Kxv3!??0GVbT7|2CUZxZ$h8S#op3ngb|dq@%hkq=#N34K6jV5YJmW zAq+FC)l0sE)^o%}DRIJ#0k7kf7(N)R{jMZhTK_DPErbh(;e-lj&rJ=K z`p8T7hB8<8h!sJo_y|l^10C#EOr@dJn#!5#VBXhHt!7A{4~yDO;#BzJ@1CB7lxva7 zCdYcZdf0No*0gGnPbF!f?Vx)j9JF2)Nn_Bu62j0Zd0?9OEU{11c^v{X>Q3!ee40_x zofMs7lw=T@)`W{UVa_?m!3ck67}xr>jB%gJ7}pn!V>OW#3y)givo9(NbK7`UNTki* z44u8I*fi?d6!i$44DOq`lyb?;x`5-s_;qR1aaP)NObLCk3KqyLz~7l_{t%?2#6M{r za=boVAh~6%AvXDpin>s)vZM`E7S<~+J(`lzV=lcIa%p3T!L;bgi)w@TwxGx0bxV)& zkWT!zh(*w2F5M7sG1~;NNPeut#6DLoLHEzV&6UiWww~z0BOBhx@o{A%C|ML!=cYj^ zX*4i}dZO<_HHrrBs$qr;EcNgnWG97q4}P++CO$51V-WpN#2O?!IcWieElnhxezw8g zNZb6x0^qj+AkfcboFnEEY;KJj$O3l}{G@l_G7B)?%`mlm6w z@o;EbY+~V(PK+K5&%R<40bp+0s(walI3YiI?!(NP0yc~1_=pY?^^%GQ`ThV0`B}>5 z=BldkE_Rdl%T1Y4dpUo0qywc2K#L@=F}+jSH`!x}0$ zy=y`_0BdMKbhP1cx%qAw{1$*fQ>Vt_%ZUxDG&c*xJGkx^h@BB<4d#i=_V z%bfa0|LLGX172qJX=}}xSQl?$O|g`{s$mwDZf$j@|~} z4Lcx&dG-6i?*q`}4jStq_7<~O!Bn*Z zY<>aTX{Jxb!@Pw>@(c8+71g6NugVPmcZ!I2>rGYK`QR>{sedcsOuQ=LOdJDenmJQH zH#w^Dy)GBBZcak`9%F!Oz_B@z@0O=nNRz7_*xkE(HOS2rqxK#TVN`*fpz zZfIugezl`l{C2;37on>PiN%Sws2sY3qX2Du>YA4rLwy3Ze#D|hX@mG@gsEziX*2IZ z>nB(#+7sNK$HUA?H5HRNiG8#|NgYV{H|WqG7AR$FTRDM zB|`xAIVdF$<5oBTdr{)7P9(E7wRoKkt=5Ba8%#ZDUBb+bI?g(dOiS@WJ0^aD96`lT z@Q|OM&?DRV$!kA9$%LQep?m(=4sA2nHZTSJZOH&;SLy^vo%kvDDUhu#it#@tXnoB5 z9K+JJz!Ch4pL=oyFs7sw=xYl_aRig+6gU+sM92tH%#1E@EyPmRl{|E6FiTT3d+xNr z0E2cvfq%kmU5{UU9?y8slSkC&*&*@JDM<&kqw~0vAs_Ivl_y~w%H_*fn76ZK&OQ@Q zC_4^E>$9HwTRwIjWmgYWec!Y7u-uCHMGkGr3HzMn&~BDSD$7IvZM_~5vk@bWRXf6C zpczC_n0qt|(RNu)AsFU|`kaP&;sJHsA0Tnw!m*0p_9dxYv1={#oKu?OPw0qS1XitcQ_Uh9E&?G6xyUQ8mNJna~&I6|LHvaN^I2X=v$DXMY^T zDtNu4*U)EHDY*5txM!k0i0OnKgnK*BH9_y{JNE?8e({}a?E9Q3#zc_R7n?pE-B&S4 z>FBtU2|4C@ypRtlJY)s>&`m%}5!KYxkyvFZ>M_j9wH0+#+T9GI-?U7V=pxkI$Ms8e z2L$$PkB-vQQ|m>0IC6V?MI_ItL?m{LcqX>j;?K_8lB|q=@ySYP^o14xBqcT0c_d~o z_3AK0Jy5kI{i1rCK>sE{{hW9LOGcyxAb!fHNK?ohp>0?%W}hM+K_;D@J`lfLyg?oX z=fJAW1M(z#boHI5Q0W?WwuYMN9!vE^JeS{-|o-Q*ki>DJnfC936lO>xG1 zD$f(%dbltTF~vhxAgfnO=tJULE9+^x7+}jiLiOABLS@A(#(pQL*|N*u<#ZKpE5YR< zq_9u0VJSct7NBi%|9kM9fpoYsfV@o&fdGmtAVj}17(76*V|*#9$T`;Ff}8UV9!eKD zb7x^o0i~0bYr__530534bQT}8Wa}t1WZ%|&bE>WFSyQd`eExBCn+Y^`EUM-Vp}dtl z-p2Nf7Yib@HnkKEv~bbWPK5)+Z(i;azZpnN&fSaD>Da_?x_bfc_#BMJy-57b*yL4Y z`n~bEa&S-3;JZ)ekYY5uY0QsKxjPP3`r6H^`VjG(P3e)CFA%wj1CaB8Jx=+iA#qzs ztCWyBK>nZ4=J|j$^@x1aLEP19z>2HiRae0?fiF6G!=o02bt708UZQXllx~Gp9>)N1 z%`X}717bHr=-U`Y-+RpoyP&9v+AMK@RHrC75C^SKUKC{&Oz@QMIL7+!(OUFz`L(t( zTmRjnU`$s}ujXberckGg=c16Wd8tGgl!9!ne(40THB&0_WeGvzOCSgeA!whGRgLoV z5r-N_8?cl*r4j=Nxh+1zJv0@@fm7JASRx9vsu&0>2w;_@qw!QJaTYZ@cOnfI>vx@y z9OBOUm8OvN@nvErA!2Ay9ZLH+;9Ct+L;d1Vs$6BEIMi90Y6tPUOe}2>%eIJSO9g$* z^(R6Y&{(xp=R-Fp;dDM+AYO8BwV_$jwA;fnU2*Lrd@l_0%a3Wnqz+ z>g!OXrrytobBm2S)Xb}!X5-Nf)iXg#brdKIk6#KVjP@e^L`D0mO+S_P7Q$puI_s7} zFA~o~lSrhVAcY6}ZT$cht%^8;nIuEUQQq(!56xj{LRenqqk4&9mRY~1l|uG%O{?NY z?zgH{kb$XVuedTe?u-YlYNa4su4+|+?C-g*Rq!Cq?eCt_9(T=2Tqr>2MMj>Z@JjVON_TjH(==@%?myjOFGe0R zlip9f)m}^AA|SuWpc*o$vGoMkySLe$q&s*x$Ketkh7T!mhLAR|p>zJ|lQ`d!!~xUM zsSZB|Bn~~Id6Z6dDDf!N3ia?P8sF#m+vYtH0&^TLA$IXaZ9M=h`y5}-KQezRzQu6=@CWLe5G~`v~5byay%qZL*h?f60cD# z@BkS4<-)hM+g;c?#I|F`2oOOHG6*Fm>!b%4H57_BOMCFhT!&n#lZ>74fTCk{O++d~ zA=5;_6osl~{Km_DAQMR0a=; z*UytcAsL^z-eGWmh6Fi_d=n@~S`@1-kFxP>_QTZ(YAk9M+h0D5j+9TXY9R8+p zWy+>o=xr9W@p6*VCe)vIwGBO0)h48ix@@BR^UZp;T(HKY>r%VIY*?={HPeCex_BY4 z@#tupeo6$628;5qavaL2ad&_kDRHp1*j%^JF5tSIB>1REu%fTK^Ab?t*pW8s{Rhes z`nw>=z2^w2c^@)I$ldzm&JogUWpXLx2o-+wql-Opc(=NJ+1%*^bZa(6}+hR(>zDWYz3R~Ex0+~VB{&} z2sv=^>IhI12EjgSfdkHu^S3(Zl585Q-Laaq{&ZHWx>Y ztB=dC9oei_*!u4e1(gf7)uK+a>^nsT7UEJKGl3m;o2TWNBU0S$q0(}IKEIE&oP%_Q#{n+=$>9EC-$*VjtRv9QQfYj=BE zj)O&?e_Bo$*~A6#7Op>^VAYv3lzB7c3>^blB~;Ofm>65|-cZP#k#pN3!t=}-YqrOo zIiv6~m^ouMa<7>);bT{JunGnLv1HCD$ObcKa>)LUGiSmFY3^zdkU673KYX~%8MOi> zVQTfnBF8BLF=q&;xX;WPxMDTONEqh%|QG0BC_Kx6sm}awV~PPPBLfIf_CIIhs~VX!b?8m%o!KTb(@8F z5XX(hE4B%FRGBkum;j3BF)j)U`0kNr&Zq_M%E&`T&HD)5+jHq(WYGRHXg48r=b1AK zGagOmj9Sl*66dg)GvHB$%o(*h@TlE0XACV0;WJwh0()VXDhsiTFKR1?DelUJ1=62` zK{lOP7dys~WQ+h5GTu z6mEH#!Ts^YqCjz2fM*(4?XQ_`q4ua4+V1HVY8+R^`<&A)$ix*$x6pfqTYS%S3pLWN z+#1|O1Z;;iZMpk>@>4?f)?K-Bcbi|yX zcTGp!laRmMVD`VkC;@*JkikGgG(yFNzVmH*zPTi%RBy!oH>~P+umS{fZSYHcI=Vei zMO88N`)PPP5szYz@s&xtzD5 zl57^^X*zzNPAK7~_?;*7#s-3RUlL%w3Q;GU&&FwIwwR=!w04Ddv*>cuS&bH#Y3DWl z?-jc+0EnYAdi)hVeWpB|$BE=LMMs>`lu(%M@pDS6qFc%^YF(ylYY!-A;(Vdf2l(BS zlY9syLg_J-q1CUX$_DgefKxn#e=&u%=>2v)`yB#38!uE_;sHIhBqYG0s5kxdY?ICY zopN3ZC#G#ZinvWcT3%da_-CFjlErd!&S+r#%2gX3Qq9NE2(=)t;&8e#g_j|0z8ZZP z=4Vqkt4?0b0y#*O@Z^0VFcVW_ex-<0cDEyC5gbG>aSz%nr0MNUe4gyRw;4GIs@GP@ z)(UGQ0C^Y?@imSF6b$jq7tfZRbhTQp+N<;i?WZRxrST>7;WdVAC*2pDY`wguEay{{ zVnJdfzK)ajYdq1OjMk&0A$rL%w6{+Z#mg+Y0P~`Zye#T6I$Gc4i}fs(xNHA>Gn-G+ zRo1&$Ew7V{h04BWRt5w&+hKs5CA=$MD{m$jE?70lK@FnQm6j(b3=6b>M%vx{zuKXw zc8y%#BW&HQkw?2(C;0bgp!3Jo=;mg&xJ7>@TC$kg;v!j$78n<;8lA7K(OEpu6h0`c%vQZZ z`ge9cnga$c zcX2}ufVqU=cKLf5Uw{$kz-XP-2FACu53R}U107IaMrChs98u-!{||YN*=Cjg*U^78 z97wu`RDwL49rz>J?60T8?s$1K!v%mvyaeb3HF^s6?_kdCmbpxH^!N2e|HX8(c%Nw| z0K%#fmDtZl7b)1XzMKUVxvpo)G!Yi7R8(a={#L3>)bAX}eDLH%?sMvGEv4?JzoNbo zAa9%RMGU3(D+F*(W!ce3y{RKv)&R0ve!@F)_3&*sln8Do!4@qK#e%jFFl!L zkPR}mZ9hzWTh=$UAiNZhmck1jKBexF6I$2R|kQlsi8{^_68ZTR^2{O9CFydNxW%kc}d~dOal@N`73Z z8st)4_3=Qmm8mX2o_MC{3#*BL@*IyC32bA$IiBct>-nH`9*oDfKFwCuub--a{EZ)v=ns14=Fqp?Hp53ocrZ$Gy1Kd1=#M5hA@nz?j2$*G zzn)%ca)g)8K{GFcIaaw#g&{_qqb6fnLlQ@%g9m)?IvvF4usR~Y$wt~iEES61y`svG zyfzB_WGE=zoT?xX2NCd3NSSNl7Ydg$dMH$pV`ekGp`nwL67?vvy~e606s+GV&LMPv z1Y=`PmSr^jy&<+TLTSe1;b$Jkq9qZkk`g^{$KNXz@}f;OgV%}av}Q9lq7szMcE|*J zTbK?;&FBDHgu(yT_a6Mmc(8za@&6ydfArvU@&6YtJPQ1eQ?!`C{~rL`f3%TZsDj#G z#N*{+y;{!6Wb!eJUdX4H_&@_+4cU&zHm+sfP{60Q8NRxmCo0nmkXjFXa>Bf%jM-nP zP+#HmE5JRe6nU5+GbI~OPEIU5Q9`iDxx(C$xs`TqYU1(KYS_?I ztwk0nxOkv{Oi>gP%pvqvqU(2o*6j=Me4sS000m?uOEvv?=!;AN1wUiG+`2V+)R!=0 z%e4&6(!9py{1%1!PB}|McM%nPlwWd@>Rr?xG)qH9SV+qk&6r(kblZK{n+{om%#OX1 zLi9lc2vjYq0|(k3x!maj6?Bm7G;|a%(sYukQoq`g<-r8Ymg^1nV)IS8E_BU+leT7G zl{VIpb4gwz_17D3Qf>MzBHjPb-21>)w(WOe=e>tFsF4g`)>WnoNXi4;s^Te!Eh~4A#i7rJO<<4m2_h^ZZ(N71*+6O0% zxe!hIiB6IMcYD1=ANh$Q)%P0Bj*m)?VNdTCa9j16+~(LwD+!g zrS;5&DC{wrLdEyG}hbnjMhtxPrNdfKlX@Tf76UTJxMjv z_wt<2t(mDM_YE*hll0C@)O+`GhK+mP&GO@TXEt3jb^XzYpQ9)<*EQ!97tBwXjFTE? zdD0zad6M)8g?Cmb=OP+UkSt|gvxr5lY>F&z+5q{77R7gm_k9tB*L7&WVL%w4tUSsqpE!8^TFm2xAg zf}->+^-xaCRT1~&xXf_=hAVIGS=eDp_v)V1$v0Ih?tDvi%Dlg@*C@3%&?I6(_u*9= zoWi4)q706xMi#uPyM=~<9vDMI&rY~-XeqBMOKK4G4L5fW>g59HE%Iz^deM)*=AJ_J z=P7bCyi-OS()ek4oja=@Hw)9_7p(X37|dPwz3Zlh+L`Z>lv^0hU*Z-Pqc3c^t1cYuR*U;{2JW2 zjmw;;shc*pI4y;gVe>0(0PD09_#R!N8RpT%6>mnl>ujdz8g;g#IL%MqNb&|2_x2PT z3glh;h_5@j;jY8G>`i4y`UUcqG0tw8JI*c`=L$7G+d!nqoeT!R9vzG+Y-OFh@i-Y# zu+H6#YMpi0bb(f2C5Oi|>HgP7NuV|E+91;DRk0}1(=leLdSYahwxLX<)0Aa;e!Tk* zeP@S4Nht?sHN99z6q<{8)j#&};8^_G>k+fxEB2x{{%nv|5_j}IYNm&yebg=Oq{Xr5z7v7cOAOBKa04HDioZJD!fe&}wI) zcz*r6$qUsRR4#P|t|kuu+07p6)L9I}H&3VL`Q~nK^Mm1${xPQ}kYMrphL!j6+{$|_ z$nH&Xb+XppTUTAH@5jw5IS)TK^^cgRS6aZ@MGxUmQg_Kg6S2lb#5|MJv-gWp-^|fs zd08IMnx16L3i)Ebdg?4KGYl`2;4SlsrEKmKOOBts*i_wPu}^8W^kDCEdgL6J_D^@) zSA(x&jjzl;L)z3fJTjb(@p?Vlc1nRXy}O9wKBzYdT0Lm}6*gbEHaYYTjJ|p`I%3t3>jZZt;eO1^%SCrYG<)rc zFatj`OI5UtxkcQ0cOcuJHmwAeO_w7(VzKd2+G3e?He@(8j_xH+Oz~=W`!#5?)EY!v zJ>EnOntW7h4N|uD&??n*u zKSY3}Npp9V2|Bgt@{jrx5uLei5=2gNq z4Z6WjF*|)+qdLyAI>Kw?EZ5#Wuc9h>j51Q{9fpQ*Z{!v;qTFJrH}uH3*V6v?_$h?e z?K`7+G%$M3NYKv3-t5re2rbGtZ@s^-_raHXo5x2d#s&tRh52&5^*g5*!3P& z+eeRD2Kv+K-cgD$J##M5dYgHmbAB{`m`*HEV<|T3wAzc;sFRzF&#PtEgBUex`p}2j z0FQm+6v408^V_By_@#1j^Sz$oT`#x`pW-Qz_X|>`TYxLQK#%g*wyV?R-nADzxOZUZ zsz;@AZuZu_j$YHchc3-Lbm4m@Z-FgQQSPQWu79W~#QmrF`u&FoF&^gSirhMs;LZzY zr*|)%RN!piqkg>`A+IzY+`-%iIG1}AABU9UNZ zC3g3b`TdN3KFRW4kbK%|9df^~a%+>JGmSTG;|lFE+4W92Hz+OHcd?kWh%`gfSi_Sj z=^`JGrk&tp{jZ%I(e@|u0k4QKbCocZFT0&b!|I%=0YuSYaAGZJbNfE>LyK$>jyl zWOC(N*m+#9T#Yn`*lQY&*+^#g$QPb)rCqZPeSQSbi-& zcDEq=rY5i(sVgbkBVIpZ%g6?sFLk;SdF@E;h8ESEGs*bQa_?ULrc!+5iKT#!Y-7SX zp(uakmZ@I3W4ELW^qp~~F3`+x71I?Wi_%4@H$-<1;C6mvR&cL%--x*4)AVNkBsa+% zN&1!cBah-XZ*i{GfsmPWhSy%>5_-&~5mtX{z?oR*anu`ZHt{r_27{hjAMFO}sZDWM zcd1xoiq3IMo9%{lo=~hM>4YAcfVr)h&ZNcc-|cuo%520#83op~>@TMgw6 zQ^|6!vHQ_C-{Cv;!q{NyM`7~nq^(CSDID>}@?Os0Ke*UU!7XC!6EItQGl zVfq^$+^gNh^>&SJ!2R-S;8Bd`32Awcc}X{%rFWwJDU)T=P0zAVo!5PeU9K8n4=-rx zwmP7shb7+*C-0-{JG3#NRPSb9b44FH$2ANTW{K`qMY2HK>eMU*xI;d2Wg&MOge=4V}#4qN&~z$(xQ8h)!8^o3j3K~$7bzV zAT#tLB%W}TC1)Di=&*fZe78_e!^9FBz)PYS~ zl)Q7UL7@RR7Y5k<}limcIXpJa)}SDi6!- z!jkEB7Lj>W(eU?z0Au89_Q6$VA=}r=hWrSJ64zgqsPA#dorh-Ei`~}YSTncyr{iDL zX3X5`xXCx)cis_?KZ={T!`SK+cXm3NCm2{Ca6&0;G>+feL|s4)j6^BJVZ$3O@LXeB(}p8WwdT zJaM6%3Tb*DT8wqQ?ek*JqJA{h#;n?D@v1iJA?H%|M$)_NN7Yh5eWl_xqSp$rdpzSO zic~Gp5<0C{PLt(6#xKzkvX?#&c71VFM-WMGC+Rf`PBMvlI?1#S-*PpL+S6nnlKaY= zBJ!vgPsblnmOXlNQx9JSxgEc+x=d}NuL3pEk`&*#iBdJnHBs&^z1`#LsbA+-*PGrh z<@m#2$h$nceX4>Ptrc#AL3cu1O*B!f9HBC{;TX;v&=k>gYfFEc z7C)Hffn-QyZ_9lN(&+PLBimAnxS58f@s=|vq(kv(or0sw|4a+#MV360yLV3AaaLG( ze#XZ|FFqGL^yrcEs*m1_nOR};#wYOmIvN?80fRbG8}_y1P|g%FzvxGN!q_RZvCdmX zXQj&ZkNBA3s`iCRY70~w=mQ!26$zTgjWsx5sp(A*=Uy$TivZXW&dLma9B`jL?)B>5 z6a}CBETix4@h_{jpN9O7ZpXqq4{1JwZaEb2(v0%LssgWGk+Paw_(n?T{4VGFA03p> zyVNMghUvf&qn@tO>O@JKlXcFa0|`&ht^*Rx>^B`sX6%!JraJFqZrN@#$n`lh?wAx_ zb4v`K5x0is7xXSEZ7z7!H2#m z10#ID217&T_@{L0fBZxG$0?ZndNi}vh-}5{S;M+_&(P33weE^Uihr@?VbhCPvoq?@ zwsz#6nv~l?tl81w+TNj7xy5pvpJ42?gb;eu|zS2uuk{x^O)yxUwP1^8s3^bKH25F{{_hNylGzxT>|}Ixc6$L6+B* z(*f7c`BY!>wv1#=jldxUO8-GS1nBFObR12=T|Ac@9pn*fmikpt3!AWl;M!|QR1$e) z^D0BM9&~ z0H-5qAGb4~diAWLg?H?NIb&J+;^7oCFEDD~NQti{LV0wp-Wh6az!yIG3t?DFpIgpT zL`(jggFZTne2`9eIK6uCwv3)Nc)qT&U2~-M07IXVctZ7gh}MO_G9P~6HNv^w2t1GH z;aN)!H49c_PVZwKjx983p{L^S3up{=WUR$_*8FE~qeYurA~QNZY(8Mgjn_=xl8mV= zG=6vxd)Ia2phih|O5xMH5NUx#&Mo0-ksGF*$%79MaQicvF-ds%Lz1KcOYR$s*UzMM zy2KC~{JJ>v%r+R{tGO-C^ulBMOs6?uocbo7c^uZtMOvFF00}8t)HusMI~!}H2=jeVryao6f!hEYWER6eDgeLp^?4MCCd(gi z^2`V~S^ZPe?n=UQ8wHDyvjs=ykjoT*xs!R8mFXW^mrWBkoVfdBo+mh&$DOz&M9U_f z2{Thr(pay}`j|@X`h2q~N;9eH(L{L#!u{+=3I0fhQ@6dX`eG5Q=vdWIR@FyyZ(U2M z^ZDJ^CTAb3m6WQS^D@TCly!UT9yceldU(Xt+)k$!oA0H+7;YZu{bY1FMLK9Jwev_f zIA%E12T(OOlY=)K?!PxA)xqmybj}c+JEo#DDEx9E-_4pC6BhfbpL29eE;2U}FOBoQczofMj+1;!pF*J9X8{>c zH`W$<=Y2@-yOy38P3E2E1?Tk*Y9yS4mT$4z;s#w$|vPBZUK`Z~BA zK4ht)FME&CXxD$FoG^-s6BTk~lK_pE=K#hsf^GlYMea{j$O|EtzmjT*|s{wZ57 z)>94PWB*hwC+n$(@N#o8FH5<3bPXPf3pJpQks)am=z6v${QQ`A-~^Q|4_K0rkGJJxl|+t@7uon z3>2D3ZSJI(;52l0w)*k(1g(MP<3=e{rRivP4mvE!jux{muy;IvTw?DhJd>{(<1BiY z*?&5|Yk0huhI!7ZAw>e0xyu{TbR%-(U{zUR~R5pI4? zc22yyV@3sLaUK=NgW;En$);GtbDH*{R~av}yf(need*e088n^0`f}Jg2n{pHC2MO+ z4SzzrH(&LRzoKKGZl2jDS#*7)Owz&eyn^9ITJBQyF{`s~o~wlMd1mxe*FvpQJua&5 zuvJA4T%yXzFAW-cdrc?kd?Gx!R160e(c5r%0BL!41G)Dl)OTdeH&kdE{XO~>YZw{) zw0B@)oHk1!ZS}nj=|~KZ@|?S)f%b}yI%fwwoyg+b+4+gcB1e3fc4)FbLX!Y|>XIuH zoV;fyM9x_T?j>9Fh~(T8H`^X_?m^G8V-o}PY&ATX#w0Jk7UgTekZyptEXEAzM;s#( z)PpoNPR1Lq_aQ~iD6Y!xKuT66bs5d##(JbpsJrFN%;22?bA&e6(1%n!F}_(khcEYj zI-DIcCy%45hEmkn;M{dt(Zoo0I9;lyW_me5Hu@W3uSSbCIF+tzB9k6$jPY_Zv$~V+ zVMG1Y28Tbv2n7dNdrVrGHc(iT6I_u8v$wCYV9lJmP_c+f?j>^}W^~=F?uzHC$T>&u z-h$2>yWZ32XU{1223_f0|__ z-jeQdEVXR&2|w4Uk>35}&ey(vlHXIFF;^594D90hm9{DO=uR{g)gjMvv6xH+jSqg6 zj5S9hBnJnEY5eqT@T;+g^xzA67lIjvh+|^dSQAf zE4ehAs99sA61*A5UkuCBqBg^0Zm-TLEx)^=h3gG1nFyUAM8^lESLGLV-aWSKMwIrt z5%JP4Us+oBeElZI`#F7OC%t6NavSXy=(72>m+XXk^Q2~YH)HzbF|$*S7H7t2RQznP ze~k7*j?v3IX9Xmk@UILO8yi2RCC>D8YwiqV){~|x<+^=(bLbJIG2slTukUtuUimpy zz+0Ab6l->#6#1-O=Z&Bbzs2VF4scb`lejKIA6_uig`Ohufx&dTk?x`Ny0W)FZKk$f z_dXw`gZDqh`X61asi9DerVjg`rvr(bwUl9;f!C$6CYrn-G`+L)GF8u3j?W(!@aNM$ zr_?AljX@gaz2;LCi)iQX8^oFdYInVIjkC+>SjCY~X_bqfhD(MHL2=GIbIOQXR3IVx zih(K8;gQ~f^kDzU1b0rJh3QHv!BWr3Jo?tNv+X!gYLJs`nf4Y)uBG68w`(%U&rr6x zmrh_X$1((WW+VKPvvg$C95qP$9tZd#)F)x7^X5BXx!xr;NXHJ-|8(eYkX$##q-;K+ z{gWwL0XgxCJ}g9KY(^gR&LEHyv{}cHT=ID}#aeH^z??oL0m#kcqjYXKJm#4#_ewwMEuE!t?psF@$!6NmIxsXA zyF*C#%`xiW^KTs#OY?QZJd~O)#HpE$G@!}_XrlfTI_M=o4Sf?`^!=k=T3ki*LDG0U za}>xfGv$8+yHGUT-O`kQ?zop?g`^o)pv*W-4t=??GBeY8Ysy9jU*4WFgxLcy0nNRze7GMSyuZnjFT3stp=xR{Csj*SH@rP4;>C{TN?luEXi zZ{=Fy(-axq2ukS0n&^2he+ywR6c$*?78lfiPtAo!d)6}7f0FX2=CSktnV&;@pXXqPsbYZqc_!`?Wb>iq%%YP&j+*S z<8^N zed;zpUiECB0X0p~PAi|13iJlGzQw7V-by^HyuCuS;H_ee`Y~F_C(9mt=|!WfDF)Iy&^#q68-cd^EEY`;c?oOhlzUrau@CND6p#} zl`T*yD}Q@$rnNaW*8g&Jgmz@|iYCX>8(1m&uJW#|Ml zGgo5n+Pp=5yy@itCTsNV?d~`J^a{PX(^|R8t4?DvX9ej?daBA`g39+et-JY`{9~s6 z=U;i7Z`RQk?q1iY&y=}-&Rb){z-Z=muk#Xv-!9O~hIH(%IhBH@0qH&Xz>sTx$@z$~ zXI0BBuCu-=*hH_hdgUveEvq$eAjq>aw@zEt%-TAS)+Yyl z(5Cngor6*6Jrf-nNAr^Tt&g$0>|AP?y}bTf>VWSWaN5UQum=2^YWEs&r%uv_Uk@|Q z>M*j57T(I{Q3~NZ%FY@nV{tKgdW%K}hV(zagrZf{@BcR%TOPL^{_ zZ2q9%oW53qjb`_>bK0Gw2iv7-mcN)P?{{kXS(?Te-fuXqipFXD-pWbgF3O>(e(x&5 z3paVMDml}vR4}Vr2gm9B9oll(^*S?{`4au-Y;mV`M}Hn9f+^N?z;#1n95~1<7`uCev-LQ zJ06T1D4vWZZ8#w3uF2IidXK^Vd5~1-Jo6ZDopOdz&VHTSQB7vuNmCz86@zLB_o8BQ zLAUDXiaWGwe{`TZ-T#^<9vx+|ScenH_Y~pF?2hx!o}O062Zo28*F%o#mis1bj#_lt z8m2DreU-Jt*hk|7{h2{WF}s%H3qPqC;JXF|-xM&r=&4cCKtW&beeL*C^BF_hTNHDD z^X&0xGLyeE(UZKo(t~P- zvm-#4)pHD4vqXf_b&id;($!^Y@EwcMe8$jVZipG+5x!w)+11zC zQqLuQmE%Qof=k?VSK1;3B41prkuwmWxRPpdQDyV45bX27DHQ#VhF5id-<+QO&S*O= zu_eblJ)Bm)?cuE-4p*JkZzY%xz{7eun%c>Pe|!9}SO$uye`0*(ub#Afv_yG~pshu@ z#|V4KB~{v61E0Xj_X%g9n&X2HMn_$bN9v$QEih*jDXpwEbDUh2nrU6X zIRn+4Kp9{-rE7K{a6{)AJshQ_mXqAYgsoO#hGzNt<5%nXMP`DAlGut(Er|vv65{GV z@G4`5?qk{E{`9+JG)GONV)MI~F8C#YC!fS8RDaAT-yH`x#yf}lX?xo8-B6;*NGOudy#=RrEagE#q`j}|2%)Eg?q!~S#MY{fjNV6PU&7?;seMbgl zv1b1+T0iK>yvlFp?`xTED>vrhJD57e6tEuZVGgC79G-V4$zWgorD&$^}=J+Af;xe^?W-P zGmqoZ4CCem9kPnXw&rEUGZUbo@^GgqF-N~UN@6G=cZOM)SB4dna0vPJhfPI?XU1G_ zbsWo_(=nWbF`Nu4gLi$li^`18>7xgsf(JaQ5_;hcW@!fkJ?4mOJ?@`k6BUNfq_=LN z{lrE`u-=ARGRZaJ9c!Rxs>H*giII<;7J?LtAF!SOc7Xrat(=K#u1n}Yax=_$tnjJ@ z`jPLBSFX)~Ha!X4)b-s>dmz~pkeR~qq zS9{o8v?uW>Gqua7mK&#~*k+_k&wljMZ?HdCkxeVKUI>pDQ{M1=N4L7 zpm~Q-#pVBssfdwp;BlWTV1UPc?bBYgD8YQ^m8J}eEK1P6i0ppV<}vC~ZqSM;u4Zue zy}_?e&_QQT$6Z8=XB&6jR4H2pEsB%is5=96GIQhGYvgYy8i7{+CEb?i1=k1j< zU&%LUuNQPh286}#(YbT_sW7SQ@V`eFNr#S^b4`CXIn$p{MzR6sD0QX9@6cMK(Y}L5 zwH6ay#cFNQBZKliWx-+R=Ha=hoV%m+deNy1Q_og#@C-LQ*Xc7d67uSiRJGnypNZVm zCw+FXKg9GWr(-PfxP2O*nOzya)1OKX-arR9$zJrI?=(+CURk25gif?rRnniOOjeyH zP4_#mF}E)1T^}0rrxAS+chv}wn{rXrg~R!RiWj?)>N#j(TP8K-FYdB(I@KQDN(4bhB9x z_ZVNR;`sn-S}uF3)zCU7_l!qD?@1rAioJH0`;eIH#m8{|gKGTg$9T~XSrze}^Wh2i zL2%s5xckOtmn%J5mhKN~073g-Ck;wE(4ft%obl*|_^E{LZ~5Y2mOh;EkvTnr4o1y) zmFg6ix*W$kld<_|o2+Ig*#c`TPwtuexCC3C(vrd2aK zaoHR@i;1q1zH7au>L!mHXS2Cyc8$F@O)_5} zxQB3O9J7SF>cWKv^4gGVgBb_evmUA@??J1)uYWQUV!+C2sMSq|tY(bYQt%$GAh+J~ zg~MH^edW%~b-D)oqqfk=6Iyj}9hyX$kRyHhf;Sl4Km*$ry;$(hn|O33dvORJDmpO_^h zbY2Nc?+vBY81L=vHQ#uo#Sde>FKKVi#E5gS6ONkb9)Cj#({VTBvM}vUC1JkH{AS(9 z#|pX9@UBbLWbL<5^ynaOx9R0se(G2mB0av1iWXp1dF)QE=F!1d`RezzO8?rbc9XED zQ`Js~-zigrb#Cl-@2@XoMIUI!)z?3|**&fcQt7%^@6uc|UJ7&f=X4$c=DyU=w3>XF z-kB}CHZi2isUG65TWTJ9NAtM*jj~zI#osx0Rth@3n&NR)pK2e!v)Am?mkGm9X6o3z zkQB{Rms1N+J_Bk@@t?eWPjj4_(N{(9kIyuHR)w&u`1`7ycRwU=!{3j&*Dxrh`GKk& z-lx{;<$YaF^*(ziX!H4spJC=bkVmy3F|UUgXO9s{n4_*~UBl==evHby8$HITss-kC zl(S>le1g|e+H&7)Am$)N;1hy&NvaB+f^@AhW<&2dlj4uf=iBnuvg$C_lJeXX0cHOV z2I&Qp?cufNc2Lp+Bl7AUXSXRR=;{w#wOZVNn-T0Yc_A<9j5XIN>f2I zM%c<#Y{p2DmM>S)0_lQX%i(Ou?}dmtn;~*9&bj1@DD(CTbvQi9jZKPPt=nYCTiRzf z#Ciil?hkYiFiVWRN|4-d8otjPO+`BI{>B&@qtnQr4GxUwE2}z{Vzhc&RdB<+{%*dg zpzV5(>a5GVo$t(|o9$pu@0HIF-+(;Snk6eEcgY@pF*zL&eK%Hb$ z)yj|sW3zV$vi)fjhwizkoL$qrrUG;Y-eaEFJN+Z0Xm71-t$&wX}Pz!cXN?_&+oeyIV&xOReLK(oiVnV!HBnQX3e@% z(49`@Hg0(_oICKOpxRPhM3=%#-&IvI-3cFgOb&k4+YaZjW~ai%Vh^07@bkAUwZM!n z&Ewad;$h5~w=t#*ea7Rud~MD9K#_7~AYmz6xNuhk{Rn zDd5)=UZa>#)vSB>AGxVc_clGdXizS!+c$S+u;yJ^CN<;Kc^KZ9L+VU>$~Pn!#QJ;< zg10=}{W_Stk)vx0zG8MxKh+n2xrc21G%xq=_+y&HH=ha^zngsY`2PDHT@A9UXVBTv zoR9;!njeptt=V1aFFQReT_p$PKVI|bQOf{LgwR1y1A`9+>6^_GXr=RnY17@c_uhBu zr6ZEO_V%$pS~uL(SRegP+HxIbc}ncwy~p^x*4XEHgV`-)m(D`*&G9Yft?@@VZ?~9% zuc`ep5Bp7x)0Zo`ho1wz{LYom4Wd`M=B)Az?Zx<@>;8j|N4ZjX>0R$SCkl$;<@GA& z-s8z@D-vdHMLwl__c$r9`t}ECd_SIz)_vYxGPjd6cNP1ZXU?f`Z!&YSeEs{HXRgWA zMm2NloI-x*Fx?miXx7$jQ80r z^eg&+(=76cr-tb>?fvvQrimByahg~Ytp)Jb;&x6bd4ygf#92?K9GZ;3ZFB;u*NgU_4$8(UfipU0oS7<`@>Y>Kxu#GVf{ z+=&g|?H`E0ogRKZ)<5?8?Sau(nc?){*mpml7*5j>cHk0KKz{HgmW*&KQN=zbSX`+t#tJnp#v#iy>HjriRe zYkvH+g9FKTyE@-{H_3r#PviXKy(Hv1o;?yFP@X&m`QdvHIqKoFXZZE3{XWv5Urmws z9zA>X-UpA8y^dM;A3bA!=hOE)kosdXg(H(sy6~g(LGA~Xec!W(AK>cahfnYG<@X+T zB31KCvPQ=)bag$mQcixcon@XRo+4il9#g`F_C4~5d#{r?Ec@((N0tD^^m@pcXfItR zK5u{tJ#K0gdz@f%+!bu}NG<0=LwDRrFC25`^*(sg)%oZ>N8%Q$jV(s-)SqtBi_5a-b^6w!mOCl;;a8GQ(%P|`&8e&7{( z|KY>r{Vvpj4V!!7^^4A6&3xo+)bo0zcj!|(HxbZiRG7yY z^Ra{8Y{c02CVQ&4OS!`|H1c`UPo2fmUXxn%xI7wbqHpk}=xA2wlUee8VY5PBk)V%d zVvUyh)>dSk&Saz`iRhHFl=JN^_ct>h(iz#lHw!w2`MQc&VuETheJ{%ZB|~M$pW=8r z9?Y~n-z-U1vNCE8sHPLqP407FCZU6KjX=qkH%?H;A~u`Pt&EMmeuD`#yAv=+vO2eF zpEA#9JG6lND4uMNd!9Gks#(kw3q!fN7dm<0!W7ExcDI=`#-}GRfx}fKa(qoWn3etRlkMbIS@tJ zzr#_*6s_kKmA|!tz@9r2zjGkk+YUHNv`4}CY$Owr-}8tgGs{&o4=hDq`CSa6DD$(- z5{tYPE#(zORf|@Bj#=d(R{%}casKVP(-~m;A@fssy>ll{NXD4Wel4 zwiLyGk&`Rt?CHEkdi!ldoi3)xRgd(p>G+>=a_67TCF}es(iS_vfo}`f)1HKmJVmEWtKWDt(!|UlY4O7y0GSbP7>W5?_ZRsXNlj-7Q`+5lT@MDVb4& z{ih`!L}u^vTq>(jx(1|yLACUjmAIdqI4y0_(*y2W0GVivN#c+Mlv5>Y#m_dNNm_0! zo31$g%z?N~@GV2+X@9b7|p+BzgH2W{K7$u74)- zWldy%7>YqCx!RpTk)7RQc}oVavp^)R9=k~xn2n1r&3{W@~oLiu;}sW-rtbYWhk zxtN+SzlWf#Xw9#9bxVq}P$X?poMSAK-?Kq1x`HCfhSZ;8ZLw{Vb0GNA{n{9Q{$2 z5qCsEbc~Zg-bZ5*QM%wJE+Vkvj>PY4P=**|mi#Wy2^Q&(YySk>Ds@0?Hi{(AQ#nQS z@HLIVoI4V~b0D5Ld867hf3mPV`;dN_sY2=qPm_AfNPUNXM=vg?KTA#hsX{B=wJr{e zOZV`>pDwIx(wt2BMWHmOAqW-m<`d9s_r1mEYwKYJiqfG({M3S~>QElx%RjAG7!|J!B>ANN%^zs)? zyGUAuiVYS-W)Ctaz+y7leYP0X+|-Gw3F5?7Tr7HjDgS9!R0DD{mv|Z} zNM$m^Bl$gt6dj@ri?-a6(3AsdYx}>#tZ7`C0n%V^5kJ>~R1F$V@p}cjO&p?fAgZ&! z$|P}BH5jB?+T1*%NKM@ee#=nL#N~A2hp3+YdUj_e;zhM71^L~LYehZ2U&)I4Kt|6K z@olN`Z)8EKuF+g-)Z0lwTw~0V-&bxTe&;~)7yl;KE_snusC*l8!xYhIhAHxUJ+HkX zH$8TeuLuq54N8(+goY)cDF$hGyRJC=%z>C*{mq3%Bc>Z4$nQ9$iWW`BZ7E883tME@ zL*qAbaIMQC^|_$OQY6`-QYkN5DmyvtL=-1pxFhj92jb1nZ)Mw~xRL}?8?Xk|*?mZEMbl8PgxV5+zL-iDPl zBHWFYs4oXFogr4oS^$<02CPDOo=j~@Z8vc*DXFl=U|WM{uzShCVg zoNm*$NfVMPYdUTD-40>X^3_gzAE`@c9nA^-Xd-u`7EPvc$nOg`Q5wg2{#Ke$+pr`l zNr^^dyZjdI)EjJ5ZveHT9FzJgVjpxWEpS^q|E6LtgrOk}`Mq#guU&u5G5{27ip1J?Xm4o87DzZ~(^ zzyz=z*a=Jn=Yh*WTi+_uUBh+3eT85B=U77nFb+KX^BjK(O#KB8PXT9v^T2(eZI9@$ zh2A;nTLdlxSAjRxFZQg%o^2odYG79sDDq26ek32_uVdJ4`>P%DT|n`l?Vq6f>I3Sl zLq6((wtaT`L*PvUr+^p0E8yNHOYQ^H$hV)q1<0S?P+tb}qHhynJN-1`gYtzZ`4_zh zivBF(gYu8TFE#%a|0n0L3z!8?1MU3pApQt=47>uCZLvh+FEe%n=YQPR#9`652l;bF zUlw|8Ew=ncNck%0tpU~pwe;F>y#v_e!K~CmK)zNXzX`PcAv{TMD`a}M z{LT&K4{j)bd_(z@66Fu!Z;?NP9^2o-6MqXYX!+Xm7f9#oFL6qK?WguHGOq#H2}}b| zfEPg9{$-?dY^67+wAam${N0fJ1BJhee#Ex_NZ~Il{5`}U0*`=`N`7trX-NLiHd~wp zE&z9cd^P{?G2+jF<*=(37zZX5du1FZ<0V1q&jr=~7<%h}f^A9y=YV-j`PYfW{}m3G z1EWBje-@HI13gQ?b>OC=U-GqzbOhtz3-%x^xQDRdxuQS5!#U~zP6F3~+rWyyYUiB8 zGq}EF@yiwdyu!aiyq(HoNd7+3Jp!Hs>wePK%;Ce3eDPxxa)Pp2S#T2R2`(w=?_f>w zEM^qff!jda>iv-X%aHsEq-)!=sn{d@N?fl84h7^VLh{=}@{=L?eLj3yVSf16P=A1z z7N&Ojy5K&-f>#I&uKsoA3wHkv4hya#EVzrXU^U8FFoCe(5W<48mR;}y`4+7DDNf)1 zSK`}oU2qa%!7YRZPY@QY{adVAFoCdOAHsqQ2n%i@EO?BtVA z2n$y4v!q}WVOyWXui?7j5yFD9Bwujqr)^Cf-bQ?ttj7nYEtK{8wibz(W%z=!a9&W> zzYEGTcEQS@W6gq72n)(;bwOElF1Y#gEH7Af$YH^Hgay+G3(7)qJAZ57Z2-IfE;ELJ zHeYyK;J1R;uJDDo4gMr}vkG7AT*h_5*}rF7!r}S9&$s~G0iFVFsRZKdkNJvV2g09$ zfFOc75MDRUrClMB$Ls)PX;fVEDg>Vf}rm^cSo&<;fA^(~IE&@}(!0|)CG*ILO zMSjwfKR{S`f@#DDOtEe$gxWxdN{f_ICj#e`1%QCf};$e+v5T^fdmo#jk?i zIz@l&KVZu`EKDOT*Z{qP9S94~K)>KF!h*7=L;N7Pamv3Af#<*rpzs7W-j%|abY<6v zplsd{lsy=NvXw$mc0>ru#s)#zpCBmP3-7mWVD{A(Gw0zCaU9AEycj2%Fc6SU<|OO$VeTsu(g zNu6;@wmk_SdqiK4rLXRnSYID-1}OR^UxJc9O<%jBZx!)7A@%oJ_U=Re5al6u3EK9h zE%~K?&dgHfk8db{S)%+B@*(o&|B~|&1upyeqZabwk1WE;bJo)i>;{%WulQ9^{44r8 z{spcmd^#7p!|#vKLbCBzvjT7SL_kL)LC*<2#cRPATQ;& z|I2pHIBe^y0&h>@i=C35=)Y3RN8@J#^6SA1%1`+4C*dctzYF|s#UAlJB^3Qzu)9{nnUk*De6nn(a z2}^DcVexYp{5=c5CH_Cl5eVA)rv5cwh%0=tQ_>UtDahHWX#7K^JZ=60ctQEgKKxnu zN$lSOe_OFf{M=^AEg&p@mVUJremlFgJ;7mHUjulj3SaD$^hAGkNdBBsp0@sW@PhKY zeE9S5lh~gDKdaayyd}uBp&rY9@*MeIQtr35yzuvb#k)K-ep^8P73>MhFNa=@zleNG z{%gUnQ|uAmGUO`IPs@Er#wFT#%a*UXp}eHGp_ES_>`Vb=JlTbDnO#aY{|w{Lh{6}W zY3T0*PsW|Lew#l8{;ZznrM|<9p=c6G!CuM;fKr6cun?D_rKO2%i7m~l~!{5U7OJE!P*a@`rdl-@* zfu4F`H*iMuqgJ zg)h7V@P*f-@P#MsS9n@|IDww>-^6~e03N)>;S=D-Z|1P=Ut7NNhVs=nl&`&^e8Uap zlQ)#_zM*`&MENt+gDm9dfP26r;CaCMAo?Z)>Z|#>;`K}P9V+(RRK8kC{}g`lOJ9?3 zh2A7^2v}F{onM0byB{wM>kDE_hKYf9t~Df*95k4}MSz^>oI z?RN+`4YbS8mXCs0s(ix@8pZ# zJurSleYLl&ukM!g?ZR*E(ANQ61fBqGe~H{4AzC)SEtyMMf}<~1KGfzmDtM7QZqize3?}Df(4D^jv)#+gtYSjFmv!{sZvmz+VUM0B!yu_(@!!28z7R zKL$S&%uZ17Fff?Y6;-5CecUt`Ako+Zu-=XN&_=)e}@@xlYfYZPQ z;4#oH4_kgpkzZBhlZcn}7ZF}k_`>VL^>bjnmh+JSP6BOvZTTd4*^vCS!taDVVqX_{ zLkj-_{4!jx07e!5(C_1XP6D@p$H4OM9w+87Xfhonl1mu&zZeSX?3fu(R{uADm z#oJf-r?BH3Sod9AK7Bx2|Cz;a1FuWri{1;!rNPT6{44Ml{s7yt1Uv-V_6UCn*Jpqt zZ}ZD=JsFUn1#c24_8cH=>t8~?)__;Qx_Zt}P<~Y5uUPua!K+dDbBJF8UIJ^s+uI(E zU!(9BEd6ERRV(~X#PriOt`D|+J9uIF9g6-v*d_LfevL0Y@lOhR#UJMgYyJ@VBcwYG{;Xo3 z@Fcw}@S=*p>u@~*6uUMR{p--P3ETme$Gpqa=AVFH7Lp%P_({Zf0e69mjo$Xy{3v)q z`5oZT1=KJ6jKc4*?C%CIt?(tEeUO{A@@=<=L+CjLUH}tK-u}_}QH6hO=`RPbM&XMc z=awBa0r^Srg7W*opAM*B_)`l10P#n_Q(z+Q?N6Iu1zuF)%RKG;?`D01GnhXVl=%Wd zdA<~k;yFT4o__>uF|HO|!#GV)zCkEBiTkEt0{1P!48nqO+%E(#5f)rQe9ntTJY+CL%_5me*(D+ zpvcvOpR)AW{ej3yy0)C8JE^3<@r?bp1v~^^0qyiQE&j0&|KtPK-}52kBybjJ>+eE* zQ2tam>%9P0e1*fcKwJM=NPZ{u>;jL0@j&2e1!l+rNnTHQ+w*MB#V*5bK`> zE&w-yw*G00zwW~qFAGY)HiQ06a0g+*@;fXc*nzO%0>Xla2n*IUv%Fv*!h$OZ3tk~C z*m;-b1y>Okj3kgFV7G zzL(PzY)4oyjj-T6!h#zJ3mzgYc!jWF^bc`*f@=s1?jtOCfv{k88_Nr}A}rX4u;2{B zf~yD%?jbCAjxf9u5tR6<`y3;fKv=K`VZmvH1y>Lj+(lUM3}L~_2b`YZKEi?x5Ba)a z7s7&*2n#MDEVzxZ;0eNlw-P+ z!G7SLg{>Gj3N9loSlPw$f=Pr0ClMB0Ls;+_VZqAxp%0j}FoUq*62gK92n(JfELise z>lfTWSWw2hf}P#W7nJd;po}jC(->C?&LJ!~`2(y^@CsqU=^nl=SoVV)7HmUUa2jF3 zZG;6QKg9Ba^#}{@A}n}{uwZ>J%L{fREVziU;32|-6@4r(*oLs+5W<3s2n%i?EO>~p zU|B!%XAJru4z`Ezi4=@9q1ug(BbrDD3{d;ygF+4c$lBp`ncJn^5OzocC(B42{_@ULJS!h$o3|0TX`kjqPO z8DTqr5`Tf~g557z-sVgE3a$(5w$M&b(kXk1`~Xuxn=kRRxGuPgu;2m0g3CjagF=m6 z2zCQ!fop}dTxjYe)^i9v0XBTh@o``qumjiy%qhZ;9ZO#XygG%yKg{WzSXc!;f(e8L zdk_{(LBHTi+S@-Th(86M0WW};z{o3a`3}T)0=s}cz&_v*(Du(Z;`f02zyshR@CbMe zJO|qPB%c+yE|^7F@CsqU$r11T)sJ%g7H|jHp5gdz;5^W_N8}qI*9y!6{rK%6`JEy8 zT_O2tAO1A1m+CJ~zBQn~rF`~a$04vFgs1HAm>!tcjlRt-EKYvU4 zOoKlM+ymPFw)u-8`O6{sDoa6o=xPp2q*p2!xIEAp_GQxrl zJk`Gn(x^7}&a;{o~WuqP=0ASC}NB>yBN|1u=M z3VzY@E9F=Qy@Is}3w9zbI0e0e3kVAyD&^5L$@L%wTnAnO%U|>LI$#nw1+?prtqv2DwOYi_;!Aj^6Y(rRZPO)F&&v0EZ`orGsMdBxMU2qLy!9zt} z;;a5J>lf@q*v@|n@k78VVEL4{eKvnNB!4p`|I~-y06XKrcHn_xzs*mF$M!c6r(SL`Z&TNdB}B{{+`hft7!R^HBq|?brC#3V#OibHFv=ML_;WNd9I> z{(%oa@$a!e+JJq)RiN!3n?DqipAN~N_2HLIv;F1324E}DwqN5X6#gpW*Ma-M>VKcp zwe{Ql!;t)=ko-#@zVxd#Xvc!52n%kYeF&yeZv>+#2f+h`1?ORhU{di200m zbZz~+iXG>QJq!N<>)Ej|3jYYsAuM==u;BCz>k+&}Sa22o6zqaO1v_A$U_0_B*oClQ z8ezc;gazyVn74l#5I<|-j>4DtGh7#p|8Z~q62FS;f;In%H@{(y;}gJEU=MHzxCgY$ z+m=t>P`+J}-w2p~7QC?hRYiXs@!}uRukppt-SD^I0>Xm(2n$vq9VsOZg{PH=#GgZ- zVC|n^je_e43!We>nE8{;7d${%aPLR?x?tZthXpqg7M%K1d|j~oKjpAs0%5`QpXTd= z%Loe|AS~Fvz-QfoH%f$VY*;z9~gt=D%aRw}A)1bD(YiuEneS?^(X~e`Kr!?)*<2 zw&jH%hg_#3zZp_~2zsW0tH3>A-~Yh&>;c7I+rF!i{7&c#s=s;_eggIY7l9kVO8C*X zZwB#;zzyJ<;;%V{e~5Tn|0VR4|Ich^J+SkC<8Tjf6==&Rz)u4E6!|4ZeiHmy;EEzY zugD*)v7KkYivNYf>;Eg`7SPU5M$uQd&T@5IjP<}Npq>9D_{b;l3ewh<-+J7wu2*9AUu)*dus? zuweWzvi*Xse~H6_Lw}jWf(_dow*5PV_!$eA5fG!3%^1D}S8R6RbyAupMFB zp3G0M+zfCTxDPx69s_N8jaU9xyz{>SJ8Gd%FtNjY!FGi0^s?*^U$uN1!UQn+6I*I9oZ(AF=!4ERa#x)i?frof*EZ&~3BZyNk9@b(nG z@Mge21@BVf3vU+u_}}3CCV_T-g*OL&A9xvsFT8p1=fGQ1_`+KN{}8-Wg)h8C@XP)t z+g}B=?HAq>_zmE-DSY8AgFgk{oWd903izwwZ7Td65Bvl0P89wscx%Au9_P0KXy;dW z>)>~Q*Q4--w*menc+(1B?jv$P5Uhi|;DRE*h4}bSaX#9B?La#}yWlri{G`I4MEn$R z8n_1B26p`|PTx+?mS4W1d?nK9447UOc=ZZj{3`wul>7+R{B7_2wf{87FZ>MS8t@!w z`=b;5dEgH45@_?+5x)gYK<^B24mhu*SAqBjpxAe&@az5#=c5le1k3_$`x-*>+YVTN z8W{Om4wnOM{d3^$0_(u5SNMnET>)Fc>r(g`#7_a|eva+g0PX@iexBnyfoDM5UR&Sd zE$gd5x~1AP1Nl?M{;chfko;xv4;1~AmVQ5eCG<1^#s5Q!e$C(FH@m!+l=9K|>k9u8 z@wWbDrR+Y}Pr-8Z7lP6s3D%%}2=*X7!866b9fzFGnuQgA zm*WNJ5Vrl1{J)r2eZ&|A)&b+d1h5s@2GsOTLGM2B7-;kR5FeDk0{%Ae0$BEcv;BR* z4A4%m4)LA98DP--+xp^)z6Hb|08fCn{5s-yfLB1x9$S7I<**Js1Rnf9?2j{G;_q>| z8`yr#VLyEr&{qLHb-?Kx>Ys<+OGSSY`ZfDRJ`P@3enQbd1AA=!8ee$pkn4nAKmQ~Z z|BJpQ=u1OiRbF8Igk>$BI3|%YrYwbA>OwN$?lJ zTT%GJn+AUyynTf~1)lJCefZrcTwcq-Q{W}gE z4TW#(y#g<&y(i#{U4HqSMZPQk3HzfO*bcPwvlxjS5P(KAV|Fpwlvc9(u;VIxYa0l4HZjW>T5uJll^|dSdCZSpE zNr0#EB_BPITL5kYcY%@*1alz!@=KM^Len&G3AhKeLE~@PM1;Hl89N{ioCMATmw@X) zTh00z%eVbfA?_h;^Dhuz_RpE$3QPiffa}0Rxqi-kKYeY&2bQYu3U>BE&l*rmU-+qz z{B%fuCM18(hc9+ZdHDI;=0{L2bxQf1BEJ4#aQSrr=YY09tN$hQqQH7!7U;)M`0#6R zUG#S&+@t7UM*J#p3t0Kf-u2Pu?}p^>hvXlGC*}rD20Cobqfy+R{n%jY$z;0k4a0qCpKa2Q9;3{wv zcm})zMt_CVYXFw2@7&T?tLW=Sdx^Z9vn2dbZp>_e|pW(2d{50gZfjc+UH+#$amTy_# z<}K^nx}m;W$kzd9evQj_4``R4@TB}R;ECK3!a?hUt*;LCr!8Q868Q$$U$4loAYRLl z$oGL4mcOXzuZLZ>evL0Y@$VY+HYn}M=AZiTr@ zbKD=S1Lw=U`v+V82=X+sbeC4fRbUe>1>i=&M6H+T|}i zDgQn2MJ@rkpyg-ln}Pfi(62s;{2c6`RpcW|eYEAbARm^0A##?(W?`4uC;BzM@Wj96 zuzOCi-{vQM_(w{4Wfl83VE;C77q|zsvKu)-`qRIO+w%f2{&gH~2W|p)fCoTZUmEha zrEeSh4uNMkv~OS07kSIO|C9Jq{ZR(JHgk6C(FSF7Q zD}N062=t5p+LiRq5MT4#QGUQ3;4U!rJNUX?ej@!QRKHC=$C)dR}FrR zC7(vvE`O1e@~MFxQOmAng#F|tA5#i{19t2H_kkzCGvMX#;qor8X1oMOelLf&fwn!i z{$oY|#VzUIfu3_UO4V(ln0QZ4sz}Xtsw+bx(=3x5wZb@G^ z^l197aNW+Y$o1j+I_%#C2IX%ne9b#wTH0gFI!&tCrWx3@cWhc|68~m%7Au$ zG+w2`pG5pLa1L1gt(;C9FbTBN)8to_^roO^7kCW3RP@h>o?hJ4ixD2%IPlV(j zgJ1D&oK7up8ffc33(3!ZJL{bTo&qm`ZQsGyTWcBZ^lbU|fc#157k&@qZ2iKU248rS z3SW4VAK_{C+x#^j`)&RS__d0EW?;A2a|-^IV!zm#g1#Bxf};Nf@u$GD-^bL<|7ZzbZRz!Y!@xBy%P+VWk9?*@w8tirGT{cKMX zI1QWw+WIAa4cGU8M+(29j`ddo6TnrVt>5Oih2(dH%%H5fVujd$$jpKL`FX z@Dg}c&+<{|=?bWC>6Z0v-m<=fThfeoz=j4V7igxr-AdpMc^vX_Mfe<;gsjEG!r!>d`Zs|Gz$>7wKbl~E z8!!#b0@r{uE#Bp8%Ws390KWqmRQ}+G@~1bHzX~aD`)v~Pt3cb|d+^5r@C?{>&)dH? zzXLpt-+qVnoLX3p^3NgcD+eyX4#954o;k$N1J{8&3SW3jxV{UlX!Xvo@E5>40G;bewzU<0vGPNkIHnH< zOxuPaoB-PVDTOcR8T8@$46w|5yGKOhUnu$)5WfVJ@telq25%2|1l0Jchg^PX;5Kj% zcnoYvGA|A+d&J=i;N%xMjO21K1bNZFs>ts>X8Cnd0VRL7z8UD*0ak!t1GM=o zA^D>B5^|!qRD1hi*8*@IsOhUfd20F3DEf90Z|7g?#}4%F0}p{VfA;~GOA`IIp!COr zQr`sIJ6KL|4Pn7EgavD!de{FR#Akr>z;)muu>2iw`847ufs4Rp;1+NXXy;$-s=;-^ zeS`&v-sSWK4-pou==8QPiUd=@IiTF%HxNDp+7LxV9Z-`$u>}#10!4liVLSbs${$0o z4bzC21(su+G^LD}F2BrvEPIb}7Ptwt^=tf%ko@(K{56FiLA=<%fbgoq7hVOfCxIE@ zK5*@scm1;EYr$I$$WMmkPnO7E322Y#Usm|#@Q3(o7Q7{eKa2XbZ{arhf@cT|wso<; z1-B3u9D3ioJQfhY`2k9#6ME(xs-V1uL9Vi!=1n>VBZgLd!<%1dTf1l(9;U+0^0hztnxj(p}y{0*4G1leZXt>Ss4@>_`E#N_o^HJ}AEf_18}Lqkq^O1ga!MQ{GTAcrk~Rn>_%8{3t_?NbLQLSae(+E;IZHU^Ur{_DdZQ}1xx{t zfPVVV-?08!*t__K>|c1p`s0d!R|ERDQ;}a%Gt2P>zC9{(@7G z7d%w-t-av%H-QJhQ{W}A{3Y{jd)9HiRQU}{{s`d{pq-w?2et1EdM6800PXaXh(EXR@FVZ~C)1#}g|e9t<$s9 zx5HuWvE}V>SbaC;mwn9bvIE!!>;`6m+dw-#k#E5DMc|4LKN^rPygKmffs&qWkHq`s z$F@_;pXf^>UGay=Yw5RH{27F8ejM?_pF~*WM}}ELB`^x?0NVOH5#I;Q0M`}1T_5eR zt$!DK4uNODnvZNIho=xf1Dpe{h2$?N{IZWZok`#na2mJ`yc%U*Lxyn^$f5kd72u1$ z-4f-iAXfvd1=ay={datizTQ6GqXD>u@_tJ4UQHYC*DLfDSVmVf^dWS4SWx-8 zKhbAS)JM=oVFD(h_P2CtLti64feZKqGt8Yg=6B22{WV{%% zxRs~q1JbaKfpd5ov0t68yfl^aq)N4OMR`g`jPpj)R{qRUQ0hW`fsrR z7Hq=~)cb6D)IXr_!vP#Z?O);Zq6%xU2anu`7v%fkl}Eq2;ivm~!ngKwoHKRUfHRn= zI_Hz{23BDI=WA!@So|x>FYYmaN1q4i`(DT24}&lYGf;PBC{CpVZU(xl#X`cpSQ2ucP?<4Cl%eVF{LD1?ua!B7T6^-|r~$ za0#z|rV}s2In4enCtihf_y|K4=X?}8N8kPDUe%X+8|v=PtpD9g&cEq;dHTGES}*hE zI4|=p486>KiQbF_gai>bk^@>m&6O z)V+D*e)Rrjo`&k*XY?z3%&@i3`vd0w1@_AO} z^GVN_qfh-_eRsI9VL#X?g&o!-kLhn{j_CEaC zj&qXFyBj#+I!ul0k>$D-U>!D~?l}CjbwGcFKJp}A_v=swrhj=C;$a(@rMorsbK+6%*K76?H}RbiQeW$= ze&px=z43Fuo;S~ZEx&J&y`+r$=??upEWt9={soVE%UAl-{#)*cJ-ncAtN#$^!|)nL zp!N?ExAL?;iarM8(CWwfJ<z#P>5SpLlu{k7km`?vQ4&Y2C|{g87U zZj0hHobw@g4Wlp)^Y9Kz{Q`a5{IKKGg3i%*|M|>38|eQ9 z&ba`*gmEbTQJM3*RzLP{r}{Z0EKFRqs%)lN@a(&WJ_HbkD!}7a( zCqHYxl&YU%9<48-tH3I(!xrqoE|h(_`PI;ipVl`xZ`INI2lPWYf-9J1&ODUsc57Te z%WwWpe%5>gRX=B3zb(20Ji>X+ajuY8g|Y{Ef9U#_-_<+$>3$N_O~Ne9!92W!_pk^{ zunZfp1=~>eb)o*R!}%_JfRyg zG48h-tiu+R&qwX|tn&LV`XhQjx=Seawf`e}>9c`e`qFx@pWixP=Je|0_C3J*ej?9x zgzp1A`CedrU-8;sgzq7`zZCHd)Zbe=#JlhbUe=wBUBMs>!4yox49vo(U*hCH!wuZR zt6%Dz55f=(!z{doIhcoa*nmygf{|b5_(x$3#$f^8!y+uf1^>eskI-@Z?myq^`efPv zE!5YmK)eL2unwD0uIG?rw|Tz3?#{K{h+Vs^xNz8V=K9Nqw6Ej=c(7D#^+icHeeI#>o+96hZ8u17#bp17PJ>L!Uq+l9mq1IbEy`O=RU!@P} z^O4U9*~g4M$Ub!a9{B?}gd-?^8+^BL2eqH&BmG%Er}n#Ko)uifJ=Fb#f4LKhz$lEt zI84AK%)>gq4Z}a@Q7^tF$zzWtA$4@!iBZ3RkN6eD2Ue6{i{lRL!V%Q_JJ(09nQoqt5<<9+CdVW{;n;t3eVKL+D40h2Hf<$Y3OKXM(W#=dec`MSS0pPL=n zg*~YCed15B&-e5>e1gx$_jfCQWax)RJvaS`yeXVPJ&)+dN;fz3q7z@ytqgtVS34D# zQ1-Mk=9TkH&Obs+U&OZztFQ+3yd{r%%eQX$hlt<89LyW~C*m9U#P-e#!YD3>kV$cWvmm{5>@CgU%-Mumbgd z_DX+uqCSM~RQ-k0-y8ji?uzq4DD`yzHO>!UoaZS8s@2X@cQ*Y#rboq#tmWy}-zsJDEr zeJz+T#&eaSUJlCd0SCsR}4dr@if31(8k3y{v5f^_qz0}t_s~>sKU8(n6*0`UB+%F?IhBKHZ z&ujkV#QX)iC47WWQ18#uuMGV&@eSOxBW*pna1{&oLaA3%Qzwf=&*_`B() zzSdd&#JL}n@CIg}?nmoW=+cHhL0tUZ^ip5ztbT&*Ck(Hle9r6swLXGA3bj5&T>Rbi zQeW$=elGc*9E2e#fB#)^`~;uj4(fijUl`pr)P5V{Tf^^)xb&y})|{9A-27xd?IZKb zeA>sFSN?N%@^@|hcWS+#Tkgj^EW$FZzy{R$a=yy>nv1@~dGXhI+P{x}4&~>*J@?@O z`i$?Vl0P)^b-g41dp+Bq{*!*})bv+4^!-YQ`L@FmjQ+55oP^r%y6Bu=;x7qrWBZ&qug|YpDHe)UCq? zY#RN@bq{gdk=9E* z&pz8we{SX2-vmbZyh_9Fk2vpp?SG)pBfS1Q99;xv;VsO;JE;9a#836{ny>ZJ$IbUU zeaq+gj?Z&n(}|DaBdq>_6K})N4?4#&ScH8zhBH|EAxGbUSM2=;%Jm4cf4LqDjxUVs zA@h{jqs$Xz9zFj(x>NO{E0foT9oU5rP}ge`xAL@Je1_zW;26rjZ`iBO*Lf52YTxVZ ze+oB0!#NJIr_~Fue~La{>-xK0@AcPgo{ce2f$LL*Rak>VIDvEcS+D0#o?~^xhD*+U zcnza40hRnh?%)3R{r}&;ZQffQ*gf%m_Ez`v+L(XA^SFd@=1M_5{~p}|9^th|ebA%c zccT6Rou!w#a_I7~X!wi%R_X5yz39YWbe6yMywpGG{UZI=d0*@MRo^e7>!Ba1_r2C@ zoqQiivyW5tVMCuIUQ+w@|0?Hs*8Y}$@8X|V{+3?qCaJ&sL8p(v4}En%m*~9egUbKH z@DD40(OLaSA4Bp+a02!Bu_$$KjQZktLw*Wop}sx^;`gu!E3g4i?Jwcd->LQ5)Rp;% zTvs>$veAE&xYv9)eaB@#(zkq{*8A^~{{Z{SPwT6kuS2;Xq|a0JapV56u5;v9fA#Ye zC!U8L*n@K4M~r%jSISSTdLg4;)}@~0S^dgfzF*_)!w)Z^{JgZ{ex1|Dv(b|yOyxfs*H3hEUx@D1dU`IouPe+~g$=l7Z#y`oFPT&CiMSV_DX;0qF*b0z|gPIovM%EQ-JclEpO~g z^fC1DSISSS`dOpC=%ioKS^bJWg?}1m;VsNVeZ8v0YoepCn$e%=>gXHrAjjl&;07KI zzb<(Xun)&@0WJUiEBUXe)4;CfQ#t9{8Fay|6*e?q^3TeyQ-KP5hg za($(*Q}sLJevs=dzc26nfb+h2fL-zilF#qI<@e#bzW5zw9()2&>%+vaVFbqEEj+cq zfJ=X;*1Myw%>TgrZvHW&{~U3z`EL4x%YLNq?_5810gB|8U|IPo`9*^Ba$PRixAbZ0 zZ^#q9pFFJ>o%oB+@=xQFfwxfpKBN8R_hi=ZTeW`sea`dn(%)}uzr!p06~EW%uMDeD z{(hqCH#pygJ@^2%UjFVPe}9v|yKMNqoc{Zl&X1Oz{^BqNGcXJDQ0JeT_tO4O^{>-k z12$m`PT>r$pq|IgNAg}e&tB?&zq5BA^h4c`#4kCoudmiiUupCixciw-y~@va82e=o zp72-7^jI$z>1t?!nv>&bO)^SpLp4{H4b@fGy_JZB#PsP#jS`l(0#4E+KwRle>= z`hG%pLEn0gOJg3%+mSE2D?=~3BYM%*(KlcVW_a)4Lbvz5o=@uSsTamKV)*NP$&-F1 zFG_v_CgBZC!veJC)AJ1Fa|^yzKPCE-`fHxQbL-2z_xPXd=aye~*{{qc-w*UYGvrI( z^8G>TTKZe^L_aXDm*~V_be8`J-wB*T-_Lj6EAsQlobNUAeI>_xSodrB?eKNWe?+%} z8@PqKp7`!Le-s`4YkiD(=NCBhb>RSx;S}opGVvOe{Jx=Y5|?_SJGGuyz0{GutU8C9 zGyg(;UzMNt0(|d`!8p|W_$dD?`q%wPJj3}c+!%UG_geeeQun?5^gb@RZdWi2BgXX% z89KQSw7;c~8vgQgRfX??_k0e>=fYO~Ts7l8C%+%jzu$2CeTME=>ZhrngZU5cC-gz} zyL`{-!9M(`ex&}2KHmHOka}hA!^%6~pD(Sy|E13Ryv^^|ItjPDz1 z;u)BQIYXZ(UV|&>r~d#9!Z3_N{XCzWuj?kLlQQZ_yukS)til?cL*1{$t?Q+KA8X~W z@z>W|>qYmBegmbx)=T{(zaRGTJud(+q5QsB=RcCSg*&*1T5nxvxei({&tsMQvJP8N z>*YP-=lyyKuV4_$`%dS3)mxt{1?p&j|Ic!s+d{?RfWHH5;2!!L&iPAt1#>X@%boL8 zID+!uk9*+1CpT9AeL3Avhkm*+fsgoW{Uf?(xOC~iM!y}X{lw>ppXdgLeveM{59lm? zm_0{f9H!tS`A={Q125dC_7}ek{5o8ZF4X&w{3QKJ{+_8kpJwh`n1gwENnQws;kDr>c~W0=R((q!G5n|8|MDEj^Vl`? z^1M&T>!BMMdj0$l(aH0!_ZQ>3-@-Zit>GW$K8(O2l=p`ApYT0@3TJR(eE)wUKIu5` ztH7^ycn!6G@b7fahhP}qz!c0uogX0{g)tb1X_$ex(DJi<667VJ^rwAwe^Mt;UIE_2 zA}qr?Y(O_Zulc$UsasKXn;dsx&*(?bAwE^~;?v@|4LgQkjkxT=%9FnIex%PnK4UmF z>J1Iu%+O1G&iN&LH1t~cgl=Q#*Tl7MYv^~x_wWGyJb!vWT7QAg(n~yy?i$8m0w$rZ z=a#4SclZ?GJuJc!EJK~|mZ$aNQ^T(gTd)IrQ0KelY5n&Qu=uUs*}!Ye!uSZ zyzcM4_wmvFrTCtchB=rwzW)@7mthUI41Jq;2R^_)9KsRQ`{)w4^0a=1eh2sP055)> z^Zm^SvoH^}pIg4xi?1KQ0K78%2gHYP1jmLxNjwc#@Chb=y)$15>i%w2{#xa0z4-1V z58sF1;M5tx1GMrt|32u4S5Wt(^_MPs@d=>|!w5{kB-HiX@=n!94gVbRJS@O_Lw{%J z%7$Lz70y>-9k!v|PkJ7=yi@fJ>S+BP_eBAgVHLJu8@lCJJmyQ?d-7_q4yA5~;|JJ> zx*ze8ImD-dUVOS7_n`P{KiPxy)kH6Q(LOp~`W%rrg>$I&6GJEO5v{*MAA}(ohFYH> zo`h+bfwwRR^Y9L8Kk-R%J_{`$oqx}JRlk=T#G9}MJFpAoy}ai5RR5ZxcdMuUrH>wd z4{%`kOP^1i-@q;0!voay?)h9R!ZNHtt=Hc#_3>NlKf2y~*Z;cj)9ZZa?qBEY;KvGdcXSEYrn6Zul@9~*Zw}6{_8f6*Zu1G z^|4pK&!&IxdCsl(+WF`DebcY!ThIS>yFOm`^>v$H&*OINbzh%Nujf0rzszrRe!rfD zIhcpq|BiSEwtkb7--aF7h4r3uz6CA68h&p29=bjp!3m5~Hx2J$1?u__E`C}s{xRiW zH2k;t@8I60&kOeCgHaeW_GkI~jeey5fj*A#!q5kZU%?;@!)q9UdS8-%$$6cx^>W=Z z=(8|q`0M*%$~-ffOBGlWe}ZFtmxlkA_ztGI4jJQm4AF`2p5r6Tl6MPpu<*)$dBd+m zybF79;8Fiz==J-_?O0u&3pc&n`PcUQ((AqTKE5>nbNlt0|8>(Rf7JOrFT%)gagO6q z&nx=d6ZN9YqrX%2w10&2QKc(!9Qw^pe_^QeTj;v52m5dcM^NVv4Bgn!%bqgqs|ZWR z{%(k$st+*dC9K0HY{531!Wq=_`H5@aAaTpL%lQe^bymc6J?UHgrBCsbKIh~w;A{Em zek$z03Tv7WXlEd_eB0P+-N^l&xfxc2H>gn zUaG&MPDIsH?OllfrP4<^)_T#2zvyD<^RRSg{vG)R7eA?|_4h{oAn_1%yT4kZgO6|p z*YFAIeck*$&iy-uwm9vJsPkHJ5bMS>05@r zOS}gk;LxMKZ|FzF$8ZAY9`#d0zaahyS8(f5zc%zs;@aQRNB%zNdX-=kw&4(t;SxSV zeLcnRhVvT7?|-{Dt)_lCdBG2pzOL;D}m`Tu@rFIO-JwLV5X4ihkG=%d7~ zJnesjJ_Xaz>PO;vbOm@1wZEk+8u}9P3arA8p)VWynxRh(oV~B$76$$SC$9U?ay}35 zVGX+Ji|9(Qd}h9`Cwjxb9c-6P&+=MOcF3r}IUhLzjnlQ0v8~ z%J~|s8+uFEHT1H#E$8>}VCXHK>|OUC=Qa0+Ko>&G7Tmhal|57Fl}jKY}Ff7qkm@{Jq*dE!HOfRT~2-wez` zy{|j;71)5<&*zd~BCiP_U|;#^`l4^4Yr_uI`VsL7oWhx*AA8hWzH7rj%3k6y32%&j z#XRaQ-<08BBwmI!I5YHJ;yw5P`-Z+l+{)9}Yk+i^CeO^*^)mR~!W_&)to;bgOTeyST|JkG7^7Z`_&V8!=gTzBH3?qiVO1uku z@WIeGJnGltGj{q7z-y@c-=N>ZoyynxD|~`5*Ox2rC}-B|~2&UW0Ym@~CeZ`Zn<{?7@*o z{ez+J5ZC^eet~`iw{Qpb{S*GDocsO;reGRsebl4g>;3kn>#OXi4@Ynb^*%dFKX%bS zasC-@;1+6sOXvHio$G!HLr~}Y4PC|1XNlj!9K1911>*Ox2rd8DzCW~|o?oBW$M3CQ zv!5sU47X7Cvm?HT2k4u8_2*rLcpbLkz|fb8S6~%NzV4^X`5t_LmcQP&KGyy-{C4mN zqyNlT&nx;hx*$4BKS!taA;VvEx*w^d`%mJNhxf2z_$U8aXRZ`X!z|SPIpTSE2k#Ai zgLn(JVF&i%6V&yze)K~9{6zmTx(S@Zm*(qv7WggUBedp!=KL1!p#Puy>if~s@5$5s zYz%)(ub&@%to`+Qee6|#Zob!g-|C$W=)KGFxBK~A`zi5WsKYiK8}AECKQQ!b;?Hmg zuclx9K01mI=Y7zt-f#E^iHBhXMh$()qu%n38U7NFb3Or+hThWMqQ8ShBR^;8riQ*x zd;o{gtA1qoKNBzf^UnQHfla927oy*gw}m^y-_ivQe~F*kkG$6`>i%2QZNm=i8U0wk z64(9`w|K7pfj;|i1Q$mCPsE?$2JQ^~p7;SC;pM;Z)$a>Se_`mYy;$*c_htD?+^fIj z3BBfj)AX_@;akryb%bwS?@RZWJqbViJR#mI*DwlW#``AhQE&Ms4F4SQJ6MGE6ZIu@ zWmq{gUthllel6I6U8waVB?>hu{rN!wk$qogXG1fwwRZ z?_d$?{3`JoJiyD@SAQNhIN$Q9Z=R?RQa1#{Fk$p#=_7_--UAl(ek`5D-&=pfTq&4_ z*%!_$a|qvio_qQ(!V;_)^Hhn~U>&v%z076t+<6C5_g`|JmoU78Rak?%p5)K*S-@u~ zeyKD4meQwcU_*Qd_o6fU)%pYaqvR?7EphR8(@XuIpfmbO6VJk1DEn9KTxfj`eI9Ck zhPe2<>7~BbS^f0!AHX@>!96@c-Cy8ecJ8|n48v=v^(o>-nD|$m{99OoHQ0qisQudb zbl?NYo^MJW-@oeAx%)R9R-o3;(Qn}n?xEH{ zdDKh&BK1oy{oK=c8CGBo>VBHUyYK<_4gH$<0Un|6Uvu`P{hv7h3^!2nwLXqd1vZ)Y z!PtKi-5o5zd#L?yJ?f=?hx%QYex&a@bsDf^^z%S`1$Y0Zv&TKu{rAxg;1tf_9EzXL zxAY70wEjCjJTE@zH=dup_?)P}Mi+x|D9^d>N1pS0bSt=n$%V7O8<>FwxPU7-<$BKH z79QXo*P#aIaAjPdDt**o9m;!2&okuw1kT|SK0=vO=SP?$4kz?AHTo-~tHC;KL+vkn z3b7AAzG1`v2_K*6VE}6XgGas8zoLH7r61{A-lK8!8KeJe`b@(N%o_bCJ?f?Y4fRtl z{fs!TudCdL@qf#?-;+@9w@ADLyRc{IN5toF2_Fsp#nP!CfN^*Obw78+3vm4JI{9;` z^+R-9;lJ(Zga3}hYx2sl0Ur#%D)AZ|z@ed!Q?CHik51hzyoEWKhXttTDdW?JTk7s$ ziae=fwtX>;T$gD0s8+v=ep>9 zNxhk>7p0!1AEF<_iOLU=AC~*j@SBsjgpY6ypWri$;gf(#n1X4Tfx3UGx4=)=6aP5* z+F$!+(G{RvpP=!)i2fFR4&JGJKluT8W%%j6>AmQFzl*}@L6>96X*U) zLMy*TUQhXHz0?_@8^a0I`Wf*#T)?HFKlQy-Hs)C{_X_&Z1q^+J=Pn9wV9IzNlZGxw zJP!*-ewz5H`dg2B%eP|mGa^2PPw*LD{rk>)DFpTP9jkoN2Mv9acnYRr#?a@9-@yXB zH}rkt1Gt1w@ELAk`af{y%RoJ!SH097;eWky{NqsjTYVkK_t95O^;tIh-H~VI=g712 zb-lugdOx}Vlzv~De~pjV{44T8Q181zya{*kXy~oqJ4;;qOZ-%N&HrqAJ?CfZ&uc$= z9)0Z9@2&N^KYi@A{<-<@UGKB?TjV`of_2z1-v2d2*CpPAeIvg~{8W9%qu%nJ82ucG z`~E}cbMG2PU=H3veZL%4zUcFYzDB$Oo3Lf*d&D1L9}W!tmiP`{{YTDRVHkl?*n(}S z=kuzUx(Djt|Hn@K3e^5qUrG8&!!m3_-S?Qdo4?N2`ZzvT{VwONdb;2KiTVt>ER=a) zntzXv*ZdrLcTn$VL3|10T-O`p`s(+c*JEAZE#IsDt^LmJ$7{Y<{b!rsYyV#LZ$0m8 z`+J?wtNyk7)qY<4`O@`zez)Vdp7*Wm&v@_epnvUr-Up$cPxN#2E4YSQZ|P30XXS5B z>{s#^=pUi(`^o4(@@t)mqA&(iP|xfCbxtGzFJTaBz3jQfzT0qO?VtD@F5uqKKN4TT zHQX8c1M%{I;@k%v_y~3XE8=VT1pWW1ldts~&Truk`V4)5cmX!x(9n;G&)^*H4gG@n zBV56qp+69>{AbR5UHAm`ywAipa0{=VzIs1<&L7|rUK;u!@e*vqiJ_knpTh;*8G1Rt zi^dIuX}&z&iC5+ z=lXfQA9Ovh#~)p<$a|^;E3gV{unu*C}tDB)oxIFS;1#<0?<<_nbe7-!q>F&<_Jp=S$v^e4onG`aI`Luna3u>kq__(Dz?C z{rO=4UP7HOc^5{W)_2jR{%fbsGu%L}-=g0^A38t0fQMhVA#-0UrWwE!WCS@ zCn$YyIX=RR|AsxnOQ`2-qHnMn9+S z`I-D3blY=!xJI zyC%Mea-C!^*Yp#GF{8gI`O;s4W9hHPaUC|H^f%{N`de`<{e`F-KCz!T`O-&&W2yVd zvDCT3H)zavi#`Ws4)N9d)AvIO-{jVLUu9t#YJC&^0Un|6zjg8hFa)n*6sBMX7GVW; zU>Ek_102E;)cvjL=Lzni^m|F(m60#;Am^puw4s-{o~LZ+C2q~D=kYqf^sVnr(VYmdOg4R*Eru+{#rlc{20#Q9BRGz>i(OCUiv(6KJ?!? z_k9GWVGb_f5iC;$PFARMWo#fqfT!bY!hZ`t;gz>$G5m-O5-qgik z&sRlXgH70ieK>$4IEHchO27=v!h2YR6VCDK&ewXW>$X?vOZTDuJtc|Y58q4#L*X=2P@VorVCYsBlY0h_P|_4Sqe;DPgfsNVx4BY#DF z4WHl!Zs8v4`tn}6V%{JO!5EA~op0^4Zs;X0@0}L9wxO5zlIS|Qa5B)nQPwz|o!b*SOSnDr|OJ0)W8<>Jq_ynboAbo^j z81{_*biJ{QzwY-AeF2tX1@_uScHj|B2&gDEr-Vtn(%Ab^k$QpAvtq>mz$=(7)`dZOoG) zo`rIM=M25p$#uLp^d;gID1FIw>=4)2#qGLU*KLR2Yd;rBuHOyR&q>?3zE(d+`n)7h zo>P6lB{(nj<+;od&%#?+fcH@Q<%s9u9n^V6;x*WS+E4qG(N$m-icgbcYhO~=@{vBo zNB5(zPlkH3C#{$3aDmPT{ZQ+>#{D`ZuJtnK1Ac3`kve*oJ!EquS$2)zkVc z=Wk&S-orXKZKLv+%g?nmpTfAKxHU)j^xm@~kfmoNxJ#{Ak( z=j(Z-?rU8qxh}e{^sVdfnfm~}=115=6ei#eOv5bH^<%{2Q1ViSUUX7NbW%tA%jaI7 z_w)#kq1Icv3HlkF8~Ia1C!f2z-k3QvN9S|t4%VU8KccVwACA5bo3I5tunQkx9}eLN zPT&+S;R-&%+k;a#2g|SmYp@QRumwA?3m0$+*YF8$;1=#+=>K&3h`<<(!vxg(+p@nM z+`}WhV9o&4`S--jumbh2Gp7nfQedIcQ>CZ`+>vs*^o|`-4ddqv@nd`NIN8|csd0zE>dq=zg zOHg00HgS1x^f-QiBRGaS-|Krt=gV^-`_uQi*2^4a`Z@KzD06B*xeg_+M;rFw1TJ9d z|8m}oWmth#*ntD6>xthKKhbv${Ydq(=6D0QutV6XBC*d`^h@nrR%fT4Bw4vX#?+f&g@CoXE z0>ou6TaIN;t(W`kkvaqPBg0>GEA*lp8+y@M_mSjH$k+Fc?pN+3{r&3R_uMZ69BZ~#Ye3@303b$#Fe z>s$vvyo5oh^$Frhcmq=~4fjyz=ZW9J6kN|7Y?BA*KJO% zKZ?))$6nzj)cP3lI84AKyn#EY^M}M2@DZ-y6MTj`->t6JCog{TkA-ev3Z`KOZlTV< zBVK?@_-N=wcdv9SLtiCchYi?*ZPq`M1P#Z~~`>UUYe-n;H5d z@e(Y<3ar8!Y`_-O{kqMm^~s;??6C#gumgK=0Ech{wV%#+(;t0K{nc;d@EIQA#c%7x zeNfkvd>{HN7?XUJVCjSCLojUQTe@rX5g0Y{EuGb`o-2;;-0;64o`G3-2Me$WORx;p zx-KODmh+N-Z|Eg{s(%fiI_$z8e1O)xea;Wy5RRapXG#2}_nYKD;`0noyUkwU!gVJz*4whgU zR-iR+mGd=NhYhIbc_2Q3GkEI#Df#_Z&Y$DAQ2prsweAu93a;T3)cKa~+0e_LhMb>5 zYj3(^kZ|8iDU&9ED!6dwaDX9H)zMKBR@6>Jl_72-{0Ech{bv?-+;4^_Mc&dJi zeg@}8zNK5BU&2Qt-_lwA>bchVR*ZdYIDf_Y5R4f59s0fWi_T~0{lo(>3@v}lSNll6 z`g+CikHZ^y4~wu2b-w6RoKM4yp}!@bgW_K@^jYFMPx{mS)X+Cz8xEk`{Vw_SSI+O? z*M&W(`?qut==)IS(fL~EHg5|*Yfsw0$b0FQ`?&}!unL>d%CF$lfDgv|PU@A;)Hlg* z!M0J)(sj^xVb937bk=-&4}E-7#{Nc}e}WshhkD-=;#0VQOGE!idWb%lRIBF!Yx0)aTRd9(BJv-a7%VTM&j|7)D_XCgBZC!8FW3t6u1L zbly*47=f04PoD>PZvGJ;A9ejO056SxPR+X_KM18?J)hRO?L&M-7sdZz^lRy3hCWNY z0PkTPHenm;d2+<_unJGj@1cKy6F7x4Xw55gCDEl|8fKu(sr$d+zJ0If!S_k)T^rA@ z#P$0ijy?h9{h;@&bza{ex~|l*e02T%@9eyHHgE^`@Cg0Ci=zv`OQ`)C=)157AE4Ij zdT#zH>ZUH8e)_+w!!ew}HGG24P}di~48D0-G3K-Mcjyc7-pIFfMf4?DHu5c<)vunb zim&WX?_x56K_ll_9EokLuxvqDxYFvM*mp)Tp zApaf~je3@@guVq+* zFHGFZ)A}s>m)=j3e~V8Eo_ar8`DJ`6%1`&Nb#njL$!owS)cKaKW$0y3dCnK1HMg#p z`Q4n)&mJ879?o(6_jY&-bMOvUp!RDizvJ)e_ym40hu1I$(=Z1Ya0wsb3htpl==kaS z;+IkS1IJpg`zWBhhebGt(#M+PEtI~l=qm_Aux9j|B47I6DL>tBmGd>&fGyaE12}?X z7^ROGyn$(W2Me$S%TV{z!}kH^(20-MOW$?$(ns9z&!Njhx$Xf&FSqRH^#Q)Lomwv=o zbXGqy*8_fAxHtSQ{m#&fuh+dvA9^1$$C`T2aAVZ>@g5Ao5DXjdN6}q!{t5;Sy`_^n zy8jXH?=bJ(YZ!%bn1D%m15+>utFQ*!umiiW2m5dUb$>qMei(w%ukJ_ZOMhwdbbYPY z{krw{QG0r1o)tVoJ&)e|7~KTU;ljw5_>%KGxHt40r8{tJ_4mMe+4GR&5!CYpct2ml z5DY`Dw{+Kre#-ZetKZ%Ez7T{VcnxDP4iiw<*ZFSx@|9Efmgn*gmZA0+eU-c>>_SUl zQTmpl7oGUG(RB>H_-LKgkNE1oQ^vfauTx)qAB=v+#3yhD=Z1bsd;_;|2lwy*^?p~x z*YF8G8+wULJvV7xC19g9*4{|;P!-hUWJPO_XGw5!i+x?q8Q(qwe9_o3e zzUYeRN`_v1w61LE^Tef&=qiRj!+Rpi{g8wyn1&g63-eIVCw{m17T`TB!V0wVvz*uY zS}!`OS0b-$_)C4Qv-%NV-G}%{UdY&oKO%2prztuY%(APnZ-+Mp7-`9D+ zL|_yq;T*2v9)?0rp6*xt!b*SOSnFfN6HwRlQOByM^_MRC1$AVeQ}rwI-1M@y6ng19 zZtP2RRrI2}b`m&)Ppc@(WYA*G3e@z#Cn)5Qxsrp;;-1M@yDf7slhUDvgiSB@2bW0cgqted} z{S4iydeKQg;%oJ@#{U^^;1=q6kMtS({hj;j8b;s^%)mUnhi-mudaG{K@UIcC!!ev0 zdeIG(Zt0@$oTy);dxE|{z}e#^)cXv`-akNzGOjePMvbzcp9WY0s6pACPB zZ#ch&a=rC_t@~KwpG_})3BC3wdBXS3_ge2;y|V$iUS9c4-(QaRa9-ZWf1vaE5P*6= zqHF&YM>l}a@CYxIPu=jlqizXG-77<1L6@LT65bf~b^nsz#%BVLLe;;{Z=uuqQ{oHp z5xV$E{nySvoLPTKzMfy=E6zW|jiI-6dqXdK?Q&kOi%<5b8nW(#H2Mt88u^y)9(|bm zFlOA3ZuzR+ixR$NSb&x>#B7Xv>M!%L$-WS^6 z($5V42j-r@o$TQca^6o@FbH+M5b-FC!8lC7*|p>M1P{>i3mbkJ;&-qBi||}OoiDy6 z{GZ?k?%*Eke95bFz7AWk4LfiSm(Z%?H}+Q~UWP3w``7(dIA4Qq*GI04TyMF)a$U6l zTYsO>{$A_9wf@}xK04p)e!jGRzqI+z-S3w+uh)LP>d)QhN9TWO^Lp*qtNx>}$GP?O zyoEp5`QB874cLSo*n>KMBJn@O@kzoK)cTd7&!R8DqLDAU9Ov^!UfIYG5RbztoEiEs z@#_=y`ugZ&y$^lf?fAX(z1I6y?`**BI-Yv|t@o+>`0VxGyWh9o$6Nb<_I;h(-)GBr z>!<&RI-iph82ZDU;|$dIgXl*}pEUHM6MxaA4E;=eBTgSRbPYpaLf3}=KitVn!8FW5 zJ&)u!Resda*U+7+7oGGczH!4};-~g+>GJp&V9}^&>8gfa=2)_a7T2W*AK=EgUXnj? z$zK}zee&G$=Pvmpm-=HPU-G2B=%$7~LY){aqANq$bIsVdetx|k>-+t!^S##ds@MI0 zY589J^{UtVaXbEO{$Bg}YHT=^->Y8l|4Yk1w_iQKKK^X}Uis!U#9$I;q0WD)-j81ph75m8cWQrDeuR25 zU)rU9fb&;S_Z>F!rH=F~b*z2n`0thF;T^n(djFBAbG;HU2{TaZC7$Q}9V{C90r4^1 zz@4EVdemS3kxu_X7=|&Z`*(Y;zI6RZ&!_8uwtb%4-$&JKYHJ8$6ot+Yrk{* z@tXhEdc7~N^*&p^?nfV=JKt;jo!gJs{I}MB>HE?ByB&Mo=Ue-o+mF}$x7O=@d9C-^ z^3Uz(z32B@&#V62eSCDjo;S|VDM@$()5gy)UE)1BfI~QkJE-fOn!hIh8Ey=}N8+dI zXZ##GhYPrbD|pp)I*-5@%)tiK^J)KQ{I>82eMY{%KirOW{g2k``d<6{==}HY*K0km z`j6h{x%Jd)QBN9Vuyyk6^h)qnIp&#kZLUHnnb?<4(xw8H?rgm52ImKG0;Mml z*Y|_lvDbXBdR^aZzE}OZ_0G-r+V7XH|LprZx4+Mpe{Mfs?>AjfAA9wCYyG+XeRTd? z@8_-k_5SqnTi1VUe?5=aW3T;vw7=JWz3TOR@15_p-dpSSKJ>BI`se1qcfD_VzloUh zb5{{oU<>N&aYH<%^mRioI`J2stTH0QJM-q4qcS79AWJ?$^`$LJ=|>PPR}?O4})YyIUP>+ClO zLof_=KYii@ID%tCZ|Nq6zWK*F{j^~RcA@Sk7kBi_Kf$5zPjpyGI2^+@)cGa!W!QjC zsP!%CJ-|Lp{P9k`DvZ!i8}?LxxA+fWhW;il{aW=m^kMbe=6nb0{zt^e(DJkL2jmT* z_)R#TLfxPEbvfUIy1$qD%RIUdYd%XqGy2zimAS=7&)eqy=)f_Y!7J_)xqo#20NoHi z!wuZQJv_n-eEl#0wZEVHF{SR$1ILy=!1)M_!)Ivahd3XGBRGarID-qgge$m)y1ygw zE1s(ql;=(BBkU^*V=yjzR0)<&-Umv4k)a=X{uA_b1GRoed;yoxdY@_iBl;E8{&V8u z@1~dfT4(hmd&saqxvyi~e{onf?ngPF;C#kKFLkoiyM=iu*F9(K!!2L$PkiOQFLSi$ zQ|gqBdZ*?WjeNbwK6S@J^5k*A;Tk@%_gR?I1%`*r&2 z82!EW_bKhC=h5fgj=!}0bNki%IM?soeBF=R@wxT9=D)RG&+E0`N9TL(*Q@@c=R3E) z*Ll6_&+YG{^S#dd+4Nuc{eAR&=hpXnznoj|+R)@__pa}?o>%?3`|(=OtNz^nzUld1_w7~xrSI44yk7O+?#J1f-h&*!_ddPW zduzSyLHO47-r8U05PGdAc|x!GZ>^WUgkI}Op76c%-@Bg7Dg4svd+pb&UiKpV((=Ew zer37o`qife`~$;CG=WP@`T^Ce6RC*)yrOmUt0c`*01bY z=(Yd1)=OVPuk|EP_-+1=-+=7J;*VEfTv$4Zd(}&x@Z0qftNx?sJGcI~^Ew;Qdz9mE+J0ni;d{^TwVqeK%q9HN^1tr=erfZ`eI)eSzgNA~ z5q@d;UiNU_tE+1_IqysuU-G#e$LJJI*(WV zx&3`~{2*31vnL~K)JmO>VTz{|mZ>`sJdad`- z`RDd~ZvU@c|J;7g&G$NwSN*yDeRTepHm}!yz3M;udYoJTOPkkgzh3p{?(?JbzqEP1 z_Ul#u(bwbL`d`|-Ui$Tsv)_?T%`=;0b_WC*-(D%V>9l!N{z52V& zb*`V+e6RY?*1zuOwdXn4-)leL>YWYfz02`u+mG}m^xB`~3BBgOwO;xXdaWmU!uQVq z?Df3%=T$Fz7kbV2s+T%KulaASm%fBv>q(yQz4LWF-=F0C@2(F2WQP;j{8Na-CursS z@d?5()cO?h0&K&sp-&UfD1FD!Pl(Up94-v~n)nlZh8sg4VLwrrh4;|Mp8UqX3Lf=R zCr;iN-?`y0eN@oP_1thQeMOD@5;`mYk~}M4*9)DfpHg=QrQet4ukrDkzaZ}s>if4w zybhDhcLS@i0d>AS_ZHt;uj_iP_tE+9-LKbrZ>|66{eN`*x8AQ;|Bv3sN7whdFR%KK zp6}fHUg!0y|Lpy}_dMtB zf9w5v_5afMd2avjy)Uozyz0;0$4BSC_q<-~y|weaw|TT) zeBAQ3)O~4w>&*Pp3-fiqZG1Yg3$6Jq{lMtQ(mxpb9dX~E>fC2h?z4uv?-s`WXXWQS z>a!m8H-=u~Y0hV$|hQ3Z*`^$Ama6P1trlA*I4*e6lBlQ32 z&UFnyUGJs({fYi1`YOW;oI{zbi{BDTU%Fq*Z;and^Q+|RdQIXj_yGHcUUY4x8yNZy z@h%*~k)fA4C!C+cnV~;QT&@=kKt1mz_sJCu!KiWnG>AXlIQLob&)|OhvpI%wcn@1? z=e({jer+8Hd`BJ}T)IZg4b)uh-z3BdC%(Y-2E13LqoH_Hb1cz`A z4^Zz<>IJA9g-z(DPohg3dhydb%iq#BJo=YU)MwCn)!%#czdcc(MQ7<{Z(VdfI57Mz z-New#^BCs2>~r4@;G??#^!*Vbucz{c9G`o?Ch>1V|4()9|4XR*5q(bS1BO10?o@pX z{SNB+bU&i=|CvtTA((|)uXVTR+8*^KL*FAlhYPrbk8lld+1nj#^Imx{-Y?q!RQ;HG zZhFy8(a)gFqxb1`KAkVVZay3OkUc%KuMONmd0)$%x}J3%#9!;n_*P&Q*5LpS;S9dC zzn9iqQdj108269W@0@%+|D(%1QpehNgM3|gYSim_)c2Ks#PQhhm-{5Z`|}2-pq~GT z&#C$)dw+y0_-tH{J@>VrI`{NZGWrqyfjl>T5Z?@x_lkU<(esKvdZJ!*ar6mQPy4T# zYXf&se_sr#?{`f;C-wKTF?*HoWv^AQN|gKYP?$z6ZlU&b)W9086k8>#zx1P|x>LpRwV0r0*;4 zyEv5ZDZ0MsFHY14__^p324UFvd8t7B9+qL%(3cFI_4C+K_Q2eJ7%;A1ka!4&;kBWc zpUa;4{=9|qy;<*5>Pj8?xi)S1UlJF6g5xB-fiKP1{dL&y1MI^A)cT%B{Q~_GZlLea z{pz33CGO{Z0H$FE=3pE4q3&1mFFAh=BZfXfJPA`!>T7?A>%Q*EtHBmLHUAEM0iNoo z{~n@`b$xwaAHTK!z3b_DLchU@T*C;AL#@~SzV`8J*E`qmOUw7#uUEak-d}qDx$}JV z{O9(s^Y!t$^JzbQe6GLOe6RX*`}JDyt@Y>5^V#yx?dP-a+iQPc_kEl@-$&=`>$&H9 z)&cr|qoW8wtuGU=z$R?LHtay1KO{babGR_{M~VL?$0-OyQ2Y0Y58x1v4gHe%Biz6( z+`&E6^#VO7as?wW3Z1jx{inYE`uMv_=il^sw_~sQT0iMIkp+B&E9jj4?mtC8<@_8< z{&$tmziFMDfAB}0yaotwY-XkEhS=X)q@zPl@;p-ejEoTd4IlbX_=7`C8vF^i$$m-}0#6(dQ8c{{pA~ z5WI#l7>5a%gej=|xghR`0eETXbzNO2L*CtArBLf>D5PA#m>d=q%{7dpCoScFyB zh8-B7-vm_l7kzw(F8(Fu7c%^;{=DX=sCNri*(y zlz)rwsr3`+Z;XC2#C88=;tkk@53tO0Chs4eUwNbaj>q~{^mU`37IEGGlK3;+Kp*dG zt$%!>ensAsk-sIb>xJke2IDYm^cQ}i{+hg~k)JT?-4n0E8f-%Sd8OwozEXY(pNipE zC$8&DpB>IG`8-}jt>5zbeZhGj3>*603-x>Qjz+%Us23-mhFMrN^obYhljNn0{9B`5 zn|KF4@_lCo^}hS)CU6GjI&H|?3f1=??Kg1o)8C)0{2@ML!*5Pp=lAGy1jld*OMG9e z$@ey8@bE_YGmrKA=!Zr>6XL4b^lhWR9&x=NsblR!>nGf&GuUHKV`E=)>Mr0D+#3Dti2LyO!z&nsVJJV} z$#bLo-M_Qnsr?*ee&*2g28io&n#-y`JbeeHk2&mlhOhw^i)*2~W!MZO3%G~E%n{@EBQvHG{umQVp1fQVx%M-Wq=PvoW-lL1Y$$9DTRDFj$H+|wSb)LIh zn1gwE2k&8>>mt8L)%($Yr|OgVr(gzZf4Ba$UivQLSB5piKgG{CX_$d`Q2Sf=xsUw^ z;5C%{I%C}5r}|yur~66sIhut-IED-O2zCCF&#g!J1m$z4&2uX6Ih`;14S5~(UF&^; zZV!9t`fvb;M*czdH$pdoQ)u-&;yzfxCwOUm-i|pxh0kyU^*(0A7jO&r@Boic=P!v% ze;4%ShXH8y8|3^ojKCDs^-9Fcuma_K$f^1&d0p6pI=@D|0UzK14&eyu{3h`Z9K)%h zZ}WGr4(!7L9KsRQ_2lnnZhz;JzpLr|A%6!t!o}aAw>KEuA z;pLrkJ>pRJBf1s(HGG0v|4e)f#W!d8`~NzpegsBg3~v5<=llWcdY5PB2VW`w`ow-k zXU(hUOXHh`Rrc63_L)alfU4aIk3`1jEDq38m} z^-mMetNX-nJdatA`UQO~;TGLpXw3 zUm{+CV>pE~IEOmFO1usmQ2JePEPK@X7k{Jkeh9!YyoM1NgE~J>JPGe$12$m`MhZ^7 z6imYm)c$YfSD>FVtUz7g(!JJvmVaIC>&DoRsbzPjqhmiN1&L5RRbsw{)l0v+}3ZbL&^~AJ7k=?t5(Xudla0*7NG~`uMZy&+YHr z{I}l6x&3`~zTQvpZ*hK}3d1a{!Vc_1o&Qq(Eq(=fZ}?lfQ~R^>%hYq5Px5o<@1X9x zX!L(gJPKuxQ$rsko`9lT8+zZpbNz1M0fzooC$6uT=wm19#W#z-1?N!vTl%hxKBfHI zhW=LRV$6AK%q#jDd2advK4Un68^eE2d;!-k`XlH4f17iC0x$|!@cM6e&bMG2>V03J ze=WaD{6f&Je$b=-8okWB=J*-v`D7njC;QR*t@yAnAM_jh+k4bY-6Vc#ScWxNhq}KA z@pJQI_(=VXQBUHh_9yx*es}QR@XxZpTbP4&*{fPWf%rYFxah^FfvyQhP}yH}hI&bxzSnx+>YWY9_0sgZADyp{ z&-L?~|K9rbk2)2f;Ndqr$4999IiT}%zWG}mMHgy)99v=xUAFu1WlIH#S9^W&3ny`$n>gemDuf*u} z{?@hDqt|u)+4b5_==#0;->dwXA5OUs58)BidQR9sg)ea8>3@9We+nD$5`LdQ`!(2v zE!c+VP~$C5^Tqji^<0tX8s5OJlmCwWd-wn!;S=n`AsoRme1_UzoPVgF))yN8R(($B z_xLA9_e&jW|F4~+`KzDpPi@{mAszn@x8V-l zg;jV0Yw!%#q2`;{e~lx8 z^^1$UBJSFWxBFy^>v8vi=D#7X3v)bYtIqRwi|!8I!#-4h)jyznboAz@I?vzs2l$QP z*zxyt&yL>qr(B=GCFZjNZEo6sT-?0+v~wQI$I<&~1#ZGEn1=J?9>OD7fyeL! z*5E0u!v<`@Hq`#+t#5~Tt8bt60n~a+?8oIdU+Z6e&F8iKYriYhm4zL64OMTt9M@Oj z8tlSbsPQ_NxIWtWoWIg9F5d2wGyEH{DS0ngU&1S>{h5#5cjjZ~u6oZ`&%x%Z@ekDT z1p9F0)Mxu+uD`&EqxW=EM?d8~FoSF8)*bx@`+l@f+|}=z>o>3q@8BcsLyeD%n^%AD_%HhR4fWT0 z^}4P@^-1HC=6mn_-}ZV#`}?YoCZPABtwZNyamLX2MeD6DkBiR#+5L5HpS_>Z`ro^M z&9~@v(s{gfeT%N=v-^k6`>p#;>K7WnXnou~-#h=J^M(5Be3Hf|^$X4aRUb`2=Wgpo z&&Tq3Oq##euu9uJ`}m^GKR6Y5aHXycgYX z((}-GT`#)+Mf+<%N!OwECe5euZ=FA>UugVyO|R#t>(F^!<9gOpzSKpb_|9d+0U1|I>fLknhcW$F9`Afsh}XQk zPQHym{if&(thXKi9{UgQ5%wLuzK?XR^VRFRR{fB?D?crIPh?>ZuEI5_@#?SZMeD!q z`9kOS+2--y^}h8yn>-h7IDkXvd3hAw3+vubkDkAiUl6eYwcagsd02qgQ1y0wm+PfD z_1mH^JN~9Kf74aaw_zt?{5kO#F@Ba$^_Ncm8S~F@J#PL3bQ?b-I;R89>DW0h(+`Oo z!Dp!F<>{u5e(5BtBMo!ZwF=dL1>GuKhZCs!3j4>f3I~qfuGhHUoKt@y`j+EwI`cPO z8~r`(CyehA{}AJ6`BeYtJ1DeDvF-~bMx*3-v#3}0Xl zf4gt`#C!M6n7PfMJrA01_|Ex4>(_dV^m7DDunbkdAE>uEZ!_;K_raQTKPu?L^;WivW`yA1FEzgbJZ|E~neRtTm_q_TQ`8;hx`#e2$ z-mj)x`+3oOF%MVJZ@?|sf%j1Jx5)bd&98{>K0JgP-zM$?UWs3UxNW!#&HsS4=YKDL zar!2HU3d$7Q0t%IZ}YTyb)9+50`*z)=HM!{d|JlyM}9p3qk1NHW~m7$Min1x!8 zr(1LMw!g*oA@AFf^Zq`f&kF3LvpB7%%imY_ckh_>GgQ6)9-!;5`e*_=7h8Xw&)50< z{}cGO+z-}=$I$!T^YQjW{X_Md_r2po^Qr!s_x}tookibsa5^N3aY#@T?yBX#G3n8Npq2 z`!V|AocdjKq531@%CG`k@EktCC#e0s)@OiU+&n!0W2c@f``Sl$&U{Jl2c6Fy_s8%T zNB2jbbGN?BtiAhZLi`kF@X5kk_yjfoG5%F}5|hu<*N9X9F?nC$6s8@$J$EVkS%O)Z zgPTzEz4kdh#qSK(9e;g)$E~%#IK5sEjbF4rX?=`b z`F!bqzn*6_0X+v>FM2+o-QVUkFF#tpTKBiDzNG!Vb$n94k9Izbt|#fdlE#1A`%5|> zjn{SP{62epXuqNQuk(o}rgN}$=sXv#x4Mj>`7F*D8oy}0)nyFLXK}{&jt|ZERUb{j z&MS^z=k=q{Yti%3^I3GhkLLg0{cAp5f7j>p(fSXqC+T^9>G;roL-k*}zohv?`~7Ho z?LTh)+3F9iSNji*57lcvT_^SX((&4lu0!klXnO4@ZvENn53N`G4~-AiYd&2k_50HC z+K;Y7>w9l~<}%8-3lHEC)Owa3{b7tg%k_1*>FD$9??9`^^FKz{fw%A;YW-D5e;1># zalHXsj{cneD`@q2{!i$Ze;}%K6|O_AzwhYRV)R4eUf>j}e+r)rw0b=MJi018gAJ(u z1xH_x(eH45A09gT68p!{>hb)~(e>dNzCf-2!qGp+=&!img?Em=$Nm$vdOZKs4@MOh zU=i*^t$)eU@5SgdT+hKZN58>-9$Gz~e+k_MyoOz<^_Ly}O^p7S>!+~p=$q`HL#xO0 z@1a}zp{UXon1fpXgQL&J=%0ui!Dp!c6Wf=1JpT=JWq1Nlq55w+`dW-W&-Gon=jadE zFF~uv^KYVifCD&!T7S#Y4`cM_T<^deM}NzH4_ZB*{{-FURaEIV+=W{I)Y0$6=u=!@ zhFM3y%KiqldOZIFbS-!ZJ5cLCbo5s-`V!Zxu;%E`*l$9s$Me5MH-+in8dbauwf;Lt zpNY};i0i{4RR1yi6KM5#{#o*_!%ev5)Kh1_3D4m*?7>H<_08B{`fbsbEL?@EUuS;{ z?!cm>e_;O!j^Wa8SLbN0-?w$GQF>k1i`FO2_tE3~{CDkRID?H}68-o6O?de$qV*NL zfz5Wb--b`WK3b1q>hF)%Td)PMpq}5C&c8&z8Mq27Q0p~)TJ&p<-gM?~x^?vXu$(ad zfcV20Kg*~3BPYMjp~3YF*md-F|FlK-!g>PJzcji}j^Vj;f9d_C=i%k+IrZ88qUy1J z9ve}AwO<~wN`KbRn)5u=m`4NN5#NUcIE3Ff58|HT%yi`6fSXp27_^ByhZpb?iaga} z{|0v9U5x&EPJIvEBOJgH)Ot++FsJ?nUFMfX=d{dRR-E~meoEXDaTc%jn0_*+-h3+D zKgX~N&pFR7EKq0J>gB%j?ys!Xi{CQL!gaUg{2A9R zzUAmI+3&(zco(C;;(7<(IQj|uOPsUauQ&GIlLmItdC!-~Q}%pq^L!Qcd6R&Bt{=d| zSL%<56Z_OM&-V(<#yszyK1ZDTdviHsZVh+|HU5PCdG+4s+KY33_PIJH&of*_Z=bVg z%um0sYQ00^JwMg2k>?N|!3tFUIp0I>JAFF$U@7b@5`nZB+{vNx8w=sX0HJ#`Go<1L7hJ4F#1!mzIv^i+M=JSZ3 z`fL%OhdWT`c*y<{EWs*l!KC>^<5h3x{@Qx2k8Aq5fp-2=)=N#kufiZqO**kG>b@B;SW7*62IuZ`mKun8aF2#(H=;zXU6=3ES`-Uce4izXtnWyy{hFzFo)vj{WR!jNb1>xDO9t z32HuzPX+4l$vd*Th(DY&pT)lf#`n?^XMEp zQ1v(Hx^R3E>6e_(XVcxHAHX4effK0t9@+Qe)~Is>Zo*xs{uTDUIL+TfKY$bS{Y~@@ z(@^7!>>t7!pWAJy`U<*ZXmee4=37N~0?*+k9KkWvd=2)!xK-+0g9W$^cVQ9k!xGed zR@W_g@8CV`L91Ki)A;6KXkF@;H2#h{?qLtw_c_fM_kB+F_WkaZ@4-2qw>7u}_o2qG z5Lbfp>a(Vo_~M-TEPg*Qeii+u~kRW?|S?FL-Tj|og>$ZzJHi5 z?Yw_((ci&)_z2bC)At3tRl1lDEHe zq(Lv=(Sq{rQ2!w;y>UOw^uGqz;RclUQ`tcMHvbN@3yW~y`S%&s@5ShMxNiP&`ab_o zGk_yFhA(ger%?Z0%>Lad%fBDwpw7$k4Dqo%&#dj=dCE?_{my>C@Ag&hGy9!$!u^op zeqM(5?`K-iYyDR2c@abN+mZTCsMG43Qm2HcEUrkr#hnuWy*MZT8vE;T1Lgzun~uJ~ z{x;l!dx830N59Yh0X&4|K>d-UudrW*C(ye;R9{1X3ibSs**E_);A7B`l-8EcE{6JNfJ=6ej!U;|#jE2#5GtdIaq{yQ1vJH*nQVxZTFq(Pw{EPN7#4#O?NN4X^j4c>s@#k7~e^#e<5D`w|*|g zf8^+|+4uTUe@|!gQ@#H0Pm->a`iI7Uc75o)L;F?#(D?V(r+zSc|1855Sb#;i^h1$u z3tqr6Jo#;CXK>#5T#P=$b?YbV=uKz+o6hsMdSCFHK(Bt&j{^0N==*Rm zXMCvM%VYKGdDyw-smJb*J=S)ALnfX*==C#Z9J@^8bufF;H z;^`;EX*~z*AHov8WyjynwTAv2+WSxaO@A_{-rk?j=ntsl(5YvI{e4)0r;dJ={T;Xq z_hR(hlhdaJjJ{semU54k>qsoxsi2YI;3=gyAv`Ll#>6|Tb#sOM+;4A)m+7RJSg z>b*QxuhwV%=IOT()BhuL^X91jo_=QYmjF-y5~J^PeGH!+z3H@mjq~ztb8d&Q1j|s` zNtu3^>w9n?#>I!~y*yU0)^|!BP1u6ha9({UP=67luXDW(&mFz#w0@2A@@bs){Y<_h z&qLXHAKajOgnc-GIzQ9ja{V6mU|f8t-pgb4YJFqsu>MOi{o8!@(H%gGQ-AYKqg#e6 z(9=(-YX(zu#)s;?JXWvPcgg2$2VTP)*oBW!onNT_qtEA*&xuRef!DC~+oSLQb@%{BQ0H+WeimPo_<<8| zdAs=B!F%{j+{B5$b>b%MYrUSnhkgjX_$k+CFiTxIt+Nx&F%4Is)?+?P;bkA`0ACBg|1}pFw zs^5S(^_e>P)A+2wEX+Cnwy$-qqc20Pv+DR2+24mXc;@I$cf|D)w7fcxORitR4!njB z@DXZ!&(S?O`i<-8yf)zhJc7sY9A^KKNWTiV;1Qg{8C>~CdH!G?7T`WSfG4m9Td)ng z@D@J7KAgfCT=~by5A(19_u&D&gZFR%hj8mA^2@_LxDU_a1?<9GnEJ<~_$8QwtMCYx zU=5zaTX+ZiZ~!;{iO7Ew>iv=D9Cx6dbA|O7PN1FhCg<6QBbZ{&X_$p;a2xKz16YEm zunybs65hcc?86aEaV}|?g==sd?m|1~66+>BhduZRQ=Ho}EWjODfhX`1cHko%z%=Ky z3^!o`mfH z8}7nWXyFxu2Kl%lhAu{?Axj|M#q||0TKq zcI5tgU~T}FZ|30*ORj=ox>-X00ajqrUhO>VrI{&rXh*$q?M6J(@e{k*>Pk({F z1G`SV=`KarbM%kwPwjd)>LU%cpAP$vZ~(`S-gHl*n>zZ`KNaQA!8N!EwVpEj6?hD< z1NAlZXRrZVunjxV^Y`K|iPQSW>`&kfX8vjB09T>LuQd)}0!xr@3=bpap)U)}|MbGgTEWmB3^VH9~xOHg!N7HM6q4gz=56$=4^-1UTrQ<*Q z{1;t+((_q#eT(-0+J7`*orA5v>+`oc7(?e_amKff@A19h369{|UGzPo1Bd@YwATE^ ze?HpZhX?Qws^0cXT<^h0iz8ndYP`i&B<_i|#Z{d6D!Ljxg=er1n^5z2*}sGL(E3pQ znCs8*1x}&r*Qt90Zo<6N&kOrgID={Dyh`j>U=^Og8a#tqUz`04cm;19z1_DS)qjRh z=UQrR&iCG(K+q*8WNTLgPdAi=Ic)dP3vhTYt*`&+8ei!-n(! z|Jq^y26o}V(a-FA?Y|U#-`#=*cn5n>``7rq#H;>*>&LJL+wdG-!T}t^dHq^*#+T7) zJ@dv}{qyS2$QP<_&e@Oodh^uzdHM_Tss8jnx=$+qa>Pq`1+U=^9K&b$0;g~WQ~yfj zmx0SLF8^Em?@-S^EJ5qn%Qvw;q~N6S2lyUA%k$d#C5?|eZ)rbebBl}f=IZsi#Ql+$ z`zFVF4Q~8v(S5ZEwZ08Uw}rj{x8d+Vi~OeWY!s~<@bSNl)_s`&ucCDUdihuW)kwDo z58w^#!XA8tPjCR^{FC~<^rAXb zpF=mVei!{7+=mBH{XN}u5WN?&|4GCuJcV`Gf^B%td$bSJ|9KRD%=@$rCvcPZ>J6N? zz9#)#<8uc!zJzXGz3CR+e}{T|aNc=XecKPwTzBAcpL2rs|2SHE=P~*(qU+1#*YnZ( z2I%a(PC2jZnDeuGTbx51UP3*ud-lih8BXC0F8%9K{W-V_*WfzbfSYjVKaApvQ1g%Q zxBQw<^`?8PI@N!Ly0cLGRK4|CK%e3HYrrb!eg-u@i*C*I@C2%Umpps$6gHsh_t`&y z$FK%1uEkp8L-iNKdN7$?<~B1jQYKU_wWH~{04EC@D2{(5RRe77yhj%-ywV?&jc3!?dbXr zEW-;pfGhO52M=He-oi5Pk&5#^Nt!Rtzd^k%X!Y(>pU!8D?itQt%F(}YeG231X)^a~ zxI$bW?!!Z9bMLdxQit_XV|@l~?slJRe@Xq-@1D9Ipw)Bg%xjJPb(n_*M?Yjv74FX_ zY{`AQO#Wl;Gy5FVddF|3s~Sy+G<|9*7+8pg%P>38rg!(0BY=t1>2eG&ZuEJ4+q?+Mq<_sQ`eqg(m+qCPiZ z8{WYwTxJg2&OBZmzfI!vQ2p=Oe}tAV8IYUhYd$R;qR6id_tGyca}WdhRgh}u;u)|&?kQLKZrg*w&4ye!ab<-7!tq2^)+aI zaq$`S79Xdtpg)6Mcnh_@EA~6^8s0d1o7*n)Yx6t9x$}F&A%CwO!FA#`;66NpT3?m8 z6Ih20*n};30UzNL-#_j5GxhJFyM|qO3sryP=K7NU`K_-ubzZ>N_No4T{JgoSew}mQfSWMy+z(TJN1560 zDg5r6hU#DAy>tdIc+QI4*DZJjHU1iX7jB`;JNi0#8t@8s;5EE~ny<_L7(T-l`nBIN zt?onlJv*tN#@|UjFRU|8J)!ZQ{+>LRZ^ByjtJG131?I5>_u&E5_$}h{a2xJG)qA=< zN595>xex8%6ZYr)_k<1NiV|O(Gv0ouj{6VZ&AJe?tIMqrV3Txrp|NNf!FYkfA_nG8gIHX*DLTA+P@bb^Y4K#ycg`>nO?Z> z?BBaQ|2yh=fPJX?5&Kg(gKJLxFQQ*U=jlsa*M6&x{~7yr*nnMV^GG?*Uyixi{4(fl ze%k*U`h8fEc-3!lJrB3x4phCT+jI0+>|gz-(f8X9yoL8LH;k@pK8xQ*w*!l?3@h*$ zo16YD(M}Nb97v94M*oQUe_u#ny9$v5O zeD!+VIy8RKdaXA!-rT$<}|6O!NxCdVwul*e2cLYn&>;Hu7HFydeQ2kYZhOX}D z%};fnzwNj1yMP_X-*j!RpG({=>)mm5U){k+IQs9R{RuqezC3d7&-(@WJ>on0VE%gk z*1y)f$~@=Qr_g8MGR(p_zn;u%%3A%jp18cKPsh}$daJuY+#T#e)o-(J_owOX{#5;t zK1Xm2Utpa6IYz%vJq>vN-$c*HCA8;c$$363{*3rI{QjOJSNJ6L>)^X+Kb^1Ty&->B`cwUY>tpx~ z+st7IYkVH`pnXo1IH!tpUi12|;=2KlUYT!3{m0bZfYy)pZ~Bzzo2*rDx+Sjf!F_lD zkD&Q@`Q!W!1O0~R-)f#C{6_z6^xm`c+2wq6K4bK~K>Z7G6F7zHZ~HT@XUMY*RlmZ1 z7UtlpqtCOy1NY%kjJ_cH14mzB{}i6Vrla?CSB~EHb^e>qc}?-jInSHUa|vArW}(Ha z-n$RW#8u!itU`O=sNc$eAKiyJxC(cm>TB%R;Wd15^i%fB|3j4L@PCY0hqeESHPn0- zzv;xQK8-E|m*I-UpO9bUbLcEzP3raZYs9Tfyz0%@<`LKbA%3bqb>>w?--kBmOZw=) zMDt!Hz5!=3epmQi!y9-DEuZOpNh?$=kKj9p_ zd=1X430qM0p3cr&^#hv=e%I9T05|BT3RnK;=zgg{^&874#N{mk)C z+K(5%>)g+#zhQn|cnhWdl&7=#tA3fetiUYH!8KTf9e7Iqwf`kLzfD+yr|_-v-IF(| zUtIjQ^s9QC$2z(VxaHL6={Fs{_1WjT&R6R(-4OiKn2@g6T>0e62vumttRadx835pnluYTi!#izx=OJ{Y(E_#5Bx8?Z=Cs;4>3HjW^vC zU0l9(>ez%8coB{b;|Q?kG^d;pktajtsg`{d%DP z%AERTbe`VkP(^nF8;-x}k40Z|^rka^(|P_g&b{!zN6*6^JcN3_Y4$U48Ll{bPq*gi zH`up&_E~%NY;(N`kKip#O{2bYQ2S}1Yr!_Wuy_g3f8PsTi(M)%e$jf(zvz6S{z>cm z(($4FhU&FHU4Jyc(0W4k+P|)o`n`92(tKaK|Ic23()oq<|Jn8LJul6t>(F^7jStPY zXnoRtzI1%(d_G&fpRK>7^Zji7hSnRZPdd*p9iOz{r1gj957mF$>r2{S()iHx2-RzS zx=!jB8Xv0HdUc)D@3X~+)*Gt-?DKu^enayuTL0P4FKPXu^VNKz>$lFA)GstXRIl@S z@A%Msq53b~U()=at>4gk7p+e^|L=PI*Yk-cp!dPN>(Ked`6l%XjStm-wEDH4(0)So znorkB{X*kI^;)m4llr}Pd}zLprhn`Ct6$RfqUWXch2~2dADS;zpR~S@7O(Y$u9MF1 z+n(>e=My*2(0W4kn*Y7yL-U2|zjS|ZonQSz*GcF1ZOa!rk5IkND>Q!5daX~_q4_ml z*Gc_C<3sgYudb8&y?1R-TzCU|Ci3c==1y1{Uy!6==pp! z|D^N!?D2~}k8j(2%KYzi&fqhgz`g$?`rq@Eq0YDJ#J{kw`g8UzPZi&~PlZJ_>&I&a|<9KdJz0<}Ji>&3(^{om2t*Wtzg6Rp+H@`UP-{!h*o9>WG~!6Nw& zq2`~^M;d(&=Ar7h+24ix(CV@LO((v@ei>TbC#=t)`DlLCS)Z50sebwYisrNevv3XO zX3_N`wDa0zUfXcjncsW+#nq$z#OVw4y$j>?>&z_=3ve6mz+Gr_t>UleV}6^WKX&w{ zn>WAtc=_k`DN$b;R-pNwQ||@5bn2fsUs3$qbNX35HAip0UO$#+-hMovOX|OeeR#ri zbmlxyaejLLTuYuI>m2>tb6g4cLU|j{cVY%>NsG?ySLecn+_i_VbW1zAy1{`If0e>pWz?3foS- z7Qe!E>vO}=o6ho?ZWnzK?#IMuxo+{Bj^2C@xL$>)j$Z$Mh+FIV#p(5WXnazApT5U% zBenFymNlsU&*;)zKZOH0`W>S93H0J8_*i_M_;Yv(HQwSZzs0pBZr*yXiJMn%d2LQ3 z;+LI%J^j?_&-6L2o4=>epv%HBoIstQ)tRT>F1&Z@H~spYded#9--4D;>#_X;x^1`% zRsY2P08U~0cZ}{kPu~~)jJ4`bXa1)1{Pp>bTWh{Jy_F|u;&!>d`eUN|U=6Or4XE)|;!a^3okn z0UzNL9KZ>z{+^Lf4W7a#Y{5Ht57&RMC~gC8!yR}458)|1gH3n^hcLGs`L4qP+=nGt zfoHG{&!OI*dfqEv&-;f*^rg=`%Xw`=J?}&IL(jX&`L)A_J{iFmxW@a$o`+|i3$4feNAx#_&(QL1l5Y#gN za33DPLs*6tcmiwi6rMrr_fh&y({BbY+xt`+exNU}Uy-Lw-zRPWhfwFG@m`$eSAC8? zKgzs6T79AQsNSBBdG8m~&$!=G+*eC;-fv!<<=6V`zWBcT>Vw@E_WrTw(B41Rmp+H~ zJ__x3Uj2;cQS~3?IkI_uwEEt=o@4IgDqM7a^^g0UQoVjo#nm5r|9kpX=kv?^9?)W* z_8i)K%bvRp$M2Z;ntiU?-v?{_ePDkN*!LRyd!XxlUm3~YC;In@&e#4vIpq7)5iCKg zV~2Wn?e7-22lwFtJcLKE3@h*$*5D~@z$R?LHnhKY()|6h3Af=6+=be2k-ofsRlh~v zkJA4~t1q-3)!TVaxu0in$vMB!^Y+g9nERj#PvJ*7=L_yTyU$mdhj*XvbNv98p#6Q` za{m69cRn@dqvtno{D~9)QRaQlytThCUEh1xbH*I%?=%nXFYa?h_4+vySHC?^8J>@2 zn1$*;@AGz)F#gbq*Y}h@=lXk#)^G1M``yIeE7zQ_y;tnLYVQsEJ<;A97rZ}e{El-9 zHQzn&Z~eWp?|fge-;Hwk+3$tcm--K0`Md8H-?Q>C?swoe-`CFVdmG=Yyx)aeTyMj# zzY|Bl|9Za@_qd-LoO{!`pS<%<@%Lt(^Re@~h&eC2?;pAU?LLqD-Td5nKI7_Ze(?Hk zKiItPITt&hd7u9o?}gXKKNJ4~Ckyh^ex}a+)4WGi@7=Fw&iz?uzX@Bg?dXU69s2?& z(0hN{IW6;kx96nm_>ULtzs&o69d5$6s(-}&IfXNraz5Xl1NAnyJLY!}ADwxc{#Nuo zM_=TA+k@V}Tc-KGo`K8G_xHGXy?+nLdkBwU398=Hl^wnPJMA9N&pz}%pY6W2e;0aU zZZ9zI^Jy3V_sVxnzB+6!xW85IYnxBpJWRhuTpkvj=VP7ydG&TKZ}soidcURkUXq3x zxB^x0=@wO={X6|8_1M45A362q+24lt@A^eYZ$777*Y9uIzo)Oq_*c1p0&9-G%>KN3 z`|l!#+*czwcJ9AzzIX1zOL*mcAGNPN!OwNi`HvDi_RD7pR~S@ z7N4{po!|2B9sQk_g{yE47T_+_`0DQy#h=4YHd^I=VbbiD+RfB$IRfEVxzR{wzL zdL5p_3wQ%>;Uj#4%YR@LzXG+M3+m{>0d-HE`cuDO79GDM_M7k=cHuqj!6(>V`h3T& zwVto~Xd*U;MbF3b7(??}obj#WGr8#boc!2`=|4PTZ7pIGKENT&{gKi2O<0CC*nk&s z4AXy96nD8Eu?rue_IF5qU3mCML~%#Z(;uNP!7@~RmHiq#g>~3~P3Xm+a9!h7-$K`h z=kOA$e(evA`Yl15|Acc;{RX-s+=Kg2_1l4Z%ReUnGxX|d;@^gsumi8*4b=Lc*zdzJ ze0KCN>`(3b9}>-P39A1-`-iXu%Z~np{VTZ7{d@q`-}E)~P1u5MXnq>+>CcH%{l*^} z^}7Z0umDwGVgC%)VIxL=%=Id?c=f;JdJpdLoF6*RyXhX$Kfw`H|1tZ|@C8mC{rC1) zxi8=fRR2}>OR&N@-@zGNb^6&O?f@Q&pXRIKcMdP$B~*PgP;d37sCNlwoce8lh3i?E zbM)Vv6Mde*Q>gV{v3~=*@Yd0Lx(DO$&AMv?spTGOOPY+-T+UIeXwS68>$TNjYe9qeEb5A~hwZ9$eID@D7w4C~j z=qm6SR-yVI1?nw-o&1fMdg}N$;XUlZE8=gRe(d@+*KcCaFfa z;-28h@t?7u;XYc1D^Tl6u|KcAg1!n*VI4ML6KX!&Z*lz+UOD=E_8;LB>^pi-XU~CDIe9%!MzgjZ1O&+t60z+G5`_V>Y!GcUV-#q|#K^h12T_oDs%k#+jB?f#-j0w7!I^8_{|lZvS!7dKa$!@zHt%mS6?0Y)02}@CcUS z@t+i3KY_zPIa-gQo==K8weCIkPd{iM#Xmjjs|Lq^Mzq%cmZZNG)_LjghIJR-!aH~m zAK>91^CR=0QitZxqSt(9?5n=Peg|5~(62Mc0(@=!2JxFQewI)5TTcEFb&jFk-`@Qddf#flHR?EpXRz+n z=jlG$d(Peu7d)3;cnk005PI(q?YF^PTJVbZ&b{;gF59{o2!Yx>U+i(|p z@f%#%c-0rt?ZJI`2vt8|Kgain!>#CkI)!E0)@S?IT)%<0j{b`MdG+RVN8CN^JO0n?PwYD1!Zo-7w_qM>{pQ!k?@IhtpTp<1`QqX=-yQXg;4<%}98|sO(tN+pz-6fV z8NT!Ci}>ur19<58H`s5&7QA%yJ@%iWy?WuMRO zf%ExodB^0R!kNvRzk8RUw3}LGe*dCtSH)HCty7$Pl4@*ux=j@N*kmqOwXV9K=dyXFLd6EVn@#({X_=`NH z`3m@2z8ltFz6{sb;Rf7Kt_XwZh0BXOzK>Y^q>rJ=?ccJ>% z+3&$;n>+6f)i=>y!yDLzs=o}>TmA`kOk?WV{<>GeA%F2{VT#MNLOHlXI;BrY%WJ$Ce_+eWW> z)W3*sA0EI%cn&Y1#(QxUM{mAITrWXSf6VnNJb|ZB>#=jWU_STo(V5pd`=;;3=uLMe zx{>35$-e2ISoh%ozBXR_zhW*O*oAk_yl%*U3tyn!*XnQYjd__gUh{l(zohjh?eD$g z-#g!X&o{Kculi^LdQP_1`Kk~MI%ig@$KZSMJggy8G`|xEuI+rQj|BJr)``r%u16YB_ zF#8vBzEJ0*@p1Y?d`qwlHE)f5%{OnqT1R^~>SF+l&OAK-CUa~-o$vI|jr?aY^XEnD zedbn!TjaNTw7&s;Pod@OkyrIg?C-!DJahC}_SfJ#+=6*nfZNdgHGf>3>dj{t-zv1a zRd2cqI_s;!+S3=gz6bXmz3L9ol^y*9`+EMToV$1470zAjHNO$Q6}ewL{cujb`Nq|2 zdF}o@bm}#~G5PKNGGo2W99E##Z@MMXWgUHv{dKqrx1i12>eqa6@v6_`TY%ee2Oh#B zsPU%T<@%lzS90RDPpw1q+1#v8%Ug8(>+E~)EA?-nZ^9P5gCl5u8h^p{E7*b8@CJ6_ zEi`}ii;Gje`P}0>fL6EaRX0PQ;(fjZ)6k3W5%&n6%tr!3{ddV*gnMxFFNi*G9-w~y z*>hr_e|hKg?+U-+pC9>+eq6*jzl|8bJA5DDBV6ZwmxnT*l+|-Pr{2p`rk@HthBbH! zt=}1a+q}|Ff0plF`s%YD!7&4+~KJtS>K4_1b3!U#mm)HbvmtAv9|llo+r&07q5Er zt>fQ<=dc5BpvH&lEl*SOTu7cf*7jW6`$zLzonE}^&G#Pv2RMN9>K`4w=R0)tJ@&N@ zPd`PU--+I<2k;1%U>V-SKGgow>}OyOuEI6A4mEz>IICCVlh$v(+tk;A*0<&}U0mK{ ze5&vS)?owo;RtHJJ@)6#r}0ViYagf7qkY)ird*#n{it6PoyBYYXHGtgQ@zdE>a@9z zoq8thTiq$Xzu5N~_4oA4#I3+pxE|vd7pM8{`_&V^eK>?8nB)7y0W8CL{Ra4`{)PPs zT*5aEGjQ3-H*cKPula3%75^gKbMl#Pi|acQS7Ch;6Timw4QPH^pVhH1zUQnv@EU6T zDf?%z1>26k?&vI^`d^{9I&N5x9Dh%LkG=<=-~f7ladBGjHs1rizdKZK&ux$U(|-3_ zbAC_AqdSFXunwF2U2qLYaOV8Iuq%FE{3Is+2%i$HNIe%Z@rQHjkI_}3`C7j^UvEw} zZ;P`zY5WCwu3!h=K-Js71I)|N_(kjE>JH8S)_!lDU+YWi9~vL3f9rbHPuEH7Q@_x4 zX#Pd(i5~=lh&*Ai(4nn_od^t zztDOztH+Z^?F{q4)yzJdhJKoq4j?0_|SSo^-24C z?|koFZ)ks^`lS7Q*W*Lyy=eVMKaVQ^9byZ1;0+wWA)LY)O#LO%e|NMEb)Ic>=G$d$ zz9ZJ=oBE3*zYTZ`r%?U(*}s6t)N$t2bA+x2TN1DSoyWZTHS%u3yfZIPS41C|&x_v)j9*8; z3AOG5`+DB`JjAV&#;d>jb*QrsSO3!JzTSkMzK8w+K0?*sus^SUVm{I&3=n4*Okr3ww?}{g*}aTY*`)26aAr>>t7-SaS5I?4Q9pY&-fJ_Pg*F z_8h(5FS^$H^}4QAKeD`kc{HaTcnDShf_?(060iCqK6`LK#y`jPb+`fZQ2kHXKZAAH zaP&G4U8}!d*Y%?H-?e;6`_*_|FZ#R|?f>5WB+d7w;}?A%>VM07XJapVpQisxKEI)- zzawq{M{pG%^V9fEuHVB4IEGW0`75G2mSGldQCGpK&-@SYIf7-V^_;PP1FfC`YpZ7# z6W@^dCr5wFzV$z3ZT&CN=WF|4C4L=V!ZCbeZZl_orXS6z-$b_s^Kb_qlBZ$${;KF) zGf>YX)Ncu&bf8~>`ZT}R8|Po6uR4s=+guK8KJWEr6v>!JN> zeBAo2{ge8I#(&jE6Y$RYqs>R_{Al%s*7LQWA9??U&hM-KN6zotc0YYR-yi*a^}KZ* zIzNrqb*SH0{f~Zr-?sT|@^{}BEWkRvvHkt%@6I92{?*Z1=i&M7ieI0##&_Ajg?F$w zr~W=hZ*^FIR!2elOVi&9^!hE5PyMzWKkawk_$hu~`~h`XKO@%5#I41|?@9bX;B^_sp!oar|l{}-vl?murHrk@fwgDL#eFyrJ4)tm2<_}biG*!SwSx!H4e zi2mNG&-54cW%`Pvx9dk-FGEj%g|F3fjNYrq)1MI6f^B#Xub|!^9m!usXMUG4@pY~@ zq2}$f@AXsQdEbV+um$gI|F4POf6s93ul?rtq35?Rek0a8k9+oe@Bu!}seg>oTOHP) z)v+u6tx{sv#Gr-t6E$J3t?cL6Wq6}*8u|E}aeMQ483G4U;~pF_=i&%W2s zF3(vJ?!ycCVEYHr`)>+2|GIB}A9{XA;`hQ@=kdt?6YRs`occkG-s-Uatd4!@Z(o(zUjDMgPb7ZRi9bPS{coeQ{xzTJYs8s; z$MH|IZ++PP=gq_P%fzj~EL??aQ0oiTn{Q5ht==@}=GAL+v*)ae&%mkA^j+p>`ZGsw z*H5^93O)TDzE)2iy;qN?ZxYvm*YF13L7o4-cN4X z-(MfS@6O;g%>E70{yNlt+Qi?AU*?s5R?iT>F`PopKVyICmH9&ZHGQ3YZFml~K2JCA ze7*Q9^2MEx#W&Empw@li)UWr6)jbT<4;=k6`x|fz7GM!p;RL3g`*DryZ{?@;RHU91 zb$Iny{T65STb$l6d-#Xy_lY}%C8+*~R_{^tTyMbi-}=qpcRc;W-}0mD@BilLdIA4K zcmf}wS6`PpEq=$*SJBmA9nKqnvY_}IeBlg6S(R;dUM}N!y9qhpe zNAKzE{O#Q4J%7({^>2*srvrEkbv_%1k!};#VFM1~2x@$u{R7y75AYdUJsNNOMe4h= zI{&7qE}h2(_w}uFe;0^1zY*&f^VI^#bHnpz&zt7)p1(Bl8Mp$QFsWa5L4E~%w_)Rr z>s9|_$6t>_=V|?~60drjbBpU2uyy>U^Hi4Mo4}=*>4xz4m!(?|pmkRh)jT?k%ay z`+W8EJ*PfTf9~j4*A)`Z^aoro!HT0d-wUqy;1pWD>rQ4O-IrXqx@WAd?hU8@ zP4>6oHr#=`(9TQq&l_LHN8|5o|L=(Ij}@4O1$g>*e)IciDDkGRm>+T3!1yxpTCe4s zlGpOZ>Bs1w;fvKR2|S(U6M5>GI;*e_oA4Z7z#MZ}cjj@1PJP$xCr>+g#0*ET$dcktxz{^tAH;-B#8 z!yz2OF*LtNe7$U$GD!xuP(Gnk^UG~9rjFb@lG8}7j+ z=Dh68dzm~dFbi{*UmEaqtLWFE^(Wye?N|Fz{WgAMsB;+7XNvw-;JVZA4nDhZ54ND@ zxBWKPZ(!Hao9pX!HP z*ZJn?e-+w&W^Lu$#w7js`?6gi+f~k_lfHDzsJ?}qV?~c zZ_)jH*ZjY2=l9Y3Nm^gh_|W@X^XWR&FI1m2U()!{dOw;zY5#8>|Iz)Do?rX#iT+-A zfPFZE>TmiBu3y3qyn(k+e^e>LDr`LMp)>>bjUa!A*d}zLpu0KDH?w1RA2|G~hKlzD~z5{pB z9mMEs=ucrCs=udeI(pl0alH+%9Q}m-DV)KKQ@^LnI(of7b*=sC^|*Ct{G#<*@3$@A zqWcN;fA4vJ^!jujx=vbuQoqprN%fmQDSAI2!ZY{?$1wAgBc0~6_&tg5uvYyp`}68` zo^fl<7pK?j8oy27`JWQ?zYlA$4L7RaJRgfc!pGu|iPL-s=;qa%&gwJYdF!+IGI=fj z#Hr78md|wa=Bvm&+cEPq{V{P7(e&Dnu0!k9cwHy;d++$re4+Z!J|C@L z*GcE`)_zIr35{R0{y29{^$(5zuIY9Dq5W&Tu0#D6 ztxsB?#_RgC)gM}~_N(hqzfk>q=X>w^lJ@td(_Otf2dyb#jW4k|4aLY<`32D zJd?&J^-G#RG=9(KL3|IqkQz2?*Px6LoKpGE86dfuV+e(C(7^?r7} z&Rf@^{b{_eKbqf1t0(EalJ*lCzi7SABW@j9Pn@4#|Lp#Y?kDMd-a9@tU#R}A=l!Mg zYd^Y9TA%vq`m_5lx}T);dGGkpe4+Zcp7&elf9AhK+x%(Ke~-5VTkr}_VEU&=x;!kx zQ>gV#68ay<`0obR8|P>JcJRG{_fY%mvHu8dK3mism+!Uu4E{N|3bj5@H*bAj{KlO9 zS^P5kEY!N!oce99Pt3Ou?S9mLFYtLhiSE-e-1r&a{NJ0Jen8v{oWV5lIjH$M=)8E< zkLS#@>f~Ev{{W`xYsKl$^xJdlO}C4_DEYJ>tD}x?DtSEpA-W1YhN|~;^XBv7YvhZo z*W!=Rm!Z~Ob?R@h--Bald9$3Gdw;%B{Ed@e=dGVtN!MTfqY2DA$G4tWQor|(56u^< z*K_!;#V76ev(N9d_a9pSqV?ak`(e@ZOzQu{_lak?^)sXIy+x@0IrcY1x98~B0`&*z zOYj(0VFNay)_2YRGn~QopY_e(=S}}8`n01ro%x&2^Edqnf77qU_^W<3Mn4e$6-Pft zXZ2ehUj6#K>RRWo*W=coEk3l~MeB9Gq4|==hvxfe`lS7T>G+Q}&qddx^VW4}evQ}l zTl=ZMuHQOeQop3}AFaR8enRz0`wh((s!v*<#>cHg>kHM#<&V4mZOix8{e=2`)khQX z?z@jRAFobtKeT?&=dJcb>j~9souToedd>Hx*Nd({sefpEs9xuvH2$sqK6^b$>woX~ z(0rkKy+3pv>i5y~+K;Y3TD^;|NBe*4^`h$!^^fZ>)Gt)8`IE*k+E4v;{nqurwSUt3 zK70II&-1PQ-g~|uJzr>ji`MIT$E`!_iSvuQ{?_@D`hDqmo!5KU6Pj<)dYwnyI<%fR zKfV6e{z?5p<3E~S=Nq^FX!U=zdgA7%*FReQAFZCG^GX^YI*+9JG(K+q-t{HT7uQed z^)Fq|Tjx*e_ulcL`M&C-33&JU*ZI8de00vC^ZnBCN$Y*<`WNk|{<{9s^GKRMG(J?H zbY7wPLiO)m-+Si^okyttqxYlr={mIEP<_&T8Xva~tuIus`9tGBnqK?Sb!feB9skyT zq5ZtK{*eEjK@Fb6Yv+G|P<^HTMEoxuf7`#}dIx&-JaN5Hi~jc^XYc~{es;9~40XPv zH;Uhs{4L2pjLAQyj%WBh-vFX#Aq}T5o8+FCG8h^@iqKv_9$l7hPYd|F^B5?|T1V`aHLP zPW1h)0!Q!#>iL-dU{1a1j?kB&qXDU^1POz^~L4+XnvvfELyMg3(fb@;@`TSxAuGQ`G)2T z)qnQ+B&|PmUZMJ=^?m91r2T6BaqG9v_tE@9>shp3&m%P7M~l~b;?_y~RsW=Z?;RhS zFI4~0=ac!l(eHh$a070_(rI-47;66-AEz(iy8}<*HN1gcxKCd5)%^80if`a^4lkhQ z^K`GR-}CE`Kdyg^Z=!EQ?c>tv$MSgfYyV?>(ws{cZaVje=X>kaQ^V&Jp24Q$|HOVD z4&W5dVCv^Z_p$afZ(NS}7xVe~-|T0)HPPkKAHa$epJqQ)pCK*>*BpOOH*Y>Kev^FG zUnwU4GS{t1)_Esxb}dAxZR@Y#mPu!8(>eWa@m%C#5$?f#cmR)K6}I3xyn-Eg1H14J-oqYz zfRFGA4&V@u;4{>D?Xd6lame)&EW--4zD`)5!ZWD(JRh6WymPsA{Pp`v+*<2-w$hQ-585_Iyd}4~_q>>66|s-?e#t*ZZyTJ+2Bb z;Uj#8GpO@=t^NwXF1&U8J>9(ZdGS5+#r0?L9rQO)>%Md9KVknA+8kHTzWMLVGjt7T zx=lx~&wJcj&+o1E>bK~8q5ethTXcMnbAEtNunz}t2=l)%x{nHQ2kycm+=C@phE;e1 zYw#4lzzIzKqA1@IOv4P+^U!{@UajZ*@`<6&WyBoDQ0K5u-3L(nK4ZTQwa;bxT7erd z4-0S`p1~$;!8Yu{J{-Uye1L&};uPuIi;U{q ze;t$WSn8^=K7|b@-hAh+FISJwX9K2~+mbUs(>USIh^}kCG}kjQ3yUyLzlv_d(VL&@b_4YVN54g1RXBs`U;fSgntpFieGT0yY``XL z!8SaHx9|=Q;WBk)o%*iP-AMd^wVubm`TUBg-&ME)Rey%Q1*dRkJ`&Kx=Mwhd+OLfA zm0%4@c*^uS(N|*h&*U-xvZEiMn^(VzkJW#|+N=LY`rbreg!}LiUcnov^N92F^j+%E z{Hot0uhwOAw7UA}25=0Y;R~F=8O$`Jb6U$)w`j6PxeziVtE=$ZK z4Q);uuR5D!j<^lD3G=W3tAB4a{}!CrKP&MKC*JBvtv%XBXkNyB2 zLhI|=sn7G+l?2!4ABJU3d$7@Bz+X)~SEq z{Fih3X?cf=5IRBe;1#9cmR(a|0(-3yZ)=9_g@-n zzxw--u0NVy>(lk3>wWKhi|!}X|J&Ao(tf^leA4rK>-yi?FSNf8m6-1Ve>N#hq?pZX`Q_r2po^M&elKDtio7aG54 zz4jNn4y`ZFKdE16{8xQ6fw*(=u7B6&=gqf##$QJff9)@tfX(Hj&BN+4hSq0s#?bhr`X+yu z-og7{AN`IohU(u5)SK^sIP;xZp5GAVn?lX^MBHoRL-V~**Sz&js4qiY1KJ$de`D0& zCftQZsQoSxuYMQocg0`hE&sgw7Jl>U*U@jlO;~_hPl^39tiYP1-{L;lgBS4Lxj%LT z^%eAW@f|zis(K)X-!&i!G!9M?@}_m%or(AD8Re1xhu-L2@( z&!hWz_=7yJP~*>uyMQA&hN{0}-|`j7qxqhld=}ru$Kp%)Xga{#xy&69u&kW8RKS-#rJM*ymp2WZA=!fWR9@>wLFJzH>8uXZt;Fbu_>SQ-RDbV$ z>yCee{Y~aufID!{<_-^G$@ZP|IC9Rz@?DVk3U**s{IoxfkJDc|d9K;Nfl2$fI_!Sj zbNZjRo-TE1{~B-gd-@(e4{!)a(4Hr&U-QMqtNt0^7ntTgS%Ipz{VdmWaMjVzdtcrV ze+wTRzasl__jw)N73?^EW%etu1}$FuxBUjXYiRYW-gH)n_1F9V*n9uDYS+5XcWpQ7 z($MgcCQ>SLD5{yJ6)Ghb7B!p>C8tA$y*Ot$iW7AxX+lLs z#VsltDi#(^l&EB6sA!bfV3MJsp0%Fq^VwTIdtKh^%eC%%-3!+KW51v8_xgN)UiVt8 zhljl%#QF5|I9~*pJ$lw<-d*VSzq+|Ha>ReXM}1;2OBjbM)(rUy6u7z1Pyv_B{c6#i*ZweimE+ zmq1^>s5qT(7VjUkcn;>k1u*J$>-&7I?Tr0W-zbn>94_g zEm-f-bC0b!=N>y?bB`I%eMZG=f1SwN1@?e_;2<~*YP?nNtJ5!aMq%^0<@2xg*{3gF z^~^hl{1e~|Sgd~1qxa>V_2|dp*FJpu0sMY^5F7$$!6Wb()Oo0z!}&b804{+m;2O9A z?tzEk8JOYsxul@hTZZ13U&1p7#(Fvz`{Z6UKlgMZb2MU}Ca~EvUl#saupVsi=<7T> z*3+j-v74|UyJxUumN1e z``#v4gFKC%e0#`u31*Q;^Vdh@uaC%og8Zl8ImmujJ@41-@3=_)1$3N;dASe0kEnT7 zpTQm&$Gvd>s@FYI$NOhrhnW9T?oIPGqJA^j26lp~zvTCU@%=A;U%ub-{jh1=;|$o0 z&rvO&&r=!X;rC^kcN%{FIS)T)@auAe=e}o9=aTha;(k9@15Urx*_WWs+d>_<3rC%vzv>sjwd`?K1i^{nw}(?{(qZT(vN(fO?LR=w6! zJ2YR?@z#2w`bm6Uo(9*z4bRu>J@^m6>?_>oraG_->;bjEQ^cKt=imj{jJOuC6>I|s z!69%M909dn2L2kb1MCFHz;RIH^?8c2la9C6v+8wTYy54~hwdvhKJI;o_7@s&ov+;W zLhF}1-n#F&^`ZNSyFY7wok#7^eAalYUi(!$G+)y3)_PWb=zc=$h1M^3ymkJn-fckd zjdtjMw(+^S=augF0sY_vsQLG=sc%BQR|tha^u9q<6udWY~IgBM`2{a#mpi8{5|OC6~F`E3D0so2K8x``qZO-RGP!a30io zFRrQYM!r692xPsPhb$-5&6wA=~s5;jCR(-kaTl-c0ChlxafydjCn!Z{1f_ z?>3m^l{?3Y`wtoqP-(iR`uPtyCd z){9#oy1&~VANT!O^WXOS2%X=$@3{4$^OQTj()LsCeTCMKd*79wzq;S6H>mqi`>Hzn z@8^tLAKG7NeBAS;Eq~hg72039Xa?e-k^{e;eHlX)N+qxgd`52lnG~U-2{h|4- z@p0=p7iq1>I5O$@r0a1`GU@u(dR9H>BCGLNH^9C4cvW5c&&yiZTCd#kmDX?QzO3_F z^`ZTxEnfSv+M)Zi){D|x`?2b^e$w&QdRBe8=PPx6&8K$ge$tlDI*(Pa^M=~i`f=-Z zp1A8-^N04Aws`GF?a=+CEuVEBt6t{~waZ;!`?vOE)rZy#jkoq|)ra<%ws`GF?a=+C zEuVEBt6t{~wXOBz*6TcR*R$pi?JsTd+K<|y`$=0q>pWJy&Kqi%yT0~s?I&*iP46#s zzEa0)KDDj)t?_D?JD;__RbT3Pldf;A7q`CL`;2=&YyNWYqtfeJ_Z_#s()U^J{;l^D z+OM^qRUg`4x#O+#rd{9jTKE53o4n6)4m<|6{}t$0!3}T|+yeJNjX#9{7(4^d!B*65 z2fM*uPd`!ldLrU|ePlfK_TWDN4?*9)^m&c4bzfF}-Rs;vc7wCvJgE8W;co()!BWL* zy+*0m3cC$dJ^P`q1G-L+exLpgw}%=q3pRjVpw?sj3Gz_i?$IAWSFE184DxAztmEtN z4E4^z3-A)udHPXj30wo$!3}T=tbe`RTNBsO<>=##{Tf>O=c0cf568t6t|*J8k)_{aE!nzuKYsl8(35i(6mrea1bXHGjGH zQSSQIeO2{t1A1??tNZ!<`{AC*wC#(1k!kBM?tVBoX|2aNGU<40y}0$<18J?tII

    z|6We_WZh4x<3sZ$-M_V-RUf*Ka>rZejawhOkI?@5-{AhdVgMWhhe6GM z2!Hb%U0gdj4X%QlpvGt5&w>qLqetHhe?K@3j(GICPqnolJy+YR*Lbz9`KskA= z>O<#ATYS0uvF_ihFZX<*^+V?^cYSOBaqC0(QSSJ-_fzTlOMO4LEuZ!Nbw6sSEuXa? zt6t|3-VYgr7SAd%-@i9~=a=zX|v!!6|Uwqo;oX z=ZoN)M^FDc&NskakDmTLobQ7t9zFf1I6nhx-t6|G^R~j@3-*Cy9(@P=UElyX2#$ij z_pm+z z-a2pG`byttx%-d%eysV+y^pB+yZD^42OffLf8ah}4S*xy47dpDJ{sX~EmE)jS#7N! z6>rsRK5Km1^r7>lExz3SRPWbqK<|Ne-1}i2GVXfT{OpSijb|P*G+)y3)_S*1&wC}~ zo}YC{Ydyx1Nyl63#jWQaNNYXDk=6LC8{l4iys9q!e_v|NuYFcpytUuB^`ZN;_Lp=$ zYkyXK=sdSQ-n#F&^*8N4Lg$Hl-_`tX1A0%itNW?$C%=K}^YHnfgX(^&`}y40$!&ce zI5+8@iUY1>C=Kh}7wo_i~n*8Enz_7NIi>U`FIta|NN?a+Kl z$EUqssr$RY&zUuExt~{S1Bbyea0=A=jL+b_2F!Z&b?{%eUZcbh!e+nPALpSiY98v_ zk+&P{0X4r*S8P3Bd_U?%^~?AU=(|AeyVukIBK(`+4tNP>-{PK&22kf;35;Kt_;abR z_1GVEQTh-^ZT*_5D|EombD*PMco)Q#?w5mwTU~^{w+-^`ZTR##{TX>fHu(@3hml zANECB`(qqwjj!t62Dm?ErSB(np3wM8-(R)=&;R-eeLh0tZ`=Lc_I-uU9~yt#p10dJ zZ|L(88h_jNecR>@eLh0ttNXsX0o|wCSJe;vT(s(Fr%lhkNNazL^U<2ms@J;K__+1j zpV~>+kGo#n`IFvX(({DQr}0s?b-uXsS^Kf-)7HQCW1S~$`p|hw9Uq#{x?ioQcDeJF zx_+tW3+>Muf7|qWUus+Tq4806Xnt$FRbTDjZOpfa(0y9tiU|`+K*MQ_0$f{mvp?f zUfg=!r`k!^x7O2s)eg-U8ei&uLi3e+p3r{E9dF%-RUbN^wO-u%q~{5(r}1jvw0&9U z3EhXrs~y^p=8Lke_2SlRzt(yhA7xwfTlJ-`ulcO~SoKNQOM8E5-$&Z_7uuihKgzb& zv+ASj>v^g3hvo~751p^l>WB7gjZd3i?>ox2_8*l`&r6-(nlEj7-A|Nl?Z4Fd%3WXQ zvG!BdyA9|bX~(@E*10aN{av>XT;vK8EJA#-~lMd%0=-rG37* z_pS4*UGDm!{b;<}*7-C($}V+&Yraa?>wc~CgvMLzh3e1n|84AltNZ`6l8wwhyJL0tntjh z2mcXR_jdQZ)q`5E8~zz^7Mug;!3A&$TnBym#&A9kPJoPG^~CqU9~$3>JihwN5%qoX zI`jaS>6&#KpYQFdtlq~opita{y7Xnd*jS^Kf-L;JPXt8{(nzS0(7 z?taqtJd}GMm0rJkpKb$skF>4(VVsYl`K<9)z4jd%uldxr=8s!{)B6jZuiWu*-~Ua^ zzx7A%`{bKDj$7b1sPmpfUxV{Tu+yWbE-Sh&kDj_(oY#S@!?P2W$__mOnHr0*~3de(hf^*WE*)_j$&*ZI}9&ZqHehvu`! zTlLzn+M)T9jm^l{-G}{iH3w?ptkZ zzZ$Q0+VWZZvFdexwL|k&TD-O2+omt|^HJ)2*82#pZ>?w5YkyI;HGkUlQS<4!wSSFQ zJ2am)K5l)v=L@Z$^nOD74UM`8gJcq()FzUS@ogw-1d0uzT?*4 z_WKB(-+Dhu*Gsy;(D_2+L+6dEzl@*zS^+n~ZO_kx(Z7rHeelAlKY;!;B0lrS?&t8b zU@O=G>VCT5?*@m!VQ>VT05#sHANA;2ryu77AoFPcNt{oC3*a)S`c?SXz-@5Pqd$QE z2(0-Nx4$}2^EbfX2zG#-U>Dd2YP?V1?a{MNE6&?M=F$B9I3EBf!5L8X^YAZ#>)@70 zzX$&jxU=i_bpdMr1^BnX`agC0QE&x312ujE{%LRqTm~7p^WR-wjjxBl0c-@DJo;Vu z55a~%b9oy<&A)Hd*CUVSZSv&T_oFCV`zyCz>s$LPcf7UVxb@}U$8E14_x)P)-?aM( zoyWRAt3K)eLhFUbTlZttYkz8o=Cj6I^_AAIb-uLe%f0W=`f=|wv_Fkk+uDC3-&r(NBTZ9jwfec1>&1@3@H;037rrGBeO{RwoZ;2G%4U%!XnPlMawA*lUO-;DDX za1@M+Z-P$a2jCw9M*{P;BVPO8g?|s+2YvIhuR-Y87w6UdlhDn93*ZvC4suV&u(N+& z`1=odmyd#7)Rc=_@w8x z*0bt)k7T9AYroz2I@<#dfx*A7#!o;$1?t>$@aya482$^Mzso%rHDDI31zW*RQ1hK6oUhn^&e2zto;u$? zbY6XZM%mW*xb>CRuXP_fU!})a`aIVCSGr#BE6R?0UafDfr}1is=Cj79O<(GLlscbv zU!nD_^=_L!>HUV*OIv)}`nhe-uXSF%uPED^->TR8q4A~8XYD6#`cm(s)cLIY3axLg zciZ$gZNH)OXuR6i=cC;5)_!lBKJ=26|QN3UNW)Yks>Jj%AlTlG=(^}N*it@+~C>%3}P>uY?JZOw1hYkg~crRziI z4~@6(PwT0jwtS`T=cdhT-H%mY>U~-3$E^>YFYf-*p8pu1V=lq&zjB}NdcZMo9PBv6 zIXD0gf;z7?f44DzRKM1I#qOaYqR%nUJ{qt`_RYPGc=WB%w{!nsC#d^Ag#Q>k1?xTe zefl#`{!934kS`0?dGhx|ug}qhCqMNAI3EOuJbIsQ*rT6^e-T^)S3G*|&3E6re|$Ts|dhLJh-Y+`9Zm&BNyKY_JTv8?x)h{)p>YNeR41Buz3$dviDWk+;fZUbsV=MerQkoP{~$WLqPZVa3T zx#xM3D0s+osohj(h&ldTEPK zTR(B%U!~{Q`--yTo>%Kz>uJ2&q4})wR=xIXwJWV&srwDhmvp?fURCcl;Co$I_v6b` ztiRIgS^Kr>b#K;qt3I?|+TzpJkM%xon_lnBYFqcE`K|GB>qGm~c(twlmpeYR-*WdK zTHiW<-1^XYG+u3M|Do|o*9+~p)bXXxm-IfZ^(tMj_Z4NwJ+Icc*3)>kL-SeVRlkWp zM;ZH|`}3f#W5*Ft^;_4}AM(#@ScgQNZcn`f#A$pD;_H})jL4UPuGI0Jf%WSVuluII z5$7#nt4HtCb$ImL>k;u&wb8h|~V`^&Mr0#%uo2eAalYUi*!*)0RK9A8UNvdfmUZUTD0v zUfT3&pFgxejaS=xALWh@?YG?hht{{wpEkYDuXbp^!yj^=f7ig)4?BDO?;JJ%7<5bE zvc&uJ6VOeAGvGY905abSY|S?XKl9ANW**gVVxBE<2iyZyueyEc4m|oD)UU<)1opP@ z_ile%a> ze8kP&fcaO!HE>%RE>E#rRq{P7-Iu&)(x1LQque>2FV`L=N%qljMsuNyy&`1vC9 zEub&fpY+scpS$RH57d2hNZn%hqxrdqB~L&4{;js|PtQwjm%5%cpH;8(Cmo-3J)Jko zw)PjdUi*!^o;81Hf0Z6@-B+mo5bukPA9bG#TER9jG{4?Qlx?jSl~2#D`Qz5#^!`HU zD|dX{_pkW}@Vc7>XTdFSAI$zk;ora1!ao2Gf>Yo$I16h1didMHPOvLN--PoPuoaAo zx9WX$*sspR`A5*tC^!bHejffK@C3X7FTtTRxBp>q6!hg=LEJjHA^EcZ!{uuPTfk1R z2OI=Pz)5fhoCD{9o&|6bTme_XL+}VZ1JA*xkGbc!8SDf5!6|SWTm#p^Bk&mP{YRIt4;%qU z!3l5@oCbBiar8FN)Q^&bPpAa2MPIIsYZ>8uXt9>%e-D`)P&U4t9WDU^mG9 z4ZOSiK+1*nE*a)_Oo!~Gy0WN{-;67Mv{w9x}{!-Pi zyKwW>gAHIa*a;4Uli)JA2cCgCpD%x-M^AsL>KB`5OXleLq}$5?I0Wi^Gw{!XbKoMl z1|EQ?VEw0D{!VZZEH?i<@~EEvQq?auPpiza273$K@yw^+9}2Z~A5rO8IRUpL-buOIb_?T`8)#6``cb8I4R57hZHpLP3d2HU|Aa1vYtH9nhp=v`Mc z*a7x|qu>On@eS}_H{M#W3-yZakNRH3Ma`pgtRQX^)cKq4ar+wuC&3-?0L(tb=^DTm zQ1dZ<3UM>w0;u{$_?JP}t0}VHb@hj+cM6_?+MiEXY=6G^OVo?nC*zNxKLNGxb5H-= z>k7_SLEf*s}W(UE??lu7L-j<{$RtUqs$zaK%$^1a-$sTz|<^e-(b+ z$ENfbI{!KLbqUsDPj#Ms-9JC}FbB?q+V3&^7vLpzsHb{;Ue!K*=0om=Y(L8}^9+}v zPq+?Sl%`bAc_CtTmZx=pSJ$36# zh4u8$z1W?TncsIddAMn}ZYdqujU+U_St1ojl*|J)=ANp%w zUih5;anHH-L){ki$1XOmrf&L*?(ayHp};B4J5{d0d+_+0huYYY7`ZV~Zh!(rikjGKI+`#eDQ zEI6C2d6BcVzS`6sc=Yry{a2TdeO;hGvi5zhU-AMr*>L2}$#d9b_xlUatNl_}_W`FT zFJY6rA9UyB-m$aE#Sb}~95`_{IezMFa^qvpCYL_pY;yY2*<{D3olRcCCeJ_P&dIaS zI-5Mt+7RZ=;dA;gn%z0)>cf0GkJV=0$tM=B zulcCk`kLbO8Xsjd?=JF_gJ0|Faqel%Pyb@8J15(}E}s6P`e)sK)F+p}-u1)2XBOS_ zN&gzo$xYZ%^`g$1r=_j9eXy@J^cR(1&sk><^~gO>eflpv=kzyygX^DchOPD0rfv^< zavwH%1e-i9GC%iphJ7)v>l+L2oBnQ`lZUX$_HT0WWIt^32sYW#?)2;<`&;h$qW`#~ za6h`Qs_phx-9vQ``3*!p2Yg)}ee3ObeR0mVZ*%jK>pjjU*PeX6eeiwj2=8A}`wu;5 zoul6_{JhgYu~hh+{-xh3d>+*w_jK|Uw=c54*V*Lgk2sq=c{=pq z_si#W<#*llN{;@Xv&jqC)_xc_@zU$P9vD}@>f*_vS2&xTy3g6Il8?uY)B zH@S0i>CJ`pn!nUG>z=%&aDDm@-&***)ct5aKBs4RUfD;_w(E!N`tbGoW8BoKizf#^ z>TGiUF$eM`i!&5jn5XoPx>1k;(o5T*t*5) zn0KY-difc*^iUU1?$s5}NB{2Q+&MY)cxRL6u%qUnZm-3~lPCYSaQ&!!dd@uAe|Non zj2nFN_2L;f{U5Fuuk%FNth3mAy?Trr{T>%j?!zWWzZd7=KB)cCKmXL?`k}7&`-;gQ z&Cj^{pDr9vfBSghbNXwaTlk#*UC((`e`!DGTvI>i<|kW!zVJSx=3(3n;>l%CKKhq` z!PO%>{&V5_e9pQ(&vR6NX*=f}<3-NHy=;2+Lx0^b7T#al=4YR03H8f53sY`CWY6=i zw=dq?l;{4Kw|ly9e)`98PLBPW)03^wcQ!fl8_p){{-476qwbq=ga5_FldJR2=3FbD z`J(biowLpc>XC=A$>D|T&BwUj-*oZh(2BFk?w2{6oP4>nIrmPH^D}Pg6)rw%em!U0 z`l|}Z)4%qb!snWgx=W9q{^z}b*>)Swc%{C1u@Ca}mhOjhPi?v9gPeg) z&cP-d-g3R?iE-1{jJMu*RbSfAU)6nn{(bVfo`380ULU$&=4sz{`N&S#eFs>jBW2?TZ|EhqKc@ zZ>js^+;cZ{e$LhVXO(%s>^rmP`X{r0UY>bax8^U(Q=fJF-dURZ%+tMJI3NAP?<#y= z>epkX=i{EH-;>O~SZDXWrK!g}Q-_7~(La7v_&oIfn0NjI@#GKfuiWw6%ldI~`=M^L zNPW5I)B5bY?L&p_513at=1hzxRlo!QVeZu6*3Zll=QO z$cb}zPV(=+AW#3vos<0g1jy_^qd)LOcnO>2|397_|D=m2`Ts{J4?pG3L-)@-tN-Hi zkz24s^E1!(r(J&XW_y6l7>tT~ku*p`~WEX6*2R7Lcn;e8qj>0A< zV3X6Z$vN2MB5ZO6Hn|R)+=NZ;z$VXNlb5i`x`(@ckqxlPCfH;vY_bbB*$giVgY zCZ}PO3$V!**yILmatAiK2b(;CO`gFfFJP0ItlI~fg-tfVCYxZBZLrBM*km7UatJm# z2AiCOP0qt6S7DPIu*q%M>R}Ucx4`kHG5>R});!YfhirgNHp3>{V3Xaj$v)Wb zyK4{pdj$Sja0T4<=qKQx1gF4hkDmF~alQd=di1C8H-CYfza8uaAL!iuss{cnSPRy9 z^vu_Z^DeO4qaTNV1Ka`6BlI((Kk(=?wQkNXupb-*bza`%A@)S}BR|RhNaiEQJ^7=) zK5IWwdOZ(~*L+cS-1V&aL-lo2?s@4H4#AGvN2$+so`V(F-ywJeo`9;4>PPiwFL$2y zS2*^9eW2=_;cqKae~CWZU*hKI1_wdSUk`r+*a$Xx^vu_b^FFZOqmSB;o@;-4uC`TQ z?s#jzR=v)rwl!be`lS0yx?bG-i#xyWOYNlVTk9p=ueCp`{-(`ioj+8+hObk*;3?Sl zQup<;9qa|Q-g;pCc3^x1`qF+oJ^kwQ5@qYWQTnKJYd)(!s=oC+>H5}saqF%7iqc#A zjmoFz*8Em|x$A}24~@6(tJ3wlFSV`nX?&C&nm;r?s=ui7r0Z8&y;ApU%@?;m>iGyg zxAvcOz0m%w@m77*K0@sQ_H8ydkra1hk_G+&h7SN8<@?T&MU%npC z{F+bWwLa_k=3yOQ|Ci`9i}`Co=F@s@+!NRdj(`i`8dz+6kHoJS+7!% zc?Q5iaLAK?6n>q1Cqh4obM~?2(No9#)cNv{;JF$F$G{2C^EM6t8n_EyfR|wX%iZfm z?{fy{*NtBZj6XuW_FLoWH|pz|p6k4NZndrX;?_s?A9sCge(l#9Z`CJV&)VN@)8F>{ z(D~I)+w-9Pscr4I(&Izt4V~Xw->Of#zohF~_fhHkr1z`!)VA(3G+yg9;&Wvy*ao(P zU0@&Ri*Let3&?z`SDm$<&abxC({r`0`clVhKDDj=S@lWRv-VfjyA9|)`Rus&<*QpN zzqNm@n{<5A^>kjfL;DHMSL*uOkJY~E{givY+g3mBd2f6E(D_5-_5CF@pEcg95AD}l zFK+!!pGW5lwXOGItrwa;A0yt$OVz>3D0swCO|lm$rGV{e;e6>Uhm( zwL|B()>D0{>s#|#_1b^Zc4)uWc&k2iAEmBm&1cm|&0FexQT5V(p0<5h`?2b+_hHR% z)kpPH>U>f4tmkR#&)SbwAGM!y=PPynxaYO@8@E1c-nh@L^JzcUc&lFPS?yBSv*xqv zqvk7hzNmWE^R)G6?I&)1)IQ^`Z_Quoel?%9AFDpJUZuyEdtbUgwXOZdtq<)lG(PG1 zv|qKY{b+oY9h%=7AGbbgUOkVyA8Y=!_ZK>k&Zl;%^OrhbrS*H;=dZN;s`l%iSni?v z{KmVF>VB*H&2OOkdb|1@#I+As<$-^*PO11n=ECz>>!dB8wV$~4)_sQRt@D(-Uef(q z=S_QmI*+wqt6uA=UFv+AKW;m;e`~x|Z=K(o->Q%5r_}kP>ZR>GZS!0EvFbzjU+Q|+ zd{%wbyrs?;RWIti()+RYZ`H@WKWjf$eN_KRpNIBujkoHp`>^J>>ZAH8b-t*2r9Mwv z|JHu2`q2HAx}G(kRj>0V9dE5y)w>Poz0pqFe%Ke8w*KPohjWwGdW<7OjS|)l2(%+V)}X$Evs9hc&-dAJtE(^F`IO zo~NxpYd=ubE)Y2QcE{e|ww+HczRQ?GCx&VU=>4tNBffN7sc?^o^H)_>^yq4BqE-)Wmy z?@w)Ozj4P8WZjgJLfIa2uH}3kn&l$|&n@{_< z#;aamk5)T0U()f`dbdrl_ivqF|>a{&J#cnqF<^n9+1rPhmzOPfBbUp=?>9~y71 z7q|YV%@g;&tocLt8yauzSM|-Wa_@^RU@O=MPJ?q`vGH9cir0F~+k^bQAp2Fl?l;P| z#>cJKey#ON9Uq#{+P_sF+F!Zjb>4M6-+SN@*!XJqx@`kHz#gz4)O^Q?KLO8T%2yxQ zPfdy9b$;fpMgBUF{i>dR-@a9^&rg)C@x$n6;5F`k2f-mw^$XCifLq`JsCw#FMR(}Y zQ@4imb&z!!cLe`2*zf7rmu~}czI-P**Lk!r?L+lb=xZI^05`#1@D%jLFN*%iqo0Pq zSUq*V{#eJ?-!kg0fUDpdsPkB#&zg1jT+Vcrv+>U zJHY{P1eEZM#z*OyzYY1?LDtj!QTx;Lq~k;Dh4x?S`l0=3yxO7tTH~#H?N{y4d`ZV! z>s9q`1A6bY)3zV>MaJD9>yT-$$3Batb)Klaq4})wY18X|($=4~pU`hxp-e(o$PKI)u#`bv~9 zs$Qj?^SQXh^HJ{pc~5ir`HQH2sM{=3AC-@LsDGt<-Hy(u_D6p|zMrr8=^w-Qt4V&pTlMtc|NUls9pLwa z$w~M%zS3;Y!LN7ZSxock*VE8_S@T)-QT^TYd^c^rQqQB$N2&9tEnnLD3Elrqn@9Jd zcIdpJ@zs9Z26Yd`+M)X|cYM^Gq5Wz8Qs*mm{?PtH*_yrJ=-{gk`D_OEui z`@3oN)3#sjM{Vo=t$MAewl$wsul3ZvY579u35_rHeoNg?rS(^7{g(Q^N}aFL_L=s6 zL+>XvUiYJRX#UW6?I+3(t!It5>a~Asyj7ocJ!^lJuCMO*>J92X)V`|DWb|BZt6t-+ z_D!#6-G}N+y`O46w*lRAv39BVS8UzeqPy+;tVU!5lU>u=iUt|+Mn8?{c1j|ZLM$BNA*+geAfC_eboGVZq0AiYrQDj zn!l=d8}RM5x}VbSNB3;qZ>i(s&Zqf9?a+N`eQQ3e{-)Op-ACwtLgP!_e`r6Yj@Nvp zw$rx1xce#f{xo0O`%k*RQr9bWKD|G+t@Yy8Ykz86>ubE#j=Nr^=a1T_b-t?JZNRtJ zO5cyp756-$^~xO|_kOJTqxNz3diUo9&2Mz<14luvcW%_zzQN_K1KYq3uoLV8wca-T zd*A_h44!~zpvF^wDEd>6-k0~>qu2eat^Me^+P6)g^!zuyp7r^;>HE<6)DGQOXg+Iw zt6uw4+nO(KebW6UT`%taS@Va^pSF1IC)5tzXHAF37-LKl3U(eO9bbaVNq4C!JC0);0)M?Unq$w~jl1(({DYD|dX{_mQ^zq5DqT zJl1}q=5NFMT07Va4tw73G@sg`{e|WWt#6ID>Om?NV>nI`%Su@b)PrA zKb=qQ(0zsGv(~Tb-3D~;w5|JLoR6XTtnqQ{bHdj06YPE-s64M_jR%b>;b318SoO+c&mODdDg%U za0lE24?(TB<yXy?xb^Id zylM5U^KcKOHQuUc9nu;fx1N2GH?5v^9`1p(#-~lsIf|vV|ERoa%V+JU()CgIV?DR- zBPxHX^F`Iub8G#i^`rPc*etjNu3Yo`V@da~`;M}c?yu|Z?)!}0;4nA>YCqHP&w~r# z61WWRg1-2__K|if13(59@Lttn1syF!~w=$G~ZD4Lk$e zcHDE(4(jvh%Qq?cHehS~7W_Nl5y<+g-^2Mn7}Z}L`fipv`aJVk-(Mz>r{f)NPd(ru zsQpvdCAwjcz7u}xhhY2k*8ZdPdam>6x!P8Jx#O+`IGIx?gKO)i2|Ivwz~Ar!H{rFPwb{cD~cuzW7z-Vf-rc zMCtpX8}#URkarJcKj-L6^Y24<3{sc%^nVT==U@F(H)ri$;dxvByYn~wnd1t$2WAkL z^~BF3KkKwZ$9?NOYdBx|b62koa}R*i;H+mKoU0c5tp{5?`(8kvMQ|Bh_vGKgUbn#= zaM!c%Qor7HU)KEf`);odV9&dp-3w}d>KdW%2M0mbcfj8Xc7weh{UrQT;50bn(buET z2CxZi23x^4Q2UFDQ~f^rH~NrCUu%Bt*BT$H@5cM?05}d#c;1gyxBu7f{eR%dQTrKqpY!+q zjpG5>0DU*u8xg;TJnP^FxCw57+u#9s1RjGY;01UIwn~3l>95DrUs8Q>f9#9rc!9DN*JO)p}3o!FucVD$&J=g@cfbC!xsQ2%yzc2Mqr2Zvr)~`c;)^C>j9iIBE z!+BYU^Rf=-WgX7TI-HkveDhAAk122locHv%0e|+d+`j9;dhp=T`7gi>UT-yEC)f?@ z{+pN&@l9Y4*auF5)8H&P2hM|w;4-)hu7MliHpo6(J^l5--v2!F5pU`SfcZJ>MrM@P0A{UVOmav*y3V`T6_Z`6P6k;0bsZ z5#Nva0dNo;0*Ap7a1xvbXTVu-5nKZIK=!vP{hfOHOR6vKk9~1}?8~>m%!l3HYrrg6 z3qI(+K7@Pp#HoG?x*2fzZ(aNZsQOvx=D>My3oKT@4EOP*tn z8qYlE(D~vte?9Uvf_)(Orh4j{plb)aK-KH#F}35?Ydx!-_WpF9xciMezs?hCTl-DA zp0z)#UgxpKSM_cKx<}g8{gih442Ty|7tl2sZ1C!)Bcs*jjH1{$+3lT=VFCx^<6!2mU?q7(4;%{?6^Y z0o49{&qZARr0ZMj#jV%9YP}}hQ#05Kwt?MX4>$l0f}`LVI0a6F^WXxg^;xGA=d9BQ zn{|d^v(5x;)|rLPI!my%-Uj@e;1;;!(ff3}9{mygC*UP@fA3xwEnpj{{po$hZ6{sd zS}$(B?pNz|;GVj`9*hFKl60qoO$|TGtY?Rne^nF zhkpTF1eZN}pKisY--Le~JOB^Dnp1pU0d-z|UZZSleB65N*IG~G)h>6w(E6eAx(~JE z&R^psdIpLD;KR(`%8Ksq4lcq?up_atj|~6diF)eT`%tZoP&(JUfS|=ZZd8C#oZ6*CgZMG&F?nA zeMhmnug||P>wZeT@2GrwZtd5q53Ls(Z|%3LcN@^X`|Pys%hxCUaretQ*QK?;s5+(2 z7gaCnJhY#r<3sD|K1!Xhn%`|y_dwga@7t#5o^C3w_nEdmRQq=ui0fX`w%@e%qkFgZ z7q>p`{lz{1ZOgCst#;b>q5Y|C?N{T~E_J@N=MSAX>3D0sxb=F!YR6sQnqT`?!QCt0eeB!cfnt*KJNab z^6Rbw8J?X@2~{U6T01U3I4bVJ|-I1BwMxbBH>hQ9@D1=~FOCis1E+Mn)6 z?YQ+?PwlwtTl0tZm$vxOeysbuY5LtW_xe8tFTwGTIls=U`Yd!&`aR^OzRr_>3I1d7 z1Uv=DKkoXP1+~AZc-8yroTHxV>;BQzZvflCKCmAg1c$*fu-JUPMaHv!yXZ$e^_t*s z1$})FdEyzzy3}z_oo|tOa9>%l7OVs7!8vdR49(|@-$q}@;0gPbfT(^&o~c3KQE?5P z`5NJG0=vOpkG>iHcCZud^62&bGRoHZqV#%hjkoGU>xITA-EU~U(0MgpZEJs(t`D8R z)bX0nYFqC^^IPNN)@y&EwzXfaXN|Y&wO*<1Qukxc7q`CD`_g>Y{xx20YreSkq5YLR zKJEL7yFY9Ga^IKMSKB&I+Vp9kNBdJdZTq}!{Y2fzK7OBg2{xR&?{C!oQ{nIb9E!d> zLVw}WPatjvya4rnn7>&48uF}z8{iJO3m$-4kNPdq?|Jma);aXV`{wfPe-e4Pw@cVQ zz23jtI=`N)ZPjbM+SYtleQ3Q>$7{YQJ9OTt{MK`8KUKZkK(Tv{dtcf|-1Tl-{-pP- z^+N62c3*MNtM?Iiy}0uyJ&)E?+d8ktt8L9^)rZy#jkoq|)oXuhhvu`!TlMAcH?+QW z-njKTkJ{Gy8n3oBpH-i9z0m%&U$xVoKeWHlc->c&U1{~K{Z_hO@6S3AyOO`E^m^Nir< z7N)@3Pq?3Bs0SOsM(`9o1GE2(pML-wz$UN-Yz6iC8!L1D(Eho%Ma;1QZh<mkChkvFg{5cOBf7dYZo#bvwZ> zum|h|`@unQ2-JL5z1Gz_nxA_+#9Z9loTr~&=te=tox;}q`u?MK+I##i@!bpyK3DEn%3^xy9j zx4zu{YJKaxQSo!1a_?93;OeEb*Fdee4c#8N4<3T&V7oE?z!<+{3nvX*^{4f?2mEmk8!^K&YAbqZVxqJ7OVy9!3MAk)ce>pz^{ddWft%nycnqF^=V10TuCIEq1#AZ=z%1(5f&<_n zI0CZIV*A?-?5_*`S^MchTpu_Ba$fC!0Oy0C?nCt_&|QMuTMPE2dc8O6J?TAYetrK_ zJMDVDPS&x%ZSWXu&phnz2;E>W*azx-%ZMvgK5P9G)IS4j&}SB`1?#|iumRM0d~=T$ zsqcqw*rVqjRp-mE&zIWTzn-fdw?4F9XnfrBS@S17Ptx_Q`>5*O2K1hMcGCNr!~0hM zJ?`tkAUF(;fTN)HcZm2SumQS8kG>E7>(=w_jehNqejjg}UiV;~ztr)XPi^gQ56{m& zcmN)Ps@HsKTlE^Rwl$wsul3Zn=DTfr-Iv;xzK^8m39VP|__*);rq5^HUv(dDW4Z_0 zy3ekM;P>ZX57-N;o_Tx>)lZ}DD!2pgdHOk}|6VsoJ=hC|_T!tA{?PnW=yMjF2NzP> zKj$u%q5FxdleYYo)^FPOmvldyc%3cRxc84$a2?zLH$m-h3I6NGw?5R>Z3ElE4p8gG z{e1PIZa+8x4uV7AD5(7v8$axckD4#)T>FXA>$x>v^>dhO>|yR+C%`Fi8k_+&eiZ)e z#_K#$w${JtdY!M*=TCYbt*3U}`wXoY8gHF1>3Y`wta_ctYR6sAn!l=d8}QxZZQGCT zA@2EadOhp@bRTM4^W8SR?n~{o?W5BAOS<1`J@>@w9%!fid1jwv+WWQk$GJ#reB65W z6&K^~Kk59F4|m_Un9MphKLX#M0<|CNhoBz;$G~xL0xUM)V3B(2imk8xTc3+ci?{Zh zHof(J%B{EV&)RQj{@WgJ-FMRZ(EHJT%bm|!->TPn)wbq~Td)189d~_ee(g`~a_1{` z{YszDx(};f_oMbr%V(X(s@MIgZOs?AUi(wqT3_SUw&ttq-3D}zw9~d9_C;FzV;pIX zk6X{aNNYXD`52ln>3D0sO4sYXRr);D{@n)jUTLRoKkO?lwC<u^IQ7~)yF+gsqXldS34SlCE#9XVsT`|JHu3dY#W2 zf7|p)?@Q~c9rymD>TTiAp%)+N{#<$)Tm{#_b#M`upR6GHUBvLeYGxb02~5`!4Ys8oCAINdW+OkS8RQ)r}q~Z>^{LBRprv;0bsNs(#~9?m6ECx4`yCJHP7BaDEP6fR~`^hmdCuTm-kk z-a1!j7_`P0t6xDq)gQut4qk$FUs!m4p4Z?!3)V*HGezosd42u(`mXo%GX?)XcnqF` zt+be$a+VfI_L1~ed_B^?NI$b_HqhlALE{rCQ$S9ycXy6V53Lx)A{nV zk2CZ`U9%^@?n7U`Ht^HTJ-nyT(>8<-O zb^qnAU+Q_H@|AmDtv`vct5e`KI0vqSHFwW^@~EEv>((pvzO_GV{sHV| z5F7_5J^R{)e-GRT4?KEnKXL239_yZqZmi(nh>AAK4Qpcw)U+6w`9<@XB zhsJ9^YFqQ)cKr_C2X?_daNqO(&{^-^PrATvum=p?=Nj^?gB#$cCx7!7xqe!}RGb3&Y~3&YOYd@eew|;(*L!ly^Yx!`ZGY+FsXO!NwZ7VnYg%((N6Aju zs;7Sj=j4h{@&q=Sd9^zyn_!b&u(e;cshfhHoQF+rdh&7a14nNE zWcGv3)_U|$e5mj_>x{j|%|~7Thn=3B_*-XFHw(SiqkjeGfh^WSuvd!E6* zZ~DBv_sfLtpL@-oxaXN{`n$rf5BfX)zVJEy%^z{+Wc_Jjedv91PmSyD^~yS(G1Uw0 zmwj!cKla^^{>fq3q4hMLb$3d%ALbePXgv3;{j<&q>X9vH#nq>7<>STawSLuhue0hN z3h#l>)jFOpvgMx&KX3GR;+*X9=%e<|xPfcNN9C{dbM9sCqVRo$&d0oc`1_JW^F_t8 z4*%ZLC_Q!j`!u8U)$^;bbM9>#e}7uq_QSr;Q`#T<8phvq6S_a<880#)bwl`jGNS5J zw_K#2x;^|o8k(Q}Bb<}`dnQy*KmUFTJ-6D-vxL8w!5UxHUwuEQw0-d2hw=Z#OuB#8 zZNq=}lyrU8ohwuQ(0SSSVukjv^K*`sYtF+s{`YX2ztlGC^zpyT0{P!tmAanhW1SlO z_bnvKW|5##Z-@Zhw<|)q5W$-^G?K+ zU+byOx~HD{^bg_ZXEdJveb2e-sq4Yd*XVha&A2|qlLN5Hp=;*X`ph$4WIpQVpm(1J z9{6`He%SyvgDnq8uKcRC!To$vFR1;|zxFt%BQuY8_LXFxW7Of-^&tEC9Le2Z`gFd2 z>(F35*Z?-NaVg-_wHftI9(@P=onRN(?a}YTzX$Gv2jC%i1j;y>j)yo;C)fpcgR0MD zzWrb4d{6tar#<}{Kl{Ey1ie%XUx4fkdqooOrR|H4e?%M0o2GD8LZ zkI76N6w=?x9Q@J4AJovjna5^M&In}QK9@_ zl4<#5A^+ntE&o!;|M*;wk0{V*V`kuyh5TRoAZ^}pZ|2K9{xA3Vn>_xn@c5tL@#kM~ z5B}%g4Bv~dp52@I%Bv1o_h!E8LH_*fwI=hi&+38ON^NEee`>>X_toGrnOS*$od3bk z$rt7V@A;hPrwNI^B*)KJcU|h{;~tW!&Ct)6tLyW@uRG^|@ayR7K5yMeW=`bw%RCM8 zx@-Tud1^DK&%FAE_0*g`lev>=diT}Wk=xsY{rNWX^YZ(Q?^A`B0AMX6z=eyzWdxlFej67}+wHf;1&ir$3%)9pj z{gmj};ivw^x%cb5e)$z{KfO5k!W!pq$hv;`DsImC^LuOe>`ndOj6ChnbcuVApZ(Jh zcV;73F5l1A%UwU*!{6rK9`pW5o6X_^E$YuA#hs?CkD< znFc&A-~CAE->Y-}OV@bjeejPw!+pVPh5qMrUt02cGQaN}=ehgK|MR$<>u&y^b^1ws zJ=}-CYP4bcWcZg;MZa9pMIpzJSX>Y zEwAs|bN%eNu``*+IRDZ^oue%`Xa1kxg1-aL4?pd_@Q%~BZn!*)PM-PI$2x!Cy7RBX zKasoWeBJD;ouBi5=iRUOcQO;6c?oPm(09=`qDYpk>_o>H{86xNA~tp zk8}RX*E;<)^#20?4)%%b%seOer8J+X)8LqGICstNPT*NZ2>e*{1K{}%W+#NSK(x4K69(9bL3@7Z>ai?7Js{pU0A zcfQpvV8gjH&&i$V=N*#%e+B-fZKpqi{uA(bw7dMPZXKCdKf&eco_78*_&?R`{2O>4 zIM<`U()ssfu73^xD)ijjx4=J!=YjrS`1{~z{-?ozhPn9l=x5>IlzaJg_~$?F`tNec zd=UN%^vUP;g89Y&sIPK!t;_TL1o(GE|1I!0!GHf;@UOss|6K4lNS>dC|5Enz>+mni zyi4#;KG6-@jd_3ciLU>P-*b*B+~Y%AFxQOppXWb#=Kgsh{8@Q^R^i`}e%=EA)TzsJ z=+b1GzS`yQmpo5`zZ?E6^iP4m?iXEyNAUj={7sVQ1@K>D4}2b82LC+z%)Ra3{bvjQ z@gtYN3GW+^&%IyfUoXw_Ij{}>R>|KD|FP`hhv096pZEJyIsZMG#>@_WKXniK`Ag_) z?s@245BKo2uXX)wy}$`Kq5o6(k7W-ZfPYb5kDq`)EB$;ytIK~Nd79{#xt;|77QU|a zqo4P~KlVDe$U*pDm-~%GexLpFda1p8{^-mv%h$Wz{BJyRU+ z$9}W(XJ3VjLZ9!+`SbUFsgn6 z>gnhA0qB1r@|+=$?u~xx|I`1R|J9vLo9Euu|C2fW-FttfyBEICv!8wVnZNDdx%~Z} z{OV_(7QBv_=R1&R*po;7%ya+i4vBsMd8Ryh)PH{-{63fcJR_GUzlTxy_i;cy{nUT& zx8;9zC$r?KtNy3u^!fZGz=LJ+dGAsee24wcl5B*H!Dr$UODCT%MUXI>(&UrJs2| z0(}F%QL!j``lGS(pN8JgjOF!%W9`vnm za&zg|LHeox+9$jG6VUfbe)_3@HS{~s>*px?sek`dT>ggdzqbWe{oKq-{E~P@hQ0^F zEWTVZKmF9#-u=a+Gqdu2hUb2t>%Z^MT){I0Fc1CALqGrgg?{Egko*tNy;0=%lYN)V z&+or65BgWCn4+8w(5B*}R>t_M} z=hF}Uq~uwYJa2)1^XspE{bgN#pABg;7dd@?&*z?bALRc*H`n9~U4~sGVt)GJ&iq17 zpUuj*BYOI&|1kP#^Yo*B)@?Yv`aFD!bsysHameBR_l*nDf6ae#b9Fq-3D-qm`@XCB zf8>Xq{`3(Lx-Yj6G84n{`;bz`gi<<(+`|DeWTnL z{nX#{jQp>1pQAj_t@^(xr_aBR#!p@TZuvel{mkF@OqajklVAPAIek9=#>ZU#6X~CR z=3jZ1%RlVNul~)PKA(T-6E6R;^iMzYe|FU6ANS-}Kc6f9`8)H_&!4+c|0RANL;dHU zaeb=)?6Zaa=eeI!``mw6_xzs1{sX+vFot@*pQ!)-_YE9Se~R}9)$@IT{>T1g{#Us_ zFYxSB{mjqr=Q9ue{9Hl*6On(xlVAPJ!_NcE^R39U<;kP|r{?te=W4Xk?U|n|n4f;; ze<$*vc=D_NZ;}7xOI`jcBw~L0ng54>%JnnyWlldLditqfd$!Z>LEj~M`l)}|bDX~J z%U%9vdEcX-`VZ&y`FpSX>%yN0)c>iRKL6*58~F3YEE4^jpLTWIn_S&KybQPp`dRl^ zpa7tOBEY1t3`)E_~=^k%2;amal8b6wrG54nEk zaBa`e`SW`l@a#?f?2~_9&$_>Z{99k+3fAI)ditsVAoRP?&xxLX>Y0b1!|DIJpLKH$ zeyyw9CC@GW)DP$M`JW@I!&h9$Grx~KHTd&%-s2|x)ADl*f1dN__crO-oBG*5e_z0U zK7>4ro;>RRB>HLpIyY|<5-~sh%>SgHbNgBTd3W!*;qU(QZ20?;r%(1qKl8jir_ay3 z;^|ZUZ$bWPJm>m3ihky=n?RrV{f<6A%kWb_0R5UrPe1kC4}U*J|L^AV)bk$K@SLds6nWI&^^IhJdW55<2U`rQ7syAggJ|B7GEXS|a+^Xyao?7#h+3is3R@gIr*NvM0_sjGh0J@f31 zKkvu=JUN&D?sMMa=54s+PCkOVnfDd`e4_r(BG2TooA*@S505*p?xE*-Zl86Y{Oadi zNBB8O_CJU`9iBYuAIs_U^R|7P>u1^}zW;SZKl8r>`6oR2)&JMXzm5+Mmq^6^>1X~Y zP3M1gCsX@3h4-xfC+GC}{1&>Z<=MbNYPU{$5wN;V~``>(cMMnas~YKl4fG@%!jMfxqD)uAjMw zxQNI9UzqFZPCt*mJq`ZTA9sF!-QR$}^}_iFp?}B>o`*ko`T4y4NBB>l=kx<;^w;4Z z{SoJ%M4x{H{|M^luJ!Ie-}I|_+{ui4_NIRBhxuKWOy;|hXXmK!bFO~oIl+FI=ZBG} z{r!dWsGoWGzQjDwMxFsr9`!R1_rpB&^S1P%8!=IB>KYvb={(FAS$jy3{lO z$oN(N59jjapTkwpTI-gte^7uk3IgLmBM*eJ^s3v zT-Co5dp?GL635-Y?w-ST_}Ad)p6O>c`k6=l{Bt_>uk$x3ICwScuGF}DzkvTKv-w}- zK5TuU@cWqh|JR&8|2kdyUH3fWkCX5J+(JLnXFd)6D1QF`Zn@kc=c1o_`uX1r&_D8g zS9jv2uI{3|e(9&4dFofMzV7Zro}pL#|E!&VT;+52$KMDcS2MyzraLr*5C*Y>5F0|6 zU14w$LI{H}$O>T)TC&D&XbGWZ*p^|#APhoQ7&c>!5Z^Ee!+hVLd(NxreVj8r+K2mx zJD&H|>wVtmeLnBc@6U&LBAxyMdT{Ae`&j4$eWSs%{~78JypH0p+n@4}>n?8eJj#Eg zk$=hWW}nt4$sgm0c)$_=BGy+lQM-KlVL1Odz6AXi*HfIcdAo(vGpz4_I_wwPP23L$ z`mSZ4_NCM=_FtgK^+@=u&%Hj%!$KnulEVl0CBbJYx0@x7^Q`R;Oht8DBX}P3bit=E zPY7Pdyh`vBm=B8k7cO8vm+yDL@u=&W7aUIcnaUmLWS%eXk9doDgJ}0N=8oINcr9gK z&G+@7-K-lZ4^84epv{?=2>;!fw~P1>Wj=%3jdT2!%x4MxMa=63zmfTL!S7+-#qs2^ z{~YFValg##%#$2XChHe5pDONe`iXgw&}ZIA`DtUG!9J6i=Zk#q!n~B@NB$3DUL|-1 z^C5A+Obzo^k$-AG<_^AS6^?|yuJ!t8dCB}ppSLBygZ+yZ(ehiwc36(U(a2}4Pv4E} za}E0CjGL(4_Jw-8lEWYOXTcwQ+85z`avA$CG5jTm5AL&r|1I7pTHf=orTC|a^#$(7 zgdRTFuL1v*BWBwx(_;0=GKc6BpIt?Gm;e+iZ_<+wU z4g0%<{TCYklEbI!fY1lLNAS#BC=VH*==qc!K2--A{(|SSf41Q-Is6+B(*1FrIr3J> z{`rQ#98hhxA=bIg?v2iUe8m{d*SorXyt9sS8?mO0uBV* zCq=32*v7kQMjj-e>h)27GmazYB>SOX;Ead2+pAa)Uc>FS8|_LC|AGqih@56}JcAb# zZ{_?y#r|D}zvS?rbwcP5-YobV>_2GuOAepflS2PR?32-NEZ2g=r{AemZlCd)aY-%+o^ut^N7tC~tGccKBYY^U&j6 z(#V74h^OTA&>wvA86iK6+s)$bcEnl5yn(L|0e^$znQp`*`6926@?0;1a`?;>KIeO%)V#{iL&z5Ek=paZc89p#%un@s z<5%3D$BO*(d3l+}`9wV6$UiuiFUg0m0}(zg7ld)bCySRm=~Ha*!V7gDJog4Z#i{di z{ys7z50WDuJZA$w;8>4?Hv+m!hzjH-@kM%tAseOU`aopxs=1qdX z?s+uNv>5SAj(G4qCB*YF`wSXBlEVl4@9+V~bCbY-VE@Et`tm6Gn$47l%o`~WrDAyp zhd<5s&2X$N6!#aUI?#8m~6K?g;B0=)s}?f%P4% z&*dMW2ZtUU*ELCw_GvVHB!>^S&*5_d`z$tmB!>^S z+u?IQ`{aJE*N^1z!S$=~0iW?eSSR4k4~G0&ZnxNIS918w{zvF@2m90-K9a)+=K&G_ zqwLde_(=Yu*GKiagwH1~W+V86!++eJ(Z9yKxrV>w@EM#P_IHx^iP}wnlsLAWpa)00 z*Rnp_(1Sw{p90o{zs~XZ8SzX0F}FML7{%GmM(_uR|G9TX{~GUx41dY5_WCFfbN)&G zb;2JU{<$rrFX|xukkEre|G3vj@nj6>%Z22xvVY;f$iJ42&|h%It|E2b*I$QY_)A{n z^-+I2*nfub2Z#T-yQ6>k=a3lwl27vb$iMR^ia%{dazgx*`T7C)gX6ja$-AB)AMn4h z{~&WYpMygmzlY*kV(7u4M?C3s!gj&S1izL2E13Ju{?rG0aQJ`3diNyx<9rA7;LsPf zQvB(xujU`12Zz3%^);-|7J6{#UuAuxp$CUP>t2ed-Oz(We+2928G3N&n_1s)=)s|1 z#QHSeuaxr`IP|Nf{*mEu|AOSWe?fBWw@N;X+x->)cUV99_fg*3m>2OMARch(Ga!7f zVV~Mgy?(&qv!XwK5iO7Ph8`Sx#M8if$>UFl{er$u@T$~t6Tkgt%o~!|a-5CNkbmZ4-WmGS>MC@dj0|NgF`>= zLGqu*&&911dT{9BgXidi)BG26m4DLDYn1c;2=ws5{)^<;e*wRN<7{Uic^v{c^n7u;Jet|=ec*<925n?Me=x;-1iFCi^rSK9a+y;;qo<4)&R6_(%?)y0=3g@Okfs{4w@l zV)#oApW6AM4>+C+D)ZmCAk@Fi?dE@>Z;we1e_YQa{c(M=D?Zr0@a9bG9YNbJ``xd05GDA0PO)9A}*or{t@*)4W%`nDUU$Mu-y}{ns`vDyOkFb7#_30uWaOg`PCjZ6X>Hgr* zf5iIq?@8asKOjHg(APXd{{5_%@qQ9M_jhj^}<$j`tc!j_Woh$8)eH$9oMV$91iexK34aJU3o)T=yvXY`z{+@;L_YGC1CwAbs%scFFNx3CZ!Ce97^=eaUgXsN_X_ zeqVB2rz$zF2ap`s_ezfU2T6|WekI3y0VMA*;>3Fgq#o~IkR0!!kbKbaA2K+ubCy1M z&b;J!Z;|AsB*%LeB*%LkB**(rB**hKB*%65lH+|AlH)ok$&2~= zC~!J1w65zH?;phRqFcoABJc;d3XdZZkGu4@v86;UfF}%IV(_HF>kZzn@|gR(8h_mN z8v5-o)B823>V1EE_5S7=yu{#1gVz|m!Qib1?^Jor9cA>lSJlVepvq(JV#6o>9V0&m zFEMzf!Rrm)tn!$9!icj&)yG}8%46<*!>3=>`>Q|R)$SM0?Xf2+Y>oW8osXXpl)pp~q z)8M@ZA5?kFm0qsL*>3Q!Rqp4d`Pz8J+)t`L=GM7FZ+Baj$6Z48kGn#Hml?d);7uxz zx$V?=Vs0;kA7b!h4PI^Viw%B*!S6EoV+NmV@OM=nckM=gJ~i}Rs@~5_@9TB4)0KKX z>}BxF4St=w<%$2A-?#d0HlT>}oJ*sj)UaC*r)f>E7zw$MD9$HnsU(Xu6*WiN&Pw&&mWh*0|grWbFs*kzr4gME{w;KErgFj{P zR}B8P!50~PvBBL;ZMIQFUl_bxJaVpPt)oQyjx0|Z>@9$Q5 z+;yn&EW7V4jDJI)Av&WLa=-tg`p4Y9D)-~Aw(GYC41K?<_v7`EF9J{7vabRAH}0AY-lcNCU8nl{?cwXSdh^@uhR=?w zKIZmNdE8|#)W;=H%Di9AoY#wcVJz)8J32+|Rd4 zyV1CJD1797LkNrND&}5P{bO!IgEqb~H`U;msoXE8N<7ZR+@B47zoGxk(7TWII*A*+ zz~ChYPa3?&;0*?EHF&4WW6s^6<=-E-SM?kEc|)65{d}v=8_Vt=59=Y@H5+<7hhOS( zpJT{lZYLxDK{ftthx-RZpJne854k_Sw@4dzzx`meo2Tmi_M5?z2Cq?h%sr#F8*{Iy zJnkA)AAelS;9UmqH+az}T7LZc$l%omuQzzJ!8;7zZSa1ByM8VHnESxUXI$0$;|3~^ zxjw_E%+N0|^wp}~AD>Zq%zdo-#9Zc$+Vbw-@22t%UBXySy9ICa*E>be34xw38+1Q2 z&t*OMA#+HdC-{BL^94_PlJo`4F=|to7Ycq6^CITB4(){$&vu9(@uWXR_ZRnwc8_G< zEBK4d`$W4-nfD96)4$1oQ1COD4+;JvbGHV~x9IOCo#c})_(jY!1%HKkT=0#bCZB}h z)y(q*f0%iJ;7geoGN<*u^H)?xpM=}Z-ybvbLp;YVSVgPD39feXNHu&Wxn#x)!4KrK zNx-82A-<72So}hKoGW@+Yj>i%erD*u-80Umxv!QA8+WMZ(eWs%XZs@&+5-c zit*0+&w6Xocuh5YB!^E;JoMRn9rBrO_(%>Pw|?mJ zST_0889tK3r*wnR2fWMR@eOr-iNR|GKZ?h(#fVdK#4{s1Y`28YvABr{# z{h^<4@VW`1{xXiI%ZNvE_-EyWJ~y*Zui+y(e5NEqAMm`1I!|sK@`Bx{eg=(pC5L~- zCZP{_qrvAId}?m!b1&y1ZBU;NB}cpIlR}?4>=QS9B!^F*@Btq%c)_M&yR_c)*E4Ju z&Y#5nez!Ug$J~wT_GH}kt9pNY>{D#vMB|6`c+9AFyuU1v>&&L_OSLOD4uxa(Mzpwea($<3mo=4j`J*`v^euPpV z)DJjgxBFbu_xby7qtA5u8h&0MIAixH>!!8J9!45BmDO%ivHHW-_wXw za>QBkP#9+k_qW;bksLll?V%5N#y>)Sr1y{Nt%13`{|FqJfKQ9?!F}ITA0~hKJU4Lo z;J)wbM@T<|e}F$Y^xy-m2fu~$({AKPa`?DM!#sdj8hnP}_i^6lGnaV-N1Rws)g)E{ zN_}HJ75_$CzxeaiDv!DU-lF@Isd|6D%;2>ukGVm^zunO9)1<|da1;FfsUtts-yC;A zv=27oL)`CQ*N=eypTsA*>W^rI@GJ36@V~>~#O?J3qtYIv+52t*Be{ zcecTs-wORt;Jg*AM)j5y_0wti3>rMq&IZC~ir}-k-PUg?9;*9g%VWLJqhGBC?=<+3 z;9vQ6qxF85u|ARHT5+E58^W-}dL}T4}lH zj=)EHUz##q%x8hu5c=hb7OR4MN}W zpK9>AhJTmAE58W)3;%wD=M9GXn|vNp`**8S9_I03efVI$LN8us{cN7crVBkd^y|+f z|GaOhUAbKf4t*Kxr?6hmli<)JPHeYGUd}$?IX0TrJL4#MU9kvVq8iNlQJej5Y^a$R^ z{VL)8nsm`GaKwN5t5pAGh8`UHPg!4X=)s{s{xx2{STEr=_Gev};RyO76Sm%1PAk3XwC<`(CL5;5!@q z0E1T={9J?2H256`Z!`Ea27k@q?;89IgD*As>VMbr?2n78%c;NL)8Lf`uQhm+!P`|H za~m0Pb{YB-RUdOz20z{4)doLL<#E?%#B;l)SfcW{%TwFk(3SD^Z_?*v)o0m$z0_krU2^PqOOE|$$;){E zI^_O3UN!!ho2BYw?r$ph*T)(@cNqF=L*HiT>s7sfu7bh4R338=s{Z~yVwL;tE7ixZ z_rKHX%|G8jK}7+4E~JD{XC}n`*}>|{(1w|C+2z${)xfAGkE&_`tr5D!6zAf zYlH7>@VyLPX7D2nev-k@Hh7)EZ#H;~!5=sH^9Fz2;O`pzV}lPG{AYu&)~1i|ItJg& z;5!(64}+H({CI<(Zt(LBeucqrF!*f-Z#MV?2JbR>ufh9N?$6H}%X{Vn+Iac>$M3cI zD&ZE1j_TJTlA4rd*zsr6iZl7;$UJxFyBmG1- zu-&T5O#Htd@%#1Kbw1dC?-TucijMO_1xcOO;EcVdO(i&^NAE8y z=k-3Wz9ygb+|cu3{=ZG>f6cSI>3Bu6@F$;j-JtNv^ZFIsuQ}<*x#nK29+t(4e*ND2 zX!$&r<7wr1(!}v(aQNW*@ZJ}~@uGOfJIOI#ZT`8)BR`~H%S}=1IWbb2`Zwuw3_ii& zlMHUv?S?L0)C09^*K_(iTK*^gs@;QM*Ykg>=h5;1ZsaG8{uFr|=InUb8?5)w364JP z^fi1Qq?{We#KnD1TB7l_1p_#J(J$wuWj%GgO?aQY493@HyFIt;GG8V zHTa;x)4TLMBn)1t@`U>Z69x|g>iG|`k)wj^TPC=o5D(A87;pScK2|-Cy>3quaqj!3 zUbn}4p1Mx#X^JGye}}qlV2(IX>>+(?>i!bvX7UfvgG0aWTcn@-ERj5+2ZtU$(^xP0 zJi#e%2`64J#(BqwYo6#Awkwtx+ZB={KfV5Xw2>e1 zA%jkK4Rnt@w6Je%iwW;o`2*AK7|IaGDzCTqus^xMnIy5s65wA4(+*~6Rv^+82**~bKLlShEeJt zsB^rR3%?Tg+ktC>&^Ph1*MEmm{}VGq|L#%hKg*v-9r^jp>!bB)n%{4TK4tVZy!}=p z^hdl+`Woi)x&m;v8TOeYd@#NX1yA!AAVu-y`tz~TCyTy@eZUzH^Fr23o-}x)!RHvf z&)~=#+NJSLxZl%1fQRSF-=8y#QvV>?1lP>hMd4TClU%!)--wUBe(H_-d3!yyNZ*(G z{4a04FqP{Be?{>m+}CS@&^PfM_hPNa{q@x=X?*Pcsy6y{TC|Hj;&a<5{c05bLVZU2 z@gqOr0|t+Gk5ps6O1M*F8qaZmzf|KBT)UbdznvhK_y4sX%8k4&5qW!bl)M#-{vtnZ z2A^;6G`~MO@g7kQZO_jgBr zHu65vcGP5JJ4$l+6#4z&kst6LgID|gxse~}GgI4f%YFf$Vel4%JHH(<@&lg^gJ=2r z8u@`f-{4h(Q~mhot*P6A_I}^R#{Bd0_>s{ptHUR6*Z2fCXw+fyH>$&-x3%rziEi6O z7|%AN)M10j8}i(os=H-gl+T!}H};n=QrBMz_dYi0*ou5|+ynny<{kZuxX)X7*q-?$ zSES~D?8|Sbk+(~7!+7=`C2#c~gnH!XDm6bb_lB|m(xKL8!VRpe`RBMAv7d;%6@=x$ zc^i9vT8;d?oEOG({3!V;{BNj7UFG+M9C^z(?oaGi^J6W)30E(+Z^=K$J@}&5?gW=q z^XB*C!qZm&b3Pl4eAaIs#&gjq`79NALq5w5USaSWgU=M4@|JLQ>$~8Cd@dE+!^Hh| ztdY;^-zcAzMm~4lG6>V%GD<#M)&9m@i@N+K+`$`!{>a1HVm*#LB*McU!}D18Z*J`hd3>+QTo*<`i1igWyb!R=gS8q!;Gii@1ILK&O$<+xA9J>HGNi^!4dQ zo=16WT0*)Wu^j>qH!Qz1jOADI)BU_0`B(iy4kes!j0-sYlkby$HZFi#nSOzOnDZYD z^VP!qgV(Me@=czP{&2i&HsaU0zn>j-#9yeZvv&&PM_mn4 z2Z#TNxIZ4g`7lcTgZS9%s@15gZGIp6AMW?_weurqdmfE@*;*8TDgPP91srbRvsf>A zyTKP4ydpEq8~nQr-e>UYaiI_8GuKTq=G%`qL*(>L`W%;6srmT*>(w>=B-gyF#>YO6 z)y6pfJT>%x&>xS`#u0gI6nOxjv$obRe;mwMFWcmtnz zy`hhI`*y77uk}78-mor##bQ1OM^sB#-?ka)6YP)U_VbN-SaQ@G;)FkVR)YK`Z`nBH zH~IbQsK1F#h?n!v@BxQU-bb8=?Mc|kKeRKSw>j}P;Zu{#fc3?G|1)YgcM|b>&I8&7 zN4wz2o8)L$@|rEfcHvW)uk&hyPu@z`4;j2=>rnq6?r%c@@dgeQ@qo+t6WfM96(2{6 z@hzn zcz;|)&yVErujh8*e~kBu`c;1z`FHUTIp1-+#ymV*_{?N|S$P;I+Qod1c31tL{Bt=! z=oj=ETo2HLV>t!?6Z>RNrFJLt571-zg8u3uYPZ^fWs%lABT#3GXJ3G2g?QY@F`_~@VnTj zn0>JPLSHTP*Z)lIRtx=MzYuRU;s-~&D=#H|hoJ|D{&m*RH}v4ppW#+WzG z{D8y%C9jXx*K>{eP;$i6#{TempWE$bF7pWv{k^MFob3lvoHg|2E=?!C*zf^|&jJ2; zYqXr^A58|c_y@!pFJfcCrx-jbc%DCQ8~Nw)dPL^4PU!!``k6;iy91)#R-wOS4fZc1 zJ?1OKKTqgeGDu&}`g-mj^n*fw(wd~t+Jl60T)>gf(irIn4kmpy|FF3~z8U4A(C`6= zPdt9tga2PyzfjDF(1Sz2 z%KGF#(}*XX*GJI92m4>(GrUh~f5*sYf#C3&Dg3YJc&7ZG#(h2yKlE5XqFwMF;WK^% z>enozKC#~opYvGX%*zFq_w>|?d%P)|#FVwl@sAup# z?;q7q(#W&q@JHRk2ON0=|Ayo5G4crxJ$!JSL~_(8c=ZIzPkdi0*lGO3>N(V}8lzv5 zqh0vGew#a*puOOE5=lH+)_nky$wc$z9|nmu74;T7vd9K zH#Wrh&**>s^(n>-`{&%MJmD6n&UY>Q z6Vm6nSFhj;N%$w?{<_n3muaJa5%(7vo~-e)k8jOeS{^34*e#*|Pos=)oyartpLRkx zjud~)?W^W9;SSv)Y!~_2{x5odGS}1elU!Mq#>bwYq>-PZ+d}_!{DYpBSO3i-56I7A z!KuIgIm2rG#9XfN{QvC@UZnDbdtk>fe&jjruX>)#j6B!=MtLqV^89}|AkE=!GfJMz zPt@uy;a=ZK@9$5|dViaZ{9A$LL+ahHivOuG)mr@EA_gv?rTmO^O*iT zx`exGmoOgW?fVDxymcCRYyOS$mS^Ox?v60dTSm!S&B=P+wmDv_=Y%W631Myqd3brY zo`*p-4`W}xGL8ODyesVQKSt?qx9BgHuZ0HBIwj1{%icfQKAgrpoqvW;q0ryGIq54- zC1H`!!w0><{Uh1N{+i@y7yj^}af!J~^?vGHcaX82|DiFycRs3*Z(@D4NZ-`1zn|+j zs`H^YwK|;WJ`>}9#TLuo&S|L%#}RqUI8Eo%4ZcKh>Tj;wP`!RN;pVB==jXWCb9z3T zjeK^VuH|{`%TcG1&kOGk{hLS0XU*wh-X8b*Xt@|)UTNoqc>~WC{B-Xh zje8k$Kdh+_^x%jJe8#!Df1}_Suf!y6{_*$8s^jI~|Ds-3pKzz`fk@~Z<9O1W`Z%^5 z<5+vH<}>zjY%s?0wg*E0&x~>oIM$mb z#(Godxo!&@2!!b?D*7bcYkLi&)ITs@+b__^t6)P-@9(p#(fHWMtJWB=!`nmuxqe)h zx4%F0d_DgIf>VF}eadQ`C)~q(hwUOiCk*KMX*TlHe!k{2_WYC?`Pt;*(Eo!`@{_ng z&rglP+XP4byt_}>F7ng;rJkQbBR_q=QGN=I{A~V680XL^`RNt?#d4i>VVF1SZ?3zY z{xRGL@^cq1Amv|>pWD9G^HY>PY?1m0>HYru1sWfF{UnV1yp0P7xS8?WEPweb6M4w@ z`e;2>bur}!cF0@yj4*Fsus(5#uJ0H6jso&;sMYneFADw9F5*Gu*>6-o=|=rDJsQSY?e9Na-uzsA zX*gcguY}7g4Smqx{`FQ0ejq-af1t4HZqm*^Ls^H6Q9 zuXUbqw;m9-i#+VKk)DS#H4lE?sMUPNo}VmZ+^0Pm`nQadpT<9hi#&T3!tNDz5IqERxnS-7R{Wsoz`SWMqRpGc(e{)@pk%tuy z3H>pjJiNP+hY6Z~?ES4Z`kOsB^q(?He+xu^QMdJiqdr$R>hlwoC)^cAevT;8^V6>8 z$3LgyPhr0(p0Vex+Q?hZbD{qqN6Fhl{ZS`>*hA05ppl2p-zX1B zBM(1{JXDX8hsjrm%M0?*Dme1+FC!0~d+B*7$kFmP$qoKSc_=aRaR2jRyH}5rhwN+g zJd_)}QScV;AFZ#unNQ*7kxy_$1)ec8Y?tyJb3abh)+7G?u>Z?)4WuC|n@-}Gj+-t*p(rRwJYcaNGB!_>y z@Bv>aIJTEM4S&huQ+-_+CwPzGxoJ_p#=H53zvS?lcfIbPY|wd!;OnhL?YdWt>jni! zyEASG{r6snd~$`J>Sw%@96sqchCbkhg3sHE{EG~K$>CFVlkVSa@E*Yr+Lqd_G1`?J z?Upo#?XEF7`v38+(eTl^e_q@P+WZi6>usuWf1RN^Z^Ya~s*itvlc8@n^i8Th?%ECB zW$-?g=emCw+i#_RjK*Qv2lL+0AN6_9C6;NUf05p=kLol&_Ia=SbuIrB-H|Vc{uBJT zFYh=&kEj#uFSi?YB6*?rkJd-YTdCe!xa?4e;E3uZ)=#~Igae$uTbNINl6X4%;Q3mM zjd&z~mVMH0BZD$d6XF3!JeapjUe)Ix$>D?e;d66pduY6?e@*w1{2Pw5=4IjzjQ~B?39I-yidsB}`a`+TuYR3~|?n!mt z_Qxd*{+7WP8vIL@`|FL3cz!nYMTY(tRUdO{Cu;L;%uO};VFo`(<$inHi01}FUuozY z4SlVu_s@$nc)P*7R339TtL?_zEe5~c;CCAQ9+ms&)2i{rUBAJHR33Bps{S$evcZ2e z`1q6bI@w6&emQzgUyimh^m&H9z|a>N`roPgnA^?ZdmH@sDv!HzwcWU@R(Z@Fs`|v- z#RmVg!JjbrM+RT9QX4OS-Gv&bKToalgv-Q*2|Uqb`*7ovcmpTk{&~3*u|}Y8Y#(M` zq47;Z=>L!O_I`1-v3;~1FR&h|GkCMX=NP=#;EN5O`E~tT#AfwxwtQE_uEm%r9|y-%&n=`bHaUf zWay7Pe08gy2PeArzxFrJ=GMOUIuFF$n`-`Z-BxIj8zBFL`v5200-odcx=kOK0(D&c ze&ugepP9yZ{bK<#L*K9Yd@gU^DB(QguaM{Z=k?_`uPfxpzjdEuubO{<9$)1NH~ttz zLf_<{<5s#?&vUJj=jIzUA0hS<^0(*bp>D0NCc1wt3jIGFCC`oX!n`5RC9mjw>Z>8g z{%^6dAE9&mJp$?7+V)J`B~+g4UNn}A7m{HfFkZ(zqK{X%7y{Bya+ytBru$4x?ZDq@Dx1tc{I~e(mIW zG!Hj$zZUYJpbo*|RJt?ivwJ8HMVt=kr+0_>0Z)HDod9J%~99pDr)X$m~8qaYBbM^WuQ|rf{ zKfP7+8GBtd8+A1#>gurHEq{3#csm?NB8!`S;YTo?&xQ+e}eWUlcOzrR3`&DT4Yw}NF-u^yHzp~#A z>jCvsG(Y73_CC?PJ$V7~0sa}s+h-c{q~rxt$Y*LF8T{&cD(2_~;wk+wjOQ=xlUPVR z&ObwsIHeEtk{5m)`opJgQOL2p==WpB+`(1aa^c_CX7I!EG`&C1rOxLG_hTg@r*AA@ zFaNC9Tdi7elU(O-w0tFuI!s#<#u?jn`SW(kr(?_WT}IxT)V%rgeJc0wgQyDQL_V+D zDJ}ScxW9gN(*W~!;3QYjrtz^a*Zr?)^)S)JR}b?OJr8<$`%{gdg?frV<`x@y>r(R* zcYP{PxKmEk`kUjbcGmM&rsi$z`RO(CbNU)#fA<_EKfRxadgSL&bvaGA*+zdqH~L$v z_Sdi19?-V&;Nngilg}&`8-3NM%0`&FlBl(nXbe~SaQ9sswsd;KW_~*bF zyj*4g+E>63HScl=qY_uUHSc>qxJe}*i6&==Q82OEdH5%#$G=q zM*SSWZkV^{N2#A~k!R$8`cI(`_~IoYA8om=R+m$My@C3?h`6g)^||hMYis#TxG&Dp zxW8|`N*|ZZJWcP<@BCBaV;`5kSGD??=nlbx$j$VPGA{9-wLB->jx}1GId15Dy}wDd zzhm!Lx6!Y0i7?I|N9k9d=ojj$$l&D$uQqs#;HXdQ{`zLM-V$z>k>`gl(DU4&=Giar zbF}dqd;U9&{NF6{pS8QbzS!FHXuZ(O`I)v7#fAKXBiiWw7LmSZRXVX&!ar=a2l4E5 z;(oV1^?8l?f*(O z-~}pAxR=gH$mb7UAI%SaG0H!7KTr>G=IF(z{=FKJzB`lj*ldNqUg*K$ z4#x>EfnibaCqIz`r3_1U%)?Lf4|t+f02B4kF`v94aXt+&d>uGA z`dhg#`3wmk>?h7L`YSo&jC0=LKcD>zHl}#8_y_1qrG7t(r`XV=9^eljjF;qn!XNtF z31R-h3k82>e~M=s^Ti^5aJ2j6f1-blcRfblBuBd#NBDqaTqJK!=<(N14EdI&6n_Ti zGnb9v4=(*NUf^pTKtBDOl0SCC;DhmkK5-!F%emch4hwp4=<&YPK_hRH!>2tr>@RrD zB%L=Ie7?b3xW9;Vor9=fbD7J0fvk%Ay5)yeoW5KVBp`eA4ptezgmZ^@w#p z!l1hTjl1+W(-C6on|$Ifq4Jn}S6xrVU7@P?*J&8M(%`iQZ&G=#I~x-W_k!Y2xUY@% z=0mUQ>&<3$z3I0fUJcUYx5T2)*w?2e#`?6uU#~jy6TSCEJD;_u=h1fHw9Tm=aC#VZ z0uC?mDO-m1gE~2UOKp3@pC4579Cuv??^C(I{^4>&Lf_PGj?3O=<=_Y6{(1HJ0p>hU zaz!6#eC+v;8~K0PudhaawmDeOKk_+aD?Oi|tILId?)oNro`;OQy?@2XXmEX9j{D;- zdfo=ryp26S&d5)mUk{A@>^n+++}3)2)~VL=kZ?C#InsSuf3N3>ldNs@JXptDGJEuSIM3ik1}`^wwaOE&y?$hGm*xM3-SzyJ zsrjGe5+7j`4*L zdQJ0K%spz%tMx|y|8o@_=$m|UT>J0!{5Kf+&s?DSj6MGyM*iDU+b_%7*yWJr&yxk) zhxtd|k_K-uc&Ff$pM-nxnq{#>|3cp0IYiG}w~;ruQ1cmk-fE4!t$%1ZpG+MkZ~bb2 zV{WZ{ZN7@TW}~joovHOJ$36K+JwF9oYkI%^^Bd)-(#X%6Q^Wk6K1zOiCx>-Ku9v1!~%tQSsc_`T-%mbFMeuJm)6zcEv{?UAx zUPycFb&~n9<}>#Bq|m67pZ^%v$r*mUwEcyvcpS@j z;c?_Y!+3!s+N=C>7>#@O6w=oSJvj96Y220c;J7cQ)>xnF+`ccyy`^mz<+{1X^8Vlr zh=jf|j;*Kbp9_$Zq)jf(LH-yn4iZ- z$xqjwVSb|br$+Nk@m|Cyr%}A9PjK{NTR-1L`kLR9zC!51p@+}>{lj*_OG0!O5B(V#pl{53|E|;Pr_-pP?k_c;vDZ(B zQ9nmjg!OZsAK&H8d)Wtst;PBtJob;6kQ9RWm9&qRvu)de|lZ74}dK!0sUf!sy(sQ)>v9C9-QGF8bgj>)e zeWTtUzfrHZGMv!kKO?=re(YP#XYBRXY}DJ|PYvtshQpU%Zy9A_y-}Rj^Lt}1Y3!FZ zsCo0}%~hW3j#l3%;@?YhtCpV}_xbI5p1X}a7k#Jsj6KhlMxH-9EzI-%qvW~%;4sh6 zd3`j`cQYSkyW!I&s6Rnmft1D@+&#~JJ|U$^>ZkO8-A$?ntts4%{2PEt9Wn7 zwWIX6Q|)iUeQxyY(gk|IlD|>EhF;e4JkhOC8;;{Wqx5T`=ojXV>|?|61ur-F9Kop` za@`xoa?yN;HoiITypQy}HK=*>$17F|`%CSPy-xa!eEv)1^XXCYnK&-Y+k0Lgt>4?2 zAw&8%Dx3cXu{n+cX!^qEtmxuW| zZIt}fpBUB;>ME;J=M@HT5gc`B-LKcF=F>lqQRUY9H{li<`G3U7f32E-|Ngtx!o2;j z^G2&NE<0Tj`d>N9xXf1bnQ(u-N2{M4x8+j3-ul%3j=kQRjDEd(RoJgPM(J0^$zfel zo)d0LtKP3OoVGkpaG5v&z<>6?j#sVGuS>2D`}M>q{VEpyddushd47<2KgWsX3mm=1 z^3rO|OV)dN2G!-oU-xC?bM=+K2l1`JjZSE&A8wP;{Lg;>UCWHx}g73#P}o9 zk9{64SzlXzC%XG*hrG|{e|g6frt^4(@z`}97w!HsivQ$I!#c@5hU&9$2G_|-)bcsZ z8(A;sNpQs3%lg(*@@eEB;{JS9w0uoDfOrw7<0$4cHX|l@2SkH@2=A4~of zdBpqp4^W5T@Hvt7GY%zvoW9)s%!@ZCfXx^9fWv2;KhG5PtDp7lqFr$4&trYl7UbW} z3m*Kzp?{zC?fa4*$L%K_PwkfMM?A^X?(xhk^NF{Kc);QR4C}MDB7HUcARch&H}&U5 zqW(_h>kV+Z)g{d9__|pfK7$W9d_HDujNK75rxIS1I=kw=Y5uF8z@Q@K@Pq)`=89&Ig^#{mSI)0MkUj zz~O^-G49~cvdSS}>Tav?e9CCM=_NsjpeJiU^}v4ZnZ$UjVB-cmvN z=@faudhluw>?HZ%HWFGN#azx!;d%slfDin^;RBw1GQ~gZEb?y= z%R81k=;4FqU2^nSa^y#H)Q{vDCs91$|K&LAInFHp0r7xC4~~9GUUW|A4}GP$X|pQrgP<3fsG>cOE$eiEz)Z{c<;nV0ep z(1Sw{K3n*F&px@ly&>ZPmp<4&k-Y2@eO#6Zj^${w@qULAv3x;)tig{ncztQOoRW`! ze)Xs62tD-;|Kejcea!vM@V`Ul{ywSCG=Km86oVHTyxic`Dv!CljCkr*y}uq<<^Fvf zs!z<#Gx+Mpdmr93^lusbKL&rt;O`pzeS`nk;Qa>wRONBkq2|qBUuy7vmB-xw8UE>? z8}(!G5|zi?28~)h`}fDGdjI@PgEtwx-QZm+kGT!icw#Qw;2Rk{$KV?qd~<_uVel;t zo^SB248EhmcQSaP!FM+J?grn(;CmW;FM}5wd|!j_XYd0IevrWrQMrGvrcqZ@Rej9; z!QkZvKhoeygCB436AXTm!A~*xX$G%0_&EkY&)^pr{33(b8vJsD*BksAgU?jCf9|C^ z?*93f1I9dQ@FIhk8@$@!^#*S?c!$Be4c>2X_l4fyxWNkyUSja1!D|fOVDMIhcN)A` zx zZ`1eT{?6w9`qAYbPtL3j_1G_)A~^O(YSnmR?&poQ@$z~5Kdt@~?i(Bs<-H#A&vCc^ zs2`v2Hjd91d`J;v=ZSpAetf?9yD$&rGtvF-pJBiD8zrAfkvHUXzTl^MpVaYT=2hGQ z==+8Ke%3p_ZnlJvAAZbyD&J>O#~eQ3$Pak$`8V?q@BxP&?Y=771%La1a9oJ{M28iO|&yw%{H2JcmQ%w1r_KWOOhHT3CU zY59-4gux39US{x0gV!3o$>8lOkGZdmesvl8#4Y-`_o;fnU25>mueH4S^^4)3XXtM+ z^hK)PzYo;l)dsIOc(cJf4Bl<@}0v)qWY+lcdPBhHH}^f>o= zCycXqlsL*>o;x)Eom5$7#NoNt_}$JzZs7-#J$aTXbI4*gY|pC`CMBhC&Z z&V5eP6K=!DHJ;-hJX4Qz%12?G&x{ghvk_;#5ofy* z=M*E(P0!ZjEMFMLxoDI)yNx(&j5zy@IFB;oymPu9=eZw;ai*WK{CUa6_2sD2h%@t1 zZG02%Vk6EC&eh|*aZwm&z7gk+o=5xp`5b40o5%hyIHD?LeJSg48wstCVy^juu)pwu z9zNjh8-#lD$#%tSkOF$}5|yv(dY=#bOZr?lWda=Nn|Q+g&B*7?=V|%$&$lgQ& zF=&ANt?lQH18wv_q@U>8KMCWnGR6yea~o=T^Y4p%B5Z>EbKLe9YW?!t$!fh#a;}p8 z5^nT=W8Y3LSL=MDJ58MDy~XG+wVQAY4nrjLjefm;B47B)KNFwe3c_LGJdZv8cwF00 zndlyWEsW0e!j(mpL=QHN+ zZ35UWy#GS-spt3Mma+cC8N_Q>C!g*#`r*`zh*!Nq|6jrSXP9^T@r*tV%!@82J?@8v z{vPJhafRqp&-%&!e&cA|@m`E-=0&x{=kRio$-Ec*0P;Z|_Pvz!cwYv5lKwu)$bbH2 zq;FxLcbIqYOne&i3a^Xwxz`e($^2I4rF{fw`FC!uI?^|Hk{-)r74uqNuVwOTuk3Qt z_kBh_)!eZT=2g3q5BfFL-!B)%gXanKb39KnFE1oLmb>yRNng$W$lHW^;vulFFz^%Gx5pnea)r9r0?P7cOL5>VV>saq3DxH{nxoNA3)?^ReOqtXudA5PdY%k0j<13D6Q9oh z`!{pH_&GUk%s*g0Tf~3n9i+#59Ew?Aa3}F9U)RwG`8hcWo(^1EE9v`qIYqyc%<&w&D(=^F%zKmMkNQ9SUh=6cA`YK#nYZ?l z0qXyR`$*rx+Z(8>RqrQW_+Qd5=5|kKp3goB=Fc%N;RSsr^Y58A^8O3rys(Y@@mxW~ z)5Uy#HyL2Q+V26@A5Hy2yZ13~@%I%)pJeL4{!Gn-q%XgQ_!Q<<%-j9?B>G^y?qrVl z{LW^5*+b+rkGE%vm@fe5R(q}W@?q8%Iett6j?5<)yM=WS|x+c&;htna`Pb zcad>E`ycP~6~);&j@r%R_0)XkvsWWNkM-~QI*arRxiHI^ulfveypNuzId>{^Jny=h z_0KRbn@;`>%(s7*e4_VoMjzzu1Lhf1h$H{|&n11cSdYwLUgPhViazD3|2p>*^UOVo zcQQZVIr6C$+u@6u*ZF)zAJo;s&y&96V=|aYU#^ci-eZM0=e|JtmX}FbMs{w`F5;P~ z_fR=k#Xe6kpS~{%k%#?XBz^x3;ux1Vm=|0{9R1z#CDP+PgWzYpOdQWutEXwgEo5H! zG5M6zm)mn5=?i!}t%c+Ki8!WjeNRQ_~573{v1 z`4-2MBp=KV&ojq)qI~vwk2&7&hIy~+Kjc$aK|ZytKj&@YC7*`o%mVO*YIp3m!d%s=b>mweJrpg?A^&w0#Cc|C>rzhjQ~8zP<=edJT*`xkw((#Ysz z=4Cq($MSpWhosNsI>+*|;sWA>zTTrxK7F~hJ|aHl8v=D??`~q=dJ^lIXD=juiJzyV zPZ6(=Dw*T`wt1|7kU8F)kl=QgGSA}iMSqX-^IB9tt^RmM^qI~+EzC1!67OU_VG-%^ z{zv$S$GQhW1f73dDk8x zk9|)1j!(#-o_*#qZ`qpk1N7wz{XZiAEaAV9dFig9|Fd6^zULC+bJ+jfFNwD8SESGVkc^S% zgP12SBR%HZHs3+QweHxi>{4M$P6ca~1T*|zc*CPqmulyb97uS;>bvu)J zI?oTqtk3+O^vyi6VH^)-UeiayEY?5Cd@gTqAkV)aBA?niYPW{(#MO!JRI;7@oceO^a*o3&k*Bs=n~T7{dz@QS9|@ z;&wk{j`w<(GB5mv^m&|zbmk8+U+mk7J{Yg9my*8Q_b>Xi(3hLdyzw&vJj`x=Up-O% z;5ntq)L%JwCG)OJNk5NyY(>&nCW)h-uVOy%HgWhYW!}X4Z1#C-CGwfN7wHlImTAP> z9wQ<6GtBY)Z5{@9?8>B{%*)GM_CInJ;`6R2f7E%C=PS6cmgfC{Owu3R!TLNu4$&u0 z*nPu%D$g@T%uigE{Fj_YdMsbBF>m}6apb2uo%`jN`{;xDYE zy!A!m?d?449o?~9KH|Y_7VHW9UCqq8KJd^$5v+p{j zFXQulh1~AHn3wo*h(6#;nGf-Ni#(V6_h(1_t><}Z3i~|GJdg8P!hG_2q_65FfcU>- zKFHfe$a7wt^c8)i$9myL<}C|}PopolwSTX66c3(jia5__UUnhn2lLo!{=M3fzC;|C zzK*#oBt4eXP5t|`BR!rgUc>EP!@RX9%zsNZ$IsiJ=>NEWNq*L+i7V`6$ zFXnjQ^8@pF&r%@Z=jV`5vLTHBQRX##ys?<$Uo%1aBELNqeP%Mh5uC42%VYj`<|PXW zApg5gB%hu}_DQ4PPi0=h+sRlSCu~gmZaz+uWdAcgcP`TXrW<6pmF zj_32|Grx8d@^5{KK!SOO-vNx~|E6Eknb6n9o=5qw{~h_mr-yk@59wy{UmZ7z^mzX= z;#qf7;xqVs8S;E4bG%2Uh<*OUJkIN#Oy=8dMm~l46c3iiY<~bPigPlr-x1F_%<-IR z=-*?$cpe!i*#Gd&$)}i)-!?J7oO$a`)NVQR>@7&&)=dE8*vNePo5Z;p7vGZfS$y6E zK4&n;dvfd8zl(WNEJv08#8%YbVzIyfGxJ71K7%|*zLCD0=YPx_ zG-`Lo4a8?Mzm9nq*K-;3)wd^o3vb_IdAx{u<2B?nh4n8nFF%oZDf5jclh0JY{*OL` z%x_?h_aKz6Y+#N zo_{!Vyzgv)_0M>|ip%2vTlUq%`dmL>Mjt44@b5K@@;t!nuM+-~rjLh zyUg=@$)JdRPS}<7nLLhIE*@q+^BoEt{hF~G>4)E=#Qj=ncj7%a5y1HFz`WD%M@FB1 zZud3jxb8Zi`6A|cZz<~Pz#{U`J%@ate}j45M`VEb_uGT?RUZb85B zc{T@vdGBY>6YM$iYyDF1zhZ{V;QgA({Nqr>eLT3oK5sqxq38h0bF;|vxt>S*h8L;+ z@j98c4kSJ}n>fbhSmpz7Qom+%zb%kg~AyzwCNsiE?9JNfsqMeQ!Ri-Z-d zpXGU`uZMDuld-!mrN{p*`+lB%>Q&Lr@GIg}zuuz-C4+f2^UTvI9^P$mccl0- zU;OJ~)-U1ZF~L8q;@_tg`R7ffJizBQ<{2MQy92Dh{SeaE-a`QWjUP&U;S=P8`TtnY zQDbb8`Z}BSi+>^iCH!N{RPw>|8!>-=&%E*_^2cj^cm4zE=ME63Y&ds`=i^q)cDVl{ zo&9f4=|@IF|DMf04XaW9k>@4M6Q7cQoa^MkKT^Ak9Tx)gD-R=HaymtVygkl*=HID3 zfM1wd#rlhw<9V3K&qC&d zv#4z>FB=^Z`TY7*km5dDXHtJLe^xOs<_BpM@Si@!Jl;p+KA-u}krd~^+LZt4%(wUN z<7qPiT)s;2 zU*{q7X!0N8~aioxxBo{^HiQoDKXvL)LiCiZxAo#c%~dr`o#St9M&_BZ%FZ@9(JzK z<3G^zi7RgC(jBc>P@gBK^nUxijr`&BAoHpND4t4=v)}}Zr*b+KLJh}R!Mx-lil?3V zUzoSHlL6|#8~Rq_7_UE^sIS+~^E}Gi5c?p`hnVBJshHpLPa=KuFBB){ne&)uwGsEL z#nk6vaGv+_2)pl@_di4Z;?pp0FJD($otzQz71wkb;{9~zr1X9}_9V*Z5V!jmqdxC8 z{5#mEVntf6Gda#5!QUeP1p6O*GUY)orwf?N?V^iLA$`$C)L)*q+`G&>2gtvc+ui$A z($9W~0Ot8~Qk+F2zOH0_Z5#PF@oz6O&l^V^`CqRp^8fWG`+1#jp3?jGV$3C^lN$uVD8*I-?vxabML+1 z?zbCLt)A|l*PWT3+dXf~whY1+M!`xf9wD+Lm&G$6i#Nz3$gzk82(SxWCPYXYQSEcSZ57bRXyToIdA&{;%_Y{)imENb!wvU+nACUoGJ& z`Q8WR_-Fo5064h^eR?3CPwOpih;i`4&jx}ZeEHsgLF2&Zk4Qc6HFEqbhBTi2SK;HY zFMa$Ui}fV%`A#{W)LTyQ|99kg?-v9d5`EyQEAiep{Ja>yko$Th$AA64nE#`42|rKc zSbvhI`B0AkkMjAa!H7*B+EVzDqv; zp4V^6wSOwdKbz9`{y#bX^k>9=-y!$=>`1&ftv@`HeDCQ$A)c3PuJmE~{L|kd7Glfw zymn0UWdA+I$6sIiRO-Gql;dgmylx`aGy84v-b9Wc(>UP&hWi44lJ|b?RQ=w!@$uJ} zzVfHUM&B*KRQhf?{;>~;wI=6sHB-;?clbE;@DGaT6Tba18pnD*b#HS$uU*hQ$Pbcs z(Z>=bRXP6p9})X{ryPGTAD6$ZefwVdy#F7H=Mz5v@UMvZAO6}G@~0F1;b-J{PwL4t zIoBWZ@wJKNk8gatnCI<3EarKWlv96Qj{7pfD50Ytlj9d}66^VKIZyCc>E854^xxT( zr{9lpg2Vg&fp`$=6aV-7d|ZBb?b{dpHSv5>r~S?HZ?PPI+n0;?CUW-PUl-4RK*IAw z^7)^WZnCb$NdYy0Kr`#vf)y)->>*D#CwxEV_z@lapibh z+LIG}KKd`k^RJQpXQ7_)eBD^UqTE{h1v9 zb$O5bF8Pwr$?=cN`_B92xQQ33L-AIb6ee^AWxWfGsiMUE$ZJ)V`%{~I~}frWVfy>k2w-=SXrU&tB% zOMD#tbLG9`W0Fq$a=a()6$!onm>hp^NJJB`TTF$0$_h2&*cmLD>2UpWPipt%Y7a3@wEx$kLU9F;iZ7* zJLU6#OOF5LzbnQc%JJXKndcAX^Sy?6K7r?7_-?VDk4d;Ca_UFr_*bUvNaO!nJbx02 zeZ5KU>qq7I}FO&Q^ zq2F(oml`KSLg0q5V6dj9wFaXIPQw_ladf9eaxgG2eZ*Zl*UC;RWu z@^Sc0Qhd89$3G+OQE!kR{WdxNffQZ;8pg?PoYeD?=u`TA`TXZTDGoNVbAItZ6#M#& z(&#$NSQcsRQo3aP~ zYdQXopA>|W;PaP%ub9UO1%y8#59U;k&$?p#bMn34$j9X`Yu~0 zPt2dx>wc3QH@=_7v;R){IO6{$9T{>x-y+99CH<8N{eD!Ae?rQg#NYHg$@70-yyLs% zJYV|*Vm-c;j|m*!jd2w|yjMQ|L*Fmv@#T`|KPcvT>PN-EQ*!)g<@h_M0&YJf-}~ug z{F7q*vyxwh|LvbB{pr$|m%d(pKB1%EkmGNY@&;T8DFyQE(Iw0!>i zB)+{b5bqUVOQoOs2C@F~N5ukd`TQ@)=aW9R$@Bk@9RKw12|oGtk`LeiVKGlqKm7G_ zo^QqYSIM8(zr9aB|0$_|CeMHA2gN*}k^DLFFFeP`*Jltv59RZp?THm8`s8;c^L&qZ z&X*s3%|D{~v;Y1aABWvrHavVvp4Z=$<8P3B=~MEf-}51{&L5ZjFj@cWengDF>qFwf zr{wcr$H(O_Yu_Ho=SzP^!1Jzr@1OrsF;CK8ID!8;A77h5{@9k!zvk0oo73_( zQ9l2Ei4UKW@BQE9cw3(DJLLQ)KPJ}mbPAqt{Bbe^c`Yh_=nyn2EJa7f1@0K`hDO_}hG3E^h7H|0SP)&j$s3-YoxC z`R~Mg)AyH1j(=R<(;v#`e@Tv~?I&OTb3#rfa`!dz`5wl(o%0XHdp{w^e@c$u{Z(=9 zKc1}Tqx5|C-<_QCZ{XwTcb}rW?~>#1PT3oOOO7XfEfe`}{S&dyPyIdl?u5_F@xS~f z0{)+qfBvu>Px@uNU5@|7|6a`VyE2|B?G-UnD6G#%MY)8bJ{dd_TPu({Lg-qq*M9G zr{wsrOZ#MU?r;1b)bl*U$5DSz_6JGuaEkFSfu{3mS)VhR=Vx-}`4u@&+Fp9`&(wCR z0mixgHf2ZsM|?cH-+zX2m45Qcoag@_XZ#HxSHs~y&Kdt2K8|>>6yCl|j{m{`D;P$C zhd=oXVqYa$K<+gX-~Q8Il;iS#)RtTP6*->1-@W#i#Pb)@e*S*>{8#hwEPS5J84ob7 zg4?&}JpbXG@n5ESAYYQc08h&${44%E>}>M>@`ZB0|G_8Z`XwVt=w%_t-z)iZGSBzR z@sCJ;_-;A>Z_DwI|Bx7XN{)Z~J4=vvuPgnAD+lDa;V;vBv;V${k0ahy#>FLc{o8W< zlds#<6Q24N^*oG^zrOT|-;{h(?)QCiJn5I6@aJEa;}3sO@Q1RLFJJVl0zT<`io?gV z>;G%=`Ol=}(Fb#$|2g^mdp;-LE3XZur~kQnUth<^VQ21&1^$Ly!uQGX^gaCx|Akop zM?WMUOyF5M_o5MdCM>;aR(g|PavZnOaYk;dgy9Z`J-63$rh$9uGzT;`z*s!;pK*QW zyJn-@G~G(oGPSpOt?@JE((U78CzyG`aD+)3y+A8DXQ%fk_wJAGkB;NuY#cwjYU(3q zIXY%B;`zXvC>u&$pF}yGe3@_;QDrZ6S7~e6q`0qIO!rON)UM$}^zi2upb{BeDC$t;mW;2Ka zpOt%^K`@Ovqab4CyZHG9{WNe6HD)e)$@@bjz?$iw2ptQ+!=I69kW7&-K(`^Om5J;%a_@v(Bfyr^S3z~}x%4V~qitn-VL2ahgF`2XG0`}Gs& z)}#B!7pD*IJGQPE)b&3GdJ52Y~1Sfg4Dr_v*b_c@jSMG zw0_t(fm7pI71$ttFDJN|L0UTVAd14stkYMCAy_MsPv&WTIEt!%6b_wMZyazOuNjUN z1@X8yvd;LovBX@1WA6Ig^(;Xuu*onoL~U*0S7ue5@C>5rHG*CeAz-(;K(TC$hQm=O z99R$dH$JK&R9sfs5$qdC*=c19zd zJb#F2AIGhoIr7Bn_9&W5iqByK_i9?z!J6nB7*tG4tZDb0Iy3yK8#NsujYI2?%MQm_ z#X)hldTpdQJFOkl3a|#?%c~ghcM1W2ngITy_SmVt=-@o^z$K;7A0zCOLs)G)!XEMj zj$<|D+s*O7BTi!9l0V#-{PitZNU&rH4(>wyu~TCKCx3hcGjSL5WT7?of>XB|Az1%3 zpYZQbUcBj!s`(q@Oo8)g*AF5H5&Fh7Vw29e7c`h=E9nbLUK}{|>0CC;c{1@m)2SYf zMp36R9!aTH0VKBb;4Mq8n-drEFLznx?aMMb(-rG7Ply3?@Ip}CWKE2@*rF3-n}e2e zX;P{eU~zTe_5+9Jy6K^x=O4P7Wzvx{z^;(e1ndq*Z>+p!rOX(*5irebcY1yl4Dvxs zxTutnCKvNY97ZE6ZoAO>t8pJU0UFTE&L9qO_X(j5;CCl(cbd3&1Wh?x!EtHY>45YP zKcVR(Xe2=#^E<)4n(ds|Sl?k5pL;xr`7Oz5%;|bS(5|si@P^Pi7(Df>E$}osIKgey zIB*lybERbzV$i|%${uamD_9RZPk8lKeIb31tsY+GErdxmL817jl&=}vfud;%P<9bu z>*7ma7hiFWJ>Efnag-_&6U>Y+J44R*xR?+yfVNu1ldUaVRq_g=JAb=`u%kh%*NI28 z2r@{pvq9@Fem{kgfS=6`WrZ;aX5g}E^N7GXB@5f-=C=p=9S+qPE@8P>C8EEJ1l6|E z-(J_7?bpO}enN%RwyBBTnBwd$V939r?0VL!9qW_8)1ME#S7f`;=yFf}5Zf(~x`#bH zHs7`z1)Zj)1uGv^TnIHr4FZFd4f8I(#9MXmU9A&f&i87J_#u6*5_{*9%Ug-ycI>%A zHViqNj}hDMHjm?b+nMp&ErJ=#YsB+(0}!jbk>1{gHAhK>rq>Gr>0|zt|Exd+GUmQg zm$P`w`-)TBy~2l~V7>tfHaozU9hyB^K$+K`Owd31`Z22@F!GOHAx}&kv zffx3Nyp9(e_vSm=rn&46#EigYQsBa3PO}i)(%LlB8m@N<4-CAG zQCyAZ{YFUMI^hSv<}JrOcD*+2|I^N>4VOyTaC_WsXE_zz;lth}AR?@J(rX7saD&%X zRIg*ky`zT@Pw(G8caD#apRHqC#BVyeyCN?W`J&)1m|+j5!@W{4=?4SHQ@C?f4nxgU zJO~{o>u)s4^^^Fx_6M>C=V~K%{GLPZRd*CP!?qvU{= zF`d+mGCrZr3%1}X*Vz_g1>Cr8p3~QQ?XIH$rzMR`blwH)xacM;*>~>=RO@^ZoU73L zaxN9*d?b0GE&Y=>Rc*Fi2Q;(Ufyc8c-8B?3!EqQi*z7hX3TPncyyaWHFuWWOX#+6U z=y$`A@Co0MH#O_d5|8-989%oHu_`3(Tb(V~ARZ$1yTXm+6(^x) zF)b5#M5h&(5t#uJ2PV<6EAS}G7BvNN>b7E#DaK5dyfjpwuBZkz(qQ=2;<^;RWS~+| zRbogh9I|4qhH3+bY6F^TP_HQF)Kr7IQqWk6k6Wr2F`Ixwo{@ZFUm}^Y#4(PiH=xpO zE`)8^sX#JurWLMzs(`SB$g!S=Z8;=DCA# z(1F&A+lT4}N=3|2JZVyR<^9RyxW}bo`o;rK>8K4(3?W!P#+?rKtHDT`3A2iZ*B@3N zy6}9uz510FTq)jXtIj~B44Nt!84^Z3+)r%)Fr`E)|knS3>Z(yI>CdK z?W4#$9So$quaHZwhmo*up;ZRPm3(8l(&m!ok4($NX<~?DAL0sDc8aVSi!m?k^@Oqz zn~f2ot&KtGle$Op#;UnJci;i<I9JY0vn_TKOHk5P&RWEtlPV0si-w!8JQLQ+98`iT3r{u~j)aS~5 zYz#UpC!rS|tM5jaL8RXsgQV!cU3YaC>#DJdW84kgCIx`vXYJ1Oh~N|}K){IGbS@`c zd6AM8z|>)oFe#ROPQcaMxSc%kyz#IT_;nhsvAZxW-*xI$InCvS#|qaP84#TbBh(Gf z*1GZ8b`Y{^Hpab6oO$5(c$5rT#yRtS#aCvntt{%a+BhV>wB4@!7keSZG2%{Zr%sjx z&4dJtV&ll13!K2Am^`kW(BW1X^<8Ok;=B}0zDV10x3H2dc4rq%DmOt03Zb}TX9=$o zxl;$;X2M}Ch#>`n{LaK4hjp@)2GJzYm>)XwXCBbp=48kT&rMTK_QmC`p5F)UGm#h|Tst?w5(v{R%EdP9 z$Q;|IQIrFB`yS56L%J!x{Ch|{h~VN_vX5Q1rA~|3athp`oZ~#>fFhG!5ka#p=0Rw; zH%2(x6ZP3|A!RB6r7=O5N->?~u9%R8SsvOig;4ci!J7!S z$){gzpFP;N{k^e1yH_DV)1rc3i>zVTiNk@}Mk>;+>j9h+MO21(EJySw5m7hgfEl97ffrdUlG{2xf}>O9SIwS|Ef^5$?08ScV(jNRRQe68DeB_-=CzALyr(j3-g*`Ym|?zSkA9e{HtRHqvQ zXIm_@QW|GKv8UQ`!+jqLDQ>=RGWG7kEw=4DkCl(}vu0d%pELEQ%ELS_$ipet6WkHwq(lIr5Cpo|^ z=ePdaDr(hmgIPq!2a^HZf830gdw!}HSDU?Y+(wij;&%|xnx<-d&EC4pHJMN+yW{Pb zL{7{qGFg`_H4dJ=PcBVCd?i)*-NlX7ilzuLyJY6slz0pAY|dY38+c=tTRzEGWju@c zA&}XgtKdFvdB5XlMSkn13(JeMv~^aFe0K<{6FlwIvAl7Fqyy`ie|zqnDu*2rTHRT3h2d}rGYf}|;pp&z8&q<}IH51}Q29Eid={6){D{0M4r9fmiGy(w%)s=YQ2uvg7Y_PW7lZ-5cxj3l^a zryw<^x(ttpx7ZoNdpjbFJmTHox;tMN(`N-=DlsCvf}PPiX9K}tVm*(-01x7N?bg2Q zO*4ziFbX{+?ZY@imA)Q!hQa>RazY@^yIynYh8w4|D^QwKM5BD+fhdBGVuuxJ>ttMn z%V{}OP5hDAd4U{CmXkhBmGV@tJ-Z*0sL?JJue79~D)Jo0gDIk#jat}m>`g*Dy)Bi& zQn}k{L~b;{*2`2(A+@>G>nIpYMw-fy^4(;#I3@K4i47&RC;W&=Me2tGgs)Mx4@rjz zqTf{+U%gDV!Q9vB6mUg)ts6xy3Vu;Y7k)jW-|DsdS#622-3=aC6U1_pi)EGaBeKdW zUO1Sb4hhNPyAG8*JE6@$rC8@Da!7`;80RRbh-cWDW1J(KvyMQLL?S2i8MmUa&p&oQ zovmgx@h8T_UWJrZaE~;BGEEgWy}T1{;SbzU&va4Fh0;!)iJ2_7$QrwMl?RTTg^Ll% zJ*%n{ukdng0UN&)Av!BWS-F=3Kw04pA%?3t%H&lO9hO&v8ik0l@Clc)XtFGi2$zqf zR;haIIXx=nP-!0MBVhx1dss!$3*ocJ8%e}bK(mU%lK2&n3NPld3SVKALD0dqsD%n# z*)q*TeD>3X5MKr@Ti3q>@}OcV{JVBHLuk(KdstiY!5GOC-v!Ixdd(p7UvI6M%O z*=JYPdzCwj{;kLX`)_#SFi%d;%s>@9tOmz|aA+T>%)pi~lX_8^fhP+u3RtUB$k3TP zKy)y#wg6&5(X>2RUCdFkgBMRjVIqa17KUztqhfuC0l5!1;s@1ToGdK2HT9S0ZIzfMs2Pj!@}(LtO4AYlfVp+ zz8=8{jGJYcYfBWRvsbmU0nHt&GcRqc672vWR~R8TVzkyhxG*rT8`{Z<%k!0XOPZ=u znQJ7sk7nyyOOYg%^)}66XA+JW>jzYJTZdkcd+U}nr{MT}F^Vv%A~9-lF^UQwRMR9r zkQilKkmwL%^qRoRm!Wxp+K{1^s6U#eu4{8L?eP{s9Z-f|4_>LW9&c0Xa^-Qx z0Ag(zKz3AUZUD=Cah(Up4RPHT;<}x|xoeB-9av}nl{X8{Wpvvv*I>y}uMf`Mq z*QEIpt_PInokHT?15MXlEP_^dkW>eN>n_5+xmDONA$vGszpZZOR+*GVS2JsvMG=L} zbCqg#4COI|0rya@4sNa&hpW11YE7TpPq{_+B#1qA$SJcWM|+HWwJ4>NbyNsz=lQS- zuMSds(ZFa`IfOeRk>P8JztMtlQxt~(BxgTs>IW=jwbZ2lMtTqY&dM7_VM5fipvG^^ zq{0~v?+Mooqogb=f8%DSIMztFii7C^DvS&neZ^bFd3n*K*;a96G|BdgdugLr9K^mX zJjpF7vhrjW?0*bYVK(NLC>2mQdcnQ?Q03GE+$iWD=b=I30s6SxHqqTEbtEj48k0mT>S_j@=gh1FA+`x9~Q}w87 zFl)Q;J_f#mBU8>5=R#ZOtswFe<*!GqBFgQE-yA9p)n!_Au|vTD#2g0E!0naG zP=v@$OQ$D3OfXFg5F(Kknr?(G;`7M_hy=iis6M|L;biidM~+~uQcG$D^V*NM`Xh~r zkVvY1A+RLujr#+67$fmZB2 zRZgb-UcYk*V3fTf53fNbfXg)=lTQ%57kBv@Rd66Ut`!l69iog3mKv3$4I;CDi2#(f znN&06(uNEN;DvDZ`XFPsPS4ISP}S@H>9MM^m8$L-!m!e;PAg|MHH|lDxE5ZOQN@R! zxf-KV7-^!@>u`)XS{w**6M@e=G2Jva(|a>}V}idWY$frD|&{2Oi< z#<=TcOmS2(LJ#7R9R)3X3xL{<)sg1Uw~ReOci};k>F5a5@SrP4F%{Xwiq5Iv+#brA zU!?Iios`DgN^88hXuXOw-u50EFIJvW&I}Zyh*#Z_p}R=oRMSR0@nCtmw{8vSvOop+ zz~QR&Gb${Pf_ivzP@tC1M+m8iUg-E>codVa+KD2a zPeXmrrHH!5?r<2_Jd5;BR{FZhRqLD$q!xR9Wn{l)j}-zWix}5l&E8AM>VDF|Hgn`DGQ5&VZ8|CVn}-OdPYJJOTu=vTzHc#Ob$BB{*fP9~m$ za&XB;qi(9}))7kpA*C}9`OZ**7Scw9?d2GkGDwRGKeUVkfzphJ?o?P!mc@PZSlGt# z$Rpv$vpg5(A%l7xQL)d7SY=W$_}X*Yh+`ZNoldy-t~0zr2f}R5lD8@<$HmIHR}MrO zW`xR6Q7(xlU^dn{AFzquhi)gj#%7br&?f;+y(qoaAl}7idK@FbL4{)`&wx8>cM+LM zq_G16ab)3r!<92G@sgvh$@9z%WZI`7wF58SLPQnRo(*P!cUb5Pqi2=@C<E}G3!&cNsfp1C;bKE|{k$25RA!C9f|} zMNiz9x)DDu*0>f$t;#Q4i>l_Xv&6o5FD5RU1zkdAn|(So5xBr>>%eL&+jlrb01~c( zC+8~LCYWk46D#I9HK+3Tn?f`n2LwQOb~Mn2(~gEL9+^^+jZf4vN*|2-bFD(z%Z?j+ zosQF>Te1_merM)TPb&^D!X!0?XS96DHmN5rI?xFlzGReFIctDg#dF-aghXLFScXIH ztpwxc=@2>U7C~&kn;e#tY$U^kNw{V`b2&FT12HVwJ`$>iAxzcAN!1%f)%JxcJJ%>* zO@-S?5~zquq{TC%5zasoQwNGZT@tbzuvG73s%?QmWj~Jkz=}cToT_d(71fha5VCWk z?3-dx_4bPD@47^%D~nHQKwg9D*UG=NGJPT_N+`^ zVx7(^DQ%I>8{+}yq_!tEpeND4-5%S{gu20J1+Bz^S9pQdF$!RBTj44uBL{>x(1wVL zAfsSAaN*mB)quV+H0^+(i%Fq%LY~T(1H6}iX;KE~Eh>YWFmz|f6ez5?w3!I~&U_Z) zBs-><4_zHI&nbdLP07+!dLTSfnt_6R%^-5XwOkJlcBIB3=R87ilMs!952`**TFlZ% zjb>saD2fi6Aw|tzIMtyBF|F@j21&t%o6X0V%yI0XQz0^Yf{`O*s9?Kmid$u})axzQ zh&wIB7;G+=>jr6VxaP{L9N(=U-GB5@Z0`8}9dWPBBJ*0j3KMlGyl_kbV2E-%x+{WC zoZ|+96vv>xLg8$(%gP|F zrD(!u;r^335?(HB=l+z$%SmGroFQ}R+i=mZx21e;OIym!l6WPfJa)MxAcj&XovSf= z`H2*FBC726aAVKRk0c6Cp!|sQC@>q|3ycEY zi0a9JfqXltWh+wwU&7<~g`&^()1S+LMqzceYz4Ryk&t`qASBob+7s2rwV=(aTz?D( z9=anM4&c9~KV`4en>Am7r<$W54dxXJWpO%w7U%-! zbKk?mrwixEz=oIChu}?kTV@YSj<2ZEOx9D)*{sxTsVX%S5D?Z+Blv6(&~~t*b4eDgg178vq{bm8r=3r4f>4+(Dx%|-L79h$&xTh`7NS8m4N?|W zyIZ@!;nKTSNDyQQw*neaqU?@`7&)mt3nrCWi@Zu~=-E2-04aW7fwx;O@1UG4pp}1F zID=c>5Lbeg-k@MMoP1-pZ6e5=oys%xZSICGG*cS8icX z9$tyCO{+ay?yh-|qskOq>$veD-f&SXsJ+}v+PFDyg_D9FMI`LY1iajpJ~`m+p!-U- zS6t@v;ipIhV>0*s<{U&I-%S>~4ni!Ivk#ssD3T4btZ#WcO%OkGpI<&9?ypQl1yaq1 zFcy~E>a9ojk1tLi+&@n;+R*E4!-bM07xzSp9{!4Z$|^%|JzD}4UFshV$ zEB$2!rYuAhZ%1HQc1c<`DgupLvYsgn3co9vd-iNogOW8#n;MlKz-{q~SpfWhd?tlq9)NWbLK^9a~Ds@)|wD$&5T9Hg3l`|LhN!kpKS5qja|O`0bi zz@{0yL=BvvhtT%nTsUBvNvUsCE5$+iqYjF}m7hI2cg`Q)Jw2A;9R&`qUFX&Ky!HDh zw@&Y3X5PF8siRnssO>|+HK^C_py!eAnvHVPbSqWM)DQ;b$Dxc#x<2@i3<8rw{8JVIF(}lC4FPsG`gC`*nEe zkM3T;Nq>B2iCx#r8zdhaZ~eAP^&5sU!uexLA7tjk)00~W2YB@G;e)e_Rh)`)>9Tkj zs?DR&k_=oh?j4=pPu3{6t=_cZMy;5;kST`FI6Aw1e9g~2pZeV1VBQS6V;9d-i~)%7 z)`KTFGu5k>ZU)`z`NPxuPEzvj`jZmQR39GQt~=+a zFRfTECyFe-Z7XwE;n9fovo)@hoq$N4$GEBDbXcYS`1IoR{%t40oRhV)RZm7C^&I)_ z`u*nxJY3(8G)gaf#;EF=ZUu(Xw2dk%OuWTwjX5UrE=~#^P#=z>DzyxDT8PgUXP#^0 z<6Z<5xX5b+lF@RdJOlIE(u_&d+i|hilJ(WhiX1kEn7B@#WXC}Wp26g zWDaK4XBR>LT8_as>9t3J6gHIdvsJUbVI5)T$c&f-8biLW{MkW7mK3mrAeln5iEFqx z;gTDPH{Q6>-VfncKz~A2PTne8*zz{1F>f4?kY5%~Q5@ajsb)w-LLEp#W=cuI6_dwq zDzHA&uoE2$(>I?7tD6tNl*N+6u2k?`#Zs3WMiX+{ud;bs_ST4?-ZWk#hneKY z)zV9xfVLp!0JOODtRrr^HseMd6=^Xb?@Ghj9c;P>7YIt1BGI($=m=AN7MiH@;MT44 z`UP?Y?w=oE){2mL|L2bU!5mv5D6)OTeV z8E5!{zpn$VT&8_jHaWMbIgAOD15&L)Sm@kmh@%p0R3R={VFTH7K9#$5-0=)b4x)(A|JbJ+ zn(7G@DIRo0$9qvit<;QAve@y*{r(&uS^EugzBAf*w5(rtr=z}CnY%N4GB7JqI~up! zJ_TH?_4Li4o`Ky(wkviV8%s35WAYq&(9 z3YIK}1qh-omW{vHsr(5Y0ERL_j&-=xYbeAh{|?g=(m*<+*rv6)0HA#;e=w-N94@4& z15>+u(OJCE2D#{c++k414l-*RM5S`Ws8(Ck`PKAlJTq-v5n4^xtt?1^T|z>M32P09 zq0W*lH2tX4q{2{0uYih%@O*(p3P`MW$tw`k3$izB+2n&Z%NTF=)R;Sn|Cc*Qk6pcq z+7)n>A)3X05K|l<0&GC9TlY+>9gSne>Y^xYy{5gCT&y_2_uH%7xwCAve4rgy)jReN zvSS@R&&va=JRA13N;e+Q{my9CA1ul}I2ob6Df?D@@?YgM#JOZl_w3*4VLAFCYqW7387ZVxtN@d;2!mSFA<}ZmJTXq!JJX zo7sxcQfhLBou;+5*BoFg+n2#C^Rm^8JbN5Bjp3p)XRT3pI=D;<040SCGHjK)4sdu1 zU5S8!YVD`8yMl)*R=iSXXW;e5F}l-kUk1O*F3URX{jv(;=^;@l;b#lqWbQ(T|Hn2e zQF##u4ZU0$P6kY$mzX#4CYPb%_2=!-o=-2$M$asl%dL#j0W~*Sw+jK()`woTQ|Zn+ zkz2j0jHj_E4@mg0C3dxN7h!lcE1UYL z9F^yxHf;?kcacqyvF2Ikuo*LV>KR>kIPXRta4O z)A^u$6%Tq>{Yt3!t8KSz5^FO`Y{r6F7+USIUFq80X6MqMb}K_8n_kaKnxSd4emP>p zcsOab;-ES~@`LRKC9Tyl-FDOu;>%%JjvM7!jAmL*)HOucnxHdlHeGwtF+3|aT0Oh# zg+_$GH1VRi*Eamnn9O_Ld;;4fPBv#Ch`J8f4#;xPasAieS@0zY(F~P zoDCWiZ#2-_RydvLolCoKOq;E7NpE!B#nq(R?c16=jv7-otuFKcmALfYB(}XUSX>R7 zjm8479*?iel~K8*4=ZjZ=<4y+)o^mvuZA;!F{zAvauY#u5+-KaTN?4D*Tke=Iz3`| zXkCoFEVqYpvV4;YaI6|_U(Dy|e9fgOwRPc_6V3?GKAXr*QWU0T_qhF<5{4dV6%HKO_8FKfmvyVfE>$>V3|vK-T&i+wu~LOwSs0LlclGl2Y};>kPBTLs-Vi5;4^3)m!xsCi0ASU!!lsDL4QSh0;-uX*E%DL zNAbKD+~82;Dj1CDhKc9I|FDzoLPaQ>yQ~(nY~R&B#A;Fcs*kcrMXPwyKx42=a_1); zpIogY)Br9>_#s$@*J>j5O+Zc{U{?ICODXbpF6Gk>pyrztPM&-*>se-it+R zzkE;;5h43n1FVQ6aYxp!i+F@JkV>%=V(zjK9NM)O@;nxT#scW1K#7NU8s#Dd=OzbM zS=I9FpraTH(B+-kekeo=;}c{eOowSO4xB;g5XNmkv$QiC6%~zkXk5;1cjOtZ_|m)z z$Bh9L=Oz^f;Cw(K6*r@2xMoYO3PK*?Y(3!LmZQQDYU9cuu4LmjbqPxF_FWucVM?1M zq9+2MBFE4Bhh0PQ+YYl~fv^hLCf4aE3RrqOX@prxO-j4zktw%wEDQ{XI^w))x?|jLt zPvpK-Vrt!*q~0gIa?rxpJax%N@dxEj=>vT?LdJk`J|B2knD*@2Ryj1_)G|mG8z=Oo zSSs%&&s3+Lgf?2x_FfH#|I!*n3Ne27!{@+7#bC%0I0KWYTpCr{tjL)7ik<^sFPv1H z>n#9CMnx0ZHoFptfJK_`uN!5g{UC>4UU7gy0-TNGM^{aKgyPgN7W*5EFM!^SNhv96 z^00kKwuRYVy=Ypz)ih3`@ZXIAo#I)A;LfZ;TI1EEzY*TgC4`D^@~$fSQ3N)g98Vhg zD31XMC@ME<+~Oc3J$A#7dX8A*uaJe*3%; z$j&E>-J{Ikm0S5dMiXS3uJp}lJOf7xNCa6BTI zn-ZD4;IRAHi!R%-kl+2U$jhD zIM7=_k>Wpap3N;+nmu=fL}U2*#1H7fIdpTx8;pC&{`riUO?rgZsHDcOR@qyHq9tv8 zF=V76fj^59SyQDrsH}}Drz_~S`hhO@y5YsHv7;R&rvSZ*)#R*_7^rr=-lcr43ear^ zoYa`Hl)5!`b{&DR8+!BQ?0ZJ*f8Gm7U+rEX_rqGLihBpH4wf>vDQ4yfA@tVPX1cz5Z%iz_Cp$DfGP>JR@P1Uaq0bQ=%#f>HN}z3$zBnF*$WdC0%sUNhI8fe6ab*uks}CGtRcG^NpX@M$>eo2u z-MQSJ`th9=-{1$#cOt?~Qufy>9q$qZ*@IgrK0eEX6g&!b2=It~)lplqd<8pGKwRML z-kU<=0I!l|-R3FsD7}Ch5r+*Id6`adSBds{dX{@_?~lf*CYPHB>C4UM;SnZ#fvafq zqQ>{~@|@Q-Rk010Nw`=AO(gR3l7Pu*Rrv;sTC*y54D1y*u69^r?a*A!OFwkydINzcs+j& zbw zx*H6H>RyPAyXiV2V{BPlH8CjU=GSee)8(yyli92tagE(tCfsR#nQ&LjCfvy*M6|@x znzPHQFgi6}@zsmVCgsX7yRJ zeOXiysIrkE(LE*TdXS7X?I83kzmdPbP@p1Bj{44v*6s%|8mkYF1aoc18>nx7xp15m>;&e=tiEl?y6zTdk)TCZe^MIagFZ@K^B@Z#SJoOuJSanXPYl7%xrU?Sx~pK#BoG(Ooz`JD z#{-WV>8SI0md+blcrQukS(6Og^OjClfw%jf5>@ZrjiK=RPO5Ehs*L+CV0WP{7m!Bg zb+g;PFW;S-`OB5+g34SAnlH-GS0nAlE#q9;0CoWSz8hC`woH0hN}Yd~?C-|@B_t?2 zp5@eTfTx0LUu@KFc;2Z!zZ9?OqtB1>!JR-}L~>Zek|dLMWeEL(I8H^lb=S+F79A?; zU_N=+Z4?WU081AO;V=W(=w`Gh6kfF(hLZ;eI5;+F|< zDt@9xs91`^=q;+$6d}iT&m$byt5kXnrUx(BjSyz;mPR#Q5{fW2U+g2$l@2v)v z_Ai*;thDFT$+RE#%HwvgABJ8)OM zJm}9W`eoZ__^l{l-FeR~nfzo0hLF!d}xXS*~?8if6WOTjNW+I`hnCw~rWAqh$GQ zHjjJ#k>SsKSJu_QYX#L#*O#hUe7a<|nIqaX$R+VAph2qHa;>P9hB9i!UyoW5nj#95 z=i0S+EzUwr0QP-BI*>~KwO~i$<(WysB9=GL`CRxvtNk&gC#po?>1leWtu3_CIbIqh%dMt zR2uyNovz~1%$@jQrDKou>B2P!m!(QDnGPns;lc>776UzKcZ|@ECu~|mQR4F8(rgFg zMzcJejECh;G`4NeqrVK>i|H)5)W`GoxaoC!o$1hPH|6OWd`GG)&0%7fmG>$xx^gAR zu54DY%o}V#rSGj_?XqONbeX$5m}Vf@U*fzHN|thc+bM^4oY>CIZQF{X;Hp_c>E`}mth<-` zaMGU4y^`&+a5RbIMXxt&%zNdgH`2^ze>N%E{w13>&F-}4vEEhKnhYz|ajQ|Dm+Vft zi=x78Y?s@{xDrpRWn<8C+j_|kWB=;Po*Di~?~bOUVbvX7nr4Hf#x?^NlHaE8(DB;i z!R4K^I&@X;fwHKmo8Wox?e%2hv=t*4s9L$;2j-zBx+-|4Y z?rP&Wx~f#CLwgLPQM(7koj0Z~Gp@!)pE1j}nru9owXASZs%q0orKS1pg^tE(9lgN@ z-G1EjL%bG9xVnp~7!J7wHro|j19yb>0!GhWP$_OwAI{NG1>3-HYI600Ue7qj7hafq zQD+vl4?rPZ-g74Ip&C7PY!toq(EVp!jldhU4lFEdMNp}trg5kK;<;8q^JX%=z$_se z)kOo;ROYib2Zihpn}59GGHF({edi0T3msPsxNAmHI1hXtSIqQf?;h&Xvm-zX7Fsnb z3*Ea!hp=+D-C(`39W@r^@}yC!nrtwOy9=W^?RLYyHt_oW^7OJ}<1e!jc$?Sv%&=cE zo4q*;U3c!qrK&Y+Oc$&f_M=H?HnnP3XJx%~0hUy78*sqyPaHYU1R&5CCk+UK&s@aIi^) zW>t{3t__TZA6TAUEmaq$k53g>B!uoO8n@PwFQ4=iJEVKf}TTt{Gm4yd>FndyO((oqG@%pQ1cdaNK z7gXwX1FO>=8Y9b}_NNVv8NI&Qw5p>~$?f#}VaJ$t?5o+dI&4Q~w3t+9Q@i9|wk-o) zNjmy)(W#<@Zxl5xw6`!y?r6fIa50Ey)2Uf)&$X6@!i0W+HqGOnISeP!bUZQi(9nHm zmCJL}6v_;`8NO9sGBdWAwV**z5CrO8rSCbV;$tYyLScuw7s?&%TkcFFq0n}F)%kQb zYG0y}Y{$kuo3lYGWv;j^PzhtrCYza1ZLvlEa;BgTIdR~C-|Lmf7z^}>avj;BfX3YrlY{b zs6)VqGC-)$J9nOTnsybsmJ|MZ!#DS`xbmw z{f=2`c$0}X^(&W_8_n817Bki|8!UF_N^3& zP##gA2sET06tGAkxV_K&{fY})@qQbjRt>AHI%1ktnS0&N)ufBwCy1YJ%w2yB|7bX_ zY6Embhmm;Fs8~}ONrY>@nzbfrhN31i9d@?d-g@R?!n~2#PRWRc94BjbfajOThO_CC z*%q!&Bti1kWz3s5T?yr12pL$ojF%BevhC=r07!f#>p-GLKdOsSf=Vd0o1~T?Sr^uU zwF=9*sU-@%ifePZ=?dsHw5B$ST$9_^PgkgEw>UcD;!&_Y1P>Wqc#Ar!@p~wdOCea3fPspvZvpy0brK77aakX-p6v{; zf^F;yJZZGbLqTTn7O#*V{bjkGZD%HR7>)YGQZaw(EK+;q><+?oA`M#VWiS(caIV8D z!71z2Px)+Fft|1!r($U?}kEg*ALyZKjAs2(xY%pw>2iY!An{|HCO${G#pP zWE`CYg%NmGxU?HA%Y<0<8H&jGwX;3YN&(4>HAvzjM4gPqx^~#(_OuX9%(g@|2kkV` zr;YZzjhzd4FngVL?(5;klP*eLJ- zpjff!eMpM83eK0_8#uM&*{`Gb#U9&AA31w#_Q)@;f8rZkscDb1d5D@_@|bV4eoNO| zXmdD?;_|2!H>-;o`K`EjG8cU%gBLac-r>?5B0g}0FzLXD`BO(=4bQIK&Lc!*vid-l zCID1!Q8h3{PraL)0Juo;%ou%@tE15F>E)@`xO5}EQ*|c*kX@eo4a-KJM8oS>n}eAL z|J!Ao;V*N0(!Xr=yZwoVaK=R^gjLwPic4P0h^7?-8Ml@`D_5*m1^qr3<(a4(j%zjj zr{D-1Amv6S!a%Yv9Q8Uag(oA~%Ph3HO~p_4X}V5@E{3yV8EExA}6c!TO}()Smk z+l}I}r&(2FJ}kS9R>_++{b{eOEt(^JKB+V+fz@6t$`!5TEy|5XmtC2ah0*P`?M9_J zoXo2*Wc#esyV9dx<1)DNFX8Dd&(QxnmL%C9muvsF*#3CDfza(@SAbp ztZ3uutm`-Q$c9^?J1+T+W^dAFqpN{E?Kaso(5-3Lz3MhfK6p_y^t!{&tlCC&tfu!u zs}EgHnzm|OBh1(+^})E`*D73nftRn-5vF$#j8EZx9y!7)zG!?e0!f@^UlxF>9Qp1L*Oe85 zrP(6+@J+-9bV89)h~{iT6p4BqBtZHj^MwdrlO*zF6cq!Cl!&}FXJ*73o+CYwe*i9; zZJWBmcA%9bhZzbn-_5Q5NZtZD{0Lyh+v2gpdHZ@o%Z7j>OMY?+tSr`xnARQ6;Z?9; zi8h7s^*c@04jLZoN0#l4T6P?bC*ye9E&1I*Yj}3pn)&^K+if9+#~xj^rZXu!VkUy& zNqVG_dm}5Ahs=;tNm>KCNbFTj8&?ZvBOGTG%@YqRbpTF`oy_2&I^4-C#tQ9co*3MAPZLgh~R14$~tFLJmSb8_qB7fjT6RlHmn{6wq4lfrAc8R~lO?xrZ zIzg+|iHFgm+%%2(*l(5m%TTvEVN171eQnX1%!gXgwNoRZ9M(5l3M+uvutT?8&9o9(O90UNZ?-)*%Lz`h>uOxWlmk(IynvP5hDk8tl9? zM7%&a9{4fEh@iZ`__N@$+c|{4Xj@vi&=BO&Wz%xYE;lcyon|~O1#Z}Ev+2BFLGgL7 z+?li%mjjJm+NA)l@VIG2O#~-4Bf)0sat{YxrP-;KvO)R=lB6?7qoi8 zGgX)QRZ9zOME1INHF=8b9`PiPkk-><67m^ENe&Jl*-RWt_%#LMAIE;sd%ij~M)S)f zT;MiWgiu?29?^0S;TeGT0$(uwz;jR{RQW_wA}=Q(Mk$y(h8+}v-GT&SZO1o&{Na}d z)2>jm5N4L2-*ttET}xJ2UajTe`($Engv-XG*x1s64BO#uqFqnS1-hbA<}#d1idbIVz_3`oPn5>PqG7lV&#S#cRWW5 zqj! zt;sXZHOIOI8o$LOev-7>;!|dbUtB-t>gt$jQJn0!l$LiFL{xAF9ou}lNFlMwACI`E zh==b|VE<|hiVcw7$~OTO6HdfqqH8fX*E#v0a$jM3xEzY}qa^(8YAn}*;rc`|fV-6C zRiCw8Xx;%&vQVvz%seMSW85NxkJvmMoekOm(5Wyy{$)Jw=x=F^0FJQITp& z5s#6zV&Ykpu);-nZ4neB--uu?eHM1=55`9FbEu3%$q5lkPmaD$z zR&>KC+jcd1L|k2n%(PqnRi_tQvlD6$wf50UXFz>a0>cYYSwrT~_1%%z=3o#Y*zMCc z&;u6KK$J?@=abCz0}tI>5TX-1O(a(D{cdnUB=e1;K;)}QXKKEh#K zIMw>v?;J1nMQ5nL#LX78Cm2elJB-MH2RDuCWY|LUmYCj z)ZTJ3B^O;5BJCOGnPsn0uK3Net6N%A9zVegCHW$!flS*Gau1&I+y=~UO)IdBpow4j zgAC~$Obj4DgH{B4w?N#9M2yn9SqAb(wY5_l#b5 zTJdJ1mKrry0Le!m`HVPR$GLTObgzz}ffGj>3zWnu)98qvAl4pmBhgtgQPSB^I#4E0Yru)(jI@PpEO&ZDIH`c$eT%HmX^08&+)`W0A}7F-npa2pzJR#(`G!%$hOTzX!|2nl0Z zMA?M4)(T9XNz&ySOcq&Cn`RAJC`RXc2$LUL#I^pVHdcX>K16^3R!aQ;9KY4*7fuv# zila2_=CbR=j*H+;m-MaANSPo6f+NK)X-HBQ3-)|3-lfE+wSS4v=a5(<%T(fmi>=$x z(S>0i5+@P>z#zF$lX6IC*@6EF$Ye=ty+{FDFHcb4n7CY!-M_0;q7y2PHh(LR&QG<= z$~4Nlo61YzM}Iffn4@srL=$?7Khx5-gUr!2UbM{%0mAZAQ$tg|8}P9ocXK zvY80Yl8}%}z$;Ko>ITH;fQ4sVQ4r8cLvuZygr2}BDys$vCbv1r%O?NRx)}w!R<8$l zB-3D>2oq}!?#i;=aS=N4Lrz3XdSTY69R;p$5^?Z;e&Bp}>E}Cv;8==n8=KWu>P8Z> z!36}RiIdsGf@Jc;KDNV7!3iOu@EcEGC2n7?X*-BTi6a808NGf(qkw`K_|>=%>ZYJ! zo$((X`dL`^QMQHF>BJuF1YUcGbD}h5Y)PeCh*zIeg%D+ghJz~i(7?!plzr51 zT{y8_yh)CebcpJ@6LgV5d7Ylg>pbw+0Aq2XxlMH+3>eh-J;$M35GaQZXfEgvM{{2D zr8!2$VT`~`8(>vMi{XTLliXno({(_98`FMPVZozl_lT@y*3Lt0!x zj*4J1u7%)+y{bhhc*$YZl0pRF_^3MmJub_Ir~$QQ$2$*hj^#3R5TQJBuC64l6=P*( zH|j%~BB_~TuFRf90kn%Cf>iNe z(;OKj`UO%buxQy!Y?n~}7t}(&d=Me8ok=0D)-*^Ivr0FlQbZ#D0oLK+Jm3lp*{AUL zwfyfLWRMawHu;-mw|NM@(mm}{sgf20dY4@1$0)QXX38jg>nh1v0FJU|(?kG%)EPSQ z82MT=CoHr+O6HP%$3`C#4g`e-kl>fbsN}X}J>XJCAgjs%Sad#g+qiQ;xws}cHv}Wo z$lHu7OUEBW@vn&r>oVGHHY7w>}W%^3howK-YT4uQ06 zqyFm-@8xDy72;+M227X&c=`&8JnoQ)B?FJT97)ymx)C#PHprQos$Q39YpV|Ia3gG1 zuVYWXdfhp^IJlZE9A3OG`MHi7b#+RINl9};=)GmQ@P%Y0vWPQgMk7QdNkAEQ z*P1l~R@ZITh+Vmr7Vh9iAe$jIEKuzax_X!COHI&$1NJ&*Dj-t6TMbvV;rx2D$_ucH zp{w({a!+!ZzO<3;1tKas%soU=-W3+k27n-FjZyv)ej@srdsWcd2}FC_(UH}fD&t6c z(u(aO1THsS}LmWEs@$Ga)aSGY4d$5&uTB5j2K zkbguJ7T?2bylWay{g&3*jFw4Pg}_E9 z9&V}I0a((>l$j{uP9n;|MAcN>c6Qvi0VD~F-VN^mq6EH@gKhd%(`SM@)2U|gJFe~8 zXF~JCzMmTmw}DVK)?3C7)L0v~!y0>0m?$2{i>-n~-0dh1rbuuY;u-_bE3Pv|;3knJ z65MmqR@^M@i}0LZt5LH9mEY-tz02I&1W^)>0{~Jgbt<66SapfA+b-Gw?9_cJi6Ifk zofyw~@X=Oab*FYcLh@oUVFZDsch+|;K2q+Hw)U}!C2avlNe1|h1W36Ge2wE`w zC>$Cm^o4#Z_PZ0EEtC0k*0`>IWF;5ObpFIe9t6TE!~|*iNS%~<+juOR`~#Gue*(*wTb~M4ggvrW6xj3drFzlnOlY)xm@B5jWlEF zS=u*p4*32=Ls*g`ff5m9z{PV7gk z8qO+AU9(gbA19WSLvir2A>V)yT;H_f7vZrMRHs2vD174K>f(wNu z{>E2;tU_+plLo!Q*VEev9H>gLZmI_f7799FHWApDVWUlQKyWR37~tQGO*;K7!V$ee4Bv<}|wJQuw_BB`5~4v+h?M1g-0O z04FcPjS#uu)5=9i1|r`Ep(B%fS^QtMFx0WwkKou+?^HF0S5!F33 z;_-H&CH0=OS71RhV24>lpD6OWXL;8h*cIuT%H@fAx6-D80<~1GWUOm9yA%?FQLKdm z(reCc;8()2#NsjvLo;56dvWUzJ%&)1bA57yzVRq9;6II=yb&krM zL4&NP5~bK|S5UeXcQQDpdOji4>z-2xqlAMbI*A*u!8WAYui+Zf1A9v6A~hX&2mAFN zW}NBW??%&a#-c=UGF2Ui`6 zX%^^8C0NXUQfORc4VunW0!sKDs=2m_fv6OOB3kdrQD6cE;7fb0cTnHilMU8%OAr9K z4XP<|hCqyXwfV~4r#i1iL6kM{Y9%M``orkO9%Op4^SR~8F+*+}cJ_SCBto%J@G;u3 zm*aP|4St77c%?#6OFcPZ18lamOmwZ}^%jgS6k0UPT*u@=!%|XkG|sfTqoizZ`zBrl z4XQbycYB#+K`E-!vRPiwWe|;wOJK& znY9g4uY5dOGo6;q2KLQS;TYSZ~T zT3k(svC}H1ZgCqpVXnbz*K)#OKCBAkrBvA@Ibobl8T^Bno|XvqtUxzeYGhXJrz+8z ztX351;PMpTn!!f#?ej`_s;H>8=JFK$d@v`+YXy0t4S7f5E=$m&l0;}oQIwi(;`hWJ z!mJ?QFZTfN=_l3TOfNuE4de5^0<|9CwGJR3H>q{7LI78y=k7g1dPFEf-Yg9CMrl4V zvm?CE%Dc<+a{Z9x-8q>7i1TnV7%$@yT=uD+na}iI)88Wt&!Kn+Q|53 zd1;ry8mZ7b^VcEt6odF5RR>|gA zHNVbgc6y9wVL929prBhw39=KiRw~d6o4kNJ(n7HVLp1RIEAmKug-Yab99H2G$4A-z?U6=$D=8xN2@Ca>NW$Iax#G|vT}vU{Vdem~VG5gejI3WcjhgkoBUA`n zHeP=hh$xYjT(Nog;-q+ zc@zd91w_^YYI5GaP_!V7O!7WtUdVIYSxX8&$)dj)DP(J>D*$`zGfQ@KS$VgxK8q;T z#$RH_?~}(&J?^8;;Z=KYI7Ga=-6--_vfgO|R8q!##Tmv}U~RyF(3*~*3v3@&|G+7u z_-R|w9dw-PeFw(hj1S{}#@FVXo*XOIDu4C_gGWZnXR~;Swak{g%I1-}Bu!sd>S`i8 z%f!2_YiqYidw^*o5HF&5K>O6*yp`;zN|bjPzvA^I`6MakUWauWyZc(KIX~ffX10Kz z0DL(?yUO4vpk4PX0AL-XxfRI-iJqSv3+t`oC-gG-33xB~3H4}5QNbxzYv51^uQGwH zvLbt=Qe)W^jYWC`)Kdg&#JNm<>@*gZ3q2OxhS;C|xK{rXX`<+!C6@;mB)y|nX0<%u z*9mzQ)$Q_PBjl4@LpSZcu7@KMduS~4;#_|QsV+m76ae9v9)4;4=$bOny9t{+y^m0& zPa5}JHFf_ZY43WdtfF&*xBVCIcfv*>yN!7xxojaPBIm-Yqph+Vshn$V64XJP9%*B7 zwiuS_6bi5HzgWw3%kH1|;GODJyIeUh>U;YQbZU~#TeEh>smnw`ninjf(a1^>6K=Q? z(rKEzQyjOZ!93OUp4vI*3}~rF>4XY9G5SWySZ%N7?Wfwa*?z90INaU4ZR}>uL)r7@ zJiPeCnUz{+_C4G70s3u#p3|nI0!nD(h<@31V&BwyRU=&Y0#Hgsg%Fj_&(3q0)v}eT z&Vk3T$lbrU26xZhyPi;V@x9cNGTABwz1^EEg5Gkg(=6y32dtCz>6?b%T#IbcM%*^1 z)->Wqa;mEAb&qE_pSuN>^#aaTB7Ne&^6!BxqgRa%nK&oC&%CawmyhC^(tH$XKC!L~Nuy#n(4mV^EwSol~C1eHs&gzMIUg>$70xze*`BfzQR?4< z1vN&Fymwf*bm~k)o z+&Yge@|G)0D!mrjfX^WJ(tca|NdcG>^$2Np;qfHrDqW$NQP#>T1rC( zwbuf#Dl^7)wC+meEfp{^$m(G8Ac?j1J|fp)GmX7AVHu;+Cf}xHwM3-3w|3pwL5;AZGkoRZ{v;m0^ zmE|(6QxU%>Jq9uB6zSO=1fE1 zwWunX>U`dgf4@SRZ>}wom+|?$Ix!s8T;VDtRCw7I{gJe|t)y;u8^c3RD^=3cIf!-q zk`BscvjP~pKu#N7|4d$*wuS05ga5nbwx?r@_^7@*$Q`LW4X1 zu;fIL$_dbpO<(pf1zjlFcWc!x)$Lg{($;j__a<4Eyd_Q@7s8?QdATUqYLayOsv_t1 zyNyzAr6Q?r5p^4v&|&HaEvYp@i>C3SL5rqDM@t((8{Jkx#oZm2ukP^kyR3`S7?U0n z5J!$+6zx1E{yN-eyYi7y^ZMyIldR^F3Ciog)=Q*J=-=^W|k|@~kd~A<~)A%b}5)2WjPw z-9?Nkv)hv-#^(i%wSgTfLdEavl5iZLZxkYqof6NNsQXzoLDEc;vx{?$666(%&kEWIIs<^PAbcTC1kau~d(l@pW?}YxVzo`>V(4aEht&Gld*mR>|OT0M@MpJxEkvxYZNDv*^Hlt{;PNX&hFNYZ((pAJ97!)e6C58#bb+ zVWRWSKouHw%6C&S=w2tItk=(G#ayNKc17ON_6^prVX9GSjeWNE$tXs!Kl6UFLa7xf zjs2C5fLl6ZV~c~2+Y-i#nh1K5hKGL2+PpT zGp+B1Dq}f0imT1YOc2%9$aM8G98cWFzs`Qi8U*k9gFdQF zR*7Qw8l?uWT>)aryhy2;#Yn;s6bK+35BF@SpNt?_jFp$S@mC=I28_hi<}we)=$LTth_d}?rx zi@Qa7vSnyV7puuO^8sntMxQ()eXJ2_K!?7J`WF(0#(SwIJM4WK9`=F}oPoFadki=^ zHc#sX_x|wpeaP^&ae%YZ4v&~r&;dTAzB&!P{=~wyDCSL26`9Li-3B%b`eL{hp+7ts z%BR*kkroy$Cox*rpv~tF9IQBMI1=0jsSK~@ zCwSn@t&8F}>!iX8>_QD(9oMd$r{`wP;7qTA3ESK@_3f*?P1%U|rul*n>k-zMfJk*N zmN849phI+?;cclvPm-4O;2j=(h*Z_*!moWtbKAgE*YEiXA+A~$voolQQ3LLrjL~WMjkcgW_=s+Y+sLv-H@digjb!j> za=B=R!K(!7ca2tOlkRFV>7rlTy%G#TlswmgVNBW5eV8cLK0|l!;dX8>cx|j)0a~w9 zadq-nN$6~KYS%B|rZ^Cf7e?Jt&exa{7DXl?T<(a_) z^E^bOMIJU>Wb+i9t#z173o>poxY8!e%w-s&i)(|fAwpX#gKRSY8VHQFYXbsMOaYe; zRjHNDP}6W*wBOx5MtO+z8f+SPV1(gn29RVk0qZqDz<%7nqbypzDmMwloyF6#EuZz& zD&IpBAGk)|%p)|p$6!yQ{v7J;+^lq+8<%DWz(wp?tAbmZJ>f9P`WSkX1Wv5$z3gmv zVGV;3c2pDS$S2&xU(uS>Eh+PAx!b*5MfAf$1tXS%|B*M^Zt6DuYn8+=*6Eb=Kd&J| zjQB|Urf|qbvKrC6?z}mg^S4T6!*F<64mxR9x5US%z|o!i9kr z$#_AsQZCoUwo0nKuz1ycQbkb6TETLH>!TYp!nP0d9Me>IECJTIqt03Uu|#I3!CJ`7 zV~O0_VC}Y39A@deC`?y^f9$=aA=Vbri@T48@J-Ya4;0xtRTd=4%K6PrY%zZEf^Wsqe1r~YS15$y4{*+ zfjS1?o_#u+OHg~Q2=Qv#v62IAeefGmpKZYH0Sa$3K_=S<^4erZRdkbK^p?|0y@a5+ZIwrj$OsCS<9(CZgkYn8Dei#anx(I zP4$cS@x19a*ngSOO47OWy(mky>Opf_r($k4dU6v!n@e5U?2slzRieGNqsdJNeJ^93 zLGO%Y-36W?e*9rHYaoJaM9|i9hTwe^BBXf{UW~@*@tt^+Y9(ai#=bw;O&rvVN@Wu6&qaQ_7lU0re>=0#CxnRz>>Q2|G;Il5>?@F`xp|%`(VBwfG}#Q`wBQ zJ?RxAJ$(KOh`XPK&yM$w4#Ll#9*b`2C+FrNt6U~Z<(e33GSXZ|UYM_3wuwxhW#}sd z*rKb>0eF`H((--X$WmLT$~g+d;zZ0v5x$=2LO$>EDxV@-bKT9Z+a)g6-P@=XnoI?r8!khG{l>*c0WBCF+$m#WYQ|HE1hmL z)4I1HiHh(Zt?7p*i`uNgouBXK#F`Go%L}tI2^26M144a$$8$4Cq6kyD9j=TvwC6XT z8QQxxQ~kiu9`rS0^t#;8!hXRhhk_Oar_me;4Cv&IKJO^cL^65pdVcbRjsSgg0hM*b zN>P@$*|@4{u!Nf94^hY;qWQYH9Gmnp)jlaRqc9(?#e*!Y1R{T}tg8w-PJN>#hy>d= z<6*<@m+_qtLFx7uBFOD=-_sH%Qs4x-f-a!%>nE?Q`pot!g#(O{NtH;Y^PF}f5&oHK z!*yow_Az?C!y#Zcl*_xCh$HW?tb7sApu5G=ibmzM1|1#!YNVGzbWuNbtMKp2t7YO6 zSZj9oRdqnv;>2%WKq>~KvG%NtCvG5ZK%9M@!O<3 zucuDUGZY;7A1~vp5H688Ec9@fCn9Bx?LyErtUm^@PitH&NiZiC*T%cg@@|E7V$U2a zvj0j!N35P8ti`9jv3@~|%6mZ&COKmDI@qUJ@0Go{#)plR{ltU2mg6DEYDfWt|nAtT>K_`fBj1T5ci%3%u{a_~o^kooGc5SL#)B zysav=6@F^#rX{z5_P3L%bTlqo!=Rg`*d%`5HXMqm4L4~zb-daH?>}Km@YkY2*6WW= zc``5|Enb0bpjNueWaSzfz#VQvNaYTLW3{VNCZIe!&?X?}`dGEW{E@qEyI3{YYE{e# zh%yL>JrF5Ry`nlr zAY08w__7`!uzy!axw7&l1Iy`nQY9U9hI2GCm1^c|g1FXdcgnB$-0rS4K01m#Ux9S{ z;fdp$qMR@QYn2h)>;P!cZ>+<^(pEa^YJ7ItD5em5` zKzNBo=S)C#gfmHhH@J!yz~f%Igz5C03DKdV440u`{}ayI2-+57=WUK;^>I zoWx$vs)-fP!!dGQ?F_T7njL8Ft=Vyn(aYi1nbC47s6U6 zE9hk4vAdSXvfr1N#JF{=Ojc3(7|H8F_c%g_={QM!54JKLn#1${ppoC5qxN|8GJ${w zSz1aItlD=no*O2N)ok``>3x&EeiXBCxNN<1@he*`Z8H*bA#@+5XWSl%$3ym%ZyIw+ zAH86kiD;WcqJ4*4tp>ZHgs76>abQ!2!&^p?LxJ_T&tBXb{s?uw##y%|cE{8HP@=dQ zGo|&SY)2TCi?ycQ5L=;Sdz7MtBe7<>WEv#!A5g@+`&2$1@B=GSS-7$*wBLZ}6GcQC zK}n+oEnGpyP*?VX%OAGcVGC{^)sT=pa|_+za59PJ!4| z!^>>MR_cxkMp3sRG|*F(IGzpixrrxv{M<4IkyrSPGeQw|-zTL6txbcld*Q7m;I(^$ zv@nJt@@%#f8sip=3j$ne&0HqdBlOzORTdqY?wg!!mzq7?Z?$d(ZGcA`^L{_x`TVSX z`zD$XzJ~|899@q`2+j3}TmAF%d2|!B_T+UW;X_OaIuWF}57(MD*VVp2W57WO7Dq3T z_yTtdooqEcSg3W|H0IQ!-E9J<2&)sAqpZ*p)1xHx)b1JxM4-hR@Wc2DCOj~s0HUDn z0&iLyY(F-;|IYK5hiHh}dJ=4J0AQg%Uf_tqhwRs4<@f~b?yL{^kq;0ANJPgwUXoc5 z?#U(3l*HMC7FdSemM8=#VodfMd0a`(lu%`n>7)Y*+AW7-7H9&M~ItkhXa6XD<0YlJdZ}G19Zjbl~_KGfXfd|83Fr9T636ggkK-svtjxTeI z8QqN**)On8gWF;D8xo5}BXw^hf*n5D-DlG|JW3FJtkxus?sya{QG2;-W`XrB#MNXn zU4Wo(fm^DExJ;IF&&;WT(uP2cv=F}p$x_|}Z{&J84ner{i}CbUE++jD>~s~$jT(q= z0Wjm8G*pQ4;SHddEJx1#l;9C-Wx-5A(yQYp@V$44N=jHaC~wl&m9B=xOx@c64oPV3m6uD4%yXeV>TKB zAsN6#x06}&Fzwx{b)!J-4p9HTfGcZ?b;3LNN#T4oXSud*y40P|UcY{^fx@cfW?*k2 zi0quZ6+RgFGe_yPwmW9g39#*xH&%O6(`QSyCtU^p>kcJoQIA8-h65Y7>1q9z)S@p1I&;8ExeKY}l(yp2)6~U&5<(BEc4ZZR% zyy%ZA^D0GW3DEfh!oki8YQ&>hLfj}2f46(vS=@iv13U&vbAY08M8Pk|5$4iR>^Th-Ls8`wvx2<*SCNHN_g3-!n+^RI@10@ z0qTiCq2?o365384P06&mNT+gm5B(lYey=G${`{~9wKVy$Gl<6H&OZJ*y}w6VApQWO zd<=fwsQgB^XY1MU<7gaj-90_P_>l@W`YN2>jfTh^fsD*7Tp;=)&!5-0Z^COO=K1P zyvo_>rU>IJBua`GP6QdvI!qb5EsE+yy}zJtZ+>-vpA&K@5?1ECmI2WTfd=|vW7Y>8 zSn*jom%4ErhHI=&Z%R|ErHUOy&~!cwZ$?w>?{qnjM91L_x(m3FQoFm^0c@P~~7B4%;QOx;@6Y*W5QUxtAg;9SJijElsa9-lm331an&|d4ya958QE$)NX%h6&n zj`W~|1GDhR%Tr*eLOF(z0YGEW3#v?xd@hE@R+u_Lscduf!#IKYp zbyA7I#sSbRjfZrDJ*$BK;Xa&?R1<5bg{-i9(U%nmis2)&wH&`Gt|xU(6JBSeWO*0t zGU5)y2Dvc|x>IqWdnaKq9=4s){SwJ(^_x@WbM9Ji84sy)<-^Iv^tC_>W8J?%f~fH7 zJ&fTpiuDJB$Xu!7^iX@G(v(wGA*Cs3TW=ge3+!8oGt|k|da+u;aL(eJQUPczhB1cc zSPVk>Mnb_phTm5_c`2Ff7|OO@k>X-kY+i`kP2Ih(uz5>l6L&_I$j$9opI^OOS~r`r zox6ua?7>y8zM^@mmo7ecYD@*4#Eb~plK%Xtj% zhs;yKVXP8?i-b7@Yz~B57i?&LPnvG;!^p^VrMZ{p_H?F+fiK6=EPfGgTukSAZN`KK zu%%C-l^;jrUY|Y_i4R~)o1c&Sqc}Ra8$>huJkp;dHi$43G!Yi+PI-suMjR#}P4?z| z$Nx%cr#rmu&o8=9`LB}9%UY6oRZcQ+>oNc-HinmE=>4KSq=PE5UFsI7Rg-wAJVL%O zlF#87_|<6^lHh5kEV->aYfy>c`@gt=hkdwkisN44MR)+9rGmV;m&x)kPsYYcqOePZ_O_9fz|M@a>>+bg*CJBi)CxIo&88y^uZs@h z(OnlwACUqSFcruYy`jTShV*)G>|8g4qergA`>B!BT1V}Y2vwOaY-mJff|kB&djd0I z9@5Or*OvM-5fdIDy=1bSnHgG15ZG-~CI@I`2i<3YpE)42((rsr3@+hr%%<_^PD8o^ zTZ4pnvYa@CD{gQFi=xrP?dZcz5Zz@UU$qxlOstr!QS*SA{#Epn0UG!Rt`~blSRm2- zbEQ9LuX7DjZwaCaC@&~t)`0Puxi$OUM^?>({|KL$OR<-RJAG;VAwUW4^$a1o$s*o{ z*`MqIT{52RJWR$0LfDDuie0JpKyk)Lc6qfigQOEE}XeYH)f^R*pNQb zC^MWibzuAD=zQLv-=}*MX6QNtIMt2fGK30S`HkOJPRHWTu^HMLX{jAiAp2)|+`eGAftPS4gh|%K zcv1cfFEm4I+w`!&+bIws(H+>UnM-+E_ZqhB2i1y5?Y%kxbcdW<=9<{J7I%;{!c{m1 zdhe2BU^PB3~5EOshTDs8Sq<^0>2jXvB;oIOv|~iq};bQjjkp<`9rmM|qV7 zjlj;zJwt{MR6tcyRLKot<|L!Fw?Y>!Xg~AOqL|Rzo1))*wyjnIwwqr*)^^>8XX}AD*hr!b2*OXDdxa zWcw-$;s_m{v<}tiqZVy|jE}sV_{cRPkS?h@$wd}=hMl4Cc8kur3LdTwNy#V*oB(QM^1*lYvvld0pd=A6PYoo0`2-H7d6Eo(D= zj;eiqB%9NvIctRkn*eF+X^t>Js;THM)>m}P_^|l%;rWz=o@8oV8?RB^G5Bh2)@uTX zSS<6T$#yfT_GEyr38UGZ!H&kJP3@BCF<&162ACVQx$>}#h8AJ0(&1oyCHZi}9Weq>N)@tIKY1_zbyM=gbxVnBgKl|+Bm5v)}(z?K`- zn2}V-eCN?511UFvFn0Gl-CGzH^liAMpb=Z@aM)8*pTqif2L{z7i47ribohq34-yvY zJZU{)C>R0(cItMbF&CwePs`s9yOQ?T>e5FcEtta@s+%Dc&)KmO{;6H&ilxrXGh+!m z(?F$%T0ZRHqT}u;*4#ZmuP9q}(#R*t&jq@Pbo&mrm>ZRV^B%<4$&u|lC^ z>-9Wl?gir+N^P(4xx>1Mjd2`BAGSIP$7SUc zRvF2aicjoa0(e0AA0w8Du`2uLfXPBHD3iI>vIc5Z#uDx*5#DvPt-p>4Z$8`FMtMz; zS0-Rha_s7rnAb zqP(t`I)iP5@mQrrb3J3i^6@;UWNd@PzlbW&e90%mRGE^`G`jv0SzyclWyEU6(Zy)+ z6oKO#XYp3$mr^-F$}kmC$y0QRVS_wiIw($U ziPT4-m67^t(%1;2wPdNlVV|Q>Y;r-Pz$pTRXYxDeuzQsU_uv#dF*VPKw_@!a9$(Qt zH)TX}wV->7xFnMA4-NF5Y|(Z6X0mnto>6i6;dbbHLcd>(hFgqEN@rPU7iKHkf^aXb za@g0ngwKLkdMz?_?nM-jjixBRnZfmM{rqdhOacwpg$$ke_NY9-8ZJ_9v*NIQ7%~}a z6X^qFm1zd9cee6nle?0L`4WYSnlodrpc#xDgUfJ#2x>qoI3!D0r2lOnJ^(k`)3B#K zLj24Z-Z%%kaI(>i+(yJm|MRQ>7{6vY%x^)W9}<0l7_zr7@lC6BOB8v{NnzJ#vJZHA z%j3BvZYv)FsI{i0*0`>Z?TFtAs)vohy6dNnBdjbW*Bpw1|5W#iLp18v&XijOcv$Uc z)j2`_5-tF{wQ~abb%2(6%5-szy@N8p$Xsp39c;)lndM79{m+%|z3Inz*ZH zIWqcW-q)<$1+^$Sh_v?yy(6?(?u1E+mpBd)YD2D023jZ4q$-Nu)HF`bE?v`-u(I7V z%&$+Aij>N{bkoEKK^Pc!37A#HOIB095>=+GM#tirVp6=@eFH(SO90y><>d1$;Cb_ZzHYrMmw-e8UfBj|2GRxFm zR|@aC3fh+X!OaUoqlmroS-~Ijzy)dQGk(e{XTmw)CQJJO-Xm!>+ z=QZ8HZ>j65JEX?&@eV+=kUz4BwK#r>ei#Pv30y0i1ZNPRj`ri$$HUK~&FwEwmUt-C zZbtJKwguGmJ-%f{Pp z)GU`%2%xe^s}2*P9?wi%#DV(h*eyi17tpzE+6^FqYPDDF%VNo|cf?$%f=IUU{o>r^ zwfzp=1=yprD4)5VxBQb#m9*u5At{Dev)VVFDk>G`rh zAMT<9DGElx%XQz!TZUb_EZeAcXi7mZrNgqm#@dT4R0WuZV3L-)z}%fiqHbLbZP=rQ zl2I(f(?WQVHQgVS z0A5$)$_?MFhzvMLA}D6?|3?=iE!`2gTab%KQc>0-x@Q1qkZZs!UrF%gx|JpFy-n!9 z(Yk)EfXb;)9peY|RqMsOgP;^tdCj0gk`MtARmuuVY^T9R-aw-9%(Zc+(n8qt>>X{`HB0m?XDcUj7P<~`1i&oXTmtD~Qd1vXCe*4zoHAuXl~{BB z6`(gG%6Ku>@U;S@mac!@fzQ3{KK0XE@X?B0o-tjA&JxA{JI`Of?(Mw{-|W3Q+zitXZl+S6Yw(hCj72u}YUs9Hb3S2- zzFHSNObSs5)S$QD^HQ75Ku_BG3i-(?HH)PT-efuwr&TA{<>|`eS{>_OmguU*854ll z6fB@%MkTz;yVyQ%m6SLpZ%)f5vZ9nZ*zwmg=b;7t6_7n@^^K9g+8r19E9f_a{=#t& zK`!E)`n##IEurv%Ab5t=8l8^_;IW%5oFMAKB=qHxs)p%ZFY67Z9Zf#t*_LwoJYI&*zZ0CLnd;Sist~J>Z~1JM1TfHPmwfE!1QPbjLi5(ihIyz_;d&- zX~wV;2NXf?qg7unD%%ER@=;I0bIq_%qQ&#$TzKOwW;IVGo3T!2G%tEOnxDW<=p*K* zr#Sr0vkrLPMAz}GoKMqfWS7A$j2_+0Rz5+QB*W1SY%?T6U@*i1XFwnSXu7i+PKjTm zL^G{RU~g5)3vjeD0B|!st{fqwPczk0I9mwl(jEl`tGm#dz*(?VAO(;VtnRgtR2vtA z$)Zq5E_3%qLNTtR&XWq1xKqE5REWGgwRt*38FNjPGf+MAeA(CvIp)jRLvvElvuqO1 zAY~#DM%d2#Sg)58IP4N`X%eEn`=6jvsJ~WFDX^cag7>C8KO3A{hg(&uYZ8vwK3UEP zz5mc1w|=Fr8jeEAKKYZTM1V`Mw;A{zv56pTr4It?g-{zQ1%ME?C>)qd$-qD_7tv1o|O;@qY2tM#Wz!oj`kfyEs2ra`W zfC553p-z6d&}jr1Fg2Z}HJS~sSehYpLmfq`V_6sbG$AF2#pRsR+WK^PFf$FN^-2Jm zG)L^MdZ&+eQw36qmolg zc7m!K7VV*nio7{?Zvyv_$!ri`yf2~Qtjs8;6F_=(bXTbt)lh{yH7KWQY456jJQ$5k z>y>$b2=qxixuQ9ECMUaFb08sc?DOE$kn~9#cTnIX0V|kWte;b3c2=}FbR=uvd#v){I>X`8Dz zbm{I&`jNE4utL)zTEp3WMIv6Nly07u$%3^iF?8c|+o95ob0srQd6M6aB^8)?d!|;5 zAn?H_*gYG(n4G?uy}jQ-MTWGSl4v>7WnjD_u|ivwBFXd^UmQml0dl7Bi(r=_dGr_2p-?4q9l~C?CkkW-71_9jXZYq zHBHKykMES|cFJqv9yTl696z?;BRP^D@xD7-?F|nBohjFg9=;O0F-mtu)9S3Nl?tp% zZ>P&q^ko$MYuo#8kB<+M_*H37u|$iKXU~)|Y9X_e%*h6lE64DWKDvS;>I>tu+=L;3 zG4ndJ5=64HJ@r7Wm-BKFOSML|ae`(gr=`p%NJ54z;WtXRc5Oz+BfHJf)hEPS198Dx zs+AxtBu>u(^2v$>K&OO+5)8Ju#o|COFo78lB*&KhrE`G--Rctq6BZ|!T-Jq1ow%xFoa@8(T82p_P@@+bL z!AGkBYH_=lv@G|Nc&J4i4bg)+Jb3k#nCD2tM@mxPEkU+J^s*f$?u$RV!*@ndm&%-+ zpf`Oix3n~2(%%)t3-~23P(%@|D?-K9)uGV6uS&d4e)RX%_b{?)MfQ( z3TOE)uGTx*R@>%bJ59AyoE(R3lO*yXCv885po9Uvu;42(dah<495NYf9znw7%T_mu zX*?B_7Ek9FreRE`34}>;;0CS(2e~=QUCH;oHB?tYgYhfR93r!v>02v zVws)0SxWPfOGS)jxyB*pD4b>9=N%hAmgu;3Et!ZRy`k4>w;iUO8+|Igl=oB9QV9eV5>H>mXbipNyh~?wW)z52wsr*D!?U z8tY(V3Z$;^~rIU6fMZHex18upSPv~P^2 zgJQgyg249KbS_f>@D4WL;$io%mXi+&{3vwmCQu)l{D@rnCrv_KSa;5oeObBol3%Od z4kkApM?UvERgE;#;V8bS&fd~)144H@uVW3fm5`igdw5ARte32SN7cmLf{FyC_FhQuJccgh8s_$nqjsf-?F#u#*^Xw1Xs{k6t>DQrrAR7 zLx|tz(YVhO%)8lm!7CzH<_T~<@h{Qc@Y_^U9iOP-bvZl_}J_WW=B2tMGCef`frs-OQT zUigze|1av-8=sl)?fF0WQ~HB_{ab&`9B5vO<9~zie?K$+Z~d4aXkUL|&a>mU_pz@( zgWrEIbN=s`^X=i>zhUN&=l>=AEuR1P@g3*HPSf}M_<%q5)vlj@t7EnA`TBGCn>~Ny-27@@f5Tqb zeElu+YF~aHZ*u34%rEBko4>@FOaFW)eJk(&G5*b-|INRuH?*&Ip6qFM9_{CUZqB#! zziH05uiyM9_DFp#j{jep^IPVypZ-O?zkU51`O{^r{EB1dpI^jZzPI=PS#!R9{js0q zQR$!J`2Q__%ASAseZ9YZ{n`JKp38T3{q48^4sWvOfB)CXO|bsv^;>^iJ|_PZ@Bg3h zOP2rq;J5XB`}*U_nfj)9{(r&Oy#J4wAAaHA>j>@ZX5swe`2QPz=Xdt}@0;`O>rT!2 z{}aFeX`XM6v*+2@A2;9I7khvE`g{1{&)}~;|98y&?W;Ym?ED|a!^`vA1Ag6{VP3y& z3CsLmyxw2H$y{-J|8M_qJ;J_P!m_)_o^R>+#|!uW_V1eW&Fl9I3ue!^uYa>}{>H|i zFeBC1GsA!E__c+`uaQeK=WEWf79H*NGJ9>?+fSSaQS0jcg^{Q z!!~}+jAQmo`CsgL_S?@&&Tsvyj{hJ1J;Tt=3B~hIOV0n5zw_7RjDPhj_D=d*Jpa7p z{O|p)p8qqym7lTV`ID0KfB6UBmK*-V{Ouk7@pI<<;{1Q-zv}t_;;X-&yMn#Hef@cJ ze(oTA_|3t0<*Wa$kCFUr&$D0rvy$_F?H}m*|8(?Cd#Jn?&;Qr>0<<^F9fS|3zx7VO z`ia61h3$~zo!LY0%=w@FdFAY!jS+N%ZZKiU9J0-{zyOcf*)gn<7lRLRiUmQqNiK!J)uPy*O`6f`&|72OuaxiU7FkIt=ty0k@C1_`@+e~p3ld}=kfXe zx(5sAnRCyd_c{08b5DX@d)*DwGAtI&{IP1UX(HvCVwj8Hu=ieRX3<=lQ|m$hKUy25 zbtCOUe_Z@7zIro^H_TU^^I7Rt9QQuIkFP^GpZRLGP`q~K_d0ud;OibKHea<=Z;byJ zZMEOsGxa{cx*ju1bN$?SHIEa`6UWsgjN|ZC5n)L`(>uxA`|`87%!UH&ce^C=ZoWf?yS^1_*!(C zae(Of~+OY@^M|C~D7nQu|}YDK+1Mfu$olFXa+Fz>3U!rJ#t)iSM zigN0yot@QtwW9oAD&o+fh}&(7dXFmNV^@^_jiUV56zzP1PX5lu#iE$!eH86%P?Z0c z!Z#|)&r_7YP|@E{74ccCsCTBKe4E0*RFv~4Mf)F8#DAZn{p%Fv{94hTg^KcDP?WP? zQSaM|xYa4jaVoq*5g)6f{rwg74p)?ayQ2K_DGr^@+d4(PKU9?eJ4OC_s&_cmfj@sz zO{&%ZhsAig6sGXwQEr4t;PEQhJ#p|6_`J zkE+U1w12gtoxf0&FP;lJ8{cV){A(5Y&sWs@u)@Dpls|@IWbY(yx}x5yrQ~vd3ABIk+t)Tw=TJ- zV#)mCS>-G4UQEYvvY@nVQT6iDvU4)My!wXHB@6E^U06Q5s=TaXK}Ffr((2N)N6`B- z3TG@?uv8Y2V^(=(LFtOLf@V>ZmoAwt8h1ALY;pYARI0SIsj20a<<;fa`O3it=g- zT-l-(75A2F3zytoys*5wn4Yl8idU8|Um+SGN;r4WqViQ#oM@u9V%3VWrAroQqKJz5 zT3O}NvPI(X>Iw>OnRofprPW$h#ry^Hi&s>Y-m|1iTd=&mT&rABUcPAjIIU{wiVC0c zPVs_@%JSmUGSS}^#T82`sx@j;wNI41kV;-rv9NUhd`g^c`ik;Os+Tx*jJhm3tjs&V zVtKKsy=+N!rJVI_|0@>GFJDl4cV)HcYh_hwwO3nORlbB8Np;>W8bCcOudXVsSgtL; zd&%O`Ds6H3;>Al>mTQYCVdPu5eCgd)D`+y7`AUm9?xoe`zKZJNip5ox+C8Ne)l`(! zZL|KW)S>iFWu>UFN_2U_(&dY&EML_^n%B$AmoHzsym&c9WqD=!lCx2`YiWfsJZB|U zBn<&g-R1LBLz*_&snD8IMxCWmTwY$bGBw8ZJiRCt02)WFVg(hoN-Ou#yH)AM&o8HD zEM27)i@s^|D~oATEvJ%-tGx4<^MF$mXyi3|DZSgbvb0iLTv{oLEjFf>ajGsZrRJ|F z^OnyqUR6~h=BPK2jbb#nU)hdyx?4GoFTSdL6 z8M$O>F%4;H>LQi8vZO8u)Ol_d9Z$hoxI`W6$NiT+%DfWuU^ju(9LL{q7qT zuRP|WA%nIK)Bk6sbQjv?q+4+6oo=+dXXMaRubKS+;?>9@-p!(8&Xptn-hz0B{ZS(ltUi^>t@_DYpzyd%o(O@GAygou*FKAcrc(waf+ z3%ck}Ny*4ejauA?*89Q=FIq(#xtZD!PR~xIyJ{mSEq*N2-|qaLNIzWrb4vBrE@pc$ zmF}gD=X4;I?x{`Y^ed@!mUaWDgQ>Kx&EfRRsdRU39;X{p>2BI$PVY>myJ$X6?@pzy z+6GQPo=RtEKc%$z@$$a2Rdk+y*4$3rrJpKr?M}me;Lc~n4zH&9!Aqvn!iatX;62%E zz-#6k<%hsOVQ&Na>)_%xmmafNHE(&O+r=eH?5PvLHbmnht)@EY(oZchk&(gL36;Dyf_ z^Q{^D3C^E=Z+d^H)*1Qpz%Tr%;f3I9#6}zac)-o_tH3|wdIR9&7aHxa1AmA;qVOh# z$HD*3 zTJGX;S9l({dEaq^&*gGTz|Hpfz|HXufqPaNpJ+CK4_RciGYW3Dry1P54<^9P>ty!* z>2WaK0e<&Bqh2TYJ?w69vz!uz`xIWI@Q}hA6&_P~yTbLgoyWnhaHqlx748K$uRnf; z2f@wjX9M^;b3TBdoe$t~TCs|s1bEYFHyKp z;WY{mDZCN<+mDR)#K32AJKGhmuS<`QncuE(r@{*r?p3&7;X#ErC_Jk0W`!pdp1nRj z4of+14)8tfE`@u*hb%V6r3&1f=QhmGLo^)Lp0jLUCVxc*Rj9L)T7g*z2qsBka1Ij{WSPj)rtVG#Vt zG2^_h12@ZwD7;DGafNGv^f;L1+Z3LsaJRxsz>j=oJO}x}Yc4geOMY-Oe-PZvUk7gH zkAR!`8^O)|F>o_~0z45l+G%?@J#LW;4R?Z{7GGqc9}l=$ehK(gGd|#TdyH~|;BhX$ z4!q<%V_rqTy%!tjVl@SKV7eXLydF9gUI>0V$_JBU0Qhn7g#-Gj0e^CU;X&{&!wjzjpFh;_5ct%8a{R%~xWyFS zu5f*mBL3ikyN!CC;QwMTRJa%1ynp$@&HG4D;SCCpf^S%9w6hs}EPF!X*_+ejV6JBz z3U`4Y=kh(^*ZGY0RDmyBWq1JmorE!O>%gP^cs_tvy>EB}_&D)}Li&k-H*z_R;EpCE ze-wNIm(v6u+;8NMfluvecr$oqZ^Pr@=6S9CBt8D-ea!|wj<1t;@az#r`3~@yxn2b~ zpI_VxFHyKp;WY{mDZEkPF@?8-4_Rl-AN{B4aWmbnaHqlx748Kug&U3hZtyqQ3&H<-vytBeUN_zF67VrM814nPPcys<3@jd;4fTbqH}e4S3y^h6lmNTy1zAxP6@AA@C?a*EE3pt}^mR zz~@ggyb(Oa*RdG5Ilj%{(dUeE;^13ChPQ(c-C=kFd?%Nq)uzw48@3zyb?|@ibtxM> z_>7U?2Hume^LB9axx=aOLhvr@jr*w=++6qi6&_T0gTkWz$dc@!EIcA2z)4e1bix&69qS~dohK_!Dnze32?I<{qgkp zn{HFML*Y(^yA|$HxL4smg$EQKRCq|?5rsz;9s|FL;~WP!b&2|P99t2;&<%Ga1*dyST>{0L~>@o0V>~ZihJiZC= zYR<23O^^Rdb{qIAb_e(xb|?5+b~pHXb`SVN>|XHA>^|^E*aP5?vIoJ<@d|-I#`z=Q zLG~#4Huf0!Q|xi@?d%EgXV~>^>GA&+yA3?V?f|c6cY?pj?goE}-2-mM#|xgv^T!8n z<`00s%;g8ccd>`SUt^De?`DsJoAt&N9#?om;rf&5@i)u2fsf-jIKbcJb~?eGoZk)p z7U%bXPvHDs@OL=B5By#B0Jw|G34%{$4}rVcBj6u!`B88)J~8kcxSTk6A$tPcEJy!E zdi;wxzYW~O?f}1o-3ea8?gpRF?g2O3;|2F}ejj)xdjPzOJqTXS9s>8VN5I#xN5TE< zG4Kc23KXj8ZYyot+kg8!A>4IX3nfd7Nt z3*OA`1OJpg03K%#f}8ycfgj=g5%6~QDEM*q7r(&OKS-3G3+ zJHUIgJHfNr-QYRw9&j7G7kntY58Tck06)Oj?I8Hs_$xf3@TkIL3Xdy1p>TbBdi>3H z+7#|kxKrV7g?kk4Rk%;#0fh$@9#VKj;ZcRh6dqT2LgD%jMf?@+P`Fd!ZiRak?p3%? z;Q@sQ6&_M}MB!0|#}pn{ctYX&Gm7{t+@Wx%!rcn@DBP=XpTYwQ4=OyQ@QA{r3Xdr~ zuJDAy^%UUOU*QggI~DF$xJTh$h5Hm9P(w|7X+VKY`r_{tcJo0RNWV32ynySYNrpyRdt}U0kmhygTRjf%jw& zfKTOeg5Z5Pe+WE>Jp%6La-!gaIDZWM2F@P`zku^6z(=s_&!xxT!EOV;nB4(h$n`qG z^Ekg7d@Q>Myok&3f;&0C4}2nf0K9-b2=3wXL*UnO{s{O?_9*zx>@n~=%=m*BasCAO z583rldi+bc92@w2b_ckZ-3eaF?glUBdOhG(oZkyRkMsM$7qSPyE7^nK)m(lEd>QAD zfcrRq6nqVP4BXEi2Y-M)0lt#!)x+uWU(Id<4{$jSa6jjFf^X*hZtw>=zXyCHyBGWs zb{}{RmmdI+@%Pw+;GeLEz_)NY5%3^;6#Pl{7E;3GfiRUY{QS7uap!TR095 z@F&=v;7_r;!JlFGfH!cxUhoj-_kq8}9su9T9t4l@zdH_r@8 z!FQVR2alNXe?C3_A873 z8v;Mg`6J+soIeWw4toqd${q)QpFIKI#IC=P9{<0x+rVS&4)A}lJHeaT-Qb_Hd%)xD zUhpIAKJa$-0QhnCAb5g31b&)50=4j@L#j*zfO<;Z`p0&e`I%nA7FQa|AXBPuKmSmzX!aP^LxQhu=~Kf95>1d zfcIq&f@gERA#exhkAPpr9tEGu9s|FXJq}*Ro&c|A*I!DH|AXu{@F&7e) zQSc@%CkFlo=Z}NG&7J^%k6r&wdi?jZ+rajv?gc-|?gKx~ z9sticX`B~9aGgB_-kUuF9^*Ji!E-o&416$q9K4y!Nr2lqzy5N1{4Zj+fnUb%0C%!G z!6&o3!Kbr(z~kIbFZhj|-v>UMJpf+J9t6LOJp^9G9sys;9tFRTJqEskJr4dbdjk9s zcKwy~_&>&O1K-B(0N>8;1b>d*4c^Xi_JF_0`Muyf*?r&%E++u4z030-yu`fzgIBRf zz;!Ms3jSMePYm48<;1~j*c0Ht=W_I&>GA)N-3ES;-2tA>?R0|whx5C^lk6VwjIWKj zdBJU5z7M=N=MRAQXAgqg*+bx$ut&hJW{-kTXODs3!X5`NW>0|M#jfv4kN@55Ht-GX z4)9ubC-~FsZt!Q>J>U)OUhr4gec*4g2f+W#9t8iGJp}$Kdj!0dJqq5=9s@tg9tS_o zo&dM=ywYDykAFtOn1?p--s}$Wf$UCj2bb>#w{w0E_@(S#@N3w8;BNK+coBOLyp%lz zzLY%zp2zKvg0JNKG4QqQaqu6rC%|ji_1Dtl{{*`Y+{x{6fd7*7JHda=?grn*?g4jk z`Cf20yAQmOJpk@u4}!nR^@hMpIDZ7(%N_-GAh*`8Mzt z&hG#}$?gQVB#raP4c?R813r-53-0Ik_`rv8{s4G@^9R9e*hAoBxSR<1RqRplDeN)u z8`$IEbJ!E$ce3ld)8k*xZUYZ;`yJqQ>`w5dT#g&u$L;}N$L>^ATuy93;M%9!U)@Lue0@B!=| z@Zs!U@Qc}f;7;}c_$2lq_%!wq_>Jrl@FMmo`0ea5@OkWU@Cx<>con<;+w}O~!)^n= zpWOleAiEQMGrJr7XY3yEt?XX#?d(4A5PJapCH5frtL!20-?2x)-(in}?_-aF|Ajpc z{$K0~@Md=Xjr92co81QfCA$Os1iKUbG`kzz`i*hEc)+vRz2JS=ec(3s0C+BY5d0$c z5cnnR5%6*BQSfWnW8eksaqt=J3GhO8{dei{pTlkgzmweoUe4|WuVi(w z9pH!Ao#5^4Zt&CW9`MZ5#(Cid&tdn04`vU5U&tN=&*9INLg1Hj{s{P3_9*y7_87RE zJq~^gdjkA6c70EJ{BLKsftRp5z{}a4;CHdR!I!doz^mE4;H%hu;Pr$`@nBy4}dpvJcHm~&L09_#~uNHj6DkeEPD+6b@n)T6MF*uAiMrndi+nY+rYbh zYsA?BZew?XH*p-?;3GM|2mESwFL*PT;{(5$^9R7o*n{AU*+by>ut&hxvq!;0{Jawb z{~70xgV(Vqz+Yn5_om0co%?G8f0Oe&!1u8`!9QYmgSWAJz%3no{Ri*Q?gJmq9sqZ; z2f?pr4}s5RkAPRQN5Su9kAZJukApwWo&ev)uD_ig|F_v~;2*F%!2iMS1pkcP4gMc? z5BO5^%^m_D#2x{^h&>8^1$zwqTJ|{jO!fr$ZS49x z>G3aPw}IQazYg#!&hG?Y$LKFW5uir`aRmeJsW} zM!|o;9s_r>$H8Z^C%}KmuKzwg{&^f{8@QMAJHY+yPVh(B-Qdr#d%$01_k#bC-3RV6 z=Rf!-oIeQO#vTGsut&hVW^nw$2eQY&N3+MlC$lHOZ(`T~kRJcJ>^ATP><;kd>`w3o zzE8QqBkUgVMs_cFl-&n@Ki3-oe~3K@-o)jEz-u^v1biEN6g0vpc~Dvb(_tv3tOWa{ImLoZkn20eb-4&K^WJpTEEl{nNO= zM8Jo0IZ^PD>@o1s>~ZkR*c0I6*!5_7{PWpu;McM{z^AY~!Kbmi!Dq00z;9yrg5SdK z1HX{t831>%2f>TDoDldN_6YbGE+-0p2j`E0&ts2+=W#g+@Nw+=yXo`w5h>~3%`x6=cD7rPg{lHCWslsy3M=6ZwR%Q=4tyqY}%zLGr(?qiRE-^(5cU&EdN zzn@)yFFpQ#b{qJ5b_e(kX8ge)~a za1VPF{4p*+2ELU&4*m=F1o#eieP4R~f5mPC53xJI?=a&JUc&AMf1bO|Xg}lkq1eEO z?lgYCq#fMM?*xD0B_qEJ+$_fpZnmQk{FfgY{q=x1u$O>uVfTWY8O{2uly@CJ4t_~&MPz|Y%gj7tdI%-;xZwlk*ic5w6e<>~ts{RKDA_d@X4 z3*2AuBQF|W1HOgh&;b5Edj#C-JyL`(^$LHg`a}3;^&+Xvm^;!RG z`Z$_y2RE-9PVgr-8P}^q@E)5D_bS{EK4!6TUIf95UN_=U2cEav@DO;zDZ?AU|G?*C z6x{s%lFi^|`xD@192|d3kDI^2s5cM1hOd)Oa38x1`~h}1cs9qU1bj2kPcOLR4@P^c zz^i|6xDR|BUuXQ_X550{&HVdV8^G`Vi81cY;Dx_6>Wzc1sWQf0`!GG8ZjOTvK6Qza z-wtm6UP1@>d@kPw{%OQ$PYHMpkE0j-ZSI#J{LY^m{SAVj_lV&Q3Xg)94>ayy&EO+y zjB*kR&;EOQ+P+tz1Aq-qwp&5ESu5J0JvFBox&puZ&G+%;aV)czh=EQ@IBnG zJn%t}8vS*HuV*g-?=#5guMgZjFKQGXQg|b{d0&Vryj|h?f2GI4EXS^Jr@{-tum715 zAFsmw3J-#t=-QebW%m;qMA){Y@@NCW4j|zb6>^0zK`62LY ze{RgHMsV{vD5mgs@MpLj{gd?gn{HRQ6Wm-c6@ov&&v72`;C~tM@qy==aRZNV{s4IJ zaU%|O;MQE@JZ%CW$K}Vs^^cAAG%H;DXL@{GT#gQ&!{uZv+y;ILw=)ksKgib&g_kJY zr|=quhZNoj9^rPzz|HGkJ9s0vU;kHn+}1r|^ve!j$IoRB@J9AL@S$ss^U(=jvehWR z5PToISK)qz2Nm9+@TkI@6`oLdc5`~%%=SCL`|x_$1s>RD#K8l;;7P*+;Pam`;u!*u z@$+B<_(Q)i%4r6V^ZrsC+*}802h;oIf66E)54@W9wVdEDaeoWJ&F5J!cp>+z3VeRG zalQw@&CdmDz|H(2aP#wp25_^S2)NmfM(}ut5&tOoarP$g7<&xdET_{1J7sogU?_OfX`vC0Ur?J`vUkx_C|%r6y6TLgv-&}(#O$sJGfb| zQ{jaQ_bS}4@SwsQ6dqN0v%(V!&pwhK2eZEpg}W5)QFxWY0}8KGctqh%3XdyX`=ayy z+7zCraJRxs6z)@ajlx6VA#?sKJf?8{%k=(EZ7{C$+29pnYj`{OPR_6YCp~WF z=Z|)UJHZe9+9Kr3~5Zt`aHYhx*@MeW46rSCl9-k)` z80~R@zxx}*T?+SrPb)X_S1CLIK8E8^13tlw5BNCtI&gE`BMNU)cpUuMSB&;+N7Lie zZKvTjh36^Ut?&|s`xIWI@Q}hA6&_P~yTbLaI*)^0;ZB7YD%`7Zzrusy6+A8t3Xdwh zS>XwVXCF(CgITXb;Vy-H6ket9fWqq(9#MFc!s80pj(6T)o5J%H?pAmSxOsm06ken7 z5V(1rZv-#jWt_h;h3hBM`#W`-k>3IS&TPZo;F}8!_k!2(eX0uF@v4zOpzu2I!4*dS zh{Bt|U4Jw3$H4Er(s=%A24Bh^2QTDu+QCcM6X5H(9PMO!Jh!mx;LoyWgTKOV1AmL% z4*ns#1N;zs9{3mRPVkKJMtof01KHi+7qJ(Dk7M_MPiHRyU(D_W{}p=`_?zrL@V~M9 z!4I+rz+2gCz^izCgW$(Ge;s&_tBkmXzz4B6fRAL4fRAHu1h?-q=5rL>&H0T@4j!JD}JIQZrpjr{H49`0`feClmReyt-t{#Be`2Ol@X$e#@!_?6LqJNR*z zkv|W-aiTFUPVnkmjQlR}c#+|5@Ek6`5WI=k!yfQD?pFzTgyY}^fB!n8-YW1X#~c0i zf%|3}`TgL3<@N`_-@nn4nzwb}!Kp?$A#k(&25|jaBYy?PK8i^p%m*+!5a${!lLEoH(mNIpbzujE4|uaf+H z$$gSvAh}=iT*(8H+a<4&e3;}x$wx?DC;5evha~@j2vcxPjX%I%O%g2{0hl!l8=?#F8Mgg z9g<%ud7k9sC3i}GmEm_fN{07P6lHVwKyW}@Xo{;=z$+a`*=6|8&y5zS=o-O$-$!(I)mfSA6e1CUHeyfx} zPx9L&cS=4-a+l;D$=#CwQ1U{_=SuF8{C3GpB)>y)ujD_Hyh`$7$$gTSNbZ+>p5y_^ z%OtOne7@vC$;&0LlYD{XA;}j?-XOVG@`&UWk~d0zm*i2&D(!-_nU6d}6(n}~k zPf9PR^m-}1lF~b*^u3hcE2ZzJ^dTv|p3)g(&ei{8N)MOP8!25NrGHB4c~bgOO0SpF zKd1B#DZQ1_d!_VKls+V-cThUxl5_PxOX=ZKx}MSnQu^1Fo+qVWru2F#{VJt*Na@!p zy;n-VN$EpUdM~9jEU&?JEZhSl-?_)|4HdX zQu+|3GcG$<|L2q*E~Q&3T_B~ur1U%~{S~FxOX-u8-XWz=QF^bG{+7~*q;$qKsz2{s z{oN=%TuNtAxjRegwpe*^m0nCm(nXK zy+cahOXETlPJxUiy=?^G9PfGuV((9%4hm_tSr9YzdUMc-gN*|KahbWzK)w%jV zr}S_s-Ad^KDg7m-=Sk_WD7{`vpQQ8-DSe94d!_WZls+V-Gp1Ah&U5v5qx5hooki&a zDczgW^Q3fNO0SpF11P;iN)M*=UMYP6r4LEz;grtEPu1V?wzmJwYD@T7KP`D?idL)J zUHQ628<*QvTjtWVqGoGu{h8I1>3vs{^45y`vL!>?Im+$|)ALa>Y|+Fw^^y+FKTEf3 z`P=C~bvCT#>e^#gt$xL`mMzWJe&0D>yyxnpZK)XU$}i1urH(Dh(2|p=&V${xq<6TM zKP*Gk-LHPw>5iS6=6`hK1sPl7beu41+tJc*I*OV{XtjItt$)*gp>e_N}&M(FASFXZ?-i4s>)3?07qa;?OmGdTF=tsfFFciDEr` za!zLW_{~}2GgBBSEgR_gks0AohUNK;Y^`WO7gzG#Zd&cXhHFPhotbiA^wIkc z%&=tcd-sZmx8`=8(H73qibi+UQn9GppzSZV=xvs6{#hB1J-qcyzP4re?EAypt;LB| zS=vq?#o^$eweX4av}FGt+D^^tI`j2;uH-3-{}4LnwAEIxdF+?!+JdM0zh!y!gr@EC zKjJz)mtr9KS0x=CMWKwwk6LKF^gfLr*}J;Jv(IyH@_svHn{L%NWxS>Bf4oH7qH8%D z?TfspwUE7c7L_^Es_mR%Y5Zsu9i#V}u}Qb&Y#g;H?hIQK<6Ep_>$8Sw``fIJ`cb7r zw)H%{>cC8Tf6O9>(_zip^mR9F|Dtm8VOoBO{@&lxf8(%qt`~<}8Yj-P_$QXG(_Vc1 zRL3sbf6d9X&^)nZZW=X=<{FhDw;}6v$F3xeg`N5?+9mp`Q=3FTbc*jk>Ak%)zX}>I zto_uYwLj1>sP^D9TCJ7hGk=@5C5Provo)vw_{}qE>}Jpyx@jz@)0j>RpILofn8r9Y ze_KXO4!3r9CEXT%U;Zsx*paCv$A8SuTy7~VSy-44tf3fD#cQf=&!+xOs#&xEn zNVDquGN|n@j@Pz)PVH#6T1RXDwrn|0bMmm5KO4mS=|XjNr8>J&-QB4TI?bODnkQ}6@%5uTtO>vq>Pj=V#kEZ$J7^rUw)0`=_ zxL&-H<_(>%FP^5kLVJV%?Od7pKg^Y8bFR34V9b@~!LdjTjlvOgT~5D zV>X?}ZW@i@bu?e5hG|TBzPQ4xX3;#M`Qpvel5=RT#9LP%(0@U5xMlUDEj&;B(mWYK z^CZsmWaa0^Jo)T@F;6nS=RBdWd#A27I8P2c&&`wF(mc8Q|7f1*Vvf-Ka20>|n)3f@ zeoXl0|8RaZ@ceKMNt+*q|BvU#wkyxgk1s!y=ZA4W7Wd+VJ+$!Nu3A$;U60y-Tf5eO zZtYV4nblf<(5lrQ9--BKYR#y>aDcX@!)l>xPX=9Utl={ov|6`jd49N6E3)XW@Mm;P z7G1YKrQ^i4M%*(;CDtCuwPd#G>mJ^^r;B#M$xJKnqq4T03O!cMC3$Zq+68F&Vlfz5s726@MjnmH4wgl+jChC5g?(xl5y1&!< z!N0z7LY(gP&DM1jM4JxMd6_8FwupPZ`1?epJuSRC&C z{m0_Z<>%Ucu~eVg?q^c%PM;5En8o$i!J)5G4%ILs`CSjYxmN(iX);-jn-N}8TKb?!zHOU*iUs7 z-AMJnNOj+2>9y(4wD$Xwj{TsIw!g#DyKOkNq1~EUfAl(6r0+CWWKu@oMHVvgUKy3u zBJK}xE{k(GKy{nvZWlV2yITFT#5p{P;xcYsAa%Zeq8aCFoYsZP^Oeri2Xel9o_1PC zzwg%Z&iAZi(f6$*`@3~K|2^xN^*!o%fa3Nb{lQp0`aSE&{~mRu#v|vubiYRV zjfegF*71Yy)=~03>*)S{>!5WJ#)>{e`dSER&ucE9kntF+<u3^`o z3efrAgXZ=)^2%Ug}G)_2Emo=n%`y&-Y#>y)>Y^7*^z z+veS;Z!_PUcdBFHnbp4wpXed-XQbK_TJtvj*K6AUBJLfdEM4}!vc|ft!>YH<%b0O$ z6xA!9k;U`R(|3(Z1n3@e{N@5W-jzJF`t4L*C+J>H>))N**I2eS)4lzWwSWC#x`)(` z)wcNQKHQzIKV#|rM<^}Uu?Ok>0R5e=UAsitk9E_M^zlGWGkKiu&3;N}X833QOi^D4 zJ(G%Of3enB*F|d@X75*9N9*!==lN&dcS_r`gVv5i`+j#^|1Z`Tw0Dpi^Z#jm;h`}X z>xd(GhssgD1@=8bi1`n<7F+MGA1uK3^2 z8@-#>pij2c>Sz8HaBJF?;yf|-k?1_wK+ie2P{ZqB>Lc`>(Bioa)_Crqzl%E2+MAx_^r`bLw7g)$6svmMvDwGla%m z*IfCzN$WD&A1jLI`e)JFHmPe`zKEUq`ByXfK{{`LHOR8XPv@gH&RR?VxweJ^`-Ix< zbkBc)%Aqw;ew@~)qv&-Q?H?3VnPQJCtB)(9cXiPto|bQ+va_f4(bLbhlKV5qV(DkBAtwVPusSU}E)OXR} zoUX3q7>g^p>BrOZA0F=sZyV-Ht{tW=+eFv97idrHF3ZNP8!cIz?y_dJ*@w+M6`=OK zPWvqh>yUbiL(b24Oy0feO;?hB@7=!lFR=B!m-0Ss>A7q>UCWPCOm6LPAMnyO{q=wP zUZFL2>Ut;U^1bscVS1iO=4{py_bkZ>ubFG5Hr_^Uy_MQrL~Wlwo>AE0JsM-S*|gaZ?;m~ZaUL(r5>7`Pix|3;u$NROKqe)aq4UGJZ<;a zbYI)E&$4AQ?Gs#eWX*vm{^?47biY13L1TTI>g_A;GaD@Q+?G-Mi@Rx`cZ8+((^R@Y z-M0phK5QLSe{zOpOZD(2m;ZT=I9yVw}oo zruZMEH9)CZ_r+pd3>3opKowKTSs0OK4}$WEc*KJ*3En;bx+Ic1LFNv^q!c%qeL4&c84FMJR*I{I&^fx zI-q_8`3Xu38%#0ErS!o$uH+GOa=6z}7dpT3`(TEm#f6jD6GsB`E zxt4)#Ei?xPQ6C0UJ%?#8&ZuWls-D9%cc)T*F<0xTj)N3KQO|W$hn@Ba5>$tX#gT8t zKCWn=ah#3vTuH}?_dXNvQJf|fX-DT#zg`+++46hpyO*Bz#dYIUS!OssTMvJ|G%I}i z_8#Ghw$88F18M!#EcPh0i)!zo`VY}Kwq0o1@-mfW+$YY}KY;2!bvAA}^euO-XrXIk23;eqbgk?{*UYY| zHEhR*?)zsxFfCuKQ$-zf?Vj+SkynJnYsZC)>3S~qiZ;@Fd)AH(i#?;)XJ|V;b(43` ze1ooAxwgJ*>6%?KLo2FZbY=MZ&|I-+x+!HkrT$??6W^?NEj=A(qyeyOMmGL>+?f}i#=jnc;=lWi0Tc{;3 z_FHRPr)x=ZABtZ^dpC5gr0d;oo$~4HsNBn_TeXgK27($|M6ZhrIULB+4sE{{43*se=p$v)r$V&y+rqj=yWWJJxEu>GQf| zKZ@5Sbli>&b0UW+->0;8G2?W@Fw2$@#YtSl8c%c-b;;0jc5d(_#IsDCj(=f8pTz1Z z+Rgbs5bo|j3;uxxj_QP|p)Q%pc`$YU6aa|eQM!$}IO)A%pzD2JsD_qIe+i9Ig&$Vw~r4_Z( zH7WDH_vpDYv+dw@biKVVYg_B};(fi%>ePxd=^UJ7u`iop(f75|b>KES|4Jzqlg_8l zDCzj^W^~#J-dQo777?+HyIyliDZRBR(t47xST5OWxN>`Ii3atZa&}jvN=ym=>2un z-nH~uwpcH%rS;8rIv2%ww&a}~PtlIf#`HECPujyt-7{LQGUl`XyQ)VsEt&NvyStJF z^j@lLsz;P9K6e*oXI^@)?9R#+^90+fQM+!Vwu*WJR7X#$!|aD;G>r?53a|wnMb`4{ey8m_&P0+iA^TH^RFA)*kd3hqWjn z?sYodH(SQKlH+Kc93%B@`Q5eJ{y)^h12eVc?hQGKe^Y6ARx&~7n$Hll(J#W!`67^ie^$5=v^Kq?fw>VCeD-AP+TJ6os?qOFmMRrhk?wCq6uLTiAMMg1+le`#QG7(7KA(e@xIa#p@>%v|j#+$uk`T#j#)WYica%?{rPtS5MEr zgX!AlIpaQX2hBaP#yx2rTrb*`TG#cWH9S3+q}K5CtoO*J#yUMt>vFNTsSmT&xAf9> zjiRv<&%xfgmT(0>2jAOQOVT>7_V~^8JThl0t?{PNTCafCd}3eo52?Mz(8u?NZB+MA zs$0Z4ex9~#1I1-1t%XF|OX;u$<7 zsRum|^wo-X{(X5^^dZ;MzwKSR&gs+-3$>rt2s=Z)`=9O80J6jMd@(oGFRi}BYCa1L zsK1@|Qbtpmq1*2b|DERCNm{>8pmrr_?I`LDWNEwTJM=kDYQIS9lrEz5NxI+3`P`Y> zF8a3x^ekb-@mi`&#QC*F)nRd5+A|Ba=iD>Pz_ZURgHr7rRG&x3(zBfO%rdC{a%!h| zo;XR*FUB*=x#I>t5bL1O?bn3Ev#$=nZXKGqu7Bzo zqJj5)YRr9~ZuB|iNxE(Zxxax7ZI_Mu)k4<=(OxrDuw}l77)E*F#i2&_E)U7X0xac)R^D)HFovAX!a~_ou%A)Hbt$XWe|J;1u{Dk}q8q+&cdp|~- zb$UksluWciJc~`5L(g1vpG~dfsUJN{=re0AbK4gb|14^YczxbtoPTxe{1a}Vy+)n> zd|}P1f11`w&D6%{X+NdjLicgHp0-+h){ARjU1sCNJr=F4&boEN3z@ED9eqDZlyw7L z-;U58ii^%=7p)U}Ydtsp=_hw4)>?YC^`gJ;pnFOq<^6z;*<4DW6;eL>&%q_dUdkub z?j1Ca*U(yaGW}iLqm1XZb914N`Yh(4*sp(mpz)o!Z%=eQF7k*?0x0{OU8^$BVKms4Q_k_?*iBZ=dt5J@?=1aV0D4)5EK6 zlXrhj-w!xRpE;Dc8Yiv^xRO8o`u+pH2d3tWy5l3e)1HZc;z4>2N^H}1pQh)sd9LIgsbh!MTWGG{N`Lo1OJ&Y+CB?J*F&cXjv!|{7+Dd3`(QNHfyM>;guJmiQ z|9+nO+1>r3Ztb^e#d`7mf&TScSM9}}-*)Uew%nCmb@KiL$LOB+vtCr*5?bd-ZM;VO z{TZWuskYK{-I{|-<-PFmCWhWZ{KC9 zvgrAo+U%ge+jnR?!;t0|qC@#5Iawd5+= zA9y-rXq#Q!SEpF^Po{Eir9aP5JVLz-5<7Y~PJCs!v3F*tc`ITjVsq#+|HQ47=g?)e z-?oAF@@TG}qWf>0&fVkV-Qo7-Gs3%9cBvK5QH^x&iS>lomuaVQK1So2wDzjsNXH91 zF^TTmJqr@Udp1rSZ#U}l)3si-=TLY5M8Bw~d*j5g_4Y(A#o&iDFTOMW?H#*p6hC>a z4t&+I%PsQKcyu-v2aa{@8bZf(*5-Be+$GwTMc>m9=TYkW8hsll*3)y|7u5cCdWI5X zB4XOQK}*(e_+jFC>SM{e#tEhC-k#7lQmbu!u1~myuCqs;(`rkOJ~H9?4R<8ybF|SX zMh*lWo9X@hB?L%9&-J_|`E&sV7+|ogLmQT$u znM~KAUiwX~wD&*rpr!Um4@>fhcs5y~=eInUnP1UGYdA6_WA~A5uH7ZK_$S(DP<^yN z($Uc?JdgGgPrgEH#;a`M_7#J|M;_AiJBC=29l7*pEd9Bd{#<6EXB^7^OlJP116?9w z&-M5k^x%RiM4tQwNvELs4ZfCPr9#Pc+!3S!yPY?QN8gM?)*5lr)AP) zqwME1^E*E7vg+L*X*y53IkqRLk!{V|p>2Gi?;F@uReGJ&l`-uE*jW zadj218z$6S=O!+`gz`*rB`>D)q>a`BqhIV2zKhmtts<_}#&75v-{PTj^_t1aD{24c zYf2{uxSU^8-%n8AeO7H;>qGARTx-^%4EjD@7Cj>tU!pAw(Ax1QU9_4P|7%q30J?8& z(zNUsR^8=Wl-YAri6!?#5gT#7Sl;Sd`*0++{!Z;_l+K~gIOutJJRNff?N|Mf_Nj_G zN)FI-QjUDg8tIs~Zl%35Iwnr-{hHb-o(IZ2^lU`y{W!I;h1xo^RNI-L^UzIw$QxTcYjvf$ zZ?%p76OAc7Yb~>jx@qrCJewNNBHI3GQok%W8?m@ePmjg#==h-&n?V$t9EwdAt#MXT zo|b2{-K}%9B>fxroRi)CvtAK%=c=izCiT~r?VvgwH)=b@_gmvxS}px2Q1)jXEq0D} zXye4^By;ImfcEEVDHgw>dvej8w9k?$*1A$XsXD0q3Ng>At~g!iM1P*3dgQU`FZJP| z+vvkeYL7ggo8cH;I%e{8xcCEBYM4!+Cl%8G-nU3@iM8-tX{O<>ZR=;K;BC8t%c^@ z;65(j9c%U3w@|*8%jSgLbk44&=k^U{S`qD&Y;w`-So-{|AC1AhkWc#>Dad>XhkciofaBbaUScIf&2E*^V>5tHe!rVU+X@Q zygz&P@o!5Gd`M*{Xlz^P+H=Y}aJ0Do6w`U(x=<@B=Dm(w+Uuw&rnb@^Sj#MzueGl$ zV(yEz4mbA2=F(mVo(C?PgY7i0PK}rz7W*msu*~|0>E0yvRm^k6@+10OhSpAU-v-d! z7T2$k^+$=TrsUMSXif0$o7&M3J%_sJ{%@u8sFMD@!sh(Zwc?sM^P`UJO{*&CKQq%~ zlZ&o3EgRhVnI+n|I9;b^TJ`JQbYAM2?+(rkXAWsy8_1tw8TI@x=^SkxIXOxH7GYEC zGc)pAMp}|tgSGM5blo}DLHGQxX(u^COIH1R=Otbf@et3qsl7_Ne~If> zr{&T;@@`QM^?{zVt`g;mHksv#b`{(}^M$UfVvP|$PrqKzJio8w2hpu!AHjRQnD@E$ zUk{=CEcIg^mlYCw9OQR$8_n`EDZX(kU+jVHrM;80F{bpT@(Y%@X(i zn}l^XMk6fA>GYm??G25*HvIXk=dL9Or;7a)Pr`gZezTVRr~E!$H`3mxHU5Y4`vu1P zGdj|*qXXzWWAwdMM&*Ge*&veGeUz(AxXPjFj;`(sXZ|J)URIEN&CA* zenD*zYf+Kz;5B2AOvFI!-BbKh`=<_Gb2R_0qv$c}v#2-GgZ93t4T)~Fjw`+?e2mVW z<{`BHT}^9VdX^FE`=>MdrS=bVEw;89wD&1Klg{jJqrInrsr|J6w5M}5{SnVahiT7k z_b6j;hsJf+D^$-(D)YpM$;so_7wkSZyI{AEzH^t_8>Rhno%S0u*WHGRlosQrx8YMw%_5rtQ;hY;YQpYE# z5A?m`fubz2p1*+pZa(juZ5MlKo2k7zJ$uAEw6MsVL;1uW)Ct-%j?GK_N&3c~VNZ+yhPMK~p@%}UPzNkxlzU-v?)NX2n_}+rpLl{r#!Su{Au1pJG zMR66Mq5ps6y?=aE)t&!;?+k$nhzj||4>1X-Btb;^AxXtDBq~2_iwdo_wi4vCUxT!D z5v#S78i=jfxUE-lX>qqEShtx`TeN7CZFd3F?qb=tAZmY?3D|a$*sTakqF}zy*SYu3 z+?fPOcf0%jJRaXa<}ow(o_o&wynnyn=Y7sCBX963I9YSE<>YtRvFBc}9g;V_n)CC_ z*US|g|0=W>yf2b>kiNN@{WwYB4%-Edf=BlbfbXqmy4Tw$j(b!&93Mxuqg)(i&+|K3 z^CTu%sx?XVRm%E==EN(^H)$TKCDOX+^W-ufCU2;G9Izi2>>S&+bUAsr_2l7d;s1%u zbqM^?!!yG9{a>ivpA1JT6k|#c%bzQsH#XjfPgjVqlHBjr3jCZL-CJMSUroECW$?@a z@A@GxcSm3Usq5X|^*^WECw2XpTYHk*argcN_zBUzbW~(dhwdR0DL%f-t4o28f5p9{ zi$56K4~`%4<|W_mxeb<6B786(Mq6I3SV8>xo3!`Ntrh#Oa^&GNPMsrP!uknvz7Rea43fP&$z!ho@BVmk&4IiR z@y~P1MGxfV$%wV-o6tzPX3;b?cP|gEeje2vkO$$v>}cO}-()^5jF5W@i zV^}yZx8hdT7#6P2jaPkQ!Ty=QydVy0e$eJW;#<7c{6=o1(}Nb#d|X2zzK4 zN4ibtMuiMRi<-E(^ zaC7Q2(I@Wn`ElvPo!l2b)W82N{rckB?mWu9dF=LNFF229)Hn10ooSmFKbtYK>gFFT zT`(+FH~&3i)eyY#A?$9jUB>=B_Vpcvp4EvV&TD+%TbqOZbNBRJ46TCCX%C>=Hpf}s zW)FO?)^ z9*-`*f-$w`|VZNv!}pv{$z8Exw=Q9I(QFyt4NIu~i8^&Mso01a%nNYxXj~ zo6cb0wN=5+jUGM7Qrw@+UdAT*jdO8$g{b})4;6i#yXD_gChjmfyR|&({ z7SGu{kFIfk%74|eeq z4bjXa;rt5h^WN%`a`2<|>b&?GgG1`BpqJpoH>nuMGfg~Cdvk`3_WtQy9bhu^T+jMa z=^G)#s|QEaU-!gw_NF(85$7;Z!^gGfxIPB^5x>Db#mTd{_Y9wG-R9CJ+6FFGm!f|c zo>4#gz})aF#E0hEdf<{j%v#B)1$}+9ursqtp`G@t=L0+Y31?;Rv)4X%oHz$QT+RKZ zQQB*+A6NVL`etfBRh{}Lzix8>;3;~B+h*dZ`s-?F|9kMGy%9sL`qB4}er(H&!im-M z{F8h~ES_ckC_(*&4`(y$*MeQJ`tS>8&1)AivS9xWJOFRTHP%FQ=p@GZ?w@p?kE`$B zxg|{f(-3TiCu0rFgRu&%*3rV#W1)goK^)z|9CR%kN1DItUXR^&?({i|L4wKGMqKIWp>f3@<$#UxAMJVm);)= zjV8ZwNy&@R&xPz8KG)8?$ycq4l3ScsSWfUzF@2vy#`IWmiyN=J`gR z+}29({bGWv_oC-GrPR^AjO?Dw9}-POk4<*ZY~gJIXHfZkD;Qq3ddS7+R@~3Mm6kO# z7nlTx=u!kNMH54hP*H`Cx8)wrn!u^(@z|Z#?H21sN9k*Q$WGz+YOkI2UEOH)*D|_* zy^R|8j`#X56+C4=k5@-qsclQ)d^zn6@4@55-u-ZXwukdKU(e)m^{sL4c(>2Yx9r`% znG4|cPI5Kkb={9JN45Dfl zjQ!aQk@-8{JFxNRL#@%QJzpz*w>xz1Xst=xwgpcW8l1jhU@lzMMD7S%T2VA4-9BQS zJNj{Wt&eq5)ssiizpsYQoi3aUUmf)Ete%(tRy$v}+J&n)4{a&FW`=I1jxvMqhU7iA z<#uqn^PRrumuB(ZK6s~ZxV1XT85*(C(O%g$YJaObn78bte8~j;OSc~ehMm^gmuBPq zM*0lamImVp;f7r4XyF7u*3=|4GlzlZc(*r4@k?-y9q72h%&`hM>W}w5KR#=Y@(Wax z(40k+Pxv&!eu*Zp(oWwm^UZot#Si#4{PMtgPBVVmL2@PE!oJFeN1=^y^3+LiQV0yf zUxe>$dpzLz8Uvi+(deDIlRV8C9uJ56+x!8?Aa|0zPyO~mj`6v{j6wf7i~5i8(Ls)} ze6VA@zyBD~L5{ItFk|3f=3t+)aW#36V@w@iIKdEK)s&$!|t>EC-;7c2RC^c@SGt~fInc+%&nq|a;FW%7<86Nmkz zvv0FS{Ht6y>$>EG^WvZ7S}pmS2+wcya%#Eoboz{xbYF@)>uKZP*ZE~f182iz>-h+0 zHBA{}Irjsf{DuTJb$IVR`&EO!=#lkXI>^;5to;rKx4gL#{T0`jMH?g2uU;9hXp2>!v_^&sYGbMbVi#n=3 z%dFh)81&Y7I+N|o*AmM^U;G7=FUR9g+T#sozmgnkTjmDJMwOFgYGugMfC6rGM zfMWFgc-?T(GVRNX2gO&e561nQt*Q^hE8t&v2#-vPDkvA$;M(k0aCg%1a~1 zYxgtX!{mPkoM$i(niyu0C$gNMo4x7oy8xuWf;l?jmownv)KTp#@15UUV$MgI;K*-P zZtimFF)yc<5%0HC%RP*}MOk^Z8_26kMn&_;s*)aaKae{sSXJ9I%gvo_lm2;|{NStP z&WgyL;kQ52gkRrba%V;4&I;pi@q6De#W_XYT6(wKZ_SxZ)E@kCF+7Yv zue@%gwA@kO7J1!T)|Himc>rFDl7kW-icg~va(tnl@5#5^NKPxVYDP~m?hfY4sGD^2 zU(l4C(caRs_qjQvbL+1g1)V+~p&ksK^M1}K)LEp|Lz;ELTH$hArD7!VSeH7@MbyON zmk2+W$*twbD}V!8I4s&qukGOM!ije&x0V;b8kl|kZtyrFofBLDo+B&CUy`rZxqZIA z_uJnKzXf^0xwyi@(JezCB-pZ36;+!LRWaZ~~Jv!W+9~Ox(vO{~;a1NOEdEXpb z^iB-CtCq#LeUq_iZxNq{C41;jVutOJb>T;?A<^DW==0oJR&SU#s&k92u$(gH&~g^_ zOSJD`&7zcV|9r8#ORP<@iK^py7=12YRgKIH+T6iCU*CU<-#ZyceabG%uBvT)Uth`w z__lJRv6VHFShD}Y@#Zpyf0iQagM;#GE}>nJ=OoLM?4Iu(A$PC~+$Ero)}M8TTS*Cd zY-&W#1ZO@C2l8K33sXc};avLQY<@?fjh>Oel8N86uOfYMDqqEy9rE|sF|F}N;y(-M z+bDEr4PJFMDc&`5_Ja#tIkT#q<@n2oXyfA3J)29qTe_Kko$^olBKR3e>FGS}%qj&xR6 z8*ut_exi%M1M58*wq%!qd7D)^F=a~>0|eV^j+$RCIr{QlXF81Kw)e*RMqn)2sCwF2 zwI$AS>QWw3Up2}_vu=$H`2AGSPe-E_GkuOa6Z^m(FZH{(+&T2Pd)8P-1LuX4bLx6L z6zgQ~-MxWv#1EW9_TY*p&IQl0u|dTZ1%dkphBHb{zUx}SFc1wUfxmy5-y0RYACIJP zCm55Qb6=PBoN{A{`@qFl?O5OAR@K38SyhP@mh*=)+t?AwLtl-~L60!kkP|s4`q2Ht z*{EWt;_jAK(ezNB9ivXC@ylFaG_TlMu(GbQlXapWX$`f38pW{nhH#vmJjCGK{0Bea z<_)Vdm+Lv^9Jb`-p|R1(Qt+AIj}OO8J9M(1f06b7WIJ>G)cjuaT-fWM{`5R`%X|-u zbwHPc;bHL$1Nf`ec)LrTUGV(l0htiq+v#_OWgEOpzP}f^e+c@UXKN=q%c=DjUB1Kp zZ2TM8fT#S4`MrnG6Xu!M;6Y&0Gv>UxNiH283-qt?MQ{5gv=Kd$jdo1!l1r?+?}0YK zv5T#c>r+FE6b$fy!Q1wc-~-q5IrK}r$ zJ8;j8wR4u|TZElb*4zGx`#~HM3-q7M%)GSxc=-Do&+S$X3h@9wa<5_xe_hxAUUpvi zl?hh;OXu?WJ#*~{M|%(Q{2#)s3-YY>!}-|p`j>|AS^fLI&6R&3udC}1Ah+1!vOn|{ z4b-pSe*zt0)t5W+gG%|HYgygc@TKIP-zc3kzjrx(yn#Kic(0cF$!=e2S$ifPzi0ov zGuAhj9-sH{F52TKzEPc=-%E`z&xTUZ&Yh%pdHt(7N9qW^F6*lg)^1o?h5edsU|u@E zx1MLr82BuoU_QS9MT3>;pDK#w%~)z&ed~V>o`KqbHftrD{@juJJZjxO!$$zqE4=LQh%uWXPp-kYZ%wl5+2`^ zpa!}lJdb&e2cC0!@&(Uq=5?8in~uwXc{a6r!0}IlV>0)qc=x7oZ!$d3y~lJ9JJ&JE zyGOmC>I9RM7`K=j_J(s9Z$i&;;huWR4r;2*JgH|&6jA$(&aPfi;%vmXI|RM1)!A#4 zy*W>gw$;Ih{n~#v-Tq3opJcV^EQb!pOfcs61rxsXXGG&kC+VZ5#7TN%&Qu>onSI37 z2QlAA)ko1u`gpCxS>W|CNqtb05QL)zet(Jc<7TN3>JtW>3-;cJW3>7RXY~;d@7qNm z=KPg#v`u`enp%IZEsuxx{T;9hM=cE>uH2n;d8CDN%UT*H^ynPIc}7mcj zC2vjPm*-gP*UxDQf4C=km(_a#?Sk@k%zG}o-BhmgE*0k+eqYpeTI}sj%&6zAFU}=d z#TjA)^)*1daT5uDmNbV=23H9YfJmNt?Zjb3(8Uv- zmFUY3*1|VP57!nsC!m@5Gx0d*+fhHt`gnd*IN#N64f(EaL&nOHkMf13PA7DHQD^7I?yfBPtBelX@;l8 zE6ok!m*O79MP|%sp>sX?gampei4JILfJe}E>t!>W3Z1>kRu}g=(OtU#BxmDHv8~>U z9BMRk-7~oBX=?%d=YYMhJ(8iri^NNub#emzo@lV0bxBvkEtYSDibR zn2c;R+&3o)|0l5<$(pds7jq^${yfbbb{08z!54;pkK0Zz^i$3!n|@{tctUn`x5l`U zF=lIw#yP6(PTs;8Gx*NSqFvqqv@_RSJ~uFwjB%<(JFa&CLyC6j!qK7=*AF7EqFu-1 zA!nqn&0wq%(5|5XniQsKhYoJ`?0XA#yE!}tnz(jUb}&f0B)&0g*u9ePChTKNxFAJ4 z>JK(`anI0>`%j{;g0y=C92wdH`v_q7V@Q{t6P=BQmm(EAKBD;2UAu5=S~x$mhWL)T zDXuX7^cho}mB@WcR_iA^qoGOiaqoWR6el-$KQYl6?$eAuJ_=uqHL_gn$#SugwbkL9lwEGMB{*-RgmY$Kubj0%(_b__~clnr<%}t6UcE!h-MQ zPuy`_ytu6AWALIs*MrExZumD+MIM_y-S`?6_PQvkA^DOs9L}Gu_ze6s(EJt0Qg70G z5E{xxHzQwLk*_}ZM|!YRbOA=r_3KuySl3Lfa*tJ|Gm50YJHwp8N1He@Su$#Pquyl} zLk8_pxs5lEH@T|WMt|HBt+H%ubZ|bVZ=GYzc;a`?p{ydOK|NTy-c!C4AABfs2p+7} z$D{kVGKTaAGPByvRZ^$k?$+nIxn*iJ!5jWS8}Ur z+}Q+Y`K~4ZkN} z$jVzQBNMD85za)crC;NN0AGT8u5O&<{E7T=Qez9x%t2=$>A%J?v}DXZTvHthI&*Jn z(igii-ry!t22;_ zd+KJmHZ%zzhM@n?fyd;*!@(S*n{PU8|1+nh`rkdq?Z4%c%>H+rqJQ~y;)N#QYX-I= zY$h>|@iTWEIk!^!SGbkTmW(1-!{L%jPxpwop@V#5A8&$7I%)1WYm?weWa)~a{Pcyx2*DOHqwX~0Gz4vT%tN0jx#Ac`Bn~|EQ;oY~Be%%Yd?u}eK-6?y&M_yM9*2{qQqxtV>x&x3ES;K8>^`7>V3SKz^;vAglvJ123r5aU0E z-=XniYtF5FotSG?fi?QchI3oX?K6g#j~!6~?b~AQ{J&&)g@J2Vc=bVKRrz)USM7Pu zcHmN6tdDSXu3(r943qoQ1-WiR@3!e)H#B(?UKLHGU&?c-wPgO0g}D_;aPT_)nK5>r z=RC?7FR1_V)PogA+fJ;#nmm5k<-61HNuBs5Jic|KzH7#}7M+f7;FAo#37?bZnk<8;DO#-Y|<#jxbO0li}I$1b1wsgC|tx!h4;I;g1PcUc}~&F`iyD zcq%#Hxi-KfCeCNxPU?t13q79bT+A7o;GJ_A+Q=Pb@T_#co^xS2*M$XM zZ9!+`j?9FmJIELH=Q|exi}qCdaI^`IEZ&r?7#v6@0&wU|Fzm14(=PZ__Sf*K$0x!; zu)W@QVQ`Q*-{EYLO*{ENFt40k-?wM$`@fx@*2m*KeLO2uAK%D(dNTAq`qF=o4YI@f zMlNgY&N2I`gFV=Z%-3Z+FCs^tVI1XQkk(D{l?_)- zIv4KS2|vHF6&>_AeAa|6kpHlNcLy!xeL>Rs!_li<)Ck0(x87y4o7}i`knxS+tuE|w z7dp5C+0t3+UFh8|Y*-a=`(CkCweP61sJFCe+K)unY)3T zKG0)synw#YHRXXikmD0;KLEV0u97YgOu_*^%--tg1dnAN%}vrCH}8)ngGWg%)`LYbb<3ea-Rm*TsL|f|JIfH z;vUy_7Wd>bKjX*A=0dOC$XH*EwU1hq3cg)kT$5g3lE0((AG`)#nwFef*}UT1O8MW9 z!$&PkZmL{>?92AP2JVc1%GfO{ZmQgR3+LHmj~c+w3Tv{n1YT+4dK0p5-i2D@`Bv>| zKbkyvZ(^=hwbRS19fD^J&+UGnkrflijVGoAmb%%QvU00gdkpy30U167FPVPkyx;AI zagz9E!gVssH@ks;GJF<~7sWS`A@g)jiSflmXA7SC59%oO+xIyW@KgsprF+Rkmh&Oi z+@O0GFelmXl#dehsXbbhb6+v0Vj9IO-SBBQus#XzFM@7ez#E4z{k)~t)aA>UC_beR zKYyvbkBJw+pLoR3pnkGrk92udet9>~X`I)&zra4DTRav+Z{RaD#-OkCgnZ*$ZREv+ zPwzuX%R|6+w@WYaUh4*YJhhdNH_5}7M@#u~<;X*Bj$IY8=T((~N7_o64tiq|F_x$20z4_+ zgWs_$^y#e!bo@?sC}3BD?S)gG`6x1;(WX5uKL+n%U{D;2%qpIoj-O=e>DxWo4%jNS zLpFN#9$4)s|2=#`uH57t4%HA#(TC!SB=;0Ay%ruC|0X|5|w9RK5XyxX(kI z7x9fUXp@0!cRb0J##8=bpz%1LI@1>k=wk7A6Z|F|OE!&NM8{g3XFLWO@O6;*FO@HV zzjn~(PR}-E=-wJ@Uk1<6f64~5%LaJ)39ifU{WyK7y?9o#r2Ipq#CA+B-^fN*{6;&; zl=7*5PNSLmD=%V|+RmlC7tDuQvZynPjO<7jk+)!eB3R2|=mt!?4Q+z*qId6(i5=f# zv934NynD*{s*;`gz{)$il9Q2-#pnk7h|Vco!>342gN94Eem%YtHKScKtlq90xwedJ zD_ockP6MXN=!Y3bKWtl3Tkbr~neN8kPSG>;!+lpjuh;aPDMmlYarf z(#g_|(rvu2q(^pL?^=11TpaK3ZtJ*@n4lrrru72oS0S6N*e`h>aRO^gRrD2#-}=;wGrOkRqOfwnulo7#j}fO=jYJ{kLISins_MCW_S+Qb1lxoap%5E zeo;UMQaMO+0rF3^UeK|EwPWQMkVoNsBmJAT1Z>yevgV5}F|u-q`|^!=PZxR=-^s|Z zei!q*xaUw@@kBZo$9v-8<6QE;oX-N@u&Hh0g@cTXf7FJK?mY+}ceFG940n9d-XCAF zz%`6-@`J#jHBPNfG%>c;Cd5l+$HRwrd-+n?&g21$vv#d2`MknYlHUvT&2>ZPUq`Dt z4!HSI)dBii=*Wmel1a4TBO z1<#^e+4CkT(~iNG7lt0jZ{Mt)X!{FA&|&(hC) z^~ZVd$Z4E4C$+gBoQ1#<`wH`Q_KwEvgulDy!n3s3JEgkN8=Wf{dl~muM2VwDqYv_W zx~}K?ja{c**^U!k4W$i)+0oU*MtQQ=&IP7pnfKAXb)X8;53td%P( zo7NOoevFtiaSOb)%3`fX`oNm4TwEvf)VXicW5|kA1|HFMjanPf?{dKjekOuL`K5xf zi)R*sP<2xzBxZMfz#Nq%n;x)Yg4m&C(kdm=m| z{jK#GjitC$wAg^GO5Qss@!WWiW<{dexD1-<>>!;ZWM~G@ccCY`X>VwD8}wLX^>*FH z^pW3EkFHdVzvAZX=sWX5d+L^gwQ-8ra zkl^TqH@mnl{`}vvsm+XiI-B|d$;bbYP5l7x)z;jlJO7$Cbrtg(lud;p(Wu z&;uD8h=+Wesy33ta{3z3rg}c7&Y6)4h%tPn|`K7XfT8rO-z1x9IjgQ%?bvE(3bY&TzNHFe3?C$#ezHJBcNB$&kc(`ebGuh+eB-c%!MrYvnd*dnI4EnRcZv3ML zVzp#A+WQCI1CtR)G^F#9e%oDM+g}f&?G)CB)bHLwwB6vf{m}qzMK9y)pclt6WyQZIS4WggfgJ{Fqd#92UGgjv*@+IOO;fZy=O@${z+!eW`H(d&z{Qwa+E-Zr1$!e zJ7Xi^S>+4mGt~ROtEY2T0zT!QkYJfPnX`&-Srpyy9ikY{Ms*{{AX zJ6XTPlbfX1RQvJkU-xZ(llR{Qaba|Q)N)z}feQy zH4mqr@#?;=&3>kaT(~DYDVr3C19i{n8*mu`&ZP7UbWu#N8XCoc8bdh|(MZ1)cM~^1 zSW4}|#kuPYWbzj@Bo@_F+i7Jjh0_CiNF?NnR&hP0C{(lrxLLTjtkas;QN4~duC07zs6>G!jSiF&&+EkfKj+^fG<=pBOIs}Ca4dC{MIzW z+Bf+<7k?T324qid$_LSA${^ZIJxv?(2W^@szIq$No5|aOxvGt9rhIzQ_#ci*cLdip zg!go9n5l~b&&o+`g`diZ_gbhSYB@Ba(v|6SPR{5!`c;lwv4u~Y9kk_)6BAR&4kl@{ zgFgP1oT>1vPq3!r1acm0>{-_Z=w(D9My9L`6`{) zG`SVv5TELK$(DF^1M4KduGQS-n+TWGUG#S2@4XK0`X*VuuOsVNz_`&`yCIwxBu##GKhab>y515D_# zMuI2CRBq777UL?X>ibk)Zt%hC`l-%sRDRFE2Av1U z?-|$@09!Zs6fPCxX&lvpD3_;LU+>rIx|;Yyc>#DLMh-yroTCLVzP$35S`%6TUZlHJ zqoe;_VfunD3&^o%$G3*>y*8@-(>ocmV-wN&_@l~qUPnzzj#rcNzeh9e5jh>rKJxz& z&3?`KR{s^6J;A=1|8<%Lbb`N5;O?sp=zsy*^!FtLdKTVR>?v6jpGbddpWtWFGvYbv z5o!^(>z;HcvUHgI%ToPU3?lzZH5ICV5x;BgCYwi1KAv{UlPMpgcjE?iZbn_+N#OY- zHt7E*c>Vz${C^WXA5@*`{~Ep0@e?+^t*O1ZHB!&JqTma7&E(iyzFSUR$GMqku5dnww7^5=TDxjkd_|IV;Yaqy8(ti0sn&aaNnD z*VZ%T*6Ibc@#2m)?VE|@*sCLVj~mUsQEIDsAy;k6l?g{R9(?3^9wg86U`7on{-V~V zOpT9nYy6grVIy zZK^F%AIg`(8z$$wK>w%k8$ZIpK7)86h!aDblGP2sZupb`%7^)NX4vnls~F$V@Gr#= zedSW;+I3d_=zy(B`r z$fwSkE`CVvu&bmSBAhuR8I1@o_|xR?;Il=4Gj2M5UR7C+Rb^yRZF%34^t^O@epY+d z!P-`>96x;u&!Fq8%5tqLvu7>Pr`q64woxBeHH9|hU8~AQT2<2jq6_Pft{>8_`nWTx zuL(P`752W@l@G;F?&iD#@n(|PNc`D_zx+CO%E;&)A?I~u^un?DspMKOoS^R!@e51! zJv@Hl`C3OA*Rva5h;Yt=X_F%x_p3RHqWa6HV9sPTjMmB-LNb>&YF z$p?mqwgS)NthMZ5ePsu^T-8r?q615*$-nShyc>e`8uWtMt6}zUO^&8wn43A1UADDr z5+88U$-Pc+5yyx2Yv_1(yW}jw88qTs-{vX~*MB2-(6w_4`D>fAage*@ee9h8$EuYz zvI5PPYu}bN&AFC!7+o{DNn|KkBiI+#8tyRa-qDr7? zkqyZ^ehYrDyCxJlF>k;83+Y<`~l5Gz>j}FI*Oo_x}UD_ts8xCg5Y5 zInt(+HhztlpO-fGn1}YMDbIZny8fTzia+P$6Z^HAgR2=au?=u*AC>f&YMso!fyasI zpub`V?PKC>g80{7g0_O2xi{mRi5?O5HN;l1ZvotC?l&`s>YdXZ>EJE3UZm(%1bv)WTD2`&OQ^fdiHOb#ZaeUK->hxo#e ziIq3P_V%h~U@N?lTwyu$z)Oel!d!#v_lnznS}8V_4Twmmt?<$x%Qm3dZ!PQ*Jm|tQnd?=t1*QO`O$)*<;a*yz56xQKlKQH|ANQhlRAV{ z|MVxHMY=%qDkpy`*xmKtv8)M=>yaIc({CrZlMR&K)*dm|zT0+$$Fz3wY$wl3zWjCM z$UJNHN^d-)e~`6EDenyRuBnf;f|KVo-V5G!-uu+b-sQGF=JhONbTNkJA|6t0bP!JI zJv}Qqi=jtVPaNaupOkM%zBE{g{FEP}Nf;uLkz8HM@*?ba;IYM`uZQm9sZz z5qoo}S7_^^PxU`gzc-wu-vv3|UW5Da9jP^HX}`YJtd*|iKRDNaa<8qLuYQ-=krBi3 z54+d{7bmABUs3x$gnPrA9NuDJqF(TeDrq$57W=t#ZQEnwi(dyHC zGjx!QBm;ad{25)I1z&bM`2rsmuXuE>hR*l#-{4DpqrFb(L7#^f!GB%oBlz$zz7G84 z*63=Ub+D$6!G^M+h9=@c=?TMw`u6zHzB){a5pqFK6~F|R+V%?uolvXQ?Z`l9KYW^gF)xQBxx^NCm(ZbnNPM4u?3!{ zA5$OV=l-<*)-f5KMD2k5ed#aRZquLiAGp#PRwi$dHCEE&vA#anui+e^D(yRzZ=>}D zU7ycdFS;$YzhCFgXz!BN?)*Jp8q1Ffk*mGcO_w-ph)Z?d{}`L=(I6LCBe^ebF?9** z`(5^FWBbbat+hneMiFnDb3n|xhDX0X;KQz)b&KISR__SjjWBoh7tEpPdWh>`U8mhA zy5X4Ot{U6t zUHP`dnXl|0ur^8$>b~+S$~&aipKDH$3qj7LYka;oyux_m>xf@B!nkJtiTIx}4Gi=z zzd<-kcsP-q^o+(T z%b^eOqned+TkD--YC_XXtgLd)>g{KHPM8AF~34L(oyW5V252!s_|kKwitF6H@!As>5iOvRk5i4S7>;;pZ)xpx=X>}J2G_1! z9roL*FRg>i$2Wa=co)7?{0j{-@<0RdFT6>-PrOE)oIr=AYJ&oH!|*ixBA-F)31%J0 zvkf}yNA;am=p^6P3SMN)xlGsKK?A?x@yyy3$ybu=$|sAb4WI3t?u`A3y~*fF?gwp& z`q7@$pr0h0rg59dLFs%`i# z$l<2zYgd==z@{k= zot(y*dF+Fge=l8UXdIp4{D-H{CG*nTeej*;qVqw%NlsC@PUW2cQNJg-dO+vh`Pa92 z*Y!+tQk1nB7Z)0*ZyNngA!gG#Df$eMM=qhA^6LhkrWsC)4-eyZa$SDBVn^lGJL#)) zig;gp)cHM```};tBe*wKu&AcVpCj>(%g5?dyzA-4f0sS*^3NyME++3hB&EYsehdAp z?&%P8OTZ`E)3i10?xm_>uEK4QPlCR|wcti;qrp8^szvIcFYT{7gzUWrEWZcm3Ff2o z-Tbo^zR4V~@cMW4fo=9nNyZ|?z((ea-MnK5vM66SSv;=_`Xf))XzhoOl8;3O+7$2i za?U;c-sbBD!xOAE?M7GVzRB0n2ltt1T>uRmq<#n^sdW-XQH zf_)tV&#BsKjl&uWcCyXHR^9|yg!AH9}@BD23A7ZWs z{%;ng;7?BU;J-2x{u}9A`dqry#Pjq+-1(64n><}CS(JX2ZufJwZjDA@Z6s(n{=&Kb zb6~D({;PAf&PmO+?j*D-`D@L!JzbxP47&9VpN}kQsbQ@?;-*EoSc)_j31cFtrkrH`P%1 z*Hh1cdp%P_zolM(k&)5WetYV>O-)}5HZ?MpGwIm-hJ4RkPJLwSAanU*!< zD|PPp4%ShHCsRX&Uh?b3R3~TrYxK|pbccA@)Q`fi+7C02IsnyH7cN|F&QpMIL|Zd&^smti=-?Z9Mq}R#%t6~dkbkeZQT55HRo42KZIegM;hO$y zy=Dlu4qrm&5(fA3`mul+8{8=tFm=v?16`!JC_;|LtV0?cxqE;N?nO%*dV@Fl4dxo~ zntDcXpt?orh!)Rx5BjzTX=my;OdoEYc7*<46R-cVZ}VHSOYH%l9iK{mkiiWwnwo6! zwraA$o%~SWPp@FkTK6vJp6bkuF9{s=ew>UiYU)G+F#Hqo6z4ownK|I2TITG-z&_;v zMOdCX9V`v$+?JnXG3Pdbhw}}u41QhqZ?oHbez2*-r9UetYUz_Y`n{dl1=f6>UX|P zJnZ|`o=oqxPA$`%7ck4ol}yWqLH`80#P_#U$GihLRR5zj;~z1Pi1Y_-{^`xW&8lD0 zzH#PTfxNrtg~I0v)jI2Z&`HFPxr*V^d*ar>%PmVjQMu8N(>Xi&BR})v^ekK|hEN@d z{A1{9e0LlChjJw=_+;R26>wzYHq8sdt6~mAJD!(cp*Y{C;UsFvU=L4b zyNn#Ep`Y@-riP&gIx@zmwC@L5?f|C|@&%&L-~L4Xq|Za`YgY_0Dt7!~a8yIxxA+Gg zW8j_Pek;eNJgfX^!L1nG)Op+Vjr?cVd1H^ZRoBmQ-eNCmCv;O@MLx69JJr}l>lB*lZ+b<(;e`99e-mI%F!`4#s$64GQ?PlsrwxKt-MRsxK=iQw7naes6Ji@we zn|RdJ-CP}QJN~d@v1?g>bNzG9HrJZow$j8bXAAYhhM$268`Z`d0w2D|^L-EN&d^qM z9j2~aF;cLWT=NIFrj}guho;%J?W;MLqfJ z^R3xyqRl*N%^XPeloKaSxE=;h*A@ej2KC)R!l{Ca1O;rsgdt@iQDn4Mf7 z%$Ujf(3zUfmYQ*`f=9Jqf=4+<{f~fa=`me%W7RC4gkCA0ggz(Z$!nlVs#Y95fqw96 z`_G}a|KsGBbY>E3h~9kBHzs%I=g*CN(0?uUBpLD;utmQ96RrHZL>FfF{B7m&lipdb zXKDS9c}b3x>*9L6oLDVV;J!yeaf7kR@X#L4VmCQZWJ>D;vhhLt9gG=W8$j*c?<*Yn z3Ev_2tu=xO`%Uo0+Te$_1?<6>j7jH4h%d@)tJm<#64kY7eP3fBugFbg-+Yr>YHIk+ zoCLLVi9G7+(7~#wldUoS#tNNH#Cq3j6;2(sXX-c6uxpOd!|;Oiu;ebNi-Wll;a2)q zV@v*!EmM<4p55p(n|wg&HvB_xFBx@X+wP5C>fF0W>osYAH_Jycx=}ifnyR*r%PePs z_xv^FZ8WCvqZr-&?deeF@*Uz=`OevM$i#W3uBQ*ibI)1+1-Y;c9%%^ZY>9Sm$*f)Y!t`sm`wb3;O%~p!&<4%c<YZ1{Hi}1m-Y7d;d4t`v zjouvrzm1BvZM65ASit9RtDE;X!lUvV{A)M-!M&!K!M}E~*G_%uGvJxRS$14>k=-@^ z5WJ<_%onNuw=Csb;)^2I1?NV1-*=O@=0)tX*Loe^ zH2lQ6EB010(ZYU5FLv2hz4KD%XUK38o)T@#se!;Ib}5gUv98*XNe3VH>xscL_^BqH z-`Ct^vu)dMO!K%%X6P&_(OHF8>^zRPNK{0!tupbfX@8wh6xQF*0p}Tg3$5-yiBi@?CIeX}{h<9$> zL2UhXY9k+xybUt$D5vs4u4!$iLwO#qFXaF6@5^p~guI%2rW14F>_?Y&Wfrx0!JKb6 z9iM9cX0PECttXR{#;0g%AJ^Ix9^cx+f1Sxoyh_d0_*Tuy_?ogOz}bP1+lT*PcoqL- zBfgI2rM%h&_)soQ-7|_*)2Vv(FDMt~%Pw<>D8J_IPgA_FT#Vvi)w-E6W-!KLe~fbj zW3WdK_|8a;L>YXRTxR^Oz2(hQoz4|j??SG1-opAG?H0wRI$f)v$!*^MXZXK{ z|95%MwDHVZt9PZIxyNhsBic03=05nPk^I6}`ECaf@akIhhw`&c4dH!}vt1tl=l{-@T;D z`5Wk{>*9$&@{WY2hMRhRf_@6_6VQ7tB-`lzZB}xs6GP{#W_*-%Jot%pSe!FAPQ2&( z)6Eybd-${LRV|NQ&D?TViT5;)&ZpgR1lWgQ)8OyRUH(=+GnX+EJfj@?G=4Xs=Uh2E$H&D4?dBoa+ZJ#@xEY;9A^ID&yz2Aj?ZY0$k#2*@o9ICO@TQI&jH!mR=q7Szv%m% zdzs&FC0q1=Ij{!T+rKC};G?gC4jHo55ZR~q3MrmUJ|1$kf4H9a+%3oc?32$tH`@Ch z)rZwU@A)ooCusLH_6-LDToib?C@{F_m=7=1M0;PxhxOOe43Es_`iyArNMiN`cBW&R z$2;IUF(pbJ9`7vbcKIeE8-xvsc)80S`T3LG_D1K?_D;sv+2z^eOE(bIa|Szo)t#e=hV{9Vh$^rLjwG`wxf`}8DYGd=tv8{PW)Bn#bh1WAcKe6%beT9Yn~2G&_0jnK1Uz@=nT&q-SM?KsdpKf zK6YN_T;aVxB!M2`{6X_BC__`|C!H_)N#|R7cUTQE*k;caaPGVRqT4oW|LxTgl2+Sj0Z^!(WIC-J@6uP=XEwpFmoSC-x2tceP(Z|WV6 z)TeX)<(SqT4P)3_#+(}5b6C$upU6j69(IMNTO{8L@c)8)0=m&Zs*!#YJm_3Oo%_&* z4$(VzlzZw5yS36?=nS177{?CzwOhiM{9*rW(c@3b2VcrrmpNu_!mPoD(Q)wj3S@Le z)8$SfxE5VgegHn5zmLtYJJE9qaI2iBdCq#j6Y-y8yl(n@9b9zLMtMT5O=wO2DfG&h z$;0S;|JSeP42#9gVF|k8dT=&*=j9f8-NY2WIm_Y|XcP2j#cu}3^l5M|95a@#Np7`< zuD#CBz{43aS5095AGnfVARDRmDb*xs&!7BU_1P>Pdxq6jQ5QEM&V63XbInFe8dzEz;P#Y&cFfJbZ%%6 z2WxyBn6}`6`x!Xke;NlLgs;7Ky0k0K4zX{QIVt`D56bEPGrnP}?ipT4fZL25K|?w| zm!0Ed_~i#Yml2W3GldFf<>kKAz)T7r?E51nYXN<5fJJ)*by4+CH?eYn?rq<@t z@zx>cX7p#%9M3NDo{R!7$1ug!yGg|>tdTA5kT0FC5fi=4{G@}B1KB2%<3~qGXS=bk zd;YFu^>XcvM=rH~B|Bi*_zpSj&B#@I>u^g7zI1JYX%B5ojad$~XTI_e#P_e!hw-nu zukm#LllBJ_vpx94&i6Z4dbO2)en&bbbRxR{%71rx+sF>^L~QXAz9q4YuE{nko^kz; zxz78s#ilLS6`LsE$a&=%`CHLTy1=|MMRddO5p7E7yA-%Xe24jdFPoNGG#^YK%dmeL z{1D(bzYfeAAg8p`^OwDPFwQfW>+&b`(>uFF&z7wBLcEmFSm_*ShJW0Se+Re`-cNiR08*thLUm4%ViyxKS`L*tW zbLDWFs7X_9=T)u+b2~5bzyH~-X*u%cFX{OZkN+VHcUk!NWo`$$IEdq^xpw|M?{hET zT3Z(`^nT_OVOP$NfQJ&!^Ev`Q>ix@R{k=xA8j8II>_-<opU4y^`ZL9Hx5@nSJ0oQJJ6FFN6)FR z;Il95*>HY*)^5!u4XfZ3UUVi^HoU=h3-hDB>9)%4g$<0s`Q=)6V>kA@G}qex6mmC` z8Wntk_yg3JC|2JyYTS&jT+8`S_QmWu-@10Oeb%0P3ar)L>?;aUr?QN-`lg}Q(m%5| z|C)lGl}YyMpLwn|dfR(_mv-eus}8IWufCe|3ffQfE$zzX|E}=rMf|^-_j~=A_erTf z=08V9tA5J0pK|R(xo6JkVh^?UqaL8<#^SjH1)W5JKrK!EJAF%4XZ2HRbiT6k z)0JnwGi@O&RQi5~A;s`|Q1&a_6KO<0Z*Xx_`~tWUj=;@MaHF~Z8Qgqz zYb$kL2!qA_aVDlVm_gZCeA zw(!$!;?ZJ$*Hh;veUo>M725#6RoBmS3L~;V1r;N{Z*MxfxSGD^#ZIhkZ)x6P=i}2| zXY>5%1A0Gt$;(HOE&7J{^Wo>b_(b%=a3d!L@m1Ib!SWU|oq}a!GabGeiIo3FDP&ck|1>;*rN3Hoj{(?EK@!GzOoeJt! z>D6oh8*}{Oy{DgJ(;(*f&iaApc;#O($M<<{A01$h_xv~JxPN{BISOyGTe6{&%R44> z_VMw%_b2{1?6#-i{Xd~&?UDAG8!w2)&a`sB99sG5<24VIUUaMV|9oTdF8fXN$I>o) z=dTux+WD&=k6hGvwVnIr;RPf1Tw~wV^O${hw}tM*UhTt9b?rj0RzFa3(ITFWhpp$H zE~Tc<9@#ihzsu37%3lf&d*!O*pDeZZ-A~`S#7GnE=!sJLiSoISzDDwS02rPIh6nUH zQM3PnksoY)Y=U)|yxP(;@PU+rz9%F-l&LEoz%O`!8ovkd170s?eP`vA%8h46dpX~x zD!Tr6b0Rs`zJ=?T^=v%T^}%a5)X!PT@45l{3i6|6HN9kwtben(B>aj#3-(_2N}g>s z)>X4-nf3hIq0v}m2zg%m@pXx8+R&BuCi&iV)mD|cKh(k}3iv-JUvYNq_-&(N@7yvv z*0U@R{hW`kFF@B1ao*$vxQ;ptx5>izTY@|_qKetK)e=buj25SNGTr<)trJx%=av}}_V9^2Rv zF5>yZc+C&{HdloU8{Z9|6Yr(}Ci>SNj4O9on_9vjZ7s{OE^Rq-ckBp0?8?yCJ>%^0 zdzOWByLUsQVtafy`hThVTOS(VcrZLJKF#zuuCb1`>hC}((oODb>7B>>p8JD%13#}L zJeIM?#V0YZ^Y|P>_x0Q|&y4Y>j5dX-Hox-P?Bo6;LwcT9zN7;?v*NeJQ^59oX#Adt zJ$8>75B}W|%7(2l9sxG_k&6Axm~#oa4#hD?7k?OfEQKyhpwD&C=~`^(HK`m;8s;=S z&wKDBf#(Q5VJ~zLy&l0fzmM7k*}zifHiO^1Yz!Y08glJ5=6(oU^tHo%&+X+|pT7R} zFLBL=Mv56E=j*X&%7@5ymYRO^;v0S>J^BOSwShYXA5j~C&!F}b8GqRv>u?AhEU?x+ zwCrrXe}a90$nES)Hg1uBSBKA_{bW{7^y%86*3(B9*U(Qj{mEzep|Qscj<4IlJ7PVz z@}YZQTJXlY{pAavee?eFUVL-mL$AG=yY5BpjoVX}v!F4T+Lp+P>iv?r2y$F@{_ZVB z+H>{!pFhf)Q+G48FZ+Daqs67xGYk0587t1wiR35_uX`x@=XK9M5*qVl=j_ME?SN0p z9(imWZ7wBOTrr2U#B0A~#a#LC51;V-Hth~5_YYp>8~sAR*%Ocm7gp&1L9WlDADvAt zA1L`=&He&wOyk=_t$lqLu-?u64)jOv;dT2DB2(=1-}Ce78@F5lpM2(HR_v}X$RF!J zw&2UBUto(ZH?c_nHYe78A6~M-X$ahg=|}r+E$4mg|9cCay=;W_OfC3WHq3fPzTBo*y0U{oXiod$hlmw)yyo zowP5feHHr_^?s{~;Yeedoxf)pd*^>Yq{ip={+bbllFUPSNi{L(ISUG22iVSRD^oO$^1W1vT5*qBDw zugr^wT|b&^%u%MV^pEGpl3 zgEzOMi#6^GEfdAXJE{}xQbg>1k2)^9yOVSxS%0{s^ZaDUWt z(Jw!KDf9T?Z!&#P(Xcqjdd4E|RvusxJfwNqe>abM+<7nYo~py;mwb84i{P5`Xbv|6 z_j2TJ9{+Qx$5w4$`4DU0+>zEZE!@k6&v=hgk9hHWpxSlz9Mb(-u>|-KlyOA@`B2TkFg`8P&Z}d&+QEXm?;;(NU1lOF)Sx?_V zKSOx+|9|M!zM^mcigy9MH;l8G@J&WrRqlTLOga#iz@L zcl(O+_C{kHp-DEsF~=0|`0ekcz5mCsj=As#h2^l5SlaQeSB2li`| z7qer>$Qh?>M2+PXc(T1?DEgWF#2vqi#{M3>Z1(ivt3$2B$2>jQ;pxEybP>JAFuwHQ zD~#VVw4hPA^>y(m?$z?#9h}WvJJEJ-*Y`o&ksdroj!`<&7?1omv#%3t z|2g(9d6IW-31M5p*q9;M2+rrn&gcC?ITI`XJUBas?H+@@lMOqD-IQHRAZxuJ8uRF^ z?r7|vnalU9T^si?Y~0!gd^6^_ykS`nvT%46_O%wDRk&^{C?b|cgmKMgB&^Ja1e{sy7#Fy@goynTGks)BjhghmH zTeu$_^D3SZUcdE1-{yb)mFsUGgVsTR+s8qfhl97F^DJ;s7b{2YAn>OuDPAW3{Q-rR%N zM59h`-V5m?Nse?S<6PPS9m0`4_`~t#_%iU`VU6j}c{g+R$J1CEGvdvACG)N~^Ulmm z=DqCFUHS-#^Yu(S3-^`=a~x$be}7SCDsACcs~?3!pN-09pg3qE>WwbbE|^Euv4Vfg0ekl~wVa?>fk$y2QovQE6<^Gz_O zd&tbKzswBCH_JmQzQMn7`Gzqw_{JMgV`Jl^>*I`K~UML1K9 z;?7%L+rE-M3@?SK^+G@H63+zjh`%p9;u9?0g{(KDKZHa1H9ig_$dYi_9pE{;gWOm^ zmjq?!10G(2*Lgdt1Wwi*59bd|t`_A8)cYX>YJk zyZ$nfDf5#~0%L%7&#AB7$g|&ZZKlL;+QGhP_2=Z_YJ{4Ug z2gi`PDE?muv3nYKf4?P(?(pTHgP3Ib0J;QTFmjL#MT{ zn$`(@Ud$apj|63--osh&nkz5qxFYXf*G5R@SU20Gz9Ybxdw#Z#a^v7trq6Ut9E`R7 zSd&BlHr<0i+$7n;?ubvncEFP@t{L4&jj(VZj4gSQKt>#*KIE6x2KxDu*N^mbc0X4b z8s^8J*eRWs8HbnQrz`#zjCIJKp7mq@4*vV`bTEH?v4Q9GG57@@@7!m`P2<+!qr`H4 z!FogO27Cy8AGDm8^<8Z{FPU`+>;Ug%aCzRv>u8M&oMwLBJyUB-eDG<;FgW4)7kFOl zu;O|jwI&lkMXYdCDYHx>1Pek-OeXS!^mDYR{>9M&z)RU+ZwXRE#^<~U8}e~$AFzRg(IFdx5?)}n(<9+^o zHsm;8;d#yHgZ<_+#JNvngmRpz{(QWC{@v@RjDF_w-22UZz8_;Tzc)Ldmj@55#1_pZ zj@BA}J~8Wd>{}u6^IP}@mE=Nu$uS-a7aHGKy6PCYy5ev_cL{m8I%Fevat|Ni%M0)W#(BvR(`Ppo5DMk_S~xRFS&mU@8{i9H(WXNE5MH%Ln_av9GmaI1=o^y zftxb)TwZ<1`7q!4w!LS0=!TxOetRTCzb)TzL(i%7Tb-}p>>GMMDqgzT+IKO&uJV&> z(9e&dM{U-nf-)$$I^kFGbtgReRyY?QIWHcC52SbXobuiK6$KHct*50 z-;3D`bFA%mZH>l??fmY}8Jo6nZtR|~g@*6>fL++QCp4z}tr?A5io=E7jng04ayPYv z9q@quSEJ|OzPZ0obRKgOOt0Sj*YSxq$yT7_YoY72MbxNtcAd<+^! z$T^oa>%6?8#t6Q}yVQNjuk-U^N2p6x?lu`7A8+dETe|$dg@GK~mM!J@b;ytL>ul=7 zY6-~EeT>_5v$gLg#+4jtTwjjv0=LQGMQ&apbakD&xh6b$*y5;@@Jp<*S^iW>7&0*x4P@0ap$UyFXh#`I;#9T z&2Pqj@NaM4|K>YyMp*k&z9VvC{{ET7dw(t6RRLVmUABkwH8-bmJ{aBg3TK-c-Nkdh z?)ofq{HmGb05NPyG^TY6;wF>7k*!iZcBzSB;nh>du-rdw3>)BOe9t$jBUTLiB5)dB z22P)smwGx$IykM9GX3&{Z;78WuZfqAd3yX(`dEn!zKxxoMf@s$c+ef|@N*tc#1GPa z8b|!lf&Tjs;F6x73a+m3aFreRMb3-HlU7#GE*5LBdd0K}n*SYB6 zu>8ao`>#hoC@xgKPit0v@TGjtw-`&lYx7rb>Ph~_^@xYts>Q+#UG! zFEgiC$hXSY7uyBh7SC%f?#JkEyFB;NjYIPrn<|L+vB@9(0q3rS@*8&>Tbvm`3Qx+3 zeb20o=Ebk#++pG8jen60744C~pnRC%w6J^YQO>B2x%)YzTJELS^V0tF z#|>Yk|7)(2>0m8gHe8bLF65g9CZjtmkncG1{bl4kO~d*7KMW1mpF*duKaEb^7|^NH znN$7E);{v#CSKhHPwoKc@=g4CD)ut-OyzLWxv`1fJpH;pSC6{6PWdoz8y_Y=zMpr( zxwLfkfciFmx7WAtcb{qaz94=ZJn$utmZ!p5E&j<&bY20xE_-A4mU?-ccd6$#^bEN^ zN;9-d`*VN4r7V!QQI7U>c^kp3c+ud3+>Kxk<^>NNl`cr*W-v65ispX|J)4i65&ZG5 zfBk=q*53mkqV-pxh5Y?Z8~W2)xdJo4lnqVK=UW5J&(HM)X}!ty3l6J)>3pBoe&4>$ zdl+~m??=%QBMhxGYbh<{Hujl@m!xOzFMsyU&X4~5P4K^`{A&l_R1Jdidvh7fr;FnF zAfNhcT6Y7t|J$WWFm9!`8=pmM#2wILd1$DK;lKN9R|hU1rfVT*KmTi4|EvuSao(x1 zHZ89n>XbmQgFzdXd-M$ldV==IyoShYVUM?&8};94EO z*^AZHgl<}19Wi}R=Q_H4mTF^`zhIl^|IUA&b{cE#0$`yJ!E%Ax+b%4AJX*xsx?;(n z7?|CD&!Jrtbz^$oZ}+~e^?$`6!MT(fTII*@hgOO?j^AdpPSbyVNwxDkvE9PwuCtUA zaq+p#gS&K`i_bfN`3r)hh&AQ+4(#p>msQLibc0c=h-_PfL|CrCr-1jsr^k)^%Ff`nx&E zC_Rop8Q)aic*Z2TD(epLL;3Z#)CBgeB**7H<-PGn5_(cz`9R{FFX2u|_AKID$Klh> zJ6vFY6Mb{ZUT9D#*_+xk2eTDRTZx~yC!F+ZLIir@=EWIao+;G)A2pcGcy94n72jy ztUMs-!SU9e4&;bzT}asy>f)<%`H96k+uy&z69K@ndlhmQn<5ygGL}zJWY|(K6+sK=qQw;ru;{vj;r+s*L){M};=jDBN z4d-t3Y(?nVzw%6YIcd-E?bDtZ{=G+d#&;`PKk{~H4OPX5X!Z5$duVkT@0#NxGSMf#Nfe7!onm+2o>-7=kF*u8N zoEs1-DbZiLF;l^UnA)> z>k;{A;_^&m$r0K*W#MbcS@@Dq^~-erQx7z1(YXl@MdzEKd(k;}i>44qnkN{pp??Se zhtR*qxRCxQ+lMCLqfR^H_V+uv@*1d=arZeBHR7 zz4UXTxSl57E3RiRG}$}$WBcE~yf;S9O*yihlVwLlPE*{^+VjQz)c=`v{uOaQH~zjm zed|GV$uGtIoZDt1+z6i!zSZpAV>3Ft$bmS8#MwI7fP z?mPi5YR}-V$1h;ApY-?zLbmyl&<64AP1-|=S#N3wPgNnlnpX#IhegeMM2-*-s-#Th z{~ND#r&ocubH!LCgFCH{`+)T5rLFrOb$Zp_zi@AH1?Ay;VSf_5KT2Qv6dSb8 zJ-yW_IM3D#H(B`U9Uqe7Yqela_RS9aO7nW-=^vcWUG#mS`TXUdde(6?E~Dr3I~SPG zx-f2y-f;dm=5w`$ryk$i`R21a+~?7zUNrO%;^zBbft&Ar4cw$(05`XU;cV&+=YIn? z*M3dhTodl|X{*n(Z4W8LJc!3CUgjy*t7O@bJ^$SCGC8zm**>801FXN0Kffo2FN^os z3Eeg1F;)y8JTe@^*P}mPO&`7OnvQ|kW-|WwZ8Js$K4Qru@RhMoSh^;5s35rq%7I<6 zPXWpHtT`(VC~7T!^zoP1X7s+TxKH7Xu~{|%)jvYJWqHn~h<FMG`R+^aBv1`@LqGB>~uMOjf2d~!NKO>RiDD6F@Wt< zxwmY64n~($HaiEgZ5*F=Wxs1!X8;yj{ERcQdyRu3TgR8<)i+x>`|@+~;B&?22?u|`7Zt(5 zo5**38l<;HUF#Hjd<(iw)a_tjnzzfpN&Pwed8;6<+G{FN&d z8*#QAdX@~1{8S4KRIq<&%#rvZ@b)FyI~%%z4Wq0r6K+15O$LC$BL{N#&3qp0aK}9jE=9GRQlz!J1VoE`Z}+sJ|pd1{-yqs zeK}TtDZW|2`rTgiW3v9rnac>w7o7u+2#vfk)l7-}J{OPrpR@z)Ay-*hkU(Sh@%<{{aa#@I%M#}+w5W+IyiHBpDHY^0b^PO#&&pSVSry@%3)L0^N=WV>d@2#oc zuPbNYKzU2f9u|F80zS+-4#{BWj)`$v_&)bL;ZL|P+ibX(V*4wzneNeHd;XK)ytiL6 z5=(E%E&HN$gUzN}w$r_~kX+eZ9g-`RBXMu_cPv@6*%p#3FVZvSTyxnRmbKIM44Hfr z@>p}2t_Ja}LvjmcE!{b>cPwU){?OMNW6yt$OKZ`h7wATG`o#wkdO2IS@9k?f7>9i1 z|ELf6J#qkkT_5mYfTg#t-(9R6U46WtUZdDF2RVmK>wL7qey;O)*r+F$gENh#mA4V-5p z=bk}cLC(`JFb=kJ9%u-Bt^u9EPa9s&^^DI>*|>?bk-}+xc;3A9v+2>0?lgnFzCJ9s zY~i_N&N5`2X3Cc%<3z*1k@sbD!|%R*_Ph0ZhrT#K^mtpSOvQJgdD=0N->Vq|2U0dv zOm{YM2Eod?UNJZHyN$iB;$)bm?>XA@)7xLbGhj9YOW8u!2JS$N#|x-Goz<(cX|K|SB^ zzsw6#{!sJ}*$FNIR%?B*z7K%2-gbgC-k0ZvXi57^*%hI6JCNu1ef#V4$(Tq#mxTC6 zq@T#zi1x#Gy&qbK(J>0qdj0>jJ^nw*M}8rl=1tz8Pp5emSfk|yotZCTE#xJJ&MTHw z)q2pyI_uHJ(p%c8W0E(l-4T+(BXN-1=(9II zq${26gIwk52>WKL@XeUWq~-kA-Ya=tYvsKCqYGR8Gv8MK)Ki^E|8Dw^=mHL4jP^~g zihAd#zSSRb7m>Qv;8Hx&0sj;}vRMb_)R3>zQ4-RT#7iajJP-{->VsBm9~VRJ|DWKt(uX1DMH-PJ$~;z^lF#D7MOU;9a9{IYRG@S;6F@?3C4 zo>zPgeD&>xFY#T$CH$TXXQz}$7hK)}&LV5PH=g3ccxn&hsS5a_Y4cd1hc+816HS{( zc^8pEv_>OqE{A+34%*c^wc3?TOS=xyeE4kmAE=uXK3}JH9ir**yWfY~4dwIf!`a=w zJ#xZs_PrQmjFUQZUu6Ghe+}*b)Bjuh|AslC{a^E<{Xd8O-&`{zXkt$z4~r#JdBJ1o z&d6lnr`o63w?EN0vSFvbvG*RIeLX&V_FiHs-jl~`J;S`4nAcJ zqNQkE`O;kH$wI~_+`NdazpMUOIzmg9{_XfJhN8HEF-`8pIW;&R9Q6n<@_1nwrDcNw~KK{uP4Fy+wX?*RunQ9g7?ud zynDfga2c)NK2>O@M8i=YmQ&1d-3QJw)|HG^IR7bqzW|KA`!~uA_ZGoWT^NkC^H06n z(cC{^wbK(j^mEw|fnTvYs;l3T`rkT7{qKeAuf?_}c51lf&(Y5VpE^f76uQ=VcKL0TQ=!Vz zF}@u8>>wtfqfh0!+HW{E3ZI!~niWf}v)IY)ieF0h{fyig^2^;zdy1tLt{cGBo)&DN zePt8;GVBiGAccG4=zQaU;EczAB`Ce!OB&t3s_{g)|cTSlN9y{G`+ z1T=l7osqPoHWYKZl=?TP5$8c0UUC9P*Oh*w|EGwN8_wD-u^SVXuQ0ad5HCs$-=T%% zL6N_5KK!-5Ms}1`pUz-i%G{0MGXWj#DDStSQ#WwWY(=uMQ#O`QdDdkg>igj^!+QXD zIpg3{tdII}eBO6r1??7)n~r^=G}}J3b+g7~*aA5zocp27N$fX@Z*Smvra2^_|9WnY z@pvX1LtPcXQ(z7b{1qEp`|=pC!yM}B=!?AvJCL7TcK%{>^63O)$IHW<-V?F|VJng! zhBZ4mbB(d24IT1c{a-wlxl7t5V(V@|*5&diT_1PD(L{xctS1lXuuJ39=te z@YfVs_NGnXfxBNOIY#I#Xu(@A;a~m?+O+OLr%d}%?m!5?^IyijZ}y8^dY{j^p1=0T z9>tvpZ8l@)MDf3OsKS7M4m|QD1lzex8f@T=Nj7q+Yr~&oU=KB zpUHn2_odSAF50~rebe8E*hbdGE8v#+z#aadgvQ*iJX`Q--DgxmJKVc4se!%?;vO%4 z#a#cJ&$@RG0w(Eq?Ej^OyUq1+(69fCGIBR4F0|k+^q=t~1os#74|V*!>`>+?=#cZS z=mA57*S*lwk3Q|**#X`(cHMnZU>Zw?GCz}(S9z9kHl`LBk8`$(xgU4Z?(7PppK6W^ zJGx8XHiEluJGK1K_3%eMAA)={mEZa<;#+a96InwJV0gf&eB+NX&3_laS6`w5`FgS# zo8rul&z{9voXMJ;!P=b88l8sRUUKGs1hsX7HkH#{zaJnrS+SbK$@{gynP|n-KJ#+- z2JxNxEyls5aPCL@Huis>N8jzqKHEs1DCM4Sd@WrvdQYzU&|N8myr(xRcTG6wXibb| z3$4d?nT9>z#s?VkvVr}w*O1)}a)NU%_5N7n^_{=uZc^5+a*&ntOflow#U|Ir8sl|d zAP(Fpw<>eKtt^yF4IR;Nmm_m%Zf(P{sk|V*Z@9N5b3PA$V5Gghv{zmf>U*L3CAP7d zJ=35~u+Q$690ec(;Y;%53+g;QQhF6z(koX7UF#kSAVq%GvCX4I|b4FZ|CU zhEQ#H(YAb_x6rneyxv+56?2WruklPizX34(KTkx{8O#Cs?}b)wejk`NZ{HGqaHR zIHPNFX_41BM;==gcvmg_$B}G|t0a$YF5}BBgjbNyHj6xq% zJ118R=R67dJonfy*{!nLXR2vOe5;MzqPy(h+3oMHo69-e#RmLedJSbCjFc@3m!&p zwu8UyVOBg>2fRY}*m9>{*D>%?Xzt5D25*^W8pKuqJijoma*C6Dnk9+yMa*FSrZbPz zn9maCHKNal?|P;ma`w7Exv9`=(9Wl{>8GuF+Q~ExZwhCl-^A~Bg7u+XA-Oc#pf zQMu1-UmeTiKSwN24S4Gv%hL>QDkv*onL#YiLzHI1JOAM!u*Y#6jw zR5lbIL%vmT|M7o>*BIs6nTH?nUu#UfPIUE(6;qJxI{@E^Jo}IEv;92lu8BF=F+Aa} z&zJQBe~I9+KlmH~UI(%+l8}Au?p%EKn%{}vcY4Qvx5(Qu+&t&HNDk+Bc3cw5@65V* z{p@uyI8b4&i@vN2TjRcD&cOlqEzs&Rd)lgniL9Onq@M)(Q9kahHKBMn z#2uMdk&_nM;#_PhlfA^p85cWlXdl`w0MA z6w~~9ew>*ZOsUG8g#LxzG&talGfM02Ci0$gRA1zqcQ3Xx`yG>zgT}p7b4M@({;73n ztxcVI>3h9*Gv)oa$9P}blJDJ7bMd6z+%NV0fyR2>k#q{W%3k!k~wV$y^_{nehU7sj^ zGQ{F1ib=nxF~m>&;Hr`}qVqr#z!CS5q$4}I+xUnV*{Te;@?Xco=ilaCna%mR(GZ%$ z$owU6_O4>l;z?fB1>P{8F#Us+|t_x-zPU+8{#{(>8CB->zR4$|2ur=pTO@|@tr#;Bfj&@oDknB zhwoItclz_JowZUF^C0sZP2<7r>E054UdMCsqYLq(oU%g8c9}af#EW!4-Nf$~dvmhL+k76rlm=fqz<%8@#2D8c z+ON;zOUu!{SP$&iec*GpM(zzN&0(yLy+hqEkyq3Y@6&$SwZQH@4KK3v5B{s3)>#|p z{~r2=^TBy`$Ko>V9eZr)f$9v}NDk4vayOR4SB66K73}4iKQ@<`=qP(0>eD%&awR&a z(tedYvOZMq0PU+i6COB;b6m91}uair!D+x5~8B_Q$&4k$KR1=-_u&s@Bp_ zYb|xLmY#mTdxP3geXaF!WRhCi(HgC0ZJP91U6wrufza2Qo$LPOFc!R*m(S`rY5yQH36Lgz=tctmX7^9Des1 zd%fmQ<87y(ne-FELF75IQ|WK+nd04jPri5e_pb4VbYs)KnR@%@BgalO)^9=|?pjdH z+=C-z=#d{2Pk?SL{{1-p>)yq+d}kr^3Z_YX*Ye-Mhj{?m# z#&?AMpnk=a#?AJB$!}(?^}zq+-KMwhpAGNIg)J?{JB%T(j_XP4`IkuBX zA1PM*rh9z2jeBV$WHYyUPYqS>+8FkPSoVg9|I2WX|1Nhi0PjR(dF1!fab-^LrhBW|L2FsKPdjzP3)}R)x(Pfui(}g1h>W^*z3WspZRUN#<^5`el|Kt z+i>Y=sg|5mOu4Ciu0iHJ0sVF`xA+Tu{u(F8p)1!d`i9}n=^_U&^QbnPfcbZn@&Co? zL5`4#!c)sJwh^`Nq_%+oN*O%ry;t1 zN*sCmk9}fn_xalXKAw2DOwLCm9G8k;hHQbUzBR!7CcII6_$0QxM=Ur)_bj9_rXTaa z9$FJ!Oa2jE9%WtV{wLk>nnIb17`toBAOr@WK7_;*o?1s8);a$eOgc#9opRH4U zYhHwR=@Z|hZN}@yCyBrFTJLG_6@6}5XO<M z2uG?X90|wKb^BYeB>V0GR|O98b0c%dWjVd?fCt5-WSVh-YR08^m3#+?>*)93BJKxD z2-x^PoqaT5?+4D}1BL9Rx-VB{vw1hqGy`pX=NQiIN1z4koW;`2o9wBXb61d8Ji4td zthwH8sqf9$fpSiBW;)C0oDb}h*RkiMB=j>~aqT}@7mv-VAK!}$jf3S?%NtiG83&8j zEN?WN#@*RY^X?+2?d}Cm``yk*3L2*)YcE(?(6|^qmHW1Ks(sdesdCvVk5_^b-x_Dc zyA~&!%7J%q26Dyae2^77M?PW%XFie{l)Mn}*+qOL;A?UTxN>9}lMB!vBrnMi4h}4z zhyjoIsHMk-CEApo=X=x0@dHgq?nfMh4v@t@ap`YEzT!yxnzu;%>HjO_vo>j4?fyBc zUA3vU1C!0O+KmTa<=96ebzV6~oxf2hDcpYy{oDC;?+@jrKYfo4UTueG$FM)dqFeXz zeqOA5WyB5fMmw%tA6IW^cUS5 z9{7+unSSJaLN-f1zs+9jdCESN+yLp6+2|QcnaKMCHsVFhAuY9LaMyUsw84{AZW!f~ zqRKUd%VBTnyuWC@M`JoZ`#NxNEx4EpPLQkaQI12-4(umZ*Yf3lImcEso{l0r-w%!& z-h1fKF3t`erOk}Q)K=3-ciFg;hP)ZpndU0SUkx5YxJmU1XCd6AgBv{;Zn#G%j2rQ5 z2Q;^md0xaP;q&EBT+U|}A4lwrmfYBxEz@IXwTR{e&(#ePulMts;r|{@)m*H5T>}Yb<mVq99} zt-{wsUhlv#VvTBj`1hYJ_QpeRwO3s070tb5TjjYQ+$!#EK6wR}Qznl02fkRYJQz!K zRwQ7b;Pcv6dQ#{3Hagg!!F6XG_uDW29Qm&hnk-!DdCJz$wQ0N4+x(K#`vB$ElRIj+ z*)Onp%xcdf+H8LonM3{llH8KCn_*u?{>Yww%PA9~^BmTIX!qr!4dj|MI{#Gkq-ST* zN>4}8Q`YlMjxyJbdre-a&c?5Z0w4?Hc}>GJfOnd`di8JDMjzj)Vx zTPlJBAG|B*hL69&=P2#Oe6jqA5qz%Wb2PTNC8p2RmN9*FDQh< z+n7`8bigMYDLX61z(0h(+KplTN5=QP)61WD zg3k!XcOBzf5OaOY!!b9t95!MD&uw#A<66vkf4^;o=fiDlJ$mlqdB1*%t~uM?o`il0 zt~IoI4YzJp{#ypizfjKCU5L4VTH)d*!_}o$Y zTRkh+6}Nqj=Yv_pUCVi&cd6n1h_*kbZ5Qoq=ksdf0N0iM2fDsb+s>_(o;mchpXcxJ z`95=#m1XzN=`*M0pXlRJKAF(V_hYVYamQTOa)dtKrLP}sTjz1j8sTc3HO!^=?}z)S z*;?aSLOYxJypq_@^-Poz7_aor{{nkf32Y`K4KbyBdHeEk`&I5nQBBiy74%b4)?%=uf zgev5zX!+w0__Z#SYsvu+lPn@VVuIn==qSS%#{W3vi;@L~`&PTrl~P{d^<#V3$)}4w zRWUYY`1i7(b9%Xh!Z&U~k#~q){0iCis+FGc-X4Bc0>8@nqWdL3{+hPxzvXw#Hx5q5 z##3M>2Trlq`Qbl)+C0hL*9H%&MBjVJHq_c93x_ZFUW6~-c2%t>E2_;}wRx)hC1=gw z@=J)++NQK_S(E3n;Yjrn>n%QvN6t1Keh=!<2%sj+pTd9=_4k0X{_}4hZ|k8cS!Ep zy^T0a{>%QYyDyLNQQV$vjK>49-UQQh#}#5bATFchO4A$1I2`51!4BC*v5CHE%M5fV z?t=RH!;?22E9Uv|ko|w3o?qr{Dj_CiPc`>=#E^sFdS}r-?)P3!ge<{#s;x?9YK>@=!lSJ37yDoc@>F zFA8ucrKMZTH(Xfrl;`xbz@~rW9L)aoKWj2H(WhShAtn(8JHf zd*YDo@F{I`>|gK6GmW{E%3s_lnd!DUFK#?Wf0pb7KdOKq!J7`wBR5z~^7NA?css%P zPq9{?06&V+D&OMtI^m<_8@Bp-kNJzpdbQ{(k-iq~_jq#X>*|FsZgkSu(7RsTSb|K} zEV&IH`WC!-xjiE=HTl+)<@B?R_Vbwk6T}^Lai%LU(uwbxyuA3}GM45AMFag7=1p3O zO?z_R87}GRLGGr1llQsA%Z#Od7w7A$fUlCfq$YsZQ}Bji@Zx`qf>C-`4my`;Up|Br zTZ+BvPq9A!ce+b&28JoLrT6XN`KletU42^6@^+p4A2akk3PP3*mX`|6tpX^lIB-246Z~ z+gj@y)5)vA^S#%5m3Msjf8uKtz7MnSksGezaQBW})^Py2I~O`U_KoC?=w+@zU-UXN z-sLn8=V$Z5CUM3-k!9B97xEeTh>4BjlAs^kM&OI?^q-Ow?tsm?y$znAwG~<;Y3E#5 z^G$4U`x7iXTeOWWk{d9j+nzrUWp_=O$7dQ%M{`2F{Up3y`jdFOV$Y5;PeJtxf#i&pAy0Fyo;3{U=*&-p>}t zdKbdeUl#9%rrY|bxg_t1_wv7~0$#3oLx*@dxVajgPHn$;rR)Gh&hYZO^e!Qm zSQ6fgpR>PM{JbE+=+yqOo@WKn@IHKRx?3lgIwNBpA%0H(>RV@2#Lo}F7ms1v@6FG} z%j4l;YSWJY>VWKv@cm<~s}ollU7w%_T73M<5Fh^xnz3iI=E>f4@x|;@O|kj`*~Ab{le{`1cxcqi242x0ku$@lpOnlfzASrD9pl#bG&^}mvAv8` zTYN@hZ&90)tpcF{3m7R}9a;@C5m zPPTY&{KCnTI+(M!`pj@~e}ct(xf?9-a)4M_>N-X{y{qU~HlJ0=xB$fXHy3r zNeHHQu|9*30{XjW&nF;{|v8d))DWT=DX|+y}uqH`#q9)1?7# zE9>HeUpu{doj+N3C;CjQnc#Zf?B^QA|0?Xijo5$ZpTquJhW&SO75TET|E{hw4mQFg zWiP5f(p{=HXF@-@W_;^iw7Hn}j@c4i0oEUFx9b0$rnAe*-Sxx;b2hS? zSn%{9e6|8UD>@e4h4^eTz71^N$S?0fFQWai&@p`Sf;_gnSaE)Ru`4>TFUDbKjK|)X zfX%g^_j7EFx-**luHmcrDIPXDL zah$#uP-nWS{Gc&;y3g_iGP-;=T^AeUhRgoj54oE*3k$U1|v@Lh2?=8snZbse ze(bHlR_`PB$&%lmA@+myB0J&_qu`OP*&i2t>98#&P<)k9x`8`EyBL>rZN(v8eS5}6 z8?Z@csRLh|S$}G;GD;qtAJyJx=*}I?spP^E;+b{ViT4{B8&?5WC2*4`2U}g`l-kd` zUy`2``*QjY@|G3YQvy!-h3YqaCu5_^uB1*#R+6u`a!MR^|4RALy|T)uNgho0A>BVP z=3kjVe6I4VEL0mm$k=!cnq5iXS;(RzkwcLmJ9E*$)K&s*p^LetPYRcskDcJFjy8tp z;m@P~d+e9b)F{R@+iyXObZ1fgx>%eUmcqy5kv9W`8)P5WHsH=={Dtx?2*^<6P zT)zQk;Z-=*`uxk5?hV4tljvDxSs~n(+$-D?Bm6aRyGe6P%t{S)@1UM=w+Gx5!T&x% zR(uy6tDgvtFJ~DVi@r$rS>)=K!+L+~xp zk@T;L(2^6~=@w+;E_mI|@P{LoUY_BrgAOCQQ4Z_81UfHyZn<|k@l3J>9fw{e`wJE? zJWB1zCgdcaw_ubHsc_4Wy>~1?&ZKR%yDF+p=`#K<>`KI>ln`SxnQ{f#mMUqdmUi0F zs}uvE@ppaV^e$)ahv8e*SWY9uYCL|%Et^^c{U?DN`M2NZ9GL%5p;z^U7uB`!!X7ES zs2!~roiPy(Wyer`XB3`t!IOv2x$va1MC0jOy~YI1o`t7V@UmZPuIT?ba!v>QI)bZa za##!AXk2L?gsXz6c7^|nC|sfEy6bF11I+Qk2)_1$uV(NS2R{6^%5}ej9)3M6ZlidD z_7eEd!60+>0QNeQH7kBv$-13RysD4!W9zVZYBv4K?;;-GkG7Mfl#qcvYuRvYPaF;B1GN9G^WC`CtZe!gS*H2FvfB~ww01#dk`)(q1U?wmuks5AY3I`@8AUeY_n_r!HfyV*JayN-?7>2{a&wH#MJk zp5zS15MwZ6sq^z6_0Mp98yL{t=d|@TKEH-NC{A(I^dHYWOSX3)TjwHM%kP+nj;3>O zHON;w_o>|K9nk49cuH%3a-K4uBL^h73hW7148!G%9}2FqWawEfiIbh)vDwDKfo7rC zflT@W&kyo3@d3PmjC~NlkJ+cNr5l;}p6%;A>z-ZjDX{eme6Vy}FcDwCm_p;=J@{tk zJiE?QVDt-Q!vhjyX0?2{bZqeM?Ny%KD&yemOUDGSc-HONLmPK(U+MY7GmkO;gh2h$ zD}r|H<|7BhyYzn_G;|hhk2r(5z$SS=BJ1Bx{om8~df-r*5m0nO8*H zi5adO{ud(KFGjW}hU1_Sj_t@orf00~k=@Ah=u?t8;^3=`hbjI%qt!v)y*zsy@_bzD zG}>E4TQeS+7eoiyrg)Am$Q;L+=S*N(Fpk*y0V%HcnD2Lo8=M0Q``7L1d7S;)P{)2* zgZ(qe8s2rRdwebQYt8>?_PFr;zs$UM;M2$*5XZcSVpsB*_Zi@FI&_mT#_3%<+&I|( z+R(g9_mplWpP%&4$C>*#@LkPA54~&iTF>`)VWY&i_KT(01V=&}j~8=Ki2Ml%94eFVH*;X{?)1d*^3DH1?tCsKV z@Z5{8BXb{#=h58PvFChw?uSOr{m^sHeI{@$7!jI3&9BZ=Xnw2z(p_o;7V)Y&^r@O* z_|DL$6dP#ICaxEKs_vUc=O1OG0T=i8*B&UFKr-@Es&5v2#c5A>l@r6UncQ#F_#a1{ zRsp)yDs-tDVuXK6`*rAKx!7k)C|lGQzy1iPx8JBj?{(>g-pg3$qnHc%m%qht`M=ll z$;JmLANVZvaK&fc4b7~^Pqdpd1<=i(`7Vn2R*PahplF@duV3>e)Z|w9`y{|TIJF6>9k=2 z_p93;_sBNwI}3i;7I|BIc&f zObR@|O+NZHo*82dZ^;PLyAqs6=Bb#uy@GivfhSB`_QPO>&FEVA&;)Wl^_1DC_%wJ< z06T{CfREsZ@0*71PPP1j%bD|f^o%cEeT*+I37jS2cisc+KKS^n^!G77xx@J6+NrDZZLB%j zHYymS4ShQS+M5ZDEl!MU6^xzs{=QL+1zU!DG4)jzIiYkEFb*^mU27KJ75vaX(033! z*$cpzNZGOUtG#J#pSkQy_{`}Gd8#vscON6ebOLJ`aH{RGkIf4@Devs#q^uEG1)Kwi z8Qb2l#+2j}oGO19IGm2Fz;O)NpNDrZ1W)}I%Zwzx?=lUPsG2kd6_;d+m(xtyb-_qVOv7rhQ*f$ z<=ZR=&gZiY_c-J;@xKoy)(!a(*=#nl;PhEnShm2A1Q+eFT(Q(cKY;j3cEn2WWN*_gYJBQl$Z9RGIyhh(*(w3|V@8NQ$M zT(L^g{O87c$!aOSlki$%gnv;EoU-5Jt0g}ich3=*xsPY@*!C1pnP4E}5VMl-U`|ke zO6{d1KVAM}-soD-a%iK7`MLtz^(%46?fTDH6NxqH_u!qxey~rBaCs6RTo~+c{qG+* zO557=--j+j&lgj!f50ZWgZBGBm>YbI_FVMQLF{PagO}rDjt|(OADxp5omC#_J?&Bq z^dMy0WMtg%nPofql#wrG`%)~z{7a2e@#y_dqjaVC@^qt9bX0+j+BKFqtTDt`VV9q~ z^IGo>g--9Bf1BzREYDXx=4l-Bm`AY_Hz0={se(qvto7XZNU^uT);}=3`Z3QHW09Gl zqp80u^2SzIdIpZI^f;58UGr=)f$ORt_RJmoFg!0QaOr5bXH2CVJ~<#zRr$E*@zIY{ zuYVwhHu}@XVZ#W#URmjB9$g9liVgg8=oc3b^S5`jk$sSE#*yb;-uJMc+TLWM06WCJusOOo{k9z7D!=u2z8u+_S zTj1lWYQ|DcnL&ZgmFqm)N3WwyOyH5Khbi+g@Yn;tt*r6vA6-M7Q(|E17^7=y^%~Et zv1=%21~8;ju8RH!1hT6i_gp#laoQRf*jBmLvvu@ZiwFNxWwmGi=xSh14D7EWerODJ z8Ruh+vyySnqx@9L-$?m+HZxFET?M|>W-M(!!WbT*FFP>R&{qv`8-b%$mB3wT;m!!- zt^iz*2G?W2bvLy34smerSFN`2I4jBU-UTl25iY^^jj^%b8>x2#IG+bBbAiPPEUMQ| zo%g9LcwY;{tGVqTp4S4~pg>mjI{I0M%xng>GyXYdjCbZV)B8O5$OIRO;9=%?o3|f0 znK{|+eICAgJN$Gxyf-evD4jE=&^v32(|g0q#oimMioA97RYhN`!Hwp07PPZ3JgI++IyHv+?K3kK+{Y8`N{1GfHwL6y+`Xth5ekVTm*tujf0O=H|1 z=U%&aW~>2D-QdI)_&szwbG+$&1z5GOd_Wt*@%ywlbFe}Db?A($&Z^cS(>5TB5yxDr zbv`^>G3Lnumo2XK82d?m8}}hIAFJ8NW+W!KW|{r0I2iVxgZPb{R}$xQ53}cPhDT|SPpZxgb~Ep? zTdJJmV6e;Q6{i~GKDEc7*V=p;=(9Cn6d$Uyq0`RH*jS!scWsuPhB&o|POH5mz#fwc z|MT-*z;_4V(J`zM-L-5GF__4j*dXS_8_BKd@(s@uXvP~S)&}F zn6Mmr{+K>Lq@9mw?=Ix9#mI9GWU)s2OMFOjL!4y?n?@b2r7Gr|*b(em(EraRYmp=5 zJmZs0v|)EhX6vp|p6D3tXR+X*54h-y%;CWA7DpZd)2%btgZs%ANlqlY(b&ZK$aLbA z3u@r~#1Y6|rhX@({|`wF`2i)LIqd~+DR+tDAT}Y3>kRlZKBvi7qu7Rua4sMN+9=Tf zKF)O~X4|~j6+2fm76&q0MO5yb3ha-+wr7$v2j3F*0E2PQ{*}32bT9v|c;kJ~hH~aq zMCHse$eDAQ+9zku!f?)|3PJkanA8Lyj_0SeQ$;O&8Dwk^IK;iLw-YYEOHL=ZpMBe{*m4}$>lp)&hzEN z4Cw3ZoQPSx)4F`&CG4yg6X^ zoAEBi8Lg>$<%w1Q*3vSvv9*PGlNRsA&Nm$4ckz^WR2_I?Bz4pVdz34WvZ|jz`4axC zjdPW|oHDtTRXN&qEuw$N?_X}2L0bzOTX}b&Mg8Pf9cam--_Ms_%bbVK)I{S!I57G- zZWzW#c+h{&y?gE8qp;TLH7?4&TR7?NwtEXN8h&>-zG>{ajTyoPYtF(~vT<-2cy8FA zIcd=U&v2yiw?n_e(-Ca+z47$w;&bEafS&(;FFM8Cc6ZieUNlsPAhr{FJ~SQ2Y&Rn8m5KsySr@ABjKNk}u-e|sXa&HUE8 zzWitYfIHJ?n_#Z5XAiP;f8v;k9RVB}+a4s;9y@ObUysG#j<(G1=DT>F*vEPT%nzCu56cN80wwNE`mE;rTv6ZgpeqjjI6?vun7N8l|7wg|j;gyAjI zK87#Y;9TSrdd8f}M$*#u(x=~cj(|8SseR79-oraj*Yp< zMOm>|FutH#s8MpR=2j-JBz~_RY}QO2ax^NqmHLj{XSv%)-vru6F3NFx+2SxIgIu zp|h}t`&IfAe33ZP@!)MHc)N+b3S;@L^&@(bEYYWTeenH%S@p@4$(%{X7(tzU>R<4@ zLrwa@ZeVkiO|{l{S}n0a+@)RCr>IeRk{%pZ;(g$sr%ryzR@k(ZJ2;l@b2hcD=->FG zyU~}95Zg47_@SSWZ*$Sq$z}1yjmi^KF>LDOHrIg0a+|ZUV@qsqdriN_`L@EwBQ=SQ z{{2br;bT}+TgZ*c?{)Ky^*hM%apGueuqy zR>bqaCBMA7mER72zryd|^EPxUSNwYk3XI?;TpZ_~Ii&0k|XL-Y6ZKEih_8UzocprONk zj4@^C%VWWTa#$cg=G_Pm(v8YNemi*{DYtc)x&Gutqv_<9!Y**Ot&+XxH0$K_bA?@> z{-UspSd6A_+UaMtR$WbyZyb$vSI!oC;pF(c_Dc9(}$nB9mNid|Bu)eqKl#A zRC;q*=$@nXeT+_>6>DS7ZsOcm75Yv+x`pJk|2h=9+eC3L!qe3}BR|{Dz4%*BaQw|9{Of*6*fXCU`XPZM50;<+r61Uqzb+bSoNoRlhGYcC@88 zPf>h&20SMN{*z(9#BEeo=5u~!OeK5T3Fi0HWid^fqu>6`STA~EpU!LBABsi>2uI2>!rh<6R`*hZs zvLh&a3SJ}ravSs71&?W40bgb8?ZkU&efwG4qNz5H-nYAMXl$EEzn5W0n_+gf{YT}LcGsZBwiUA*{dLUEz2wfC&GQw> z>~}L8Pv2yAb=_>j`v*3**UW14KVfzSH<(@RHA(!R+W6U3_NMllw8l19F`rc0xt33Q zV{jz%Hc{gKveN6O5TA1wo3;Rg@Q!?wMWA+0hQzPW!xzALi1Z-HNFKE!{N-vm3$ zl5A+GgLR?#RXkA#YtMhBvxzf3ifDj|OrG4@O=+v)f`ZJg2M);oH7<`T|MWaKuI#~a9XK7Mxq+5Kuns+Ad)oRm{E&5Y z9GUtC@Z9!kI8X3R#^mSe%bLI1Df01dEh@tvl~Fl28{T&@?OD&9DPbE|rtew$(YZ7S z_9Z|2``L5zvb6ZT z;+qzJCVi5nf}KUfO5AL7lck zA)cgXf2A+I*E{w%3r4N;Yk^U^>uhi!xV1mrNBbjqHa+}o;+gU~`;g&#_D$@9(BTB0 z8_b<#1I09IkKeSXyRI&DL9(zl3Au zs}o%61730lj}D$o_KD0({uKbP$M{6xir_D@c0c(mIejdh!tM^f)4kza zU-U)4Fx`v7ZS1Fwx2YeBMYVfB8_qeoNN%G2hCBHV-4moZR0A0$g?s_XvCu}{FnEd0 z=I;37@PYWx4 z<^1xLzZ&ZcjMT;Q-(?zURz4l^dTjL*j?n*6$%^PX((gZ?Wv-7r3+ene;IrrL?B2IfdRz+w39x$o>h+seT=}ls`yz_u?~ojiPb#I&wT~tg{0Abfh!pew-^K zR%GR=z7xpRl&<^nnz4gf{N}HtL;R*W)orpb9c9i8c%G4!<}T6v+52K|Nb{aXzdQN8 z4D9}cvHuUj{xFoh=CPc2vf)27e3Ro;_7c*=v_19)VA zcQCff(bCb)3NvM{@ypOGq;6|`5O4MV((vOZkt#~e(r?H;*7&F zDPHtvoylmy=h1wGc*IMXpCeNzBg4p6x7N{!O$|D*m#R*pa)$TpoH5Tju6RQ|8`_2>m!A0tC^x@7OgM(J&g?>6*;lCTU= z#WV4d+Ck)@z1SH46#4$&LZ;SvvP$qL{YU2+Y~{q=#()!IgQ2O?qB8tbrQ|V#yavkOt7kbz;TwWq z`5z9DBUWQ7N9S!v4lCrp1AE05){X7}8ip=oR5s^lVBa!7+}H z9rgCZ`C1p^f4iZRteVRFM}&tcnL<4F^YMmddyL@292%ML6yKEad^eK|SLY)fw8xmb z>iVF+^Gtm0R$!7n`K$cQi)Ho%rB;<2Z|Fj1j-0;Vxqh*hZjNU~7IHIPRY-|Jv>F7V)#&;4Qb}Q@jPf z_H8eG$FeC3KlmZ@w0CPiY@Z0fg%&&FjijBn^aSW7=`(v=UI{+)!-3^_l z<{d7uzlp75$zGm)!Wzs8-!JIbUV?tp0ZiJnIqT-mT38W$@iq4I;*eakmon|whxGNf z86i357rbw;;hxWv-8z+nb-y1r}&b2 zpG4X1yk~EoYpvHK-Q&oE9^A=2D;E8o(HXe2Kstkw;oc05OJ@jRua?ZCwhY?3leU!C zI?|To!>?$oE$vKOf1<77;kHK6)@0hU)_!w_J95VIGM=xFJg-l8|CTyw)TyRV%5(LkW8a> zZifc#)E69Kd$)VYVa_wc_l5sfeqY3U>_ph@LU#MG-#PEMypwJis%M=a&D+B>$-i4^ zPx83lsh=8T8_}9C+>f69glFpenW(-`AydhAT?^m(N9CM<$_mLjamXmgkaYsvZ}G~% zm1|zUY94;AO`=iWb+lOd365dcFOA;g|G4w3<(}?8e|@=U&x~`+Jr8_-Zn@`p^z*2_ z>_T$S`|QzQCHEwT=e>&YvPW5dp-0AjJ-KHcI5`(zK8M`1)do7ik!~YBNC$az5B8M#KQ|Of}H=o#zqul%A zPq2A6U5)*nvv-omWm}v?S>5w<-&3J>k0V2>?Pm71X|c3D?CiD`7g<4j9kkmq+~#eV zZgl0_c@_m%J#`myr%P|ROvwesu~z)N?lF9S6}CKl6@vL%+CB4bv*CU>>fP(T-u*4= z-TSmqv@3GW7zk)`9A*OQ;W%oNm(<=C_UP@ z*n1?M{EX}qZO~GC4YC1xLl?hmf$tRcPTyP9boGK5eC|Vi2OgWT8`>CC#@g`*@RKeu z++KT9Q0wYW+OAD9EO~P;GS6=rb2DQT-TaZyZ}?w%6ZgDbzItxM>eX{|*EIgX;V_mS z;N3p(p!oD`XkBz$Hq7Rp1YMq(M_k&A&aUTx&w_if(b)!F>h2(|l_fUn;Wx`dj&gwW`dr`mqZ^!EKm1z>chL01PbM2$JpM7+)Djxk)0W21T_o21(AnUjgFYMf+YV-B z+uYj=xep*rN3s|AZ-S1F^w5#;Tfu#@^Q)Wl{m?-cw2}=yb=;0UxTdu62z1eLC4Au< z*LvG(9?mZtgzW(OYUBQvvOf4Ju}4=_JDZMB&%ecJ()io@#)SNr!{G(YLALqh?Gc`$ zH6@zMD?HX{j{xRnZJ~9nz4$?XYtQel8FWFr;x*8O zlRiZwH}koQ|H!91{CBOHD_w^Df2r3#-s`oe1yMp${I9Wo;Yp$q?*jWlN4AOI44+BO zpx^<+*y_mc@AhABc9kjq>CPByPTS%C<5rsMFIW9D^{{P3*V`7?Q_tB`?;F&+gnCJ$ zUG!kVxq%euqjZ}hu6;a*}=WM9efUH$|#UNhIa z3sL1{%ajcM2g*Ir$Jlwm=4{fsa%!(+z5fdQwy`$VhtrqR#Ig_5 zUngt4v>lzN3Vtn}Sg{W|y>%0{3tl3+$!cI0-xI&vtNI^>b(2p*ddS1PZy#CMW&Rhs z39zc}a-NM7e+5T^Q~Y(z6SPxP*mYt*KI{JgH?53`HZO;_HN=PD%Vz&i*|9p+~%6A;cYV-H`QFzcpv*ng`GHe&S92Y@*8)= zAR9Z9u@_*2t|u3R?)EjZvELB$5I4=}T8ZtU-u~x&BdKyqz5VTcBURsj$u|b;`>*-N z#rl3TUpAk5`&;=&tiJ!4Z*W&zz5P%5Mq=fZg~&RI$Ry$k^5JKalc;bAaj>(Y1y|wc zl1_zyQFPH;pf@rdQTDY6=gLxcg+>X`ij}yBV{Xm zLu0R)&6q9P(Vb`^+Qx9Q}$za5Fx^y$SdQ`@9J3 z@b~{?un#x7niZZxcO>x7FL-`3;wi3;Ak) z+sESFo9y<3zguCh-~C_rb2n{z>r3#p0{qN5oG}MC#6YBT=c>;54ffr!jx|M2l7^b( zV2D=ap9#^*&s*Wc$XHuy-pFsLxhW_d9omQcpdNAkQGRX&yS;N7aP_Nq=3w`0G_s`r0$_vZ0YSJ(gl`#l3>CSehhkc22? z(UJt0Pi0L>$|P|KsAvHfYLx_P4N|r43nB@zB@nmKC>5+EP-|v17AuITEs1CwrL`h; zYipSS`*8wpEP)J5=lguU=RGjQ;L=aO-{1H9`(qySe&6?9&OP_sbI&>V+;hXPN9)RF z{ZU<7f7H%%-T2&R_|IZ(QCq5uy(nvKSKX2c zlvcy~7n2L#qi&sv*y2F{9a6eq-?TABP)JxeNXm(YUj%!Uw5C%Wo0l4|2upicx*!UH-+&9|9)9D4aXo(Q-?y{-yZ4;2%-=x&P#E zQTTV0mI&=c?||xd_mk7~nue!E_Nvt5u$`8%-%x9`bjp{W=WUF={NHx)FWSEO-qNmi zYy-`-{T})~@$go^I~bS__bc7%R}eUB=+{=OU+Mm9fiI77Ff1GVV1N5trtfX;5ZH^) z+e%_O$L#Qd+^C&q`DwD#oX!4F^!_O8TnMs}aG<&s8$2e9MRAbszvvik)W_9t?P-GN zh14%w;#1&egFlM1M0@O`FsAzFbt~73eL`@FCT=3`xbUpAAu*iPai8@cn9skazJKwH z)JFYl4}nJm`_BCZWCcw~sKw<>wfyx5H~DomW=q za~oq9yNxmOJ<>fDD)Sx6>_Dzb<9tm%zS6Ff5|>HO>l{lSXs*4ksiQl;U$+noZBj5S+rHRfZOWm_)cNw#<>Ke&C=Q-bgbm?M`BCA9o{u&r^lDaGk%g*jBpQU%hk5! zuf{oWwLcBNth$R5mmLd^(KZ|nad6z=7_u=2hhp+|B1_=2vvV#nqvqqQn8&_mJLB2G zIQxFmp7UA9u@#H4b~7ZeMUJXC$yMSn1TKGhX2Y!zcTA7dTUY z_lkb#bKb1XWr@_Sz1iqk4Bp`e5BcVjrQ5kTr8`~xSKN4rE@JdhF8!Fk2?n*-h;Ll7 zO%JKigWINu4)H1CxW(YGd{b;#ZihBvuuOAi)Yr|-wCST~n8w>Qk`xD%Xrv0i%uM<% z+9-^pjaQ+K&^hpL0zQ+$oz}hhac(^s+ff`{b&{Y+5rE`(x%)!}?rAu#u zrf!C&0zaGYQ>+@@yR>|f(a?A(VXSCu-^^$%M$uz4I*M!+#HCv&9d0BvrM=fW^iS!T ztijW6Vti70mrkPjuYT_(4%r0a{g)!kYW~FNZR{PcWod&sv%-Dm;L~z|jANQbRJ#=qL*Luc9 zb5t=#C!*8E=IEpNa*EzWubQvv_IwRsPdwIqJ^g-rzQXIo4+K{pJfYHA(lT>^r$uwv z(a&fxO|zxaSw>nZ&(0P{KS#@KXsjZ)wOBf?k&U0GJukL#*RXuFjD;HC!HjR3e>meC zIG8XN{;?CdtXP{5u{OuXS7WPjE#DV8Y(LPrzP*=tA&jfewZz6DZ*RmJhbKu3vyQaW z)`V&JqhKRdyc3O!c+2#UB1`gl&MPy#pD?!hX7WbGE9!d+F&H`TD}QhN&%Lf};&-tx zH6IwV;b+2~kMjuU;pdWvT}HICcAjflUEY0fH8;JqtNCYd?FtPE>~isaJ?FObp}{WZ zM$BievN^KkUTmlN&}SC@*aZXn)hBW0`&?q~s9g_jw9qa*FT9UFtQfBQ=ygWh^n36# z+Dsa@fGH+pJej)YQkQ6WH?aaP-X2+_Z;=>{4lq{F<$Sd6mB?eQanZ&wbI6XI^NeXJ zz^(JHOvdK|X*5aPX`tcw( z3&vLVZKHQ7c9A|UMn-FKfb$Iocd!N)_G)pMI``4Lr8>dWvS2_7ei$gY9RH7&5s^e%z#1kkgBwP&`tjO6;Q_nl$UmS`+kdv?n{=H1r& z&N@tE9SV=AB;_cAXM_ zo_@UZ%fh}>!tb1H?)u|5BHIY(S311s@;`~OY9?-Dp!N?%p|iooaON32S$a$x<1V^! zKo7tD19+`9z58Sn9gZLKRPrKcS$7n`M`g#$Lza=uptb2W>|l}?Br8Z}5N{uW95#~n zB|B_~o{EGuc}k5 zO6Q|XABGPuxhg78h!@O-7gQt9R5_2x6P0oDMB;r>{uJXAN^jT9aJZI2W-lJp^3hUk?LYCO$;*Yg5-k zqhY=?x-J3lCRYl6)c9Yto^Fg)tk@VX+K_X!_v^Z_tgxB$N_)Ze&B)aIz^~?ECw#CO zo_8xWTa{z1Q5?H9(C%(@;}Up+Rrcz#!glVKXKlN#5j$Teb#QO(>fO{K`RPXT*Eud9 z_pa&~Zj92s5e=m6rEb+BUe(NR*LUzK=9C)H+SIfRnUF-?17Csa!x`R3s$v1<1 zGnt>;nLmTU-HthRZ+*h^h+jADdh(^;@2cOmf7hgW8<$NwboS@my$aVi^(kC$x(i!s++M*WepkWzqx04s#3+HDy))pM6DU)|cgbqo zY42lTETBC@))~L)=q#6@yIhP8a}m1Ch3LA|&~y4& zdR=VYiLL*OkWXT3xwZcPp8X34>;GR_|Dgw+)p!}dr%*0;_Mp?;0o|q2Pt6haMX?I4 zHI}n;Gy8X2V;!uq{o~fyNvyFsacgX>e&m`MT}wW=!Jc=L7rLP1{lo#?15SIC7ohc9 z>^T^d5weg~k0m4I-+S~NjLp9gv>mhc-^Bd8iFeIE@rW4CSevc2I$r*H0DM(|FX5pI zoNZ*i)7-lOJc-Y^n1A9uJ>e{<^M>7U76fNo<8ami&H_E)EC>643}@n1x7fTuF@3e) zR)=h5@dS8>bOZ6MeDl$zo#1OP_=?IiQF(Ru&y0o{@C3;+@Ug=@p?ZI0R1I@nGV4*k zf^oPCf~%4^T*dZ9n&WUK8KxXO2}c2NH55F(Vaq!)9^?8TVXWe2%MQ}@KK6RJRaBYy&B~UzXoo_qU%H!-b;TtkF@3=^hvVVUgQ$p*C|^;;PI#( zRqJL?>s@nuWX*q5PWBzmG5u?f=wE#i?+;FPEoC&5&u5nR4Z#&XAnloI>DlT^|TR}ZPq@F5As#Q-8d1bflYnM5N zf7wiRXT0hlmgvTi`c>xePNDBQPjcL^B1<|(l=)P*@>Gt%*O}i)Gt-I(sq~H~OYxWI zTS4qwvekz(X*`%w1-IG84EA z?*(TX4XypLMLSRELJwKhyusL%&l%g$@MP^P-*1dM&4T}vZ^l)08t?##dX&)GFB%lWo`Lu#gQk` zBSxh70_SrtgW6y{P#e8?_Vh(Jf0A)sII%@G*5~0@I!_Yf{?8!KMlYjfJwDsIQ@~`7 z8rU&Uf9i-0H)>-|^PiDvHpu4t-XL_xe=TosBCdHX|F^p3Zv=1S$lpx;&r$Dl)E`;~ zZ8Me^e%EXWGKR6TY2C^yPrk}hM{Hcq8*en2;4%-pt9^#oUxh3$9ga<1gpje}Q}+2s;ue8d z&HtzQ4vphWa)y1seHpTl?E9}a%dU<7HpOJDAOB_ateN%Bxa^^SjikvoBzZN+`qdVR z+@F115p6rE;E%f;v1s# z3wX}5&YBs_~l%rGN{h9NACh+nI1oQ=q90a%`nKmUv_reQ-{Bu#aj8|_@MhTc+(|tTk-vf8 zSDmX#`K5eHU4HTY!OVwo*jTJRqya{F33p}f-Qw`&vresZIyUX!W^fKCMd#yF8j0CZ zR{L5}R)V9Yk=PVjNzRrgVp3!i6Kw^uNDlscSqY^r>9ZU=n@bJOyEwcn7t|Fs_HmH6 zj51F0dMIO%w}digOQ&Kt!iSp@OIwoXiu_HNv+w^2>r`1!$f{*hL+xe?ggu-_TD1FSyFpnQFmB_b3BKF&gx>p z>?w12f5~`mbG8!e#?|!~(9QSZ0 zGCw=832uMV^lhK&^mSd@%U4Zbq*tY)|40t(u3Ht5CjIJS)4j3DaY|H*0<$E9Ok3EX(qLuIN?DTI1w? zSQqxWKCCt6+!^EW%LZ2ADr@!o3ST2`$8Be@ z&a=KB$Q|fCxS_B2Zg@pAd)%U%O4)qJdctWHWrh2Z5n}$*0qJ*xj5XVbpG~Z&VFlZt z7{(Ua=Wlf!Fv63uNeeC)KAE!_N8%i;%j19VaA!DhX-%=<3eKo0?%3=I2WHh23%0FS z))YsQ`F?&)v0x0IQ-i(M8QwazrZ{jh-!h%yCfQxeGQ8VrQ>-(5H>4w@j3~2Yl;9)y zwm3BwY5oJu{YLWYEF*Yg-8c)}oe2)l0GDOpG`cq&N;v!AiZ>!@v&-o}`_mo6*<-|> zsyUJ^U(_D@?ExRN`+l+C=P>TgSB}ANJbI>GJg|yBD=mZ_quH6(b=Sk#W2p10T<(&h ztr#w|uoo_J9In&fahiiY{?FgqKEe_1814vHP#5u1IR|X`@mus(Q9i52@LoILw0yqR zXX0uWgvcvAPb0r#16r~h@TM`A2ax4e$A0Sg;AQdRXq<|7c&_-R_EqIalg52NhJVo_ z)?@5;n%nY2Ro(ls?TbFNZU~0Zh6KwlEjZ2t4%w_i(38ghjUMuxAz1aDe9F@WjdapR zcvck7M*Q8<(}=@k!4~zK{x#1=qhQMbwsgN>p9yTbgP?==r!Bl3siPIY<4f?bd_1m> z$JD>aj;`Z-ado^29MG>{XZkywrG>(wi!%XT;C>(D&`~=O7wy-JI?AvVS8ptGRR6vx z@rRaNyfdrj-Xf#=4@KEE_Z6Ad?L}EN^}N5v``tX>Q=~WxIW>!m+{~Tre>=hF7fsf> zn15%sbUcwz=yP7&_2J5d!mYJ#=4NS2$BWR!56F9dDlvTI!(Gf(KMktL;qVR%$V9?x_2?Z88w4${OUKP68V z^=Pi{&UJ)m`pkw3Z`ayt*3X*S_M!!~uNUpEhfly4#1lG~mxlK)E4BDSDEG{D`<9f3 z59VH2Saq2(b`)nv8vaGB7G&WncrUSkH~kYkyT-O!u^{gFHomkO-Zs{wEhpovD&Aqo z5DK4G77mUqLkCJgHq7u}KrElo0p{&=d{3Pv;i~D_rX$gDFPUacD=s@`^`QzHEAh|6btTb4%CS$hPAO zBU{XHB-0Vz+{V~pXOe#NN1n58GFEHM70YJWv0_0nR!3q%rTaf^Mi*V6bH?fZp*dzl zkg`GO*MwFL@Un~VF&`F7{{jF0G;r@s_6DHCEaI$o%__CVN%BjDv%N^Z;sI!3An-@& zag_dgYdraHsHWi`cHm$R;{WR00x zM&7pK;F7lDt;^bqw=Hig{_u&m;(xAe!w$jgZFD{guh~%4>>ScnN8Tpq_?GONS4exc zX#4OPtUX4!$ywC09XUn&Drw+MHuTW=(nF!l(s1HD<}kK^F21|X+aA-s>8i70VM)sb zXr?h?d`tdMmoJ?Oe>&d%+S1UBQr5B|T_O0#3hLF}^J-V^bk>&&7X^ZO1@ifpCm37e zD4ADqCpL3)HED&Mna)Gc)0uFkP`u>;GmFh)O*PgFWSYP+!yzl1I8bm&gB zqdG^39DF%(+mvIg~)>h?J+I!Xc^AvxE#zbdj zX}>9HZc7MxLVnI2nds}tD4~xWhm4vUsV%U0;|-~i+s_^_|Le5#;P0kU_;|51%ovwGwtaZJl!itmhi~;`_?U=|btf}~&mvTNR z>d!A7ZZGfcHa`>|zo^5f``>9*e4)~?=41cv^jenU{rzCslO*FN`!D)qWXYMw;AP@t z;%8rnm&JHhcb+CWv^!tx&d)48raM1_k6HW-z9xS5CbVaK?`VFuoPNgm+0S{-I{$0% zGuEVde)a_}g#c2)F zI5xj~hflms_ZBL@%f#M|Txjhxz~5@%XVvhs1+{M+=5x)?%&yQ!oFmv#!aN_}QUedP z_#AaK;UC^u&m6Vu(weAtMFUDRdq;f@qke!Hel8bW(wHz8zvxT5R&owN{7t-0`$827 zX3Gla9mI#?9l2;Gd$$$$Ia)eLM)}_pJ>X()54ac!E)MfPct@kn`zqjlxAegK2KVZj z_XW-m{3zJJF;~2FYrI|#Q2}; zj`6<$^~d;MqRsz;v=y@3Q2oNsR@zeitQo$}+@txR_}-($8@G62G4m8&TLeFhk1H4a zuH~aqAU^1tV2pkdK6rrnj65D*2Os==z4&0J#Rofh_M9a?D7iS(l8YbxndD+mn7!pO z+N%%`qN&`#N(idF+{{!lQX%3A}I&Ye;}`iTQL(FV{F}PDSUz;OIOcP4fWRXtln}o~HTW z1;;_gQ9SWA;VXVlI|q7eQjhS&f!N=I=yempx%5uic#qgCxQ_x@B!9IbzXZnv6XUAA z0NE_!_m%P_%PU>>5_BxtEwnGKeX*;p@sIN0PHb5KmLeHc^5=Hq%jBmn;y&lZjl<5w zhn{-mQy;>}yAy z+yz4&bKFVwCsS7P>>TbQx`#UEnEjsA9h~c_D@1?(2#u+(eZ+sx#+OJuSoxo~c};9B zDhB=#HW&C=Sn~Kqud|*(b7qO|Y9Ce*`2_zuVjmuae%1fTGGf{hJ1z1AKjd1;tRZX@ zim6pPT=r@5B$#389TCgJwCbwb8&1?{HNYYn?cY zu??I8KVLY##l@cc?(wX_6IiF0K*P&wibX31Fir>F05W0_8F4Ezq9qqDtS(NBYtyMV z@%f15lP;~iP4M{mHuGuIQ_!_mu|h-W0-;{S%4s5IBmLO>JNkfsv1IPgcy1g-ee;p+ z(Lp3TLyyJmjX|er{)O-Ae4ObQZDupBe6!-TobUyDP4;G-$eBOEL7voZ--XkD(DR-? zo(J}!11`r-B^gwAE9~LyU4Z)+L?eQ?i|@n^Wc~?9ZN<7L*MohqknhX6+fh13fIAgr zrwwo?gXFQj$g%17a^C`RZ$feREx3+11`Qic3~l0*%*?^2!FZU!x9>p*XQ>VEm5)c~ zT{gC+D&jNb^WDdOh~`oaWoE-Gt7aPBMeNnMnb(PwO*E}D_s@7JBN@7K0sF1F=&RYp z*gjx{r~DK?O8Wa9oGC~o)+PK^GKb<_rZ6v|(P&)O2z;G<$lO-E%k@pj6Vxvm=v5o8 z8u}XDvl;09D3^1N!%M%guO6b#!*d@!y^{B;GW;%w6I%e>%XoX-0y-ebDH&L$>;AM}w` zikr9%x>Vf65Ba|Y#W$nxIF`#j!b{muOP5`9nixAHFi zJ^G&Me~EYPF-70g{Lk|)9umvHns?b!V(-#3eoa~ydu0dVi|WJQh&K_9jhAWp*vhW8 z{I`)MKFTT2IIRcR4r6PQzAxds=7i|tW`2?ZV)Y#*7a(&;E~qEJVkjye(irl$v45ug z&|-A|jPeV}BmGl(zD*u|f6D&9mGfs>|9YcCNfwX6qO=<+7qRnkH-c48bv$RM>G?_f zIjWDP7ktJZf@oN6MAI@0wv%=zY2qicv1X%o^W-t!qdR`3U-Y;uplHyH< zj_=t2zq9|B@~?g=&oy?M_7)=Gu9kPFomPGLo6|dneHz)EW96>4zab-7xVk@>H|E^c9mm3x4@U*>me4v*F!d*fywVCVh?7 zKPIlO?@^a%Q1!)h`!u_*687G#bqn48&(x(D2NxW!r?2;~Z!#wKSl!Dwsg9G#b9P)F zrM*c$(T|=5gVhi84VA67#y2XjPHc~!&m2qnqw_|;m+<{L{80nQcP~<>aCUO6E!K|{ zpto|;)J9{iV^cACR8IgI`PV$lhs?qOd5H?Nq&W5t&j0Y5ofmoOCh|t-NV5M#+KkVa zPC4Xer8ykaZGQAp3{LjfyO%{*X{D^_ zYp&J*K!BcT-}kIaP^_NE?@W@`Bx>fUzo&R zYhV1U99Dni3nO2Et1e>9bB)!j9G*kON!{5vz&Nx(ctd~cTUs$R2U;-uxF*+ao%eum zf#F@uf9~;3lXLiG@jEefeoOW%!O7n`;pL}hy|Qg`=_^|&8?OW=-}TCelOKQOpOYtX zU*hjy`R~aez4FoIY|>_tR?54fcg8_wRDL=8>9MleR@&CdS=7_F{qsAskY#=SUG@0- zZOrn~d;-^EY3xdMD- zgOA$3|L~gZyelmH=smj|JPsQ_rNjv-b+#n6{CDu!mKR}7up3hsD5 zjsF<#$}_5l-f6)*XJ}qh=&e|KfP4d>7q{!$p?Q3drHhtMd(fXaW(wP^7TjO z_}ti&Asy=T(!lB7l3v8uS_VJc(3^A4gM0_COs0+$_*W|YYcTPF6}v0J@(mFWtAh{D zmS3ZJnztj@@NG*reL-T`_3nc&mdo?o=BM6&3%%{l=EBe}~J>e%}zMkpAW>tgDA`f^XtR0cu-aZVMtD8r7SPL9p-8t;pLgse)8_Dea@&k?vr~;hi`+v-${sOspfO1Qv-=r? zCXqfd!(HIw-^IV~B58JJJYR|Y7@ad|{*&iL?|MN;VVyk^H=+dm$ku7CzZ0T%jUuHn zzE;|4(X_Ej!_I7_p#x~0KAkkxzvujmpyLan=V{ROROZDLVr}=f=79?vvS67BEtj^Q z>V1{@@VGIYI8^2)?elG8J+5b+mwu@66YWnR9{0)69Qr)IC-_3}FIv&F^sgayx@=ZT z>)xlnJ@hHT?o%86*Lbmhc(ag!)eobGz8zQ>S+e|y95 z!<+v1&F4SG*ZqHBK0iZyJlx+8@r}^BshrT6{cC7i#O=MQTD{H=ZPwtE&-FM7c6!@@% z{UPZO`BhcpCctB}kT;Rny)%f7AX#1drHB21=v@sCzhYvEXDBv7EBh}MGYxzkkKB=a z>K&GC_I~4sW4td*PN*2}twc`v(<9<5x-YkvuXBX-CVWYqrkA$fbQAww@VMSSo3qcX zN5m>|HyU1RJ^bY$-igzZi?7#>>=PuF5SJ%L3F!p_NwJ;sWYKg+!@v_ z*{GG*b(cpm8dtjwVytAy2IaE$za8}LgbDPA^1`|Ju5@(q!&T2m@mFyuvSvPdVNZ3d z+z~x5T5g3^F4^w{?~x68IZm5y~qqt6i4B1 zAA}Dl_aX!LkAb_hzU(mEV{N#x2dvgy&L&3IaW;He&ZFUzKas}wDbAXxkLqs?(UM@$c#4){Fbt)PzA3#s9BCFD#4N3bR-Bp}CNhTu+1Y9NS*Jj+DmRTjR6!dq zr!nSc)@;Qu3OObB%ZBABc&$L^b@8ika{ogg>@$Mz{4(mY<4`frG#4szqVevNpm`$_ zS)=#M=xZZ>n6hK3?il^I4~^15D}8Jq5=|FPczBnbS-GICX!ecXx3mZP+L>#2xi9<6 zuGyRZxhw0gz+_iZ`s9iQHASw=%uPB=ouBrLwfzI)R)1KfKEYZG3$5n73<*s?{RBSpn9%B8^>xzv23uE#>mdb^HIE z*Gw7@T`@M@dCfy>|2eIob6Mc#W06 z2VP^Pf05U$vUC@l*H}8v5ni*(h~qWi1MYZUQ)|QhO?b`w-|3mxoXA-8#A}9HV{~j@ z^D6cA#A}XIeczDRI4N^1UUTp{(O9zoGSSNaL0+?*{>6CBVxIqh$!p4b!Ml?<+tHUZ z9{tc6`@?$&z{ijC9b#RPjV4ByVR*NAx^!&hn^ndgtl=x8ccLET?98?Y9lj^7HyfVY zGS9b$^+aP9jd`Ey|H*AegU%rsCn4*QUad8owMJ`tY|Yl*k$B}tMck1|-o{*G&E74h z&&4{geYObq&_*J?eETB3eMQNfUE+LMC$SC{o2`L(W2yLmDBtd+sE+_=z;_nTa(FB7 zX*kHX;n$d9jP4o^KSvLLfirr~UV4Xb7_lNMwMJ~iCZQOs z*y&{pb>*fmOP+4{hPd%{V*S?HnRf1Ec?lmLmp={_vhhK65E+ zVYyHB_q~q$vZ?dAYwz^EM4eTfEmw>S)}`ouq}Up?R}#ICG_8IU^>)$DeBzsQQFkl( z_K_!(ei8S2XE``LKzit^apOkpVef;of=TjB674?9+NQmypp)3jz!{5AdIvJZzR%9| zS$EM`-xa^~$bE-5>N|THg7t5Q>qxfhjny%SJRk6F4{KB=GRN*D&hx{&6&G9Wix&TU zt8_HQ&>$W==b$T~y;j%JGi_ojxpPI< z``LJ3JWa?S`A>}9b05v~t+3{q#y7*i#D*~oxw9Jh1!MCU=2xnJfDK~+z2jkGt48AlIgBJd*vx3N%%)Rb+K3_!{>%27cv>n_iPLlc+J1;$x@7--zmysslhdt+C z0AD;G{x}&vc^>@oT*!X_G8C{;SZ~Gj18=AG`cN^}B{|4N; z6aLHK9>sUv1AGy3=-Z5)aQQR#EH#eO3vWjE(EXT_lQmD3wh>)9Hb3P%B%czc4~`o* z7ilV^^q7q=8C*xpMdx2CZHsRU_sXa7P&xhUU3<9de`DsU{D*O3+cJBRCO-Ku z?0((xe`s}7zPx-;fjk0Nn9%;sZ9^ z)r`T{)kzbWcRlH(OBsuv+k=c%Qczc_3O$t&)j}&xn}pXN93A! zuK8wi%`)o!cjcP5Y4=!i%~^MTxm@#Ieg6u%W;A)eO0F@W!GBk-*?IHVlWQi~wEMqX zu32*J7v-9E8^*7aYqD(^dy;GBFitVKCe6;*U9NfBZs(iGH5XE^E!V8F<(gHtT(c@( zu6gpoBXZ5YF~^o`N~rIf$~B`X^JVhE$$am=t_~;do60r!#{;w$kqS|AW3^>&=_{pOI@W=X_6`TyuY% zt^NLZxn@1QG2Z^Q^QNzqYu@~Z_OCZxJ=?$DbbZnOb$>Ve*PC&+_WR@QUtZvjmuoiI zaDNlICWCp`lU(yd#-b;==5lL{jxE>proNu+Ul*vpZz$K4QRY~3O_2phvi}#XQ;{<5 z%k=h1x8Hkl0(KggOIrdQL9h7YBevu_p1WxtK{f*$+{_nco& zALP4pHhR>V=uy!<4B280`@D$mY8YqN8AP9p)wzl~H8+)4c~tKR>Xgnn4IIsRKzP!g zL|nYLT5K=uNrce%4elT?*uT@zRi&ESgK(v`=E z``+r9_Z#)`AUevk=xoiUoMT09uS3UX4{eR~>X)9uXB8V(HgK-txq@-UwtTF-bd2WyCGs@pxP1m^wpQd+7psmc>XN>zJ%}3iOQe@AU~HAPlDZTpZ12Toj7KTs zVeh+0Uw`R4+7De-8aUaT%{i8TdZT{Ht)v;AQ@o$>F1pGR?r3)j@6zF=|3&QvDgG*S zJoy{-VrTr7eD;)Huxww)prB@Ja6Mah`uV^2uG{Ik8mM8$MQXG=ZnYl zuM&>n`B(P$CdL8#mUTC)!F_>E)TMTRPrDxgccfPHtB-Hrc+D>t^UIAd7;&+n-)87J zu+Z?G&Al4U&dU|oY_<4_e4OTR4!6-c!_sjtpf9HZ-)g?;9&pZuJtKcE54O4hzPk9g z`}YtFE8tvE)JZ%x;V$BE>+S>p2I9Gd9D_Id9E0nJ6&pL*@0_MP#|_eT$3nsE%5gKE zF|PBFo=q(OVWw&AbvIK-6J@qiwwf_YtVVuMF~;N>)PtWSzG>dA_;UHLs!1gcnW0;kdIK+5M$k*^CyA_&Czmb;w<9kM(3#V>HK6Xc2&jisMf!I zp7P1T4=qjPJmritjhzb^GwsjgL$&%`Y=c*GCbEUIkk%PU?vZrO>$_|cFt|!ivCdO& zcev@dyM6{~9V47R>`K%8eT~&46f*?4RF~@MH}8*}wd}W}jad9Hx2s8eFuJZ=bA|ycO)}VvntV8H=D5;g-uL!d)No6hp~EkRU}Pts}J2_KgGsj6Jww6cGs&f z`8E#o<8TOFO}htQ3H56hIJ9E=ZeahLxzLe|pSJt~*|W;eGkke@ruGHyH(ZV=zoiB(HLJu^WcQ7&2PXErxHX|H>boJow`_QqY|_RO z_`PCp5dS=tv*v4_#vTvvRNRdc(sXWtdxi_Eh})<;i_78DvLoIKA05n^t2=!)pIKjb zc471LXb$|W#9CiVXv4u8oEB|kXApe4;9=sM>2A(buf*=FZ>+B--wfXG&oUcyZb19p zq%BdKjLGWRTe*Yf&~g`jACpLXRqS)?zK{@ftZ#XIYhc_C!!F(f>^f66Ty5waJL`Xp z=SSm0atDyj_l38I7jyQk8xD-F$h2=0yFJA)rU(zAeUVY%J-jE^D740h^N-ke(SdTk zuke4DF@l(5hIRh&D(nRsPu1O1yDuz`qzzGA`2Eqda8=yTt~(P<`X@ebuO$W5tcogMJ?s za|J%pX^(xL+ZcT_Gv?1>XN`?vfVhANf8lD*Wu)RKAzQ0rH#=z4sBSGvLvAstBY^+@tv5%Oyu(@B zn>oLchAb1nKUy-(bI6-L(Lj9N4?z!k(3@zvCXOCfs^8Eb}5XreoAJo_Tkp1RjMr)beco@fG@s&m#cXrcm|D2vj>KN)YWSz8yMx0pDXoF?IVKiK**iFSEl@{eWcei-xnZAR)>uEt`tp&5>Z!^pv2G$DvWkS>m z467H_T707Y=j?Y`vfE00a77EYd@ug6av1;AL9bfkx;cLyipkey3r_7>Z>s&W{ z?S7lD-G5grG)!MAk+nu0MZ?E#f5DBY8a_=lTpQJQx@I`(e=p?L-pH-d^{o?Lx7X5jj;wQM*>oF3#@Pqm z?qi(`+3TG2ni$=dCm5qe8;aBU8fCIvis6&&-%DH=*WI43bm>vxN&DR;$O;FMqeZ*3 zp)q1_6}h0@L@Q1Y_j|KeyWPY(gMmU>h8XV zp$qz>b0gJXE7;K);^)MHWa%N6Z)idL?;=Yyztz_B{8Uyt?%gkHjT`J=#v1S&(sdTE zBexId?-HPM7xeB12XSZc;MN0-x*x)vE zS2g%2mXYt%;U;&@w*GY=d!?>{t$)oMP+spck6-LM)Nk4C_)?UkFNsD>#=Qhz1Mc92 zkE8=zqJJTLWIOa#(aRX6_B%(GQeTM=`F+!1bMW^4@Eo}pO?*z(j~wdPS)|xGr2X7ccJNy1OlNZ*=`8CE z(#964iun)TVSrR7EFZVxo}sV+t?#gFpK zVT#U?RDU+`D~73jDXxOH?xg<}PR}8uIf~yHZj;bos^-iY_a@(Kz(?#}&?e623qOo4 zegY}}-Ha!;2Tri=9P6jukQ^C%8R&NJmc!`-_-xJ)G`=fMP z18$3Ihdm#(_V}Yd9LHJ%J90~*MTa*JS}dQ_F)puHlomyYP0-pobMnV6fWAb3qPaY1 zZ(^EzBfdt9%b~d>XioH&!yQ=#=CH1!nqkk2*1oE}qv>q0MQ0Vz*+ZIVJ8I-{(ND5}I`ngB?hDK{6W-^5_r=OkOXkgMJY{~>VB{Sz&9$R|I?-iKdBLF%C`?TWWJlJ9QD=d%>E zA@=?I&g3M0XZ#0pr^`U>Ln$VY}OpHGY-c$MDE(djPzS7cPkIiz6A z1n1lMdtKLtj3)gqEy?hXGmWw9$2;G4M@lx=mEup*yL?9${MBWqzdq~`v7#PcvUCl+ zE-LRz4omUh^`PX>^c?~0408`eMz!BLI5WQPcc@!!S3xgxxx=P=+q!$EF2xwjov7>P z_c6Ym1+1OOgUi<%)SJ2kyic28B+r?}{|ofJfV(U@q3sT23*XctUvrM95IfR!I*Zp# z%>7E>T?{x)E>d^9b`O~Abwy#~u**x;EUin?0Y{c>rJc+x5n<%%I za!uSBVD5<^3Sj?J|Fl-_j&O#Md-<>d{?{O*;pe~|01ny24lozRA3O&mYZMzG5t-UYtm+K_DXnB7jv=d`mj+Rk9w5ieXm7yA)-X@(x!;W2KLyVds3 ze_#Uo_Qc+cmoA1@^4*5FoW3o>HtZsEc{`w=21i;F`awJT-N1cLcR?QXuw|%m-B##kuSGX0N72nLV3R%{$R&p3 zNyH)HtVrm3&eZ^K8}P2)+RM8cSxoi_?(o^!zBobCpvYJWa$ z2lWi?#AqbdrV-&*bSOHxXQ0s_`^%AhY5pWTpXN?1UoH8>vrdLq%eYG+xEOzfXAIh{KIM^~-jyi0;HRyHE}y3Sr<7MA}T_Ni;5ZT4uN`nkTv z*{4F8N9Znri!#@gI!c#CLLIzltN!?MxE=?GV=+}E&k z{hiod;P%aU+Qj{tS_;1 z89zh5qVDcGg4TYMpQYp3y1=W*{F)QbQD-zAo2zy9Ce{DQ4a6ZMJ+i^co<(2e&wl8A z{e6d=PH*119>u%eNjxO(V8j=d`u3bZnLOt)XU^r!kkec6r0%LBCN*)~)@uhO^U!5oDuCnGv4&BzM*z15xiY(z_awkrg+)5xH3Rykr4q4*FnEZKc!J zD89wlr*^j4?Px6Y(p}5J3Q;lzNzkVY<4@~F*V3F z8ru@q_Sk;ddg>P6%wmk==)r&MZ=&f#NdGot|E2PebFg*Sg>*jI02J$|lJ#o>>uCvl zHq8!)H=XsSImh8mC7ZfxOf zc16m%5p?InjkWunRE*-+g z=_uDc*HQIrUOX_!Y#=D&!0s@2?N1mZy1O~9&FgtqoY#HycLjZu?MpJoDYWN7M`>oQ zNn@>!hvjq|mNxEqd7T(y&spO>`0%=QsnrkN^LaGRy7nfFkv&&u5W1I{Oqqu{=Mbnp zj=7ZL*BWhH7g;iPoUvv(FiZY~Rt6o^{9?Z`^sKoqz+EG2OR@344t<+x+bf2FPiO_% z?3uuK*h`?U8sN9;Dlxnt|0EhKX?L0-}PN%^j>_3UxL4*m#ou1{>8}6 zf5+~3EZgOaIiBz%*neaj-|aB!br**08imYz*~4|W2C-S5X~f<%O?FFS_YO0YExTnU z{a7$FYPZ}7j1|}|tB^}I9~Ui%+ASYMPsxH#D_0oaUFwHzx2)j3bO>dzKiz_iSUF4e z=*&TeKaf;Y`~*1D8efC$vgup&V!#^xi`T7 za{F*ENQSR-v%$2+z8$A~!3*luw~(zrl$=5wXXrcv-+b>h>^Y2?o4UGgFvHRvq&K+X ztMV1?V2rFai2lZKR+)oe1$a}Rh6BrCIG$u)%t|c`JOEXU9@e&gALkQ8l|TrHahYT8h{M9vX3`Nc?a@vKefaAo_E0oX{Oo-9|@!{ zXTEwE1C6JZpLD^eIU2b(ItDjk2N6!2GK^_s!D)9KO=NyaCmljt`RJ$2yRh_7?k^yQ zB76MIrE>=o^Pn`!FSz@8;9Te6q(|YC_!1O|e`NoIJx1>H@VxB^f^RC>j1G$?eaUSo&-u{n-oe)fjDm zRyu9UjwaS9>9l*Gzm+^cN~UDj8-qf z1}Iv!?ty?_Ej^dAYEv|8(I@3PnM0vWXixM2-K~7TXSzG6e#hH}$$w*i@MvZIh zXOU^QD~_qH1FOA4))uXK%3~bhOfod-M^4xYKJ@)Q;-e_mit+@K;cenKUw|#me+RH> z4rtAM&xXguUqrf0fIeOTJb$Oo`O=Hq(M{9wRbriOVr}$5mxJl|>y+DzF0B4)PKpm{ z4fH_!e?upgKB+mVZ#T?nUM9YCb07S@_+E)lnMFMxP)DPqmp2_5pN-Hdnmd{=oZXrh z$~|Kpy3QEQFP&}Cywm*4qx_E;&pO)M4PADz52AKcpmEni1N&XNwGIy@U!$Y9_d#G} z^*vnw;q?<6-W2Mof{w*&+PL?*92jDF5$?QkVe_#e_~$VH;r6jZpHALb*&@pRlJ^SvSN#3IyerT-O0zP&X7B1L zC$XRP3%+U1@iE5zp^I|v_rO*)MQeBY%aKu?w9&EhIPKAYQFp08cL9%;)Kx<}i|xAJ zp|03^Qp0&Z`36N}6%F%029FaRRdDC6Mbq^Eef0M|$)(o1@LlwL;h>FuH5as02k(kF z2Y;y!{}FK~#NSn~czuAmC)|tQd)TMyiPt{`-oBjI!>{R^Y_~Cg89V0p3u*DrkU?-XJoCaPLItm4Bx84eokDI#9Czedh3y`qKO@ zc6~MA#i|dQJc;^LZj8$qJDhqywCgo&{?=%hY16tT{stYSQ${*X=#~=8r%1TeK7qb{ ze{IxXLG9|yocOAqwJ&hF4cD{4rSeB$OZWe*J8ZQ{-hcH)o&d%h(0kRN$0_#`@V#im zsCjiCXPOk_m`8ey)#YYup_{mB9aD{U?ZjUz@9inv zF%_G1jq^IiQCkTuPjutE*XFz~pZEL>R{=hYgK|lq)X!5Gi@D}@_aCV`M6Xo?hm4f& znD=d0L5MV~AFVmuW!A?l-SjDBr>>3ojo~AtyJS4n72Z6ZcrJb)Lh!oqF9S zJmY&FYY%NbG9c?l*mZ%?RcW`8KpU!G_^|4roz=9HQEB)xXlD<2c{r|}`*^N+)^4Nb zi)}2AYa`Z=Pmx_zzuNHuXPw$eLY^Cl*3pjo6Q*Ai`LFsBK)%o&@%riJjC6{}_*d z@_grz9XDeSa8P&NoVnwMQND?`72Ekn(j$*YmfS&KT9JtqV^DqEt25d3Q}@HGp8<3M z$$=WL&q(tuF;-_g)9Sa9roOHCv1H#8Ywk2*+f~25XVU>eCDMZUOhF&3b!J^_566*>D>$gy>)7L373))-zxnILu?<&k|kLEq!>^K2Y`n!sas{Ag`) zv$h@!KXu?ob^8xeFaOQ_>pt(7*araC&HHdMeF$D!5^nn#e{$*-?Pt;E zOy-2Ocgs2ceB!$`*Cuxv$Uxh7o4-kcHmv6-Z`+c9%iP^fd4Y|~R$~|xC+>rY?&uy>Y^YpI8r~Y}@>yLlDOSTc$ukW6c zWtQK*J7M_{Gbak)CfdKDw=udO_MH99!}giRrY`Wh4;&vzre2dh^%J7;RjLv)kHW>v zloc)_z+R0{Z5!`~ahJ6Qy})yIiFL1;KaYOAHm3Ehj@%5N<5zCBI+j(T*n?0598Dp6}~; zak6jsug~?tlell}d%jPeJ{&HD3sX~lTU)~dKKD{1pn ze8WD$zvkLE&_@s6wpqSU*F1eUeip<0_n!LH_1XC5xckVKHDvMDtFAg&c6auvbMWb! zgKtg+^!8)?y6(m&=iGkh*57RoS$y{)_p*;2ee3TrFYj8PYU~U`Z+F*R)^*Q7PvPCB zXWjZo%_ePwoC&eO+W;7ckGg zUGuW1zRn?T;@9cxsl<=g+|&70t50>~y&D)iM_ga5b3E7FJ4OAN{qa3h7Jbrk{SMa5 zPm(<0BqM3@irI6lvHLMNzMHXY1g1}&{rvYZexH%Ag)zLk-+A>d=77b2O6|AtX>&%` zmW=55JzaBo*Pj`?H{71vBBpyzt2ycRM&{l*-mY3o~zjh;_pJ~!|8>}vg%VV!kq-H9zC!Sev}Baw= zYi2wzI6uX|@er`>7~lyDUX0@@o3}+qTlU|6NBooIm&Ar}eO)R0FXtMgs{43+K6qn( z3HDaber0jquMV4aW5Q_ey>_%TE-Y+WO!gE)4QiE;$3@*<^6wk zeFyRa>o51HqKnNk!@r6vx149skBrqf%&MPKN15bbwOo(P<3om9C;ddSQxkk=D{J01 zc+cgXth2~RhxI_@Ch?nlsptNZtP^9+9R zFc#^33i$e(Fpd&jHjJn6Ts_@}apyOKv13oqW12yoqQ|^(QF;t4ENu}jiVgz_nb2Y1 zmSpm@+Ia5EbKpYYhj)l?2$$j=E|+mzdmN61UuzwMrsOAdJ2)~aYoIqtSKq$^8u+%c zT4Qv8=UDxFc&?aAA5Xq-igdqD(euYoA65S%;B9sozGigzJlfQlsy*N7#;6B#JigEX zWMA~zy|bqK)Q|1Cm-;%Ue%}`;b9mRzxdHVc!CL#8ue{M(_g=i=CZDxm%i7nBtwZzp zQ{>B!StCBd4s#JS@lH;TS9*%i1a``%jd&1MNcKd#CQlhW?8Dm{! z!+|<@qNcj2<6R@X4ZC?eXPfeDzAV{O za*ofLJWhIv|EFey;sJ+p%WfMsyVrH0+FrLcA_ok+&2?QH{y&0Gb|2w9lKNbMTF2Mgy6m%2`TJz%NT9xC-MZQQ zV>ev#cw}|dW+|B_8c!`(dH8k`HtW#YXBF-)JFC!rS^CuE6VgZej9bTqBFm=`Z*Wc% zK4VL24Whu=C6bGFN1W=#GPbjO8vC^xgn- zxDmPwGM|6<9nx$&C9;t>i@B;cq`Nwwi!AxR-PT^at-*0^ZK16i=7DTn-P_tgTZVks zVuqW1cj1ODkp{=uy&wtS7p=S#zw8{3@q1SMt3=bDH>v_ztd8oS;|ux8eky9gTn3 z1k9U&*=qmIB=1aYWvR5k{xM_cYuF>K^ym9~R{)pJRUb??tT>ffv{6Xemt$qYi*O=; zn=QBZ*_n?%WR(r|^DaQ&vgBO&%H`l}2XN&f&m!M?#cNfUY$zvD-_z9hRWRR3zF(5> zFfT31_l8bmjb;51FJo=p6dA#KFp~8kca+bfsWP{BHv1UXUN^LX&xvgZN%ZD2{#qYG ztPe-gnlsU>I1<4T*el+}UU6DUxSDpO@MieK)sZC&c#qZp#E011Vw${|kPc<4|WbEgb;~-DN9Q1Y|Df;~ zbC7R2;AI?eeAQUFfw%B5&IbFR0%xOn5B-5QO=FaWqnEfZ8eA-Z_TJ%6zMkg&2`U53 zuSpKbKFJ%pHp3@5!zg6jn45dH7~u|NU!$g`C^{Z}yd%~{$D_GFXGIs@hVGJ_w8Hsz zQ6M3uW$Tmpkm3)ug8E85M{p88If{>V&e{kcI%A;vny|AyatwSl^6ek&o9%-3MH@Bz zuBV>|;@+>}eV_fF4J};7`|h~+AMyT~{oY3TnY{0ad%v9bk9bGE@OFTQ&TY(je4QqO z17Ka{s?79e6}J{2fDQuJRu{)`a1#5NqRBjD58*)l-@3M%d$Chn#^6gQoJKAk;uAjA z56NGr-~+tcd0Ch2GzK2={Dj8Dr zXx1zs?ZOu0f6?~#@ljP*-}gB)fy^YlNdkm`WI|Mu;A>GJl9)*X5=2D-Ma4=YRs&S6 z?;utZf)WfWBUmi-E&*E2jE>a`VyxF?BG#hVD$;7J*V9gdt&>DsfdDgz&HeqIGZTgs zdiVZ3&-2H8&YZK~*Is+Q?X}mgspbC){$I=gM+zJ27YEInM+=Rbm4%yY9}GHb>VVCS zk=EM&!G`)J!EhQhs=gn3_R?Tz<$&Op+J}Ohou3u9)ea1{uB5cHsl`iVIl3(9oTkQ;f$-&mz%lReo{|bJ6f{F6n=9jy- zKMBuigl>a8}O@MkF$vy_YmNa*rvYSN;&aR;ZgtCQ*is>mG1Sr>q_7CzMFT> zU|8>38^7UQ>!Wv{XHuW;v}qi=DYJ^RJBe##I%(p?s#Ey>Ds@=EAHwc_ZQdQOZ^)yf5edq4+y?QpsP^Pi+lL$g`Pt zw4XOPmUeu+LpbkOZP~29{i*CBb{}u)DSO2$ax=?ve(7L`I>WzvaiR=j-hOJtj)_$Qb*z*SdcPhvAp*8kyd;f zwW)W(L}T4bzSCp*RJZ2qWH9-0tPhIaDEq+k_^^J$8dL21Fn!nFOcm!%CQv?pPEqyO zQ%-qY>5Ekt?d(EtXnoH1 zvsi?D8+zVO|AAFLEwJa-?)AOlt1%_c@%PB9b@R8zSpSH{|5s?41s`bOTZ~WX59`cw znZtLs>{;l^uT%G)hdF!WRJ<^U{~LTrj^38U8Pq9PaeRS5k5BT-=+ay9S6k zldrS*@{@Hzr?l^p$9)5tRSP0+*-*??8xF!dKE+;;QB@TwiH#$hF<;Do1H7~Md5FKj z{C#x`ypTQlgP$4x!{h2U97ON%qaz$PBd>CfgZb5(9~u}(KSdAaqt-irBWDX`r}~6@ zvE?6ihSlcKfZ8nJHY>&JC{3p-f|4@ghtGc=V>AR6Wv#xW1lTj9FZLAI)mTRD&Rk(1Yb?nh5xFVR==J6 zRm3!teJ+GAzjUfM8E-Y`d+M;|tzxV;j>XSCPh}pohjl6QFoiS0iv2+>JEz0f`M-!k z$lb`NO4r#STaV!W1Y(Wb>qGVtyYA1pr&{wazjqTjr7?E)wXJ)W}?!(#ozMPdz1|BZp z;s!rcz|T}Gu3awowYvE!9+GfsIk=}Y(_5^%hFN&9k^c?F!ad!chR+50u4Ha@vA@gS z$+jqS_IvsiVf>kolr0esKghm+4scQ);fruA+{f6~JKqiHIptF?h6ZK@M&vksL-4Pt z+^W04h6j!TpSJJRZ7BO5yzU|ITAhQebJlIJ;#_ghnCt3IH@NT_L%zDJ6*wxc<&8@n#*O{_ZjW*MF6P69|GdtiiKeQK z5IXL>^NhPj>~r`^u`kGoo9+g&T2OWp@QA>g|#)AlLVT*krhP9!UXS?aL9y}Av! z$~Oml$aBEOOdDBW3cR$ow(>6jl&)b$bO_@Cf9_g0_?ZoLlvz4@#Kxx|EnBi-a9aHv zjzeo@(;h^N<;M;5Nhm0PA}#-<`lA{_aOcZ{)o3v>fC1 zx<{Q$@a28E0)PE3;yZRj%Orz!ZT0$7xQBTxzX*KzE8@~gztf%W#^1YVkC@B-Ajo6) z0H4g-vL(yTHSQ`K0uBu`EFT=50nwaUKH1F8JkEBQ^j-Hr6!JYjH_CU2b)dPqASVpt5#zFoy;jApsv*um)xaV3iW%fM_`|)S(aFAT;6ony%#vhZJ8(RQI%KjN3Zq2)!yPv~p*x11<7cfXCUln~@P9N*@ zjoZ!s!|PrBGwTiX)BdEvC)4BgsB9H=wNu_r+0;DacAWftUpbL+1`Mw+cDg&;k%KrS zciKYm#{E|#`WCoQ-l<*obF-+f%J)7#+I66A8^W17g=g^OG`q6^TOlzkK9Gd4$TQn!fzwtiCa~-fB z4V;%aGI!f+YgiyZd3e1+y4LMmH#3*CQIVEWKN=kAqTPGbGV9~84nyyn$WxV*>>J$! z>x>>)|B^J(BGsXOseBdX+bJhFF96O}12gJ{b6GJs3%*u8Sb96<)pn!S{~)BdRTw& zP>=i+ys`@yTIV{}lr|5t&UO5Pyrcf5jZCYJ9i$D5*F%4{Q4jm`I$yBHs2=WfkuKdf z-p&i8YoEzVZ*chx(giQ8-Ccz`PqL=8;e4NoOzI;|@}eyd4GV0gtn%vYhe@BM*R9_e z&tsnf_#b$C2-wT-N8^+HBz;x(3jJ@CUPbz5-X*K7q`YXeN#9jpE$=(D4*)L{eN-9o zBR#L@%x+viM`0z8y>F%QMUlspr%ClAkLeyH)qkf||MC7V;(c4J4_dp*bE}mnE$|!G zX*^!!(c7Hx%_R6|A7mu%c}0eI-Qwjg8;9lRKN>&reb8I&IVk>6I(VlzSuXbE?x#P- z=Sc&n9V^>A&*eR!bJvQy) zjsJjq!RUz26Ccg0p85GK)2|qG<4zgy%6)6T7=KkBGdKQ*$X-+o=vQuFC8hn7n&)%FEjhScNyR3WgGkVA$N2*#|HZ0 zn^kjG%EtT!izc4EVC=*N*~Xql&=St?JvQ%K=Ur9Mn|{RqskY)PNJdW&d^^%u_bD`| zAM>Yn7n%L)R|6Nd>A@c;-mk|wizXS%!}#?rmA)>bkBjgLT12d$llALn&S9s}r~CyI zCbo~Z`{FX)kEtIi^rIWy;s`va+S*?g+#Z-%jDKng@N9QZ2s{JdeuXxx&hjEln%3G> zpEId1OnnPuV=Je=OzPWEiG5f3kz?J^*F5@_4_%sOx_4&*r-j7Sa9yx!1LqufS3y&B zr*1q(=F!_?ap!j0ap&T=am1MvSWUg3GiUYGEj*R((|4<_OXPuf-lftL{u^8~Egc~Z z{Xl2o&Oskt27PXTr&o?PdHvh|1BAh zIwV_FB-FDnR!<9f+`FF58|-!Ipl`kVrM#}yCDyqGFJ+rKx6-?h@AvK_=lpv2vF<KzDH?)8#_{6sy~vo$T9et>dhKbg=w0y#8Xth+gO^@y{c@62D$IWp%fn zWa`}Ok)QAfluw9@S!2s zadsblR{CXnb=eZ(K(_Ol4MU*^pZyqH3v#2*qRD?COrMuyJCXmyVea!?1uY7bmV3LFo<(SI!}2rT%a!} z;KEhl!es3iB;dmNlTW~f>wr(U&^%o`4IK zaZ@G{7sitB|A-3-<%A2PC=r<}%OX%Z;o@>`2y` z+nc$&Q~N!mvFA(Yf%&H23>~}h_g;MD%smfa&kZ3DMx1>DQF#1OYb=cQ6T=@uHhV0x z=A*GyKl!L}%u@fttN!c2^A`jTT-N;O1G^Xh=D@=HBL}AYj~>V~Q{Ho#sr;Z{*vsFA z$M1sgOLiXbK?mdB`6Eva@VyE>$|e4EUWMs1w7)*`bYG)$L}Zut@BT0~y5qS?{=8}v ze_V4aGzU0UW{o92fIq?&S2TLpx`Z<&}Hi4gT&Yr7bfqkT%alNq) zd>Q|_Wk<^l{1rL_NA_^N$g3QzQ%cz{MOj7V(jzTyJjqpGrk@B9`-j8e+v4%?GU~@@%~uy|1;g| zN2@N%F94Q#m2P}dCQ%=OZZiCfZ#Dgy)Qhj< zUD?R3DXEM(rFy1pM=AJPq{4ewS1o+)v8qLpVzb|(cJ8szIg>C??j_DF$Bx;kx*Udl zx6{SXQU+pAOUMI1xX51TGr;pJ!S^e$0mkksbtYMMKjHhw;QMxdi7*=t%(ND3D_ApS zC%`UQ6~p%wU&kGIi~byTI`cnTV5~ccO+#{)2VQ*$TUXX;#6y7h zXK`ku8Cf(7-~US9{SI$a>XlE-45WCMJiq$IyNk`#MXSzzk~5ss9<0P(fG(837Tzvh z+fOWm0QrlbHt$-6zxK2{Uc6iRyyzr7X?x7OrUjpv`R+MEE3JgKe&jO7KS1AR5erzl zw%WYx_9td`{5D8?DT_SvgWGCaxM1Om?qKKs2@6j?=e+P%?z<>6_Iy3dTo)^q;o6n7gb=40>tzb3y6nH6L$sAFDFRrW`mSDHilal}FDLYH`* zauwi?=;r-Af62SS8o>YLrMxA`WnVo$Gu!PA=A71CwCi^Kp}$Ugt%`ionV%3sv{qFVgx;XN`i)qbFQn2k%~{X_F>W@-P?E4C!6LD z4Xl5_<`u$`-s4?GTf(^x=z4oj6MGXT_Gt$=+Dq>)uwV8d>E&NMxcA^a#3}5bx%+!o-#AB49XIj6`H=V&cT7?5>&}ZUpIohv2da#9 zp5&4sF(rMY)9`oN=Z)$N>^)mdUqfH+TS~!~Dbt87xLyx1qQnh!$*fuZC*uMqZAR4IslPbm~d%%s%%}+Ktd?y>nQRGh-b-bB# zu|Kh`Pl<;r`;Taj;(){48VLHs=kNe^WKS-`hWX(C+{q z24@}wn{R#uu6NkyKRgrgU#Gu~wG+9g>^jRud93!G=AN@@e>?5J8Pm5LYp;vEy6-_ha(-tv^VhJ?+!MDM zIq6Rl*&_&{jb0$!;A0<`-FY*d9nNGiNUJ7 zy|tH889O6cf$S!_v6C_CjL`Ti>^nuIXUr#lo9b4*&6H8SiFMvYx^Pqa%@F#fy%qM^ zp6aIE_1C&KhJRC509>9rH0Lyb_&LtY>?|udJdQs7iuDXHbJryK%4z%XyusEvK7Dhw z^!0_=ClRi!;ytg@=`${9E;>|(oj1p@&ITU3jkA9c{#ESJ3Tnoej9uF#ENRT z8Xvbx@;r&ocfHqtu^SlD<|e`6D&i2chIToKJIwt}lT%BMdVu+?RN`m>Bfc;3|NIfl zx7WB+{9|aK<2i5B;W>e%y3;v%3^4BO?%tlx3)ZvSEYKDU2% zuV3doG!LRl9`60x3~klfmTvr|A82MDY=E&{?|a6IPja5cY38$+dqFm`&>g~8Y*3xR zY5_KLtsmEr5%t)x{n)Uht>6=9;kJjzT<@130W=?bmn$%d`Q}gE% zPvhWT%MM5!@U(iJXIhqQT+qUNmC@G`#4psl_$)el`ogr#ao;#o@=sPKgs-)W@%*ZG z@k#0Pc5)8gYJEWK?l{}0dMY*911 z8~u|Xb7-s)opK{Sw`16+Mi0<>c$2lRyW+z0iSzxz-TsUD7Gb|&JA5lV0eu&}W(zdj z#)-7R!#CSFQ909!*Vg&g>nGww7~cVOHtWm}IQVi2`5K+qM2Y zJ>`$L+4kD0+pK}F3qPmP=4sTSa;nEmJhQ}pFW+(MIx3k{)iE*F?{?^f`syYoNXK)N z{q8)|uW_jFq8IUgmlH?t5!%!|twmi(GK18(mFxC(d;XvDB-8o5m^FHLCApwMR3;+JEUwTk7ZU z`x)Dpu(uW_pLoDg_(3r-2b%9NW{-yt9EJ}_-?aGa?KZ3~?sFXGr7xjN!TU7Nlic3u zI8Vw+=eiTUMsqEg9fqH%zZUGiYgjN-KQnJP{k>tw7-o+LcHw$&6Ki0+=303qSB2@X z-mMsi%x@a~JxG6t&|m9});_22P573mzXwEcV&4GutW}h$N91{Hq&l&EQ zKk*^j`PE_k?t!!5r*Zb+Cp1d;oHw%m8{i99eUDfeIS=}!Geyc|Kc${Ug2mj3Yw&9DP+3tN-TXI0U}kt{bHc&bk67K-~4VjQ2%9KP|MeO~`-wE0k}-6rvK_y438TL$=4gAGK!VC?_T zu6Ek;U{a6>}{f+4BmL9G9k8I!h?zQlj`A*;a_#LAMa?bU{{kCIwa)y3s z2|i7~`Mzv{!V%dDqz6dmmwcx7C!j|tO+HFWfAy2Q(HpLbhzB&{Th<8N{orgf^Cz9g zP1$_bZpTn`G0vne2fosC4B6mV+vv%&Q*QAVbZ{So>^L4^uKdco6J0@gzxftp-BEa` zYz~&ZEB0kaQS2;lY(ikO>F;PI7Xkwfn)99rVE;sYog=qbNW8{89Jxv0OM;P0}N|FD0J@s zpfLO#@fh_y$k=Z~-m<=d7s+PO;Q#2f>LSmRj$k(WQ})Ec*lO*53l9B&!)u%;$%5xs zu;0J^gNOVc@)>Vq<77;&!2W#B4>l%0$hsm1q+#smoXqS7#;@`h)89h%89SkLi-SBb z7Ea?|iIc;(+^>Fn zVAt(`KR{fZqu4@b52CCWnx_8F;_1@+-Ld`-PqX_AJur!*b}i{fEQT5Z+u%Q%EBvu2 z`^{6`>o@+heD5s&ABFDU$vC8w#n*X}^k?w53mTcox5Pibq^#Dzsrh8=()(Xy{K_vF zD9_EDXVWtxIV6_n7YTXRlZGEkdhdSyfx4}^?&{+cE-lA~eJAT*b4-jX&YxFjhWi*3 ztUSAtd-B{$9^&!mE1%AES?L)rD}6M0a1-gwWxn;@Gtl~eGvBY{J36CPt^rxvO27Pg z`cl-jSrRl2GblK9;{zd_*ShPNimVjl3E9|p?)}T+SH>;iKH=fw_t!-( zBo;#MAJ_|k7s;-ZzqzkpG^7LjLN;f(a*J4t#3-AvzmGpZdr)4OdF^&$&FW;hDqpdYiP#j}#t+kKIqq+Q)r!fdR0&Q`Jk|Lrf zhHSw}X7F_S)f*3ARC|nTP_Mqa)c)*PdrJmd=hxMyQ*GvXqiS;~V^W)$Jk^c^ym99m zQHzdYch)+FRu7h)E4&*U4&&AOzDVCaKbhIx4G#Zz!`;Gf*Px%yET6}J@>*-*n7tNs zHt3O1!diIVXp(H+2;XR|z2R86Ot^=vIf7i_h^(`cGa9Sz{eE576Q{k6>7^Spbw<3M`?)|!3S;gfA&`vbD= z%f=sHyV?&B?Blln4Vw3uy?-mX${4>kGLoz@W;4bWDT-x!!)wS$<2S==<7X1$JnM1V z?xt_&(6_kGC4C`IW3NTWkJDkvK#9Dl5np}jjqv2zq50BN?D$)YsZ)BT(spg?o-KJf z{$9>|qKuusU^C)M*{kneESS{KP z{T%`9Wh)l^E3v6bw*w}#uO)v)Y)#JKfAu9X9ADMXKBo$d&6nRUwAK)CH(y#&Xt1AR z#%QAU;v}oY$5>1q4^rO){FX32wR3DP^cq;avGqY@SX+PGOxuq+y#;ydGxykgiO(?) zdnRXvW)GvB`sslWTl3b{zXvxZLk)?|9eyeYkvC%0}>H|AvyW_1VbH zBdNiynf0r=%bs?gFC~pO_tdd(wp~7|=%8^Jj{|0ZUBekM>a8HAruGl2mX}z2 z2f~vE?19=t@W7uNhY)`ooT(+RVC{E~ocJz2vy$)f_A6$~#gakS7@wczlRUebx<0?0 zJ?ak@`(LlUK4R~+9&h_Mv@3gtU@zLR6xjy9wS2`j)qSU;gTLWStNN%s7A|I_T5>=e z;I<>yfw!d#7?^S#cr5R1u_p0OHFXQ_WJO4}Bp>u6Hg#440f0C8|rR=ZIwfb)1 zI%yk8v+8;1cs&!9UwaAgI~V)Wq`#DHoYzy|`AW|*nqC zblM*8;)r-_i7S7a&->cA8dqR0^|aQ1*DpK5_NzD_9c%xr-t}csANx)5`d*FKCmx33+!urM71(ErSzjJK6V`w`z1ms>rRayk(Q@R_-fN&vxY=tBY(^i5)0|RpLi7Cy zd@PRV)4_A}-%iQi)4}VrwWh%B)vT)r&3?~!;v2Au^@T1pyPkV$w7#_G*g+k_O~2*) zP|SYz1RFoUKEt=1HCEbu0ejkxJtJ6O?5d6)j#`;>4x!W?!vGr;qa;Q5fi0B~HkM(EQT zV{(q&w@{ML#IEp<9}t5hp>J8e=I@)Wjy&0L{$>77g!S*tj97W!NVW8fEw1XBA7z;9 zzQvXzy3*+UE_(eR3q9y%ibpS7x5wn}GH8SJn(PU>$1EdK06eOgTjG!2rS;=Jvo7nG0#r+)2Ra;saKGm`fX5#mg8Tg2L zCXwGmA7$HJ&%69J;_qvD9~W=O&_09u>k1^(j^qNXeGj@(?7PYLF??SkUf@aMyWN(2 z!qncY)F=NeyZ*GmQr>l5->RqLc>Vw2`w+gbqMp1~&J3}yDcPfKSR7{ZLme6TEBR6r z>bs41$zzmTlUI}M(>ALu7hel5Xh7mxxco`Ge&xB3 zc+uiPl3DbA2s=}p@BcFPO>1Tn^}ZB)PftIt)5?ZFm>;}jzsiqgZL06fum|d$1CJJ8 zjlYkD<_wC}^NIE$fuDR<`^UZs9v8>{KN@>dV1oFZ>YF5edhBtV{cy&jGMYCl9sg6k z=f&P-v-?Yf_J+~|cg-O#1Zz$CT4M6eXyE9gjyTM^_-^a@woS%NNE07=pSt345}xTF z&zljrjrK;z+PpR1_CBX?0r|Cuy6{fBjp6*4eO`Tt+nOy}WyzDa-TU(=668s2{qgY@ z#Og)BJfV+&B!B#@d{HdV`LU<=nSbBM*nUCmJ&k8}?7v|1Obj;pz(zQ$Jp}dbkOhb1 zF)`kxjW~>i4?ASD-EGGbsb)?UqqqbaP;rX|(^tqpG*;KE&=%3V?^8y);22=xLS9xo zl9{)NR$Jq@Z7V;cEV}--5H|L>ZEO?YWCvTRbDP*w*UKjC#3qdWMz-N=cu%M9LDVfC zrn+maIlj@dRXwTl%wM0_{Pnf+*|=cmv+Z(^#PS^?PK4ldRSZ6D*CHP=2A!=x0u4Gm zCkda;KGu00`Jkyi=!dQ0*`YQU|41GR4tHl3%aQF_j13!|^ ze4GATex|z1JKZ|pn0Zb2`aclIz|EM_!Dp?1*@L$CnwRS-`@l@ozYL#L-A8+5g!U$4 z=ge#E^B*={N)KFf$8lQGyPm05f6@Yt8mH49XV(m8EM`E$`0#U5zhx8p(>!D=c)T_4 z#iEPp`0Z5B+{z;pE05A(iC^6kR! z&w~xZRc$OOClBMaV&08LkJ7mu2ll1J^DpoTi;uec)MZuy7s1DdO~M(OQ+(Gvme9t& z+cihFKV(mAhceT50v=B8*W(Adg>pW|)8(||kQ>ls%Lfvl$7<-cd>|XZP5DG>zt4|9 zV*U$H%zW(cwm)JedDsie-^RH&(Qf@8z_Z84@m=$-cY~{EzWd?}mXBjA^c=hr%T8k7%KtL&&L?KduM(N8$1idk zwszU;Z((YbnenJ-(J5YibIGLl@%bbTXV#{{!?rQBHOtC+jZza?%v9 zKx;RfI=i?d!V{C%dgIpLR@=BWRk&3q8ti0m(};e%(~d3X0*AWOjA(a`>F=IpEXkqI z^U=Z22qK5SiFh7KVYzC{-9XD znul5YfU4_J`i>n-F#2#lyxXd4P=;>|`M>a-=c^-+@I=16pQ~-^g<7MD`R++am zdfJ~X*h8oP7S976u=4C0u6jTG;+ae+rc7ytiX-L+*50_iq@3yx(?jHT;U_p*u0ZYY@k_V8N)+ zKJLEdhaSF8_Y$tsJ~w;Rjbn=Z+s9nuuR*8GHZJsGr~I(=*aTlF$M9|4Y3!MFt+~#d zWL(tB*#gPOA^ItJ{}*?Bx2DCup||SWYkaF4Ff~$#T-ylU);XYUIYm8nJAg@PA@X6Y z?lY*n!C_n!ip5Zh^XRS_moY|f^xLK7_)$!~ov~YRfj+;a`|YvGLR-%At>C$$`9j~8 zWyp=-No0|&lbtl5=c1l`eb8--Gh=xe~~6@KnPcZ|wdJJpJ1anHw7qp|yR1Mspn>m13x`^dB1 zaUuJYrhhnl9R>48`>F)vlJUNyI}P8D7(Y=UqG9Y}(o3ZyOybw-nEz9QSS~+FGkkB~ zWv)}Zo63y+9()Ar7C!V^*kN3#ywku<*+%nz=-%l1p*d?;H}{7>?B0l6eODzm@e#mL;PUG7gdC?-s3K6tVfn>hSdv9^A3jkyjR=`9m|KO=85_oSDSH>7?JuNY-`7K{oYH)#BO;Fl*W4<3D8>K*M{r}B~H@#ks0 z#4Y`|eM2@Z<$j};cK;@P1I{XLSC%=VQ+77R^N?Tj&;^qw3U0E=yiEJ*cib;fH0BI` z_rsTU)+JO+d~51y2M1&`%XMpAo;t=+JigH_%-f2J36@Wf^}qTeUth*x{SRH_(>z%J z8^-&<Sxv) zHS>;ryR^kRH*6T*Z{z>1d>NIaOp`fvRgU5c@}J_<+1ZAysu%{v{C6Pdn%?h?axAVo z=3Knsm@%<(ROQ5*M=8HIw_;ShE^95Xp6I)o@s~DF@YOK(CmFl-y|BRk?wN>xP4&!G zd^ez9#^f{X@6{76du=V>t@5FXmR>h7{_ZW{-5p5f-9C%i&wqsd*q(EkPWyb1&e~k? zrLnG+r)bx8`>=V~I_JyPz~zU)(->x-3AvyD71+)~wYv&iYu_ytZEmZ5uTZ{f_cMl% zM}Nm>BqaqywQrOD>q19vbx~XGJB2Rd-N+{)RQnsgHy1h#C;wXtnMY!;zEv2i-N}D5 z*obds9X5g;#C>SIzl`$$oK-~6+69enyqkA?zZ$@2eQ!V(Z@jszV3+PAW<8aF6WyJb zABJ@{Lv`&!mL0(T;q1W#;`IfIQKouIn6EBk_)XFH;Ik$);u7Hwae73vD;ZCAL!r;W zH>PXG3}7dl!NX-Q4n6g zDKnJp3^pu(hqT`mwwGxwnZeD=cNI1)XAXfutJ4uw97FCuEHKr-)9n5o<&NVDXit1T zZI~4J+L*`iRP|AF`i?cHE-!QJI{CbwY7A>*V>ov8@iD0H>Qg)Y4(~Gx+P^Rg4tApp zbUXZ?(jNmKe8Fz7{>w&q0_-NyH~Xxh;3wY&SH`JfXi$#{ZofqwKz~)unWlOfw>CZ2DkMmsFhqMo{3Y{y5JfcnUZ!7pVs@Jz=e3QL&#KKto zL)M_lnb35;4Ndq~#y6e&IW6H^!+KjL=$G*AVZQYtUvk2?t7B(OJAm`QyMMO5cJ!X& z)+JAMV?!x$$*5cqsPH*k!JbjreuF&>+OT^;jd~K(j^8L^QRuOm8yv!-31>W?bxr}$l-h6B2< zO?!K}M)QGorxW>sUl}^2GsE}0KO-ydo!k`0hob$?QsSt3qxt0RtcO?jA^wz!Juha5 zG|b|UTN!H&?`_Z(?TuWxN_=KW;EpG|*YEDjI;StPPk*_n?xRJOjy+#7ma~7c`M?0i zGLW&n@Wwj_Rv$QeV9^iX-}nOG7JWN)uk4-0=Acej&0`z#YSwI6gq`m)VA;T38&?C@ zuYsTM%sm_S|1){QRlq09(ZBO+^N9^tE%-r1`y=5`CO7Gfp&LA2IJK;xdrZ1t<85Cy z)Zaa3n7@75aQ}ibzAqc%?_QSS?|Pm)X7Hg5IR^w4AHDk<*8DeSQ~SZ>f{t#ZspCj; z!9Uj`yFXw=Tfbnx+1bZ;IGI@IPVSqbjU%btCoqtAVsdvA=ewJ@-{CBt+~a`04r~>7 zOZO-a3H^yAi#m>iF25)Ysk483e-=AwKGS1|=mgi|a*Yh00b0g2;JU8(? zohN-|jT0xHXCI#RJn@AKJ;}2_zQG$g2SvLN?o9G^00W&3^LQlRWd^FAyw-}@)0~C; zwTOE{p_v_<@K>r`Sy%!8^wieze5df(^Cf(Hl>fix+ao;R=DC7rGtX+CEj(*@zQuDn z&z(GL3-{Gt8*Ht=E*KhEQ`9=5hCW^&JeXA)ZOf`D+Ly|EPEFB$j*^dB$0%)1FqB5V zVKqgESjX7u_j5jOb~~^P13yoV;Y*&vz0A(C=;7qDXczOtS(E+c;FA2c>o~*x9pHL6 zr!?9%rqnv0bwWC^6!H_&mua7t`n!-7Zm-iivG)LO)&DOPYr@_GU{5zNdKYeko!*`j$>G2bQ1&KAD5uh-s+9Vgbapeizz z_RGn;37QkhaztHc{!ixN90z0KziA8(mz`EnLD@~52i^=#3XN#t9?X`ac4$W1n3kek z=d{SdoRVm0s^HsF)Uk>Brvum2mZH!Mz6~V*1i_j2NxY}=UdH=$-ivvk+EUautEOnQ zb5~(#IWh2OkiPtp!q!=&FMqT!G(qnx3tK0(6pc3P3Uf_&q@CFHZF7`fT^K56?YvSN z)xF29%WDc-%cyU89d#`)>=1oh?vAuBC%%3dpZNL2n`2%42|2ifdkZE(FFdrXv9vO- zHpXR*XBhZLTRr1pY^{v7XFT+`m2p_(VO(vDw~g_(G9GJOj8Ee_J`Q_aZNS0q_le`O z#xxKZ{$Gp-y35{~%lB+-Ea9|MtZ&^z@c;Ar&gQN`7r4ef$<{f(MBFUz?A~4loR2~m zj?BHv|IL%!|8*z+&dCmF?jYY)_+cI$Q_>Ux=3($C-OQ-B&it{?@|@|r!^~kjJPJJq zy)47m`5JeVvPLRce^u-m!H>q@gKVz7=&IY0Gtoh>_d~CtFRhciZEMGWwH}6e9;ZAUPXD;;=z&~^0pCR~%^8A#Te_{A*-6qPj z_Qg-5+)-bZ$~Smw6}nt_hJF5ObU((!|1damjCl@2>+O9!ol_afxH19@$)k4KfvxVg zH~PD-j7;SYx=AMYs$=Uc=FW(HHAUTJqYAp6$xYp9BMZ728#-#Vdn&&&ek*}>Jh5yZOh-PK@9DI{J_vIDHAv(8R!a1i|Wd#O%V;wVe z28ww!jhVWKZ5}kL`!@QqGCim}*|=k^N%yuLga;hE4gNaZin9^O7|!_TmK7{mR90}5 zx#KLKk8?(Q4)Y(HG_!rI*)$D39bVaFwb=q6<6KL447~9+WH0z!Cva#dj$jApm*$zN zot@+AH_S0pcjxx8_pX~cZ|FI*x#&U@*=1`@QTCALqSm$0q2bL%ZR>fS&T}Kr?B=4s zZe)#b;(wp!qR-)jZM8K;Irz`)TgkIO&w8HtRhGb`TH%M2;b;4vgy*bm;oI`UR(N9> z{A%C&mZB-}wGwz$8GOwPzdOj?72<`Z@U^Azre)A!53=HC?74-Pl|M) zD7?r+J=xB_zBg_+*BQ@7cC?QxY3lgI+a#Guv}hk=(B6ACyso|5+jJ)RC9nOBF`i+j zqzu?8xMe^;h6kL;vyc8sI!T!g+W>yg=q8()3&)xGnF7Oui=b=N-OBiDe$N_zzBHPL ztwL))_sm@z%GozRIJSiTQYU#FXI0NUopSO1ze4}nE8Z^}C!b)>y5~36854y6S@@Be z?Zk>>eT$ZgZg#WA-zEM+2KQ@f&4`zEFs3|ckoNsV$E-Co!iY}e8}eXN73->7>!q0g zTC=U}A?I+ef%Tf@H1>aq?PB@h@}pbR?YK8x<5K-!(x&K1__mU0)#b>Xx)X~#3_7@@ zLNZ?aIK`xN@0Q%^f{vyktEaE>Hl61))@^c_i?9XZGiBy0h7fUqI@@<>Y^&JYG%l1K z6@QQXe>hC`s_hsA-f6~c(G}sKd~UV>Ka#paX%6lOPHM7XOgZ^ya{6$c>c(&2f@b() zEAy`QLck)E`I5e+Z-V*Ig!waA8`6C=cSbg_5uWmFy4;SVoh!VbpH^VaDKL%C*UO~a ze1$vTxs&9B!N$6F@{mhYvH+^=UY5$ zdG55+(?YH8Qa;7gXgIapwKf5g% z^dIp4+$7saK8&47{6IdTufXFf+qj1t7&-X%bJ{q#s3h7oe|mw=;)`}w78rwTrt{CY-*qXZr4Vwi<8p#M{N2&?mwXKaoT<=x~?@9W*$8?$b|5M>>B7i zd_XkbQ-drAUEc?d&#pmEh5n6(uC>7jJT;G4v}`nVEvKfo@cV0wb<44h>;spv_kOsd zVVv*vBy-(250pj^?i=jynwRRYa-yI7!e|mKL$g?;ce?x@@Dv#zeozWODE$|HAe)Nt zQT=p*ztUa)SaO^%$iFK}xr3C4zgRrsAUt6@FxT442ELQ^f2DZBaMn~BKC##nri+$l z!V}W|g(v7P1)C?#E{KA2l8?j-B(I0>DUAvahgX(HJ94fm_-1WsG_=Fk9S^Rj|Mx&g zSWkzZERDLswMacW)JFIL_e?}Ll}3+jB@cY@*qCbyI>%m9@a1{e6m;bDFL0YBAAPgd zj1~i%!z<0`p(mk9^~ewFP0E|m=qBjmR+Do5Dc7HJ{W&j{Y{9P`dGS>I)C+u&Kbnr= zk9dUfN40J({c8yHle;$H2j9RK4--#>SP?U+H_wJQ@<=c;=$C5J(I#V+Xz8si|Ol-^cUE5>4`)XtRA?USu_hv_5 z-)|S0>)PSfUFZ-MRgoPX>%C2#;^F8IhtX&Ef%mfOm%zh2Vmw@O&EJ@V$!2QG=`DgE zc$rGy_YL+Pp69T}pz(x)(DGUKolCp8Pj10Kq1)5pYcoosUqPq8EzgRGr{`ta}1 zFU1BR+iE}JbFc0f86Tn#w`UuBR-4|Z0`$pE9PSh|rQS6pb@#Mv?9bF!eY)$)*5{jx zJS#$XsGZRB)kQD>6?~*$#>=NKyK+oW>Flj(AJisL(YEqQMu|}*uv(~r!e}78l#47JceDoiY9+zt*oBrkF z_aHh+*ZkCi z`(yb9JMrX(tB}hi(~wtXD$c?-AFI0yS!p5pZP_-(ck`Ac-)i`ZWYw_q(x%+2vNG$sfynBzUj zH-U10w?$80ABJ6fJ~}BfL?o*unqNt5vJoZdGtjlv<2KdyA;vc8o!`)=^q7oVhflf= zcGW=FfNLY6vxr52?K?CUyd2tGlm*=m$=;aRTolUUIf`cv&(S=`@a)Tzv4&E34&*t2 zXByAJJcrd38N~bw$&Lg~m0d~tpcfr6G^VAf6uq%6r=`e?P8q^JRf_)DHbU=@6o$se z^h<0{rRbj0C)?qhCFqj*@XC2HdJ{N>E@{~Z(O;xTzF&M?UXacHFg)oXJgHb?1DDd^ zNyW9ATU%b3$bT>QKihW8G)srmbT1rdBI`$k6;>?8(M1_KRjra z@}N&jhm`JD!}z8DEk}>G^hxwa>5tMQD;THje^s@%{#ZdC>5r9^k?#1P>WtTdLzYb` z$Jj1@BffL~Li`#gpfAEtEPWBa5rV!*U&NM_FWpdjqx41bnH6uw@Sgu#i^u+uuofrT zdSWH8maefGI9WOcbqc4Cd>CIdwm$e{(&Iewyjb6iEMvQD>*3`NU&PVJC;C~_**#nI zE^b$|=v>UMCVlhp|4>pbVW z;5ivB=2H>u%=-4RBWX|K_r=KgE@$(nF78Wk9eQ}LtN+rcb~#+Tv3Kox8JmI&S)91w zdos+7#}2X2T-UL5ulgo@aGUA%**{12L_ce%UCI3NYc*<&J} zV1M{xfoaELL+99x?vc!#RG>dMIFfwlenh@fd#-*+z7VlYwD+_Nd!nAE7g~y~>PhMj z?R5KIWc;7Hh*|Q0a1&ni1@c3x>;@T@J#OXW#yaW6p}p9lo;T%-eo=VLjsA{*z)#Mb z>nBb!dQvJsH!;rob(WmTIP$3r`(7(HJ=s+i&TE$?^wPTyBv-^x{Ig`G>7tG^d`aCqC@@A&!7Rl^|SI6 zdyl{*zFsS9y9FO&?Ga#)z-E*c*mIQlkukf%GGZ8(Gas6_D&mt$@6YAT3-#}B;XLm_ z%D*DtAj;oF`CX2T-K)?e=THV+a?kimhi^!1FRwYKL!LuF!HxYNGmjzS)U^PY*WV+*u(b4-8y8XlJ$(;r>PP|_cD4j|rF^-aD~eYJ9Juj;*z!?+UnasEIXs#`qSFde=C^{TwSok@9(S!b;}CIKhTA!o(Tv&YBS z$~{%1$*Xa`rE#E>=AqvUj=}}OGK|h>!A)aFpRD9va1$T-7r2eEeFRnCi#>fyv-(y` z-vkTMrtdw*`Aq22cIXoE*`wp3OGls&oWl(mE05EqOz6^4=#su2NuW#m{vfzh-it1U zV|1w=`J)`Vg#BVqrkVNJVdzq0j4rv&A*ZBCHS}5dzYZBGPMhX1Cvn=;Ty-LC3h^GN zP07eHaoTh(-{Z7tv+qROR7D%2O)G)HxJ!~Jbg_G^HZ^C$Z{c=m3^58TyVoxQ#_?}` zD!SL-#=Crsy2ukIufeyQ`DQ@#MEhn^Mr#v&@@vIi!5`|S(uQJREB>g$sf%y!UVkm= z!n^&%K%S-guwS*a7b06n`)5f_l>N}~jFT_gOY^I9eAp~rTG5c{YlCNHW7B+wHl|Px zUAe%@Yx}LeG~YPG&g*qxTg|Y1H6JIh&i7SNhtc5hE$4ka@7f2l;hP>9%eM>o79Zyc zbymmf^q9W3?rf{h#pD-mYYZpNOFa5`-rLEmGj6Io3e4;_hXw}lUAhl(QKCu<_oSr- zl1bB=5u62Mt0_Qd#{N-Z;Yq2C-njQHGhb`|2Fm< zmuXDc8~d+wifU7Og2wW@wcYEt#=gb-qwmt4cf`i9k#sYbM``Q%A0M-L>Iv|I53Wea zE4p_JeHJY6OJ=VD`z$nHeW+o7JnX#6?{Isg4UG4tgtS)V@_5>h1q*oY(Q;^VFIdN6 zA%7&*8OJBh!OB?w1t+y1PuH5$dV4sbO#Is+>^F-OzANA5z1}q@#ky0S3u9@+oW_Jp z`5$jbYgp~Y_n{XsFL(1T-hb7R2)8QgdmTAM^k3!5vE6Ep!go5NGmvML|69g$PAt7= zoaE^j>x+0}y#0IK#)KtauPKQ_a6F>@t*`5WM|+KlIqr+6ySo=S1!NslLz1uY9Vj zjPZ(Y4UMI1-COyhJkqosZm+ ze0i{K5&G$8#L&an)bc$|zAPwz(<*4MD_j1kj$oT}I_vld)-U0JvVjN8`0IOFXq$v2>-E# zF^041c3A6{aUTA?&SDU| zm2<9*$iI=Paor%@+W+4S4QxUF*<$(Ar3WhLmtaMl)+RsxzrxpU`F*Au*kKKHm-K-2 z4dJ!?hP;&J%y?54`(R#Vw7*RpY zyU5;K8bkZ?7gM@o*ESGK?i=jnabGbn@AChuWKZm7_QMtwLtm#u6O#J|E7%LGWFM?; zQgzYI?1P1pllX@Ip93C(TW!uh!Pa5bMWN(NgTzr|?}YonmM2I2eTbicuaWj+Lz9|| zD)Bd}z}KkNIXT!d&WKjxhf^V6BIlG~C4NK|_z$_5vv4YFN@rR)=T^&HxUd<9nG5Ze zg$G(`AC|ssy!eKpAY(jBx1seKpSpJ~;xQ)eEt z@-KtNHV&m-#v?q}H~GkDEQvUN)+unjrMCxj_-GfG=A7ErGV|nxg*O_bwfRZU}aJ9ma%VvAQH* zs7}$nF8HSWw4b{PTX5{#e9pY8-7N{Hn4R?F+tX4A&#)2wo2Zuct{b*WISo z+#AYVP8;;Yr&0`eeICDJBc;=E{r?qnV?X%&{J_Wk3m!2d zm-Ewkr*iH|^?;kIOXuNTvHF%%zx-+)rfY)y-}6oG3F4Px8cmM@_pvryTvf!D8AHpTx&2=z-UH zl1hTb+1$0)&yHz0x}O=W!2aq6Y3-{t zU|)F){MEU%Zp%LEbAB@PWIwvmKT=u+I zziY1r-DPzV`1s-vHIGBAy-eAIpXFUVyaL_+AbplyK;;#8YAI(sxi>tpeeWgyS8Jhp zj*+|boFjYgDtOZ|va=c+PS0Hh#47RvtL!B1;O5Mc=Ji76ESoZ0DC?!4w_a|p3*q0M zYmBVVTU0f1Bs9F3a@qJMyjFWtWSFUYp@;77#-Fs2`N%ReI|b)z{5P_lnZ%$U7Pw$C z_iZ?a)w`VL?#=Wos{~vkuM7COmQ3=EpR9P;%|&$%v;HW&Y($As0IewcfbliV*V#Gw z`VI@^FxE5p{dp>JPv$Q3tNp*l@{ER##M3`kdh#;A_V;#>=QZxG4m*bK&f>0#M&zkC zxjX#CK4t`5^wFBHHzxVi=3Ch36jNmdFtBabz?JXo#D6&(s`$m*w{mXJ!TGZ=v7_NF zvf*D-V6NLto1#Ar&UuzU$i>VV`YNwlQVQ!e6L@AIMO>l<{z;yv(A@t(1o?^B$a zi=US}4x@}77(E?>k;eZHa2UY6_UEU*9Rz<`68bloHYC^U`zIC+wEHRA63=h)JwBek zWY@8WE|8(D=r*Vv0(wV@hY>bivsB0A5jx%|gu z)AIS$dgZK#uY0e{k5A+hUn((uiIXx+_S!UK_WD^~zwQEWpx$hLukzC#|8Mx_L8o4D z`4hzU<2)F#xbnFB%r(d;fLAo7 zhq7UC`d;0udXwp2e3QeU%RM@8G5!b0U%~nkjDNs?&*{dt9#}B9mzjmW`g0$*U~R#I zxpgz|dCc!+@T5-ef%_5j>t^0le_*_Zf7}LmWb{YK1?!pnc~;+SAI|IaKQlfT+zm+I zQ|a5q^y3<((FgV8O8$G9$9O;9Blc>??}B%0zI*o}<$U9{6xvoF{J(n~TmEAk7SJc* z!pF#K>cb@oeVCxSi6ds?gWZRY((d(=L#ptlX#e{|d!GSTxh2M)p##gGZ{zNdjZSy{ zkDd4^vlp-kybM8GSIswk`F%tCjB z@rWwmEZda+hxL9fu}y9;{rBGB@XzDBcuRbaXVFj1vAMr{yXDK;YyODwvCaJIA$-w> z1g@lPr_+dD%YLUbVXRs&-*88^;8x9k>Sxd(;l0|~!B04`m_8^b)BQ6Yer(R$6_ZQ; zk29!dYD;62!&oxRbac+=y@C6oh8T|e$EYLAIigekCWqj;5p<7F9K&{} zGoBCVzi77bVi*4_xDT_HxrncerE0&|IM92xr^evfJFQVXfORO?X+Lipb?RJW;(d?D z?wrm()@42CLk*qjUw4u0(Q8UQDZWbh+)tR-Nwi_5{m=Qft-3nNXRs;IRbwWACY^2V_IEU&TDcN^ynu~F<-oW&O8HCK}NDK~yn+H1D#%< zHXeHWSod*$IoF<3)!oXPh{Ia-3Wk4w(1!I4!LYUuFdT%Oa2j&LU}W+%)=N4t9m2W% zB;VYxf4KKBclzp{N$E=52e7TxDnG)ypYc=vdvlC!e;Ew?usyUvKSfJMF^?|h>=@&b zO(KVP@sX6Xt7q2J4`kCd6$4yXW^*p2l;3=QNf&S@=f@8}<@OrB_jo=sd&-PQXHT7R z89K>|75lFI^@_h<*@xe=D_XDo`HHqH&w9Ezm{()?L|b&%kB7O^nL_s!?v48HUYr|o zAy4Pk$j6xb>7e22k8Fr)-_6g=xZAExa&1rriH`bb*fZF7?a*y9-$+O8rOq<+i#4?c=8SM*_=3 z&H;f`?qkOHtp59Ezxw>&zV}{U&AV&sTXw&<8XtrOe1E*={k4z%*L&}c4eovqo6>v! z)4d;Y*N~N#LVj1xuC-&yuR5x*IpvdAb)=HNoOji6vV3QgCy%_!CtKA*+IRi--S?(b z*Mi_~(sprAbV_|*&93(*(Qas^BWT%&8AE(757Ym&Nh3GTb0$Ub&GsDuE?%b@+)vDb zI2;8p-QD#i-)y*WHzIIp=sP(WhlOC^!JcMHRPbo%TV~bQ8z!NZ zeS(5ZLy9+eGT4N`OO;84Peb35U~d?ORyu>d;WPm_g;RdN!S(;7Z^R#Mzw3&T8+A6y z(SLFT(%K0C@0eCbkR6ZC!Lh3wSHLDNm_h?SGM;{cm| z?G)}Hz-C`t8f<&AjF|1R+m}!mu?r63{|Qc2iC)xt{ep4VjBB7Rv!OAwpfxk0Iafn7 zuku5KeD5yl9`^6jWv4)A#V2P(;MdoS_KR+vMjeBxD~&qSse1@?Ybd-Ri8ESB$j44- zmIJ-Ow0x4K>txfPmzRkC*>dauX7A18tE#U4|8wqTxJek4jEqS@B?;DvDN&T0#3A57 zRjO^RB>`K*;6SZaoCw5fFg|UrqE_r93A8o$RxDPc(E5}BecD*81#KNVJSN!IgjfZc z5`^aa{_Jy3a&t)-?C?CV*YEd7UOD%iJ*>U*JsP_OC7~rw_Kw2DLtpfKQw2jKQX73 z^C<9n&PR;*1NzwIPs{n6e|XOBGlT6PkCh!Tkn@RuU{0I=q@07qo=)`*Y2EK196974 z1dItedx$Y__s8XsIe+HFlwfZ2sNf?B$j$losL*a=!s`l$tzTwYC(WB%nwuRLUzuU2 zzq#>oYoR^HnwiB|u(2fRcPY4vg(;9+G3@uqw%X0)bv~7RlwU({`~|Tfr&zmpJWT#a zaNCT1ed40x^%|@0zRTy1(Z=`e&>rjw8>zeTLvoIf#^&&MY$o)189c7OShG#Pnxmfj zeOdkXv#(5_Yb{*L-GypDdjWZ){_Z;&d~UZ!1|JbF(@V*nzl=4_;2Ye6=fI-5>s!*< zM=Z)%|NWSDRDUPm6#o;Tjn6GAT`ya|_BJKhrv>}Zfn9mHH}1C=*3h@c=#8(2^;2bj zN*VQO=5-8vG1}0)YEE+(jayG10`!leIjzijD{%e^ng}nouAI8aUKqHT+{b2)GDD)L z&_^97&0940&a>m=D@`3}LhbZK7a6|vHwDWpbI3)=e!XmtHM12sveAb&{=`~1kMXpE zgZbcKKsI=fSPK_S=Kl(7VcBKQ@67wGne*(?+g!S(_Dh;IMJ82`o?pJ9v}pN0}Sb2iw|GjpHG9*(`uZm*3G;Kf;-8PmLb zwBPKn7zSRGYQax>3HTva&fq6+l=6^)o6_(0$GC9lGzwyuYq`F26Tii+!7Uid&rh)YMJct(!`%nP=I7aS!mnIfgEx zX>}WI9B}c}fnI0ms@x8>T?@~Erxs%Aljly(Z8^_s|1ZjJeB9>#kKn;f?CCl`5i1|% zr|*e|BuAoYNc$YU^JvZc=E>GL(I;|9v=?3GveXqTJor8Nn{H2@-}DA+`o6UJ`FxjLmwcC;m%NwU*BYR;Kx@Lyl+`@k&VSKM%TtY z!oNl)?6($PicHuEuTDoMd_F`Cg8P&o0&a^J9DAWrG?2&zLXPrj2*m3-dpR zY_#L2e}?aWINzGcIhq{Jo$}gnURY!1z^?L3k?)%K2JWsO z%)AQ+{&Rbun-%C5rL_B|_}1tQ+IUG zXJy}_IR$U8I|f8fK0XFDdUtt?=BmcAHculjUFnHA!RLDhV|k(Y!k8T!nfv5 z%$3~!8+833aA~Y_!AlWsEnA|F1AjG4_6y1-Xzjvre?+;mbF7&QDEH8s_~EUzKac%}WQTa-A=;Bo zPtOBbPefz#S1kq}Jy-HwvYwc|nUXJp?^5a$D@5F?)&3?hdUE5A70Qd=B{wemM#mGc z@%?YSo8El)BLi%^^3p!{a-T+g<-_?_Xbpc?@V@$+i`Tri#M-?Edr#Rnr>=RJ|0U=- zv+%o@{h?!K|JF$6okG_NUDcUL4+mZFVDesa0z_W-f%5V#{40Tke+~GxNbm<8We`;3aY< zv=+xNgKI$$A zE#sa1<@TofDXM1?$7g+k-}PlvL<2SI!>ymh|0h*H&8@$ED!B`&L;XM;`4_31aV5_I zWQizT%7p4xexO3G5EG8F&Tf^}xvu2FVHfPm|VPt=5 zd@E7&cs*@3u}?v!tZPJu$`<)~-tC=n4R)B%VUL-KUFK@+Ggo2bxiXBs#@NNxZZo`? z702DB)XCzGdBqXf%x~tcQ$o_&HZzy^Jxxw5%F6#0`|S?*+a*1GyVd;`^uFCQ)wH>a zG0k2_Z>&F&lC`mLoaKJ2_WyB;?$*+}D}F43AE&{Oij6B$++HR&C2YYvu+bTQ#Ls2pr+WKy zj-Lu&N|kle`Qp)C1>7Cg#J#p}@=ft^TJL3tGVSCCOuk_A+(erRR(lh+>T4+5<)=!D zygk$LQw3?y;;yT~@Vop}-tWVdm)>c9bLW=DclnXL-yiZl13#6`JNbjy$97vUHhkU5 zk>Y)KX|_%@ip|gXtP;mXH>Y9H4Y=x#@-@Wd2)SU|silKMO$Jsmw z9oe`xaK0Uy%X=dmpLb+q@oC5;$;T4p<67cWx#t9#kfgpV+`a?YAeynuXzbZZF)+Lg z3^mdTqh1>=(QVL|nX9tVAorA_BD7lz>{vj#ZvREpV2j(ECB{sqKv(ME%GmT25Il7|4?zBChfqu&P;%=_nK-&Z^D ze;E6|-kGEC$G*pRul85Qz9$wy@1Kl)Zw)Z-A3fr|)Bb^sX`|WE6I6m|mYMUsD+UQUFcZIR=uW=1m2$?B#LfB(C^-@f(cAoIS>m zIZ^htiu;^8tamdjIQ!U0jxu7W^R$lKa}64j$@1c{w-=-xkc%-c0L5sW~%p zR;aWe_EN@F>a$2?$eQFn&+TXBeVuPQzub=h+dIR2`0jlCOpb3}_|{w~X07N>N8UdS zzE?9Z9e(+h2BEhMMt?a8+C3S(lM87^A~C9da+o=O@Mb^p&G%W$zeBuSfc3C(u`i6B zW<&F0fA~($7_7KC7}h!F7QP?&VlezDXDAM_Zm4WB=Mv=483f$T3EK1EKO0=Q=Fh1Y ztqF`ATKU)X3)ke~$FT9I$(Pn|qz~Vme@#0v)ZZCoty@fdCFDOSBX2}4v07#De)e}+;}0!*Lvhd+Ic#)qeY}=a@33? zUjTNuy6j9V6qHZ7hqm*fQ{A1QTt(BMfvz*(%T{FVbIus#L!;6QYW6y343^Cyj)by0 zJG}C19Z#g~?Wj{ci*)j-#Jo1`#=gDU3Qa_YagTkwbfAamBg;J}Uh1Aph;HXUXy zzZy9w(ky=wI`8E_!Z+ zw#puWmVv`E{>{Y5TgYwmjd!28bpAT;(D%1xFCkX}dlc;_ zrdWzb&gqCgrK_Nqk>=EiRawmQ;?cUl@G|fq*>NdzFdf{Jv8B*&)-LdMmF|c=B?|Ww zh-GtP`<(mbQiKC^$U9v8?>B2_s#%W((;&Y4=+od8So^tiGoSKajQl6q4|4rm%@Aq7iC7aa)uR|vvZIA<9VNZ?Bmw=@BZ-t=rr37Y|T1c_(tH~_^0Lp z1N!N1CJ1v-z~if7D=bF46Jv8e{8gf73b0VhD*&l9ONKQ}iUx6$vQoe@1fyy$z*_?@R;seN! zgl%E}wbQ}-G-$sF++Pgc`$L-+cBBqN|M~>}D(XM1GL1U9=wUyjP3bzKy`lWQ$nX00 zj&-6p#bYYAF@cj!|NU8Y1ln(X7!i!WX^b_f8kTl0gm7obcNSZ3wr*@vXvD4DBoSy*HdoE*KhRo%~9< zL(`k{9GSA*?7u|Q4$XhTv~7j1Q#ok*O6>mPffwEK!r4ss+jRHakKAuB#_-3*YD;Ug z+voQw*HPF9`7|JW=*EH2#vtU=VEFVTLN!HJDxlGay`k56U(C!E6mmcFGg zeRK}-J>a8(^{Dxof^cL~kUW_2l_}T)!;`G`arA4U^NE&D(K9q!-LIfij!8#O{k%m{ z$uDAJ;tSUpnn~!hrp6as(|lt=UK6oJtB`AwWfkOpJ`&%b1@;+?;R?obIrPn4CB&-; z*R_Am&kG#vKe={CexCO2;lJ8>=t>i6e{gYlB4Z(zB%=6T=|L5o_aJ{s=so%=$`|ZFrD$Jml#uhyp>anSHFS=> zVmi9jUS#hZ(7DEnJjV(>g>6&%v^f`ZfxR4l+Q3{+R{Zil>_U>a)uvzP+|ZLevlbG&taCKU zCQkCQ$;77&vYwxYu5veRmNFmew~1K1Vc@d1oVWtTaqbRyahz){?uSzUHHG9YB5q*C z>GtvfdVyl|8vUE6po2eevEJF>0zGe?1x~e|H2TZVUu58^KR=7#R{yf|16Nx*WBdsE zv02{^;*Yz1o)x|w*{A!>J{d1LI}IEaA!jdv4!Qeui+j%*ZRo7j{LkuK_1_#PTbVm*C~ea0qzPqZelWM5G~#N=2eAMhQy%~tN@R&>Sr?PHucnWz}A z1IQk=myTby2A!*dKGU#erQMhky2*QfHzg!nlzIPBYDoJK^Zd&c&hxp?gOj-vkbSZD z`#`ep40OvShrZ}NKbRVluhP_?oD!PpJr7O|UFkhvn;Me6*pzu9IdrM_oSI_#DDr-% zCv#5FEtj4W%J-gcOb%V(J^v^rbe{M8z5`>f_j_`3XoB}VBRO=o_xz*eknB^Y|971_ zcQo{kOHeX7&8WV-o%OKRvH=HO-KQ)d`Ae~7=`pV(RfPd_G} zp1&_Qy{~=tKBk;V_hjU7MOd$e0%tVj+r;;KE&YJq1A@YbjOVHVJAnb zxIbjJFS)gZ`jQ3GeKY9qXgS7yN56}y6Q+*o-?q)z@Pnr!Rtx~lsd&q6=$ zg5f=2xfwW)#!G2Nwn@KEk9>ly>F!TX4Gmo#44=z*#Z$JG2>&FRG3}kiy_E1&mf%C_MEjJoSOT!&6iJ!&-~sDe-64{M&PD;i+YcIpeIS7jvd{T{M z`-JJo+mlHWS$2( zykwrQad^o*Kko37c}__&eTbLL?@dp3mjfD&sR9SWS;jsykwqJoVLYF=J(C1p|_ZWd$6A_hL?OFba(}9hE`_5PXXvFFxTO=XQTLO4sGp#et&jF#}l?$^Ak^qmn`&! zH|Te&m1y|M^zZVMN7KFW)47wn_(?F3*FN%H;5acp8XFoqre{7HeGEPt_W$#dS(6NH z{ePX0`u}tINO!|XhaL?-oXDDFp^JZ7J}RAfBp>|>IF80g!#^9X+a*;h?|ZCnM}WQ2 z?Mh>GyF^#Fo38JyL$Vorx?O3PZnt)Rdv=eyo!X1h?W&@5yQ;J<-L5LFOSh{^>(cG2 z(z@$*RcT$i9pyT8yQ;J<-L5LFOShwbr*2o3)}`B3rFH3cRcYOIyQ;J<-Hzu@-4570 zb-SvxF5Rvwt-EeV|DC#BRa%#BN1aaHj2_6VUAi5xb?SEDwNtkPAKi4jst?+0)6v9FEtm$B!|>f_jmngiD4$v5PO zweQbOB6i(Bw6z)g=L+&5rK78r^ZeKjeAk;Td^O3HDZ6Lo%8saStJ%v3{KK}ka7H@) zd~0_p`l#O56Ib4u^TSS=ALM;zRL+kS>NioxM_u+`IdjkB%qX^@BEMZJo91=?n)G_gK_S-Ai&)-fy5b5>bq0Zx@gJH!EFZ1Q+ zJ=D+6OSV&5o7u~97Ayz-ebVpP|K7epHWJ5HaWl`w?3bH-Dd-f*=A3@!cqh+Iw7oz! z`rEJa>;)HrSJ?}?@&S9ffThQ4|6UCK9p4#eNuzycoFR>08{Ck6>&PMFGOR02z6mRS zenxiDUD+p94#Q_PzXd-+ph$KFD}vvA1HQd=We-dX??4AHC_XQ=nm!J&??2^nat7f) z(tQ96o_kpFnvn#~T|a!rvtP@^pY+fVE+MCE+(QL(Z#q=?aON2|;}3cbe-h)J`5XL6 zAK_2>(ZCs%AK7saeRMeCsTua*%3bm)rC7V0uvhO|a(&y$LxOoH`S6{sAfDHE*XPx^XHsN4StMm#sB5?n&VBQ zfdf0+Ys~HK_(QQpVQX=GNU?qH`~BtN9p4DHm#y+~4m~CE>_^t}dUQk8k?jb3Q677* zI@#)EgOop&G34#Trta7d9NXTX<}22#=RRa(tE0{6^4lpdd(S!0vO_0>!-vY{Yl`x( zZt;iWpaaoJHhDmj-<}4aT9LFrULsl`&)Hd_a$xdobJdS^(ZjqC9eS8P=m&-t;-HHG zR_a5(`3np!%xBJSf)+BE&mTey^Pz=GbH_#ML-P;EJ(b{3sJt0kc$c%rHPFJ%(86ov zReINspI?tp^oP*EP4>W@@-K-7>k9>UhOoSZ`0is~%gY2fs(d#{<7d!@$3-ptgS} z{Dr`u9|Ql+f!{hR{Kv#Q_PQv%FB0A@jkzbhb9c`1;9dMx?DE&2Sp${=H+LOH>cm?y z{M1+uua(1V*iz^2c6h4@-l}nV>j3mC{t%ytN3@p|Z!5N;?hp5cR}s77@zh%%a<&+` z8_iSu_#Mp;;+;d~lBtfL_wd~_jDNTP{O)|m*_*oF%6k5pSYF#d&*8Oek?R3$8uAn3 z-`{|L|7_rz*}?c_Pd$vhAs%B_=2_>4E+j7R{^D~&?~QTrBwOj9iA!?gCLDXHDVuSw z!Q;AfY)nwW_U)J3Nr6*>#1q^24+Gdi;vysdN`4H!HOx5j+h4EX9p6;mstIzhIrmmm z&ats#V=iai<$L(qcV0Jq(>5d&&TIboJz%xsg8!9i-}v-g;)boaOYXDMN=Dh-N0J@7 zq&=fgFx(96nY>qQg_Tt@s{QMGUofhVBU1T#xwSu-oBTd-TSvd8so^255iZeJLItyt_y%)`CQY=_do35k2l&0t$HWkmF%5nrEksI&DnyuAor@rxB8Kn%7a(V-2^$M zrR$ZiVgdD5+HtLVCwsHbg6K>D=MzkOn~}YrDP3^qxA3z*Os>5(nXj#g*zu8-^tFm_ z<4PCgKFqiInXfYM@sV+H_r9U?ozpY7u1FZcxeDIJkFeU8QZ}3M#AR6Rnv;Z6^1}lr z1o6Gc|4R86IDlVS`D=17kaHot>6Krtu@fp!=lu`=YwNF;t>O2QUq6~~iXC1?`M3#I zc$yXH99uf{SAg#>;jH}dAlf{$w6yJJ?r)MT-AbFUEqU!xm3i16OuGXrpWXDTXNUQPVI533ix?*Q5Uw`w@6h=VgA>l68eHvQET?l$^(NtD?)*FFgO`Uh zf4$-%#;S9nbNC+rJ%3o^R6c|Gl*=kD%?*H)c;UnS=EUK0ZvK`&_^SG%+egLX!q>`6 zvIn1(bcMFjo5J$-lC#M8rBDcG-p%|SBKA#jVhvvG8u3sI`TZpMx+?HlP4%a>*1#)^ z@ng>6f7vR&Kg>Bl%q)o+Qr1*z!9R@wW z>q~5X^PERl5Z|$T6L1yHUzA(R`lI}n(RGWdOAgL;rM{Hb@_;q74*gzwR1W3x{X?-^ zYb+^|)6VL6qQR%%=@I4YQ|^LhpVclHI)Fj?L4zCb_b~lc?C)6D;KuH0%niiueNsr= zo?>o+J8=y12Wkx8A?Kd-(}&I`Hk12qI{fk_`)JD#&Gmui z`qD=~{Gk2K8l|}#M*QVuWa-<3^2v)EY#++s)&8`tadzrf-T%Feec{)ME1By{{o!x> z)PF2`*PLYoe|-DmH%9jfwtqr9#05qizj+`s75+PgGRldVO?-}GZ9KpIfJo4BWD?AGt*4{IVcEt~J3c+%+Wwh*a)W*|csbuf-{k)f_`8w6>EH}G z8Y;WL@roMq1#Ko@(0k+%+5~-P@Gi(-F@M(j-CvGFmw#wTpi+HNFLWun-|!jlo{Oy2 zd9YUKVqWH^@PZ|?+GY{EPy>#q5{D=`vG`sqTx?{8ldEa}wHJYl3&Dwtt1WiutX6XI zLDQ;R2%nZBBb+*lsq;k+jJKQ|+Hi$~kxxyXuQaHx1DH zyw&7`!^}(OKym+FAn6N>3xl5q$;hluE^K=>C;bmls9Tx@Sv! z8|AC&2?vt#N28a?%|T<6Nz2GLF&g`Q!|;owZB;zgGS=e`V7K`1@xAN;UQ9D%OHxdk z?3D+hl_2s@GF`umvFC`dMbC_TIXYo`weIpE$Cq%bxaNl$GyCYe+Ja!7VlyPimM~9? z@rx^-xi(6Ul@SLrnliPnOl*S3YrS}8S1uM^YA>(Y84RocO!k2BDv#U?cgj6C=9)gd za!m)Z7l7Xc%9cVa%lOkgXV}9kB?H%=p94amg1a zUt?nAO4fi2IlRzlr4o$9N(=Iv+`x|ta^%D5sstBSIzju1HHjg3Jq#KQoY{FIJDS3-o=TnBHP~~s)*U9-tn)rOVsHV!3P!z)hVM=TUvlIfz6)>BJl8&4ww72K zZAG5AW)u3zthNs9u}hE((n$;Xzmj!9>x1a4op?fzr=7i&9m?2=zmxS&^|nCA`=OCQ zSun4id6hn$rxPHDp^Eb0GQ@ylIS8F>YS|$rpU7 zTT{@17W;gyx1ED7FC7S8ZNPt#g*;Q7rs|2mw8vErWa(>v`;5-S$XA~pQCwQ>ITb4^ z&~Ze6y^Y7onOaw%u=?6I!(x8pF;v4F1RU=^g-%r2iq^rlB-f#Q| zmNhZwo&i@UP$n*pa&f~=xd7!(zh}tRITsEXpTPULfmUASO(V{Ur(6Q<94&q?7Dvh8 zD1b~*e4ub8Su1%`%UIXBvet_Y>;vr`t!)=4&~tZL`4TvZjtz{nf_Xc^RVDK#I-6>f z3xWB~UNST^`{ajK?Bsd+e+~`hpS)~^Xs#b~+dO#0A=$r_19$HXzp;ajC(otyWt(+R z>r)_DJZh{3l zYIhKAsr=p&i}J%L;JC^tNx^C@bn&ht2JmPxj$DygOWcOtCAFNFIisPCvZi@ zU+;S)j6V4*{wjISU$%J7eC!3&mvFx6w@o+ue!Ju5%wiJUzpM06 z@oyXNtG+Ai5IT8CYj+9d1h3-<(7Ju&)0Mz0+=|Ye_YS^a0M-V{kNgV{TXDkqg<)jP z;r-XT`nfA(H162C-!o%QiS+gGmI&TH@rCzs4$I(ewe~r}7kU!=VeG2m5_Fiy2HEVr zIpf58jYoOwm6upPdHFr}P&RO9)}dnbs0!{i-2*PQ9}@l~L$WES{bNvcb?4ke_9ken zzIaqd#_Un{Wu>Ehmlux;WEYRJ@=8Vpb9v4v9#xE7NVgMPH{(mQXj{1Y5V}yCdhbUM zm0Vm5{+76L+7GbT6>RTNF7RjT%*F0J3Kq!^y-VT$-2L}G5>HOn`TP%(Z^N5EU!Sxu zTY@8N_NdW%#^xH}Sut#apS5bH;LsQ}CarsddkOO=SSpw+@9f7kaP9EKh2S^Xe)yAu zFtPfFy)~c!nO_hbZ`KX*!Ox%#$$T65T4*Z{I@KDIi494zM`s?y@8iuJCPtPcZw}x6 za`*uHY$xBd9%YhiTXr4kEc;X*Igstv5v`Brxt_a8oxPI7_rDw0@x)v3eG`0I<_jK9 zq?`}hv-w-)vu=v!_nLU?u=xA{usOW`^V&gQ=}3JSKA+8g#wnW;`Gw9p94vg=vKBs_ zZl$!2=3Bt{ke%lpzBy0ZCoLG2A1E0`YMiI1;^*DLT<3t-Sv>nF??36LQ3ZiHqb}w9 zV)y$3zOVGY2fs4v6#gE#tN74G{C^PMs=90Lq0n9P4n1<${6ml4wcwDIJ7-j8?hT{f z%l+!8f?UdF7gHjW=-;CcM|W1=j!by&2Fb(Z$Y5wf>y^3hz_rsG8f8A?u+e+> z90ahF#+A-(6Ftel@gLLhSCqTwQEnN?UVjjK{=vx8laQw;6DRM-ZeefL{Zm6Z`!jKI z+*5IdPrg2P;P*dnnfTj}m%Y{avHA(aAKJe=`hIfc+u%tu*wCW6r^*+3z=c(`n8M$E z{FaW}O#F5b9d9MNG3#Eqkv#PW*xTxS(@U$d(V=r5dVzTTU3N%uldCwR9YkI$2X0OI zpQc<6J<8@en=_g_xX)_Wne1JLmvN8xe@~gwnxCgTtMj?v>WYu@^OXPS^7r%emKK&x z(K*$sLc49(rR4H?p)g#$`9G)3F8B_2g4g4t=B}&FPfuyuvU$p!f+u;uV#=)r!`tR> z9^ST~U_{&8%_G|86^v{v-JG6#)9dNEx9mvIz4_zx+~Un;Q%W9KHpPl34&$yn+lmUc z|8nnkB|m);`?^an;odsd`8e_=@BC4pFuttajqKs3QSKVnP@RovgdZM5{^q$j%cT4{ zdd6RP6*y2k(`iR-bo|h1BM1+_!rfET#uS9ZV=my##IDzM&(*ZCoHKimb2xpiy5}WF zM{}mM+zS05!(J{K?fqWG?`Hfw(RHx<)qRgT9$Xp7am8UIM!_|J=L}$V_MXm}k&QXp z8>ce2$z~3c--#|d zpo%^hH3x24yRL**KMT#yfOa{Dbc=fqsoXh-RF6;4I(>q}&($MCC7gGB z>q7C0v)_{5@#n(mJo3qrMt+OmqyzQkPcpc1j2(KGvtHV#dB1D8Et}QFfH;9BW*dB;D+H6By?J zcyb^*o-fp}i!)8+?9iT6`y$B&)}Tp$A>Ls;XJur2iDM6;=Tn(4v!?iMWKtjazHj7# zQTFmZSEE0GZJR{O%$(siZ`1W)9^UTuYzrGuwL_E*5%$hjch^>#n)N5g4g zy!D)rVCV@>qffbTUI=^|mtg-3a`t`b^9#}KF~$LmanBn*{Ev(oJT#*(J!Ds|7-v7c zqUE$;dmH0zpEtiP%(~i~gRghY)Nr_ve5f5AYXeOqLoN7G7{^+xZGbZl=;K<(;o&~$ z_SXvE4WK`@A3p|PCBC*e`c_O(7Jbdd$0|75xF_TQ@Ym7@y7b!Y8h5J>I$|&)GNh4Y3|pCK1K7A zg;|u5f2spqH$$^#4Fs17;BNr<)B4njFXlwP>PqHtCv!)h>Tomj*8U;K_%mF_5L9@o#@0oXjYzk}=VK%3x`bCRJQ@Y8_=_CTDgL*H4~;`3Ez z?S416!q+w_r|I3?S;VMMM?R0+{%-Dc@?kFKH+$Qe^Lajf)2!UN_?ef*4I3|cJndv_ zi2lzVXbqA7dD_sp9QmvR{8tXe=0a;jKKiyZkExMIPQ?}lF5Wczg|4&N&&+mogQf(| z|LYrT#%OR=s^L?)mUw`#{>A+1;=K^ zwbh>yIu*YCzBSh10$Wv%VpBdw{z;AsHrb-%_|x;%tSQP-qi6a4yg8LD^fvub|?q`m)5?m@X7p< z<3p!|2g$hB7woX;NdArhv}E`Y*aFZ`HhR=f=mtM;8)y60J`8Oc9;_XNUIKrvwv0U& zKHZkhJ(H2~7ldoaP7RA@u7O_mK`(nnFUZ{RzmQ&P{iD|BgGvfm30;z zicbvxCy4*a&%pczm_OO@nxDbWNE?wk#Nn`}czhfgVp$8`?)t(%e1EJI(Lj8PmY4=I#=BHG1ww z9yxc6&zrmcQFG_%II(m1Xb=5<%kB50Z06PEn>jLOb{sK}^52`eS#(b5SIp7$h3AA` z0dCnGIykdhqXPAt_k$Zx+dWa6pDSch+CZX40{n=eJ5 zNJO7tjq9W*o;$|$iJ_;yCN9h4XRQ^@W4RX&`kD@XHIIu6H@V~#fp@*d(Z<%dD64r_ z8^j*wHK(o|hV0I(Ar{~x^nb}4!4)fSgd2}$zliNZ>x+2AquJPg{#tw_oK3tFuM~)8 z3nh#Gg=<0ekYla|Z;a@YG5_whpq#nxX)XAUi=$7w7VJUJdvg;jN0zv4@5o|q&>u2S z86GNNjV&HyVT(}Cs>JXi@7|^@eL`7{b||};HM$I5L8fO_^PBa0Cwl~~**W#n^;w@; zucb4~)*-!FbfL9bYqpW=_auh2X7Bj672dhpA0AhGVYp>1d2(#dKP0bQ(c*t-MNT7p zI<{{&zTY_pxiJP^5PiYp{pPXoI=sA-b$>eRep8{(;rluG*jVqUFC4^g_`lFCvkBfF ziySCt43Y!oZn-9vLk_H#d>HJ?3+^?4Prpy*z2w6Xe%qmGhE9sYd&yz?ucnjcN6|5( z)*=|ZM>-kQNhhCU&Ss(`Ud_7W%)CBRd1#3%N56du|gxy6i0c=j;Qd8))v$oPRdO=%nP>uavwd2V?sytUuZ42(9dy z%zSDNv=12L%)Rt}N1k`-`;zfpbC1klZH4|a{NKm7M(2;3kH>nMkBEyi<-*yA?rwB; zYJ>kZO%eo`ksCHj4`x?idt2Ojox9z8d&iTJ#|EY?y|5QcWf8L71{Xb~`sfw}xbnEB8qWZzU zV&?8x_MhUp+;tWRhEsmeZv{bxG# z7i<5?<__;3>_1gjl>KKmxc=X=|18Sv+5R*3%RSqFGTrx{tt8g|Gt~Wloc5pYa?URs zvt$4H8tb+v=l}SpDP3OPgZTl~$YF#@%`_D9Z zG0Of^WyRQkz6lOH_v>BupS98UAFU^z{m0|u9_&A6eq8%cl@(?GSr9c(|8DzFIrG$$ z{pW0O8EgMJy>sr4YX9jzcU!vbKMzLDowt{c-M1z7(C;9(-&-`V$F~2h4LJ6nwf`*p zk7z{pAM6#oPSpOhFYUPOKT-75Y5y_wqP61F*njYe?drk)Q)NZje_jK&{r^k$pTDFZ z+y0}w!<%C4Kb5>YUi(iOWslST(~V}M?LWUqmV5HXquE&d&sTeZvxIlD|7?=O zY5zH5Ex4Mw{@?Ni%ww*5vj3#HIO=VWU|8(+wjQz*VUzGi)st5ZI@~xZwr)ql7_8)9V$8Z06B~^6tzhnPt9MvDQ| zl>LW2!ExJvs-(vmnZMc!eZ|bjzsH9cYyY{Ym-)!)oR8k^KdV?PWt-^B`W5T5{pL~T zBhhX9>za?kBkktcf2us)%d`KOIf$|Uux4+Qjjk8_kBei+{xbj^|8LoU#(?9V>_6MT z=*ciR01T;O5*NF+kE4R}<@V z4{?BZ2Z9?E3!(Th#q(6-Q=Lm}Sj*^wa0`BE#fC8l?WeF0*InM+;~j}EUqjrh=BRNg zzEjQ-4rd-@YgZlR?rfj!#Hm@>B-hjDw+-B!zr%)A!L!BP4&K?9gTQrOX2%olx)+@M z=3XpV9%Y+v#7Dk4nAe8>j=e9hv43z)?H2#`cQ|j*61gyJEm<<9ap@NBV^}(+Y3YA+ zA4566f5C6v-SEJaW}Q>=)t?{!;lyyQe@I*NUBM02{t$P#q^&Q-7Z5o0ofU!p)}*fk z<5vxgNe0GQJQoK9qZ5;_nCT(FsF*Lss8v6B*BZsqXiV_0sVTp6&ED%y4)1+b=W?tytI8LVGn;$k%kSk5`F_;7XH8>5-!;__ z-nXW)fB!WlCN?24GL|(maH{gsaMlA{SDzMm%E#|r8Js5|k0vtGoWEkOIlJVXL2H{; zNSs>%ac;^z{0ozNcxr-sHj*~#iS1jPVlQu-6$~F_f4muAT7AF5HNS@z-u|Cpxcb2h z*3`%61Jjf>n|uYpc>bCVl+oEE#Q|iV>&Rp0cUoj5zl|-OIT?PlBkR9D+Ia_$hc3C* zd3Ou%(*4$MmCYDlaFBQt{Md>`aqigZ1HP+iBi?Oe4&^mI*`*KP{j=~M=3hCPReuxm zDf89g+DdH8g;W!cMhR?mWly>G>{t&qO$VT_0G-dvkI(n_jF{#Z9m3i*(01;t~{ zU3|JXqIvL8b4KFOEj^s?E#w@}mj2syPO#1E`-TxAtC-v&^uHg!b>>KGIs9Y%q4oGf zk#l*}G3Uv=vt&9Wd*~ML{KF;Y{Db6N>1I1L&xfx9|FDsTyV62U#L(=#4PVmgLE%!q zHFHL*bhIzjvKs$SxgBbG&}x4z8GF)O7l-$f+j`>#@{c=lmx`fmnOB-Sl{lb`8=RPQ z(WA~0C|*)HbK<*`BTK=XHOHFFA3hN0j-n4oSj&SkV_9eNA!)9hSdAyYz&QhG!dZ{I z-apFwRC371L8A$N=PreG<}H1k6;8T7?x_P~xW66$$^l|GTCeUK&bpkKIL5k*E}o<>{%cfHu$@#5otmzMEXd))cK_F+rNalZ6^A4WS-OOGTc%VmUCML$6szyWYgR3NU`lvt|F*Y*Vi}1VoHrn_9eo8)lh5e~* z)0Pyp={#<=|M4j`{_pT^Xxpp!-HZKmM&`k4CT49!TP=I~8u~W0cKYzp6zJ%u zd+rVYg!}VWPUViIRGn=Z&e`=5k$Y0Db@AjQ^Vq3-gII% zYp}KU1c&Y)h=!v_815et8V(FU`RLy824GkW49(mj+{{|g0vzWJ>w+QU$}SkT0>eK% z{;v-U9ZKpx{t9xQc;o*gF`M4_Gp^|ByNJG97+YumtMkGh?#*1d_!r*2Ip}fh;Qx;? z{Rfh|`u`GlTzdT%7j^ajUHbQELU@m!`{@2fFJAwG^=NeP6X1xRg9-zOGY5Z&hNGYh zj%wg=pCQjY3jCYG$?^Bqq-nGFA@1~(KX zTazS%jO;ER8Cr#mdX;)*vMtz@gB+SOgPKDL}5#&%28*ow%j?eN5i$ajZyjV>T;t!rd1gjdPWQyAMHJL@}hckndEHo>d!y!)zP zXI!!KnMD2C)i$&^vW?j0s=)JcA)OP@+$m1Z$eZTjp$tRscOh@S=*Sz+F?jrT4`&2C zdM_K_HLlNe!sU$Nx4@Ngh6k7PE(l!pnO5BfLuXy%`taJQIeU0iXf$*7bLv~}xXhfn z@6V)-##F1WbG{lzn)v%{-Y*A+&i8vqh6=#h7~Xf`+`-Qp;p{UWY|gu(l(R-0iL+k< z+t7p~>0=<}s}sA^$EqPm(*FDJMA1I+^B#S?j33;?qcyGzk7K*xaYSUP@R;qvDm;#e zd^n8tKh3JEK^B|%L6={@3#?PgvGGHrw~3D(x_O-Uop@R9@a~^zqk7jKM7`A1)czB97iiwqX@v|+{rKiVuU(Gq48vo}Fj;qT*-zFX`q>UQ?HSW7>%)1MC zXJP=5gF4fta(TRy?jT)Z6Z6q@hZ|>iw-aY~Hs70w0h&hHZg;+MKe%)U>qX9#y7#qJ z@EkywE zk@xS$qrJe0@uq#@hVv{}oiDt@R~pM3=;9+| z#x{%`GEPq2VUa|}89nYi;u<9vbq-H+AN?(wAKvE9-k$j3b>7F$>#xnb?(^EfyV!Z% z!n>o*>reUa&8y;FqUW|5on3QVBYHqTF5$QIW1Ua(=DLAzn(G!fCRjT24`}mPbNof- z_|YfHBNH{hCHDoMdV;#Y;NExL!Syb94%pBnv2u#lY#>&1Bj2j!cX9Sy+q^y3U6VVW zxEQ#4`gR-NruOn}0pE0&LQnO+!nd<~`SxYL>6}qd^{(aHm|nhpmT$U$yr+88_?Fzu zw*tN?cX?0s&f}YW5j}mIz_$$dT$xQ7*?&ZzqD7t8(%wgYa^0Jv^AWPMr0^#=GWe5i zLiI$qsvAwOr!!Zg*H#lFHawzpr>1?*S}4D#{G_tG2sW?p70{5!C!!;jZ;x4DQ)pNH z>YL~uzDBNH)0RoQvGkol*`v|7pYPfu#m&>+-wxeM46uCi#iOtdxH9tK=NuWCd6>KQ z${iWGk9Thz>Zl_&%;c1b-IxB2?`r1?+R0!)WL1wh@ov36JNeL}a-bCwv+$sG6Z`eG ziUE{eQg(7o1OZ)4|*y67%22JA0W2!Qh=xi)yPgY)+c)b%FdaEh{53Ge!t>St>`2w#lelwd?@9gAUy$<)8=g7ezW9cAlKOG&r0{8%Q&QGxkH$Vq_ImaW|WIs6*99!fvYhs;T($A`U z8=5s^zR(%->>kFP&6vNAJ!2;8Rw=g7eT=b|@qPolp!!hk;U?<6!ui4sd+M9Yy;yXT zHK7r?+|&34OW^)sTNZ0w9CML96dNgaEY;P0N@^dNsxt3M2o;ZJ-KOm`{s`Ue>!{Q3 zvOl0B^30w$>0X{SmQCPDGC;71mQHec&f;!6!9JnC1A8D~O}ZVpYr&by${rkc;pTqm z+@b;2glh5&_XM}b>%pbEf~#Q^`*7f{-_=pqFihi6j*O{q2Kg=AR0Ho`{wKVmoW`6T z#Xho*xiP$3mTAt|YW@zMWSemqnF-!BhIhHgdT(X0bMER>*&m=&7C#VdE5-7d7IiU6TyXHdDU|+*lttI2+Hidnu{bVo((M{=x9TT5}CIwS0oT^v$ zmyWtH^}P90Ju`0yJ9?cr!M^AB&*}qj*<(%<2bdc+oeNyAuA{iS9@4Hm7@7({0 z%x@6f)6zp>{%6}~hNfa47rsocN$AKAZHj4#}|He$y%=kE%w&@0G7`9saxNIl}K)+$a?aa5+=()5td zF~~kJj5hZsT-eELe@Ss-7O_PeS{E*(`OU#)V&r;oDY?_c{wR9BcX7XY6E-^Y4q6fK zwDL~trS62>$C)m3*YOr-eH3mC4bhICqucsZ4{g0|+DeS%3IB!toHHr6B;k8W<}7Rq zXYo@xi=W2X#Xg~I&f*Kk1IXA{vaLyzE-PT1ti|BY@K|87u?y8QX89P3ziQPfFM{>W zU>kPu_D04o-P)5iDii#QRVSTHa(e_g(7WmzthxitpLG1mjCbRhbHf`KpBH{<@sx04 zB5jbPL-`TXuz}o${;o3nu&-+_Z=3Dg{pM_6h;#NcwdXm+nd*(?U)Z?%e3RQOvfA2S z@$LLJp1Xd58b+wJiry`T`z;v zMsfo;VJDN_WB9O+CnozxRK8R`qAk|8bv<)k|3HyBs}me#FMnBWzVEbI;ZNPFd(rWG z&U}Y9YyDp$zq#5R$~u%<*_U?r(a%cmJ*lJZddj^-nGK9<7W#M*bG3o@&GRPaw(j9B z6zY05(y8>j0$E%=#<712PA^~DT6C+IZoDV_o6hg@-wf^iUaRlrUuqMqf9NbT!mJ@) z8!IWJJd8up1NI?fpv7(4ON&=TpM6|}K zXtF-RtqX2q>lz-Wy#?>MbRUoX?!mjlQv>7+!oOh;#HZHBuKX?MrI!CVF7zUDf;(ZV zzF0malt}I)hin&C_2zSz7I6;$!!;fcZ`>AApLhl0Xc#v@zV^@vW2npRPX6nSHrwJ zGAz}=`Yi3O`^{b91MEj0Eg)|fdXd2&xt;1}qeJE&d58H*>gW#hsWYwJ1^8ZcXZlIp z6|FhSCx>-xACFT0MXwLm<=;m2v0Qz?+e_Sg(#xPH(b)rTACdzN%tJu;cq4m!(2(+F z-N*MP)_3_|7Y!bjD~spG$l~P5 z_VVIWt%)!C9a&uG%3`x8b!G7)a>7X#zkIFx-i>54wqq&>XTIw`wrKD0O2+cOsC=QRY_GoDHQDcb3TA&R%kTo^J&IWSafJ&1up z_I?;K3G)TuhDZevADJ&$5pRmaejCrfjLZ7s57DS8Db`X=XpS2U%W? zY(>wkYS_VVDw{_-7Bi*|NHSxS_yDZr&80IhTpP z+494_;g%Uz`@I7dLoJq1JTi18^OStGV%>60acth#Rir(lYOQ+pxh%9E~C2LD%J z|3rQzHG+@zj8XEX5g&$RrSgSqP1}rLy99rB^qN+8lr_zw{Dy0fT+?O%ch{PxGxsAR zL*2R2npO{9>$RrE%#B%Zc~|Aa*VCFd8ag*?8s#2v%T&y5thA3+pqC zFB99&wm?lnsJhU%U3zTGn7-jnFJVXHx8Q!2@s)jKd=+ammpf~T=)aZs^&hggPr*Kp zjlL;B`(vEvC3*(VxKpviz)xZ2V}r)yv-yo)8z`|t28Kf!q3ZW+)345-*8&^+t8M1o zkaV80{lZTR7Rm^guwcOt2(PvOLU?69hq&_@Y&udu_#Sxa)DKv@kI)ZT3u@f8yBhk^ z+WiD|q#tx&yOTQC?r-b6i|1zWym!Xu*%w{Q{^%O^NyG!+;+_XD>}U45!v9piwRnCrGIQDFPQX?UBripr<$-s#OfcnX>QR!lz014*+`E=|M+T;V@T~B!^;BwebJ@hKKcJg48)yszci+p9VI)a1wuZz2PK)=~Es-`)bY24r$A z_aeT)Z-*CBjV&x%9@qLEc`O_9U;o~bYGe@hCC%?<@IQ_DEJm-?@26;A{Gs-`%B7le zo=kyR4urymIQ1p#iW!&S_&H^|_&3$q44buf z9PF4V`}->NQq9wi+#&W2{BGXwIsATv@1F0%23GBVwAY@;o=E-~@vDAsf^MY)ipKsv z!?6#|V(r$q49e-h+7WK`EdPc6pJ)7fj((oxzSDp8rSIn&9oyMo30K%Dv{(uMUL z;J?b(lwaSb-;y2G{x7!)mOlWCM|0Ke;q>lJ-f6$O&9!+?1kS+EgLzrp0V6x}H10%+ zvv!#rn3eEQRz0>^^6+IpAzA0dH+uh_J0skCMUXGbLm^p^g>6c*KsF$=&fSyQ<#QNg z>?}qOnA}lwtk5qLWRsGO!Ldm_GUV7csRZb!iTT~6d8QxFCiS$zzjNP*@h5C?ZBmqt zwMjkdl}(jRN;bBxvAF(|D$jqSx<;0N7u!;~ayF4`sj$m`@_5V`N{(X;zyDp&W4OtK zi~Rn_8pCY2>=uo|f5N_ts?4MKF0T5{v3(amd*WZ?yI4TG(Y}jf`DKsvU964vT})&S zqJ0-PG6&X?zKg2Nqxde88S}r}cVXmTl<(rX*SmZdPx9>fE*|H(C*MWW6^@pVoIVKy=cB@8ViR zC%yYF{=WU6r!{$#oB1!MVy=C!S!8KSy8?VF1qaIyBG-`W$!3JvWyLhgLw!)^Z#7MmG|Bv}D)@HiC3vzOQD&NIKaPWW3cTsgT-^H|Zj_tcxL;jxQ z@m+{_W9?;TUB+H^4K#JU_OcHWd$*UJ!CZU3i<1KC8HWf!)d>^!xXxpp4>1~c7qvAzq=>KNWEJ3-$?h_+qd zMV0HjsB(Q5RnfkS_fPNgU5s~O=*f3+FYR^OCXUm0aVvFVeHTr{mG$PkxPkH~>btns z?W4Q>qsw2zKe9=sds%B@jPQs zjrLu*WslEyfu6aoC*Orx^H0!saRoFP?Yk&rd`IzJbc0iSq27EK+h23$>KML@Dy?aC z_%2w}w)M28_2#?C0Pe0e&GB7a>CVmZ_%6aOd_Apcd#1Z<8us6nZn;?B#U!_{#uM~i z{E@c*QQt+iY%cin-}7@%2EP10=zr1c;h1rb@8VWq@5y)ZQ(!m|-$jYy5xaaBRk6N{ zN1pHUUA&2mkne*1_O58(#XjDji0?u)9__m@F!b!ZsEYPo+#*=sb9@&s2$mS%#W;69 zd-7e}4qiI-gX8pFlv3wtzKddg|0nw{HkJ17ySPwudxE};$vuvt%Xe`m-_3lQxCX~} z@r{8e?z?#P*Z&mXMV0JJ(y?VXdWAMU-^DtfqkI=`xlir8=vM#J_%3q4(%pA4f#09T zck!EP+OsD{&SLGBFX4aFckwK+#QH9t;hlUJHOA)6eT%?(Jid$MN4k6$v37p>EF=S> zeHLbYJJM%i_}}wctmxa_XYuNQW80!mgKm2ASu8YoKR%zuSKYF|JXyA=BYhScOSI3z z$nai#7IS09@YHdPVc)Agk0E#hW0>fceOzNGS01DQzSTH9wZC=i1DXJ@;q=4?aUIOl9bE$wEVY)zEU9ND&3XA~4yvWj+%jdzk&7k8!yljEOMor=NM z#2WfF@DtymekxAx4!`1Iy1_p%3Vx##rbG%TQ$jnnrv=*^i2n#PUg&$9by=c02U7dY zlyL2P#y8(d-@?z1KKAk#-@P|{C*xd2j8z+F>J1ExbrEAd$b4ioMhBN^krMjJKu{g9h$Q?xdkA#T2DPc2GurWsL2444lI;_&S@IJm54mA~;r2?;h#!oL|@P z*U%{}p3@z84UY6)IQkmpGEQ;I{hV^QMvps+7QAuSv$y#iWkk0%F?1<&xPt-4)ZaCf5t4{mEDagrv^D#gGwiSojM^1)~?R!kh0ewTxTEWU|W zvlx%!$5ck+kliTDjLV7Dl5ZXyRAn+>s;l`LrTHJ~v?cwP`0qNMt?ZPu(e(S_C0c{iBK65u-CbV`ZXo91?E8lW z57uyQ%)l7spK6FlHzH=Mm{`vsXU9t6RWFX?Y5FaN#^=E^#NFVRh!3s*FhB2(LBwwi zCTC4SaD$NpYx{(D5>r2KHu<%b!{$9}`!_}hH!LFd=k{}g8#Ir1pB8Mt=c?d_b;KZk zeG)v#c?i(~^)|fix0gqrxtF{@1$mMaE!=BU5ob*Zvj8UKTc6Q^rQAaPy7k@m2FU5ORC#`3G2cB{=6eze2Ck=;GlQd^t(&l#0roM)aSSs;0RfO8xv z#CGWHl%#D>>#1|Q!SFQ+^chexaxOBll?8>^v{smn1=)}al2ww>< zkEe#Zu-!FS`o7$llITY22PK(u_C87x^b&kG@FfjB%7NG8}V` z3SVUBIV#g`lrOS@Hlxo`Jqiww#}}CZ9((ddJ`HY~V$M-LY4CO8zR1cR+IqmWl^A({ z5V$XNbXNk9xz98nx=-K?)c|vz%{fD5o4e?BS6CeKp(ef{yqYuQi6Er*G(B@i)pjiY4hjk z0_!PL4Zqh=*Rit``#{|(;IGm1Hn*&MZISbWQahhpoULA_$&!Isz=gSvOd zW#RTqb(i9$=1-#hB$%NB1w#XdsuJ&)7>HP}unq#yRPCLQ2+ z^Bl|QP5H=EPq%8G6U=MyJG#{p*?gjO8l#(h!g+dd;?%@sCLxaT!I!v7SjkIm?JchnBhOV-X4({m) zjhsV{))9*NokAT`J}QRs@4^pfkp=f>d6vDQvt3iJyB?dt`IhK*t3U3jGyF{Kx#2nN zwI=?YwZE45^CocIGKRZqh&ylLUbtFf&J`cr?B_k_Rdi;q$;M`D&J#QLs688SY_>t< zvWYK&4z(6(Y+dj>aA{7;h#~H2PKdpn@I&w^9x!V$bP~J9>0Im(#;ty0W1_!*QO6T^ zP)9M*U!tAbdBF`{9CYnc;-DE@iD+N8M2%Nx+!Y5shkcF4%^CT{&thNTw_rE~PiJsO zxkhzN9CT8o06G}Xx>4NRQKvkR(RaG#)2HCl+9Tc8Tf-Ium)7mZf#}8TTT9J8)rpzB zuc{+8cE9f09@fU#9=2IAwudfx${Bs&zngi9j+eZcdC|IEc9b)`8@KknZ@2;MYJEu&^p-laesY#~8MthI#viUD*OsBlE!K9?qM^$>Yhzq z<@35Tnk5@t6c}ZbgqMDP2-N!ZvB{j&Nd6Ubv8pVrqYk@KkHiOhz;eN z^kf6mcR9B|#wk01va*#Mx*?`sI{)qqk6g1p;N4zxPTR@D@M~a(?~JZrZ+F-AB^x|_ z5nLHw(%CNMNst_@Z|kT#*)4Ov=m7oW|B?6Z@ljRR0{=cUKxPucD^DJpg!o8;FXWX- zQYMK?0F??+ui8oiZAqZ&r9LSj39&UWzD7`5@RlIAKXZngir1h?@s>brMX3)OTie=} zJbcU~yh0MlNr3r%*EusVAU^uIpZocJet*no&e>-_*IsMwwbxpE?UdfL&$Yi#ds4^w z{B^FPOx3Z#S@XG*d5}3R?j)`*V-3wiXb!|b_9>wg#ws|mOXwsx-tDv{^eySKR;Mr? zIS(lEoAm7rU0G-0^bS)U!e2)FLgHUJOo_7igu2?eSX@h zF#GlOXy@Tz-aic%8T6mid2ROm>vdixjfAZ$|3aOY8J9!Wu@}H~b6xmf(0Q5t50eRs z!KG^UtzkN^ZRhH|LO2`R&t42}{2%JP-v4`bUZFWFV(+&b*jBxHw$3Z`UDhZ09+E-I zf;z7d%%prC-=CK9|AfxVtS?M2B6IyUbzW!1Zn=kk6rtyme%;Qq;yI}EGGQd?qVxI= z-;#qmuh~3@bY5mZ!f+up?uN5wCZ|r1QF&=l>0z*W-DX&MS-hzewj*K^{wH`f{CD8g-mc=XIZ1{(nd3^{rsv z{wGr8jnC=4o<5Hc{?p=vkIW8lyVQ&ygezo#GCOu=qhwGIw7kKly%b$($ks zihL$88w(GaVD|Cr>Acc{c~{GL8b9xYkKDUWeDKkEZTmZQUQdVVytbX|f1k{F&*Ojp z^tt}`Ccg=b9mHPX=}_z-6OLgzuPEva*Lmdt!>^^>nqR^V+8V0iD-HC)HUxuUo+b z(LcG)uk$*qU;hf7*MBhX^Xa^NuCJx@vd;cnIxp+&|AI3*uiY=6(Rn=#KMm@V&L2XU+&8v48ct zGgDxRE%4lR=UKDQUvJ;OCYZOwasGAZJUXv!j?e47o)#TaQ0KKxbVug-VM}jvW}Fq4 zj;MmMvJM~l5}jADjH&b57OwNUpRz%nml+!?wATJ7@cIg!m$eR_t@AqFH*=1{bY6GR zPEh9+T<6Zxd2Lf+IAZTU{e3#G1LT*vyV1nE^Xj}T8RU%43z(n(Z2IB; zvuTG|$6qP4EfbiXhjxBr_Ur4>&W>Q-F{j61$5u1|J6Z6YeWTouWHW7K^Rbtg42p4! ze^)$lv|cjH#(On%zSTUl{+ssVUSbfSAJiwqk1bmdK49QibH%P~u?HtcD38PkTWL$| z_``hk`?j(iU!*dQlUB?fU=oADL7jnAMZAHAoHD+z<$E`0?L&7Tom`f$qpLS#d?@mY z9r>xGGj~ME-CkuuUH>N$B3~v z3{2c)v-)r!v9^daQs_gMEn`mQxB3)u#Ymr~**j#v(MR828JeY6FStf;bK7fP7=mrW z7URC?yA3(`jCePm(mZ3zm1pU0<@q?KEGNw#?_SL>dB(~b?bnagL_E8)Mm?T-&s8H` zH7Spey(f80@=aP@=Ur*)dq4AGM-liXKRNcE+$U4-QDahWQe!TIg+H+NWVT^Z!`bpS^C$Cd~kHStELSk@CL|FSp7nCuQEGj9E8jB(Ic_{EgxH znG0#>b@ECcsbBI)-ZNt!)lt!F@r>ilqs#+$OSpe3d(9kW!o)Fg(mG)#SV4Mgn)`tF<9}#pgV& zgjhq^PT@sH=srA&E1dTDwfJhgjEo{z)?4_L3jNKGRE1Hc{>+t{Ha>&rR`}=)P0it~ z%F~ZFPx7GW$g=pW-FpjtJ;*%HP!4yhHrOqB(}^oAeYt#$GHfy2DOtK8(|_?>8{Ps| zJGCJlTgso@AUtyweB)YWcdu3>+yfUa$oyXEcQSXXL5o}9R|^IZ?*`ubDF2ez4&Rcp z^WmKfG@JV^#tv`mXiZdw0~VELez&AFbCVjlcr!e5r{?US>^>=zz}RP~;qGW{_})JP zYw4@Z*X6)$3;i|u7jdNoW>*d)PCs=@-M=fZ+#vOj1-|LT@EL9!;+|t0vUfh;hQUY0 z|3St)jWNo2ZU3qCG~n$x2fiuC0)+z8T*g`e46#dRWsFc$WNy++RMxrkF<5iDpU@hz zwqBq{yyNVhvSyf?vQo1xSw$?=49eV1yEU4_Eq%W{2QWE^geA8i<%IV|an~ z+U%Em-_WAnpV&uuKeZ3{z8UMvybl-$RLtTOVEl8|6yUs1V4MQ1?jiPAzKV8#Y#-ts zxad2X-z%}^qa2u$XWw@Mf$i6r?K|BPAMT*tzV~nZ4+Azg0~_%*JhyK)*25prx1$&T zZo}2VzS-cZN9o&c`qoCjKDH0@{)oPPuXMgS|GQ|9es#_P&UwMU$=Z6*K4Ne9xbvZ( z3}A3B44)XtdR=Z`QWK{}PTqIfmk{%H#JeM-GwvR)Ml`~Q-O;YB#iT7BIV5AN zbI63ZyCWuxuKy;^_vyemnmLuZJ>R^p)RLF9($AUJoX6fheAI*vdvr#mT}@dBOjhQ) zmMr4i%t>oD{E=PFP|84?Aw!* zwRIElixNIqHCB8)t-8O5JRV+e5%@G2AEpvpd`BvLW}PkGEq-PN_(6%SU(P~k%;6+r zjyfIKK~EJm0jCV{2Vni4f_^e~&#C!MZ~d&ODd}?VQ5?T$4JDR+ zKmMA>Zo3}%5kq?6wa8U@$W>Mx@?*qKllij)s|Cpt*ES?i#&~xyhhocbW*nE^Z0%=8 z4h*cnO6JxsI^6$n%}ssM`R699;UAlucZpZ^e>FFSVRO?!%s`Qc|9e1S{aeI!&0}vh zG!m|_i+QHq=^MJ98wScjj zcuG4N$@uXw?(~z_Hp!YkYhZ5gR>e68#;D-$GDhK1g-UhgBQF#K5B$`j74a{%?rsUu z$%%XO&!Uwn!<3sn+ZXbf>L8CHZh7GWXzKYx%bQ~9-|Bp|FD*C^k>)%ggVXLza~?Q9 z3H)phwS8vTJWOLA;HjtQq38vh=c%1yKRCmia^YpwA>KrN4&sGZ2YFLDe4zRq-eg?J z-Y&>ngs)u~#NX;L-h%u*<$vhA-!{gaZ<*^=GRMrd`7byl@62@`|L4v%vC|W(;Y<2C zd`asG6t2U+^5=XP91xofbFxo(mEfM>gpBdlbH>PR4~5Udrz?RWIN|lfiyX|Y?1eL! zTdys$qYZq0KHr+(9($xW*PLSqXU!tL=;1o>uPv+sKF3EWw;!L}boQr`*3KHx4bPpa z#kt!rQF{|_7v)RwVVP?u$U%=NA8N>7WkY|dh6tPMGOK{M|re~v`jZPD!;_Q3j&Mt?O=UB$ZfW$T?m zJLj%<8nQYL( zD|`)mlo(j&^2wt9FJ8mS`u?#sj5#@S?iv;ugtx3=4+h~~0=!q6@D{zCP47a!I9Vb4 zNgHwia>`1j@1m|0*HG3&B=hcPZPi7o9^6liZ{~-pg=)i3FuA|pbwk120RQ)RGV)K0}YXa>H-TQ`? zjhLpAm-zWli&Ya^`JTRf(A?^WRMuSbqz@c*wN8GCVcJffam$Cz&A4pPgecC$r7s^m z*GsuUVhVcov`dO6bj@ z#qC|m{bSX@rxIGEf6z+|{4maat48b;^e)2tD@~nVo*7&B1{&AQKB;1d|C>c`TDmI*3rEcF!8H=aF7 z0d&~VC%Sf1hC3H#t0*Hd^G#7%d6W~F;cJ~0DnB$-9=(tFHH5=)7QXBP$9Brjr>t+P z1;_C6fm6Zu;jP3aMh4w9s(W^59kOsO1n+jruabJES@8B#Z+lo@4g~9JKxQog*1){h zIneb?1~@t|h@+fq;0}bdaa8bA@bvBrv~~C6qy0twkZ;f0gcn+L6QY@)5BU4x{w{jJ ze+KTq3YGr{;r@$I`F{c2HwNqaN8vv7YruWVxo}s*Jkx=DxGqNv-h0QM8auehMqoI`JFqUTa`-oJcb z@XxGaA)S@*i;rLb=hm=S$NYV3SQD|){;$@s+qu&xv=96BjTY~J4PN=~$p2lLAspVW zSN}P9-|=_C`=S3sc%KBWA$XSt;e99Y{(%YauaFs_Ly;N2gD&G=Ei*WQ;n$TJcGK5z znPKq^3vbdHhl_8>sz+4ySdTW4JDAP2)jH=Q&)7tsPS1_@B%=q%ewHI?U$qZgg)XCD zfX(gi(*AoA`ijz9o;`ZZKET_{+Tg$*cpSNEOfh$Q1@fC5+~d4@`JRoH%N-j7c4vp5 zHSQIgW2>CSElpHY3iuiDD~TJy+2BsuEBRhPZsOjF$RFkQ&edYJjz#yJUX0BQS$~4o z`)a?rhm%o9X-tVLh6I@5xK>|>|$E9bqB^*$Mycc(3; zqpL&d$Ogt9_Eu7-*x-H-4|Lf_b9dKB-DQ{NZC>jjg0PRdiNK<~Tz#)?4Tm`tHrucOqj+yD~0w98s#% zPG5rKSW25qrR@div8y7_wA&ok?iAXUI_Q^G$57ST2CU6GB3)T)Y_6qxq%z0gr9L~~ zh^cjY4DST%$^|B(7niz-2T<4z99AL2W!Y&Ty`0?pB>d}Do6X%2EF*V9$^8SnC>s|n zTf}~+kFr_z_>L{i-)(Ixu`64)5wTakOxY}ZVn?IwA<@yvdF)o~o_8#eIZAZv!MZi- zY_4d?_H(YR#5QtoGIYb)LuB$V*D08E`2955Gakt+o5H!bvN=uQuh6df?SoY7Tk{mp zX71lgxO9=Po<-h!aIAF~ z7dBe%;6wk)K44eyu8js@DD}NgT@tTKWcD;uMwEN9V!U!kmJMBP_v}3EwYlg=8mnKP z5=aPt7k(jolJm8{UE0qJYkw7cBIsM%mpdaDL(ic;Y@`pzgME;_l;&TC4m8HRDzd-w zd58{V9docf2!qXGFsKU7!yQ2wl(5GU7!;8%a0tzT+&v<5z}?|yUqbIP2j|Lf)5&Aa z&9C$>_KUJtJfDo1Lz^NaitHDX5i=!?xE*F1=S2laCP>;PL0K~6Y+3R$+6tE?zbfu? zODgA8Su2E3H@9gXiKF1(I$iHuke}syZ-(A|xImXXQBQ1{smuA#-Yv6qA7k)+c7yKY zUep-+QbwPIuMg(A|4L$97lkOa@vUK~Mz1t}}Ee`$#|MugrQVV}g*IZf7(n*=Md|NwQ z)k?l}Eq3oTYzC{puL@TWd~}YpbmGJ7;E9q);#l}NOXn!ZPRhCRQ2VRG+W$W7>syxT zvBQ;TQfXS9SvPvjbio<&bLYsM?&DdW3i0CyTKo{-62(9=)s4JrG>NHuXDF+Jpt#8>FwN_<3;i&f#XT3e!9%1gacM>S>lPzJj3Tnj7& zc9vc%{1z!6b~G*HTx)o~IMcU1ga?w(Df#HP_$$DZ z-&eI42~U1MuPW2iLVp$}1lD7VGyAfcxd_LjKK5zVA1>3+hbv8-uNGXHhChw#TiEG< zC3uoq8^n{o9%6g&-2pBMtQ@xzg9R7~UJ9H9MoNoyJIuLDbPJ3&&F3yl)~XollCqna zN8zthCWg8NZjwjxYVg|lFc^mS`?vI4?n3f|V^YtgcU-#kL-4{k8=YdPAFP#@{wCJ_ z>4{V8E5Q$;ucMqBb+Ar!AWQ7rlC96+9=x60HI+pStHJNDn$wP*IEuEnV#|EnsXPbm zQ5jn=RyW^-55dYfwPY_gQ5);A*btJD0S=+dogLSZ>9a?94`QQy=~Q6y+t^-rVym2m zU9}N=+S}M(cVc^;$@{F4Q5jxruNA=J9c-^+pRFP7MzOs@Q=c%lo#kqYqTJ3n+mbEF z5WiUAn7e<4T5<#M8bW-R{kId}hCb}n1}|>F4@2Sy-90`sm@@J{>_RnS{5$h9hsCQAS4EDRAaPpEF#(hPz$6uzya!AI zz~phpIUAS+fJw2)xxmDAB2Xwg;9*15h{MduVPJfC*ukl#EiJtRlny-HDxEZRJnt1H}Z_|s1)SDhl$Cx-xk+V39eV$;`esL5m2pjOK zqrI`(!cQh`OtviyJNgFV;%d8@cdiI|0y_} zgkMD|<c+{{*h`zmjoPGrs%Dv)~KHStH{lpEb@dbDUA6$vBZ6 z&2j!a|J78nAR_{1az3EI>n!^mO6{ryNYhR3z@od(7Fu{EykVnSM099 zyPCLFtd+G%tj);|SwHXU75QZ(Yv^{$%J=SfSF$Er_6+>E3jYH9)?pj+F_+S}qes-b zSBRfiO)NE8HwB(mZED}HVaNpbZ}&D=yqUd-I!nfm+UTDyz9(fl*pCb^>!gfX`enAm zzQ9~}!8f5}Su48C_4n*C4q}XQk9#cR2p?w)`+DKiQr4j*?d=ZM=Kyb;hzs#c`fb5d z_WfsMq`mAT1(re!)^{tOLr6Ai-oMNc*HXH(0E@3Ug2YYG&ttc;CN$YZ_HfB z-L7=GgHrCsllSe6tBSHRuOg3(n#dmT!)x`wydS77KfIj%U!uAHizOYs2>tp?w#dP? zz`4r)vuwM^rN2UXnO~u)kZ<%i`vdD=MV=(GtOr49%^fED|_s}@V$k-o$R-ok)@=}hop54bs4Wp8uDK#KH5IgcSstt zdg!k3-;(xKxx-!v$jzSkFu#EG57K2ZU(Cg{4iKk`ttAk}{Q8x5l`Wk9GgYUA3=sXenF7oD? z@?-h_D)RN&d93k<>KG|`kblqAVV#u3-H+PsrK^xY}nDgQb7{3n4) ziC-{gSu1tMEiy_nhpXeiga+W0*+nnfs9){0_Xkr9;$3RyWJ&ZB&JestP=ySvA!69{0 z2YNb_CZruZX;R;JsPE*mUvlO^!^g;mpPU`t)c|y715JB+3$Z@aKK^0Tzx~Cf--%63 zVi}7rZB(?jS7fR&RqWBJ&j9Bgz@f5YUR{!wxY))1H>*u`${B>`+my!z|E`2zu8dGG z`#Gn8PvOgQ?`)dpn7QXv#1t2DN2T3mBxwm9xjdIu%!3amg6|35JK;rDCj*nNqyFE& z>(V7g+aFmUHrhtH1;>uJDo+4etqhsVhpq^Dx7PW|51ZQ2qfd@@?7e-n@(7PjGy4{| zn0;*FA6iu>{qvyvE=;3e=u$t|FCYCXGQUf|VmgX>PB!~x?fWLtFYKI?deIjhzXP8L z;IZxn3mzscRtYQ;7B|!09YI*Eqpv0`Z0B$DG%Ofjk``~l0{$7&S0n~uA^0O}nx(Im zb70poZ`U$^`OIS;^C|kt-v)FbPv$cf#y zvS;FuKX$%|>`{@_I}M-kX4Y=>Fqz_agk11)HR<{652EQq)dICo@F~PEPNEamQ&;Mn zp`UZhY+Z-AxYU*uSpgi{;;w%?< zAvDiTcs?y(`%Z^kP@p3k|iiFHGO8 z?c*%Mc7YjZurg{td_=FOAJk8t!3!jtm3v72$XBGG*JVM4HX(_`07>oGk zb~7$X3+dfud}E|9(B=x}+B&ly!F)QHS10X;#cFKSJmSOE5~cLV@V&5A2@WdnJef1( zyMijsQxcC(u~Fj5{YORicbpV^)yRz!KipBidc!E@zaV0iJCQZ|*m&tB7G`DPCU0y_4MgDeG1U0GWOhk;NfJ-W1sq+$bY?E`TBm5 zACXI?ztTVHm+kJ68!fpk-fL(61|Tor1@3JJ?$z*xx8OSoEt)1;4jcw`RzsPUCO9$vC{=c5$X<^QM z@VS=`5|alwgs(~KfcJCYQ0-Vt)}qgyV&4%uEf30CW$+KdrPbsWSxf9Cd%!ivK(S@S zoxXFsobSctmHLZ1{I}v}D)I@xzJIud z{~3bY;KF3qFR8znI6vWaE->4O^}b=Sp26co^4zycm;9Sqt1b-IA@3=?%XvJ3h19!? z{)X1*Z&F5RThg2Jm8Z-IjtQSo;Zc%)ocw~X!bb%z`C(-QkM0YWY3U1`hQl>xAJ4#H zviTnCT~D9CiZ172&+%cS?8XkfiDwt~$bNeSHsL|&A_k(1uzBU|9e(@XRrn~1e|!q{ zi@cP=Zxqi(ti5}%X$nmVO@KRDC$JF^)T1^&*cZt=RPquNx`j1Z&IB6d-+lbl`q$Z0 zJdcbNW#%1Y`ZN#Vxs3BiA}hCIQx^O0Yvlb2@{(reOEmNKUB$Uo^p(~gBGKDVz8dn0 z-coR1)_m#92l(t~@holq1>Z-ptNs!HNU<+>u@?K7J7+TV@}bgYPjswF(~Tx<869@? zECclZwt>38Ni8jw{V{X%6#85TeBBQ&HdnlnU2=(IV@t)avs=Z!IE6IwsDZW`IWyZ* zu{+y2UKz5lvT2I6#%xW~YW$}E+?V5PnXEI8OGTHxEw3ocQygD5r*^pM8?a5?f3cOW zNFPZ&e$qqp@;ZAtd6xZkJfIpIU z!o*40D_FAI&#toYSiZ@+(2UJp+7WzRANI}1I#O+Z7hA0rgZe4HOWj9$&){Z%zMLKT z{F-6nChNm#+(cG823{KAWe<4S4_?YTVgt{5!OLaT9UA}rJPZCf?5-^7XC{3Uyo?3U zmY98rHSuQ=&s8S=2%g27cy9L`TvK9;GLj&FKS|?g!5TTkOPfcNx9=@p%Bx0v~}}ABE#qjJFpZz2H`9 z5VtBnhgNiyUCZ4;FqH&I`|nTNiu*ZY6}_*2Tll;MQ24&)`zL z_d@eq&<_?|dY83D`04%3x6GA=FM>-JzC;1n&>myEi7Ucm6HQziz&9!T-7sAFBKhJU z;bVudJcBPjp1+Kbeav_9Z52NDSIK)0AA9=P89w$WGq1(Re#i55 zdw4#Vk1ZqLKg7peU!RYCJBVxl7kq3LxMJeqIecuni9^=Hh3GtaTv9J7uW&B8qREhKwZp1pKX$L+Bs!zQcw_(fb|wrk;ilC;bD2I-UV^DE(fPKT?0XJb;`G;9F7SsV4nq)Zu43g2Yy zP2=D6&&DPoe%5JmL7(;&__Q}AWfqdh^l7JDEO|S~bJ>bvbH`miXaYL*N$D#F&)rJC z`^i_`mXvAww3AnCZ5h`Mo{+v`#N2g!+bZAiXZMm<`cU1KlzIG$Gc#e@ zlVS9Kn5!FGaXD?l{E_eLuU#)&uh?c z|C+SW{_j^j%Q{qvytN3~z==HT`cp$r(nU&lB&&K^bJT#coRJMGEAb7Lb!?GMH+HM6 z-rsQc(WZ3Yt>#`fwhviOY`@{(17q>48sh7v8rC$mo}RRmE}Bu3>?yt2^@aJIOQxcat~+Wr+VNMPpgUP<~dO_t?C*p z?ddd6ab($?d%p(b&DB2uW)HBhKSp`!hwOE} z%xCCE>K4B#AN%WSa3b7q>UzG3&BHoZa4mQlJX?@&;iVO8@Ejc&{Sg|*pVhNgeA{0H z|B$~biPfu=XJ@2(xd=Fv4I&N$x(o;Q>s`Eytp<91#!vt8AuxTQ67Ye_ z4EjJ}bL>o{4F!MD&`C@S`hh9hkog)J9M7waWtZuT6>IL@Vn~-Z8)#!E`_(G`n~4ci zw5lvy*8Moze8493cSbK{o_(zdjy2!X3H2=;h)&^Losc!&+ZgZhxh}mOzt%sv) z*+U=p;)8XN@f_e+{m9!JSKFf8H^JAQV4fsR=4o-c>Z~azZUf``ML9Yj{Au^Y2L(3C z=#HL*XG?#}%FD7JFIJvz=1gq(Zu~4pp1igxO^fbW%s#)i;= zy>8w#y@7h;f#WZOa1^|_BM8UbFgUt`aD4M}3op_lM!F?NkJu?c=58XXNBrU}y0rGi zZSdgN$uImL_-?oMw(;KAqAlEyr7ufa6CO7CTLE}v>4W+fUU!yGNaQlnkBvDd_^v#e z=6{SgLh#-$akK~N{n)?z7CM*%C-p=^15wa`hOefqPn=oBe{M5p6|A#w4(~B^J?wR? zHITmVfd|Ae*W%OW8?EXE-$Z5}!oSGwvKMqS7LoTv#(I=zd}mqX#6A&Y?mwq=%N|_z z8j0Snd79pZUZXXiv*hSS${cEPH9l{m2V8~D$bp_k#}_W`BFDx=r^}E#KN2_>8P~C_ zX^GiRoT(F9#B&4uRO0*?$nn2qk9~Zs@{Fdhy%jcH)|nm5ZB@j|nk?cT9fyXJ>?1p% z?R^b{%Kxh>I4?f*8P)a+dwt0EqB9nKY(c7=n@{lm$*_Wgu; zL1*=H8S#-Ssizs;O^Ek&&z=R$MRuPF%x9oe6FtWDtW9>$H!c^R`^Ddd=L#R7@A!yc zYR1fR3IBABmHpz=c|}vrnEGRf;1k98%(RB7^i>144)2T6XLzab$8_G4;iVxt;$q$f z=bE?R*Nk7^Jv}mSv0jb3WLz>P2jlTE9?2IPllZ?51>TbP-&uQY|Cy)HQ_c?I4YGdD zKo2N+B*wyDd$5Z$2Ttfu#^bEc(`RVTy=Fde$f0B4-(N=Lpj z^E)m&JML5SaLzk1x1ktPawmofA9PyGSLfyQSI+a7$v=9;UHqpFaOs?%eLBx^kw?x- zR-+?2omMs0BYq;M-=9wPIL>;%$n+gO{r#d;PxGZpZ)09$jMp7k=ZT{*2)c?e=}Kr) z=1}IOn_mGq@MyNjkhw_|@v&uB9d5m&EHT0>4BL zAv#vk^A)Ws&u&K7S29)^qUSCeQkEmS?vg7M-yY2_-L002Pqc+^mfl!=45d%36{l^T z&tGmvTY9_>+7SIzJ3OpBta+L7AuXthFLjm&3blALN-n z(!#s3*>s8yQ+VS%d^}|CZWSC#wPHFv$9QLfo8pTtdb(y{BYnGv_B)|R=|lJIFJ51d zbLRB4jj00O&-i7Hm42-H8|NPBlk`J;w51=-WN#1tm*YxLO-4DS_U?pq7un}i`=Z*lYr1&#_ec0CUeRO)S#INs4 z)30xc_bUAQ#E;MN>l@-7k6+)k(|&!t1IMfJ>l=?>-_1e4J{eOPbp?RQ7RG)A-=d^P zEWf^Q0+XaW@Y7?g0bnA2eX?H~k6)ko2#SB;uu;QJzrLhZ7HpDsTmEQ)pkJSi{V=e3 zs`TE>+4%Db?uOpqJMG6e#2pTo&+lz*=-yPueD)}^?*DAunKgU8`Fv*07J8X^^wj!i zNms1LV!wKlXVxOtb>)rurXn^UKDw?!o*3*f==(E%_!+Wm*dBP;0TnCz$w$NLSRv2o z*hJpZsCQY^+3V8^-n-y8BAbNoWB#3TVpG`Hb!z?LsjuFBaDsMs$2i;F+|hKm*d4{r zc;eoHthIw!b0d&*9PneO9*A)53&6ty6+`#IlaGk3z@3su5*KJ5$L09=PSK2(WtzF} zm&aTFf~)0x*1>6>aT>biX-*?;iL2MYz~Om&a>MVM^9Or6+?t_rjwG#fXs^$n<7uDj zGy*FJXEkC+@ogbSIQ^%srybf*k7<*8ak>Hj^tAF*Wf7h{;`n5jk+<#UdLMpgzT*W( z__<*_dVD|flOuF)_$iIE?Sq47+XrXOBZlBS=HK_(&GqTPH_+uU{Kp-v2eEb4ZMJ=Z zeuv?skMK^9^f!ezzgw-$xPvXw9qZcZbM#wSzooxvoTLAQGoO?H2U}=Yfl(|xpMISp zZ8K^4wAa&LVEP=34LYzT!tlKqVFb2D7?arN9w%=fZH-}GeYQBa=vDoT9ya|dMMjXg zlYV^7B@R~=Jj0Ov6Zu8P5}9y`7u<2@8k@v!=tKShjxN@{85McG##6aQ|Jq#qb2n}FP*-T|I~n_Y#x8vd;Y=>=#=^Tr9!%EoRX<|) zv@nhU@b)oJfwh{^y|?)~@+yik5?A2F^9#ZEbo`4pd4Y-fcH zgYcoD&623=j3mx*?EVXWebf?QL=W*s} zr54$dLcIZ-){z&h6*}mz;I9uIdR&VOd%5MC^v$)<4!BbJ0lWy@khy3DwmY{>M-CX?~4`^ubZ=ez>R&2=B;Y9 zP4X%T~RW-5fET=eK#@%kwavf6MbbJP+mhU7p{|?jA~e5kqQPhVIH~j^P&(UDJ}l z|7dw0T+@=of2ur3)wEp7|2TP$tZBKD{|WN!tZA9T|5SN))U@RDKX+G7OH@Qn%Wb=I zTBq~A>Cv3#J4xH}Xim#4-e2V39bZ$z9WJIV_}=S{L}0ZZeAxuP?Ea0|h18LCyG35g z@s!&l-8OJ!E_hILP_hOms4~l$m`FzO_Gey&?YKw-X02eev>4ow}Zf4<>2&>M|bDXRSW|$TJzp zRp}`bFEz4bY)V0&-bf$e-7WX!8m)_S4Oug`z}H(QsdYkMLbD=2`ryGI2YLKvzPEzI zZNT$U^6$9)3~nm%Spqko0dLlVH?|;dt^;@MLEL;692y|c;N)}Q(Li|yFV}-lgUtL+ z&riUq2s6La^HXqZn3+G)BRDpk=W07NA-G?@D_fN}WDD;1fp`7j-3f5c#1HW0B)E49 zybFMLK5)(t&b5PY9pKvmaP1(tb_hB;EVu@4sq)>~Ej!9g+&b}Cu3=QbPX)(bE6a%o zhN7FOV*KnG){O#Z+T6F)2u*&nwy@^2jdN={^XJrb&APd!XYMy^j^8=E##nGuP5 zYfj#GLrq}WtQvpn%$kmIGinY_D5yC+b$U(1%fJg*Hg9@^KEILUXn~IV2N|I?Kn=tW z{Nfuqty3#@4KnOl+5d0R!!(K{=0y5?2nbT6pL=~-Bj zb8K-%&hdLIa(eHp$T1$`dGR2l>)t^|ch?}J=lCGQSQ%lQsE80>zQPh@0dzn|RMmwZ z$3)GzJVC9KwuHu77?03c0(|y^!{~-MH(m0oDtrk3>>xcvk6n*AHs+o-*cfiB$-pKRxcY+EaOMb<9PzQE_17x|ZO;|9)j&m1^&@8Gy7_v5TZ zo4BVsz1+QF9(#9LM;-U4OoWWZSokvpVG&^xrsWXywPtq$%Pe*U)V}Dl8nkBXg zxzn`|nm6DFvcK>{Ut)8X^9%UY98u_?QgU-qmuMZk+hly|NaRfH!jwtR0Us$RK3`7O zR6lZnA9$ys`w)317Tpy(>m$ck!Oy8z_>SClPHbK853gojV$Bb&MXi+UgQmNYtBxZd z_+E-I`d*1JdR~h#j_rsrx_0uu6Udtz4_nB>SbPCGpHx^!7OwjOH#xir0Irw zW2+0boQjq4iQgnL%=oIGs=^NVVmmyua)`QlQ>0olgYWJ6nz0CdCH_~VIu>X~`z*~s z_MF%U-)7BQ;{Wi5`X2aO3;eYpP}Jmy_xh2es%#EV&zYxle%4z*JkK|kclfZM zwO4PA@c8EP4o~)vLnck-8UCHi9QqajQ~c;wjmrCB0dti5OSwxT&U*|R?}5gp@4fW> z4*nbXhA#-Vd(As)fOYrzTK@g;NPL(w63d-l-`rfo55MH>Mu&QY`jKh6n4^Rz=JiUO zzDur4m-c+JcEGp0@!y-qyPy6WJg2ST?4K>SSL_W{%PBvOexQ4IXs-IS_tazuwzfUb zKrh(Vb|Q3`&$qA7RnvSUve$-Ty|tijv!^eBBsIcMJKH z?$4)R7k^XtUm2l0Q*PCLmkz>rjxmAf_|SRL6&q=uK^|WM`$J^zmZ^=%5bT4dHRi~g zrZwVIGXkDnjcxiyQ2M0OOV7xY!cf#deU88?8>kJ|>jpBU-NB4+H;r--=|(T{G4gJG&oQ{rs(gu>>S3$gCZD29do0Cb5i&Imw@*fWd(G#Hr05Bgg$jJOi$hQ_l7 zG~zG$8)9<5@^Eq8n?-q9KjuG$^Lgu(8uq~Ag_-LjV6k;l)o=wiC)%K@I{JYpAp>fl-UR2O|~=UHqJ zpF-yi^l1)f)y%W6MK({88r9Lv_Z0SOX8vHl&c!a{YTKX=8Am5}*zvYu9j>u+`|^1| zI?iRxu?_7=9$V1Y$ey$FQuM2L=8|U!d0c&|q#aEqEkBoh(d2XWRiL}+s>^KG5zO`G94<5^dvzAdaRN25raCHXj4OPZaudA18Xj-tnG zPR%uLwyC|6H+k&0`o`V@9<&u0J3nLJQW4kN$6DO|S%L8ZYiR+#Pf7p78E=vMm|L-V zAFEUAUYq>@`yk5)ubJ~!Ee9<>lfI4uZzb8GCCL<>{L?!-#P}hw#|${28VWpoKmab7s7e{+6)!yaxY^f`Q5q zocfiO_HrmK??H3Den0)YxklP>hg)+>{E-}wry)kY7 z;e|-g-+(t~y^rKQI?R)bzGuV&bSO$>hw=?RaNQBRX@jh7AB6G^8189<|2e|*f!Ag} z=;7^9K9`m6)cmmepeZvS^wAK?r$%@NGoI6JLT6^4(VFoZd9ZIB$!i$m(Zch@1oK2` z#xFwktQ+Ccc|Ki_J(#Co89xu@`Rxc#BV+tic%QnI)n11(Hip_$Lp`4a>*@jyqk+M? zP#w#Md!7n@>*3o-zC988Ry*AD4$q=jgebg^h0+T~c;4jM2VWPv`I`7m8~W@UHb`Hc zH|ObNSaYIppRFIDzC`MK9on#LS!29LlQMetD&((d^qNj@f1@(`(RX+4P}n;Ld!LEc z*7=bq4zdm(fCqK$U|qx~wZ9P?^In%>G`WnjjlHk=H?k(e14Mr4-o*1(e$cS!Qhe|W z)<0wy-qGu%nQJ!h@COy`xXHJL=hyg+&NTur^54i0I`{41|4n{Tr0?W^FTWVloA`f^ zUq3p@5BdLypB?>H2mkx|bzrMG%zqodF8bJ|8K#`@8GGL2@W_76=nDw{wHe1EY({UC z%{VaBW}Jwz8Qj2O1d?pXL&LK~7j(%bf%Vq|+p4=%?e_%6ZP+0Mx94lb*qy+hIJiIm z$Yor`cj4Qcq5EQ?S!gow8oEH{Wdh%lknP29UcvXoo+kX|qrY)~J!b@k&xDujq@1kN zihY^*LT57%Ql5GXcV^jt{}bwKUdi5S*&8{2_Dy|{IgRF>kLGkOn_;x`NM0&+mXQc2TF)A^b?{@F7PyG)pI1vkX2U`H>lfHiFZk{&<@thFf5iUX&_W@XCyBG0G5*Z%E##tXbneN$rDocqZm@_N^O z;M-zk%mv7-$nGt#1`3CB-mPs`fi7$M#|bmQ`y_Aw!kN0P{jEu}beV(h+iuWh{TEq4 z^>X}#gjS5H$hY{2pBkqbz3{nSVmus}g)9zzAA`1! zK|hD)YQ~W}(OtqDx{2d|G#`9}KZvhxTdHP!5~CTPMS+v>$JYl#SK!QN;P%JR)+gX~ z8#pg#5qrRE1KQ|hy*irD_`vOh?6rF8B5#;dx`ti9Ft!n*YF zuDbEyuq7MFS<=zo!^97K?Y;-}i=ju+?@D}r_Pwzpi_039j7}y6J<8bf#_X!cGHYJ2 zugxw(Myh)3;hNW}e=Bl^PTgxJxau|%pR@?wc$K{=yVRrV7VT1X*cN(UW4$geUzJ@{ zUXd+(+1z~Pd4Y1j1Hb*F;i4|P`CN7k*LFtASSU)A>8dA_br_>V8f zUhkXh@Hjh%)`^`YG#;_@?g)-Ym?>k`+a#v@4^{N>5q}_bk(1TDtsJhj!45}-EuT-|fD=1%GaYtP{0ZqLef*#)-$ORzrHinmeS@*GW>@pv{B3mMv~43zeB|z=4Z#7E z&j)#FF8Cwx_Q&McFZywW$I&^vZrQ4ctcQ*%qvTVqeg^&j1^BbF-@c^m_kqIqZ7Q~N zq08ulK2MC+j7hdZ?#>0`UpmZvWS$EecX%UgL*0k(biq5If9wN?ZgUxbBA&>>xh|95 zcW}4Kf!V|lrr$r3{p>7@?!Ti&y4$C_jE3Ky zSxjG)$kgaD0_ZWuvgdJPi%ljq^Vl0UqSKa$o;KU-3BjVH(>aT)NN zL_bDsm<4B#^=Gt+&)$@=>1df3$Q~IUv1&_oMW?m_WY^&*sSxgUFY6?cBype@KpG_!~y8y{~N$y<)TbtL*Uug;&sS?Vc)T8YJ(#dy~6+m7VRnBHH|a9MCn3Eow} zbpdfT&2-l|#-e%W@eUqlhz#f(7oC;nV(!7CF6Px=F*wV&cF@w$7@M$1`|K;SxUc8t zq`%si?4geWlfyS66VF990ro<->~-C-x$5O#LqlTow$A^>dS|ge{Wbe!u}!ovmoh)% z|Cm94#WtbF6I52>}eeGsGnwb~z4@e94U*fO$t`K`1 z@+Eo5?%qw^QclvO+)r$dt-X}N7FH;}3JxvNy*!vlWau%}mCC!w-U5S7?ER&UQebu? zeU$lLZR!gWJ6xBm!biwU9eX98#JiC*YTeY^0&U89!7^<4TmASLN*_p1w2wL6ekH!B zq8E|#{qkMf_c5RTj-0G?=Cd1E_M$)c<>zGiyDnV%3Uz0IPom3c50*i1mJynFAM@T0 zF8G;y%T6-bbMsdAOvDY8v75dWtU)pcKkwqdP(WNEk+%h2W$0iYWIVEWvSbkPK~MA! z{}DE0(uLPn*Q#}LE`2p|;vM*OxOx8zKOb~0^+-F#z_egLPbXyr4w9ccVfKpM-I>IEQpejrdVEgxp~vTVZ}_htOh{DR z{jRywoNB4Wkjr4L@1m@e`epuJp+7Qz(x1@$`DwE~Sobvg(9Cyp&YmcgRH&ItPb=*Y$E%XswGwT}9`zn3~ z;J@tu-sHKs)AXZYuJY?0(Xk_vhP#%?p58IAa#YFuaapovmvcX*A9%`|CG$URlIom+ zj;NHj)1OctH}TOjM4l{m^(JO1&mv^E(qh#sG}6LcqBFuT>LPeioL6EN{P`Q!z4R(U z&VD3v50&N>xFe5YZ?N!LU@vnh^doJGughx2v^@;|WhVR+JJd*3Si@L^h9qAld*mj@ zEVLOqSEQiPKl@ItkFm#WmH9bCyD{!3C>x?3nfr;npKtCr2HSSxqa^dL@Q)JQwD9lq zbMIrWUZ=jZX*tGSM?W0YW%A)Esa z?fW0zhwx2gg+l&;U!jk6r3<*pnkD;iS;u5e68p2*6*ePFp7v*gKfX;JvbH(~25N;b z3s3$vX&13Bifr&0_5K)s`!N5v(9Ul7ZeDTt*W8rVWJ*uOYYB5nC&h z_cgRFbn_tn>SbN5r!KP}Wig&BDI;s?Zoz5tweWo)eB_70GNqJ}wNzlrT^k9KSNN3Z zkmvKQn{VRR^&{zD*ta{w%Dv6EQtFd^*e$_62yQ+TEF*P^f5c6E^99?=qAhcb4I@3) zJdgCe0qh>dU;TyY+P-3V)_%s9j4%2ev#&AUZv^X}L)n?Z{$~aIaW84t1^X~Dm?nKN zaVyvl>E9l{2@K?$;HJRpnqaw0g7wR{iPR_jK*}VC)prBw!W+at`VP`%Z`lGK%igk+ z_s9A6Tly{EzszlkW&VH4SVWF_DN3z-j^}5CV-x*U%22hA^Ek0j2h&c|4dXoV-}u2% zes-`wR{W`0ZydZ`?B_B3fOYR<1EI_+8Q~fp>_8KcIg;{G%i-&bZA$h;rU;5|jYV}IWx@szN^OFLVsqYoUMKs#R*?;(qGI`hDR%qiC* zrzU!fsWUB>ecYsLm#*aO%pPov6XhNG{JF{3E_GW8+9_izVPwpgr%cVKG?DZzF z*K4)g_1AB5Zd^oOzkQN7ZOVY9cd!?4$KES3SXN`-N}!Kr#6>7e<(_9^i==a}YI%j& z~jDpYL?-7aaw+F?$kF{~nCV3^U$!N+}6F2VPWSqn}DF^ojzL#mE&G^G* z#N(55Qf?={nDq4u>M5JvkW;x9y?p)vPx+PsW;+s#N!qHW{maRd#y6>7r_M;~Qj{&v zle~#u7j>dTF-DW`Rb-RAt`$usL*m`4+~LtSJ3JSV-p;rrPV*|(5AmO^j@LZt#6gW5 zuHC%&hb6tpXr9aPqu&&V-xq89qVWTkrr@ViB5;ZUzSw=Iao(h4@_?lktWQ0h7j0SP z@T{dh{pgCOw4pKXu_>$#v}t@wj63Sr`KEmnxoufVZxrotcV4Hob366VqW)a!7uu2d ze%iI-$HTpK;N#Wcp>JG1@om1{%U=8kB_lQwM-hDqx})1K#_q*fzbbBu$(KJ4u00LY zJiCUM&4~uz%=9sXWKCj?Q=VLSbcEm`dM!COcH-U$_OG8mHx|OfY!eU1d=3w*k)I}m zhn3*lM>>Ch2zjHbQWjM ztDU$vbcV=^y>mVsT29=)^Wo5gqz4)YfkScNP&_!4fG#AF`bXlEKT1EDKahBC$j_pK zf)}wj=e(=teFYqta*Jc5&z|n}k47hnoGtu9VnH=O*^u2GiQQm6{!-9}%t3QPSx!1| z@r@neY3A3mY5-?k@w0wXcpKmCmD#>o@D}o5C%*YziOG@BkmJlzBdVd@X~WsmRQx!* zdz!+}gY)mK$ED~WwqAYh(zl__S#~vXG5a-rYnFalbd>uSA5PVOQ8A|X@!{x?CSAL9 z>nqpj)hTK85uZ-@tPTBJ8+zg91vby-E!sYjHQd}0*0W=V?n5ss{xDl47>{ACyA4`;I0(3hU~oVxT+Y+j-7Kgwc1(&W_TyYDt-nEea(jYBoHCA8Z!Rq6OA zE@978*fZ6|xyRoS3+GK@Hy}!;Vxn= zEd_o>@G)6ii&;x$&64$0=M3rtpIB?@OxE8bTSHD?47P0iQFQp+Nb*+>igjD->0Q0W z!uv?OOdEN6Jx$g|xn(`QlX{9+OH0;LmiCKSGmC@kX)^7}dDlD0Bj2i7JB#@xLpwUL zuOg{WQIF7&^i9^&>E!ic!xH~d;j4SX_+J6vB>wPNa8uI#@W@TvJLF(K5?I@gqtlLO zos%_G=J`1MRd`+i+t40hw&gJK(__$4!~cund+F3KyieB9bZM8hPHb8WO#^Ay zlLkNArmQuQeVVl<=5bGu_-dQqD$Tnix8*fwc#1hY;NqL?X++L%-U8nrjI9^F?TfM| zgsfevD!hdAgW9{Nat2PWz#8wYqM!vp0mCpkmi44yOrn>|C~+#-wJ5541i*Q)^o zIdj_jqNcB6Ej;)!_`w?2bEz__?r?0ZjEr^LfLH9`>wDDzcpGb~==HTK%8lkc2z2eA zs*L@V6COAe8axD@2@e$53lFS>_q_$Jbw{DcfdB1<*4lyhx%}^?9aHs}zJ&jc75;ZA z{I5+r&HoOb46Hx2bGq)E4lUE?4`_#jBipY4o`-fu=qdaQ|8tB!!~c5hLpe7)#Jiog zga__yx<=m)9}Mw88$2nz9pQmZv}49m4)VZm>JUEIC47)Rbu~qNjt|}sAME~+80&oZ zO;?85&tM;AFIUNY9GYhFL7g@aP2-Lq@XB{-p58pVAtw#LBjKTPjtIRPey`wMz0Iue z%FPmM;@_C>+nD!Tng3hBgF^V>T;kb%5kIuzE^O!SX$P`ZH}og|E5Z-c;D&4@aKnhpRYW zHU)lo0Dia$o|Xqs>j%e#7e>;qcZix8nE>rlf89&)Lg0TV^AO^N2W+Fwa;=!+DK-d zg1;N_;~($mSa6aPKoUnAi$2K*~A$YW$)dagA2 z(amDpc1#(tG?iHJA`g~_WH$J<$ZUOW;^$%J^_23xJ&bovq2A-bY(g0CDue&{4#2xu zA40rqK^UKE&bN6ur?T#@uryiIcawJe7x1na;#GbT?;6fG;axs(TF!|3!1-PLOkT!Z ziu^JHToitGpYV8ir<}Ds-#JS;f0@R){Zh^=C&alj<=i`K{k}9}+pH(8IRZbEV~;dx zW_|JAT8PcCecJO$b6k4Bo_!e*2h= zn3kl6(OoX>B~LPO=BgG*tom5*OdY*@#a)?V=fqBhzX36DwNV{0;(YX)E0mtC^-kVP`rv^LSZ@jC{yY{X+ z{gotqMCSE+E0XHkBhYE2DEv3))ycZ8zcR0`03Qx%V@9;9F96@94hQ$ri*82xAn_}k zDYt(Rx^MVQa|AMU4Dy{K#ti)vz99L0;H&H{-lBhIJJfI1OM7?5Da(g}x~stXiBK06qG z8_nLr=9y6~XB-EBPj)lbpx~m^=SSDG`4{xBLhd?udUHPnR~9~^*9nebhpa+=!B+pW z_^HUambNP5{V8J^V-_F0w=%Goe`eiZID=T((@F1T&TlaLm1x@DuH(4@{%pk>Of+K> z_O>!6c)6UzkUD(S(L0oNnXz zUeq`*vu6YA8gpd!n{{`~-?%sa$Jb{`oY9ZTBeXDOBIC0^qHkt=$WPm=+qf5=G1u*1 zuD^huAt_3|yqPh}SpQ$@-aJ0)>fZnVd}df?vOrdXaY;g4NN@ub!3Hx4S|Ml!q_tPA znLw9>fEu+bYMV*o5*R=SX>CJq6XEtYGa3~OO1fME)E31JQG2glNI-4EfD#l)P{{ZB z`g~?!h(WJy@8kFQ{V|W3`Fzgue!tIqJMZ&8=iIfJ^N8UA;k%Ktm&M@R6M;8)xNn{0 zu{~GqXN0yqL;Q=guATr^&B>x?{|$x)f^*k+<1Z+)1e-PyEhV`9> zF~R#FXJ7$G6b9g0ce?OLul(t4<}24;9+cij=dLLN<~x8lwmkbuj-3(l9(Q&t7HPwo z7Wz4GZXm{L!>RQ-``jqq3Ez%CuKA3Yb2fD5#>dxO4KG~4)m@~yj zhTh~nbKxzX+6pXD+@)uc(KPj?cVUYz$NSqrf07@)6P`7a6L?*EcD-PMw@(poI|swt z!_c$CfguT4l7T4&{!Zn55oe&{{;y6dAN|dspZHK=j#0IU`X%_b72Hk3IV=9foXcXc z2eAU!?rAD&t^BRz$r+lKH8Q2pOsH}b>nbPD#^n9G$bn#LX8&{^?YJJ{92WFUF3;w@ zja|s#%sb$HZ?#GO8o3?tmKF0WLdThd{O?PzlOrg3>paI9TsPO))`yKhJl$yh`!AK- zRy3&;UEGdLb*5giK71YfTP~0cp0QqM4?i)^SpP)bP3z;(A)#@e^=>zNO3+tv=qkbE zMqhn=&9&e^3p&h%9y6fJHH^#Gg3!sHSJIx^nudIbv19q@^!aY>Crhwx`Y_Jp9PH`! zoaI_M3cMD9D?$HWH+^c}JTKAHJCAsNWwNLD>hF{+OoaFLkQ2F;b0SL;6a1C5SMu-0 z*1M~T-9Cj+p}fp%%2M&o<3jr$@bna#N#T8uHD)G_YP?=&I%J_sv8A(L%`v*Qx7lbj z0~^_IwwwNDu)oCBTx72`Ql>NFBXmMHCu^A-Is5eDJ@;*2h)my5?YfqH59pf}%0xb- zt2yJQ%1~a>3w9q(PVQ)9Z2m&d&98Is`OeoY})RWWqlNpDC#-&fu{awNT> zHi&&M%sR!aTEO~r*6C(d6W?9N{nq_AUSP{=HLD)yS+tMM^wBie-1Z;p6Zt0hFnOrzY&ytmvWxI7arc3OMHUy-<9EMeIHrYUW0dm?LFjaC}W3R zU7_`)#vz7s%zNkaJD7WKaM3F6#Nl`JOt$`K#yo_6{s_L--e50tq{0HDyA664dRT{n zcICC`OLAhKics*lQUy)C-~ojIdiqpbT3bjWAVqtQF^S3|bG9D*-D6uUVDo^$3;9mjs! z)FSSJ%c@>Tvl zbo2X+th*q(9WQ*7Bl#`fjC`)xRD11xqck{4@ef z^nOyoBA=&;{3X|n<9?U-8^`^(W^>j&_M7O=!xwTEl=-Q%&vQQA|F-DzXp*u(c0hfQS>@)G6 z#w^N5?yI=J(&L%5#V{_}!g`Xcg)>=9c&pd5-$d_`YnZRR&>tAcmy5Ms#x%+cYRi)# z|DQ8UlYFz3GZTH+(Q!0mF=t2{+>;_(P@ZB;5DSS-T5msrrgMmk z(YMy%YeZ9MDgW!6&_j0^G!HqEPP2G^2XsoKt=plI&ItWJZEB37=Y+PQubXK{W27-@ zqU|W1i>WXDGZ6hwy!ZG-`d$RhkI?pB3aZCXrls!dZ33AW!R)?ME|Mw_YPU;FqnXJbY|g^L?Fnl1|^FW6lUt zf6e{+8_jt^1NAqtUwVa9 zGjFh)oGanzviLs-dHD&5Ux>=>PlX5mjx93yDPc}&N1UcU_e|9gP8pv|%Ko(5{mb&|1spG6VH)9>u$ulBtc9PSswnpQxN82caSCt>8z7DCJ-ZKtf`ucfH zUtaoB``le=>GR*TAARf%^vua{^bdgJw<0)3Va$(#@tjDzzo6ZLbd~s3JSm>@qC28I zHvzu96MgezctH9_Ixo!}u~m14Dxa_s`f)!}U|q30fIr(^2A{b&i$c7-k+VB&J9eTj z$$^gt(my&cSTuDbFKUlNLHdyW(KB@9vG&!r`be~7CoOa{xIJLe?6bPqL=Ox0 zXzWGzNR1#rRdeKa?A#9G_2|#Qx=lj^JLpS({9T;aC7a!W?Yh|2EHr zf3!d0Z_?*0+(#`soe%z3fd4 z$zcn;4Zm!N@TB@tS$BjtP43Vdh&S&!$yjOKLt`c0RJ%5BjtK3bFUvQI@TU6N0xr>h ztTCdmZ%4-H1@QTcbk4^xmD34C~0v>(vsIB*6dGwnLB0L(!|9@;= zjrza+dG*qh;NPEDQ%}OHZ${}6<<%%XBK?XU>N`6^j~eLllL78MNF_f^d~pC;+=*<+ zzp?TXzl0Auls*RXK~0{^XVX}rtJk^`bw3n-H-4zitH0h@ zSo5_gtnANOduPNKvtXSwuy5Bf!uIWs^^cEsvi{%x68*o`zyDfth0tSLz%%L#%4d)I zf*XKCzM$Z6QCB+uxV#?6dEJ+Q;eShmrY}K*AAO1bFBZRm_c4CiM9c{qY;@T7iM7?* zcU=1DUp{w-<|p7UpZi{(eH6dNT6L}g@W zWmg)G*z&qjelPr|d3DtPn=zlbY-Dcwd2KBEoHl+sKpV$m4D~h~(Run^k#>&xOypmD zQi;zrXAt|xY<;~h8eibqvypg0;BV|5{z6_O*>O|FZ5Z~2&c6Yye?_2-V zLstUpMtqR|XW&L@lzJF5KhzXqlTSTvEpP%NeO%eHdg8npTRZh^!`dbz0 zFLoZX=PySeYvSG)pJ7~V-I+pcSTK#!zLgo527bXlzF)MZ1a#Mm=Iy(%i=T3COYEHL zWU-p@hHn?6Oa6-~YFNwQ)Pvrtd^qUDCf@$Dlxx+8uRVR$pSDo7FDQ{%G3q zw*REv-*HxctnX&&BJ`DFFW7*sCihT$h-{a`Po40YH5WLh|7<-l7W|{|Yn)Uz>chpB zKL=V$Ch8;jM0KDm!PCn6UzIDtXhr|b77XMa_L}gS#&@Lf8z3fAHp&PnF4I3IbKhT& zX7o9g3vm3*dpG(=zFHi9V&5-WBmJc#D4%LK>zvAwQ*2cGsqA%n){t1E=wgjqYaV|= zN9g`{`C+NZzm==VI#LI5jJG21w$OI4uC(W(oYa*y%RD{O0X}3;cBCtfyUHKr{zv4| zx7dF6F3&zhHu|51&zV@j?Fb$qFDiN$jOby?X^OzI@fa+Z)34&6#pD1*?nv2e$8an?KE(*Qyq-yd$K^0C zkxbuwIqNO-7dwZ~Wenv%^q;pw(@6$-Cx1Fb{1Y7^8hImdH35Tkza{URZFyH2@lkBK zA5%^|bO;?0EAM7xzG{_?=pR32z4@NjsBDh-%h8P3I>KY49S?lwWbwd-#25zRy_3ZQ z|4JVZat8Qk9!Q=Ji>N=tV8>$NA4HB)@0+2VC@SzakR zq1%dhrty*QGI^e{;scAvBXDf`=C_BxzJN5u=!rE^xzwhyg-`5oJ`yB9y?3Vn#rU>60 zNwxhx>9Nt`WBk5YJr?Vu>`%4l15sZ<`maBa8N^AU%~%ike*e$py$(74ze@k(?EkUj zUis&vC)gC1^jA!b^~^la;Ud~of3da`Jvw0A^YFobV?M>!kH_x8t}_E^%&|nz1K_M! z|8bw0I6t(D#`(8Euc_GiHt3}}y8NMMpxFV&x}0ZQkZY~0h!6Bm^Sfr=S71}5gXfJx zzkEp@Toci=<_~1b zy;@<*j$r$UyiMS;d>~6Uokk#)7?8@XgMVH|-}IN8tKduVM*rtujXdvf^Vco0b#f7~ zN}lhEz=VE0HW!K7_=hU`t?@DD($mqoLwTewy7<@{J36W}~Phq%##_4t3$cu zu*Uuu-2O+6v{+qSMmj{5;=@5m20k!CK|Wz@Hc{;X&2a|eFO>RPR3I6F} zZe!Wy2>%F=TJZQO_ff?1#6a?sPFrQzl8urnXp}|_uU-_-E+#;El zUjaQep5m7s+=J|8k55@)5<-sA$dDtGyMpA(_^Hji2>A(*6sd)BYc0 z+J7z5es^RYC>Ppa3hlpWkM9O@c{h;9`yXk>+Mj;>!|fkC9r?PeO5^=K^pX43h>q}(W2)z6{4Y!1T@mE^oIFBx#yL?=eJ(I#(Cx>Q1XdQy)I@=oPcuZ?G_*Ti=d?@r~;rl0ej$+ym=I(4#Vc>~`g`TrBXJNNfJue}#P zQW@So%J)>BX@A-yD#x>Bd^dOZJ+J*{etwsS4EDkpy=jbhpY9%^Z{>R{*Zb~ygM;^u zGkVL5jn;~BgVu9yY49%c(7SSrgY(WdgLjT{@Ow(IVw5wed2ab_gL?RGEx*j1^hNuK zo5<@h+J^+b%e@%==QI@sM#|Iz*R}bH#>~&m-3flL znf3OVY)>$Y@#*5+l8UoDcavwoZK~-Gj~Q(QS8?X|B4Q~e?RB%b52I3a%zxMZP0Va| z7P)X8XR=53OixhsUUV*J`!naw%Q3d)v)91QdEeyVFN|lrU7Ytlt;n4}kMq8#J?;+0 z=e+pn8MG}~(wx5V-j+smVM;LT~N)Vad5{_14A&OGh~o6jA|uh56+ z;DSF_=RpT@(^Plq_Z7F^T#uf z)4VJD%hCL0!6q6iSC6x6#xUkrGv-m)@-8xhqRRsCI0anCP?j-W%h^t$a&l)&z$Jn6 za^~k47wNb5kh=KL-kKtOqa(B*k(spyvwlmL%0mZmQI|LYpk0`8fz;Un>cWYwznC2 z%p+%eK4X6~W1k;0_HV|}4Ijm#Ta;#^A9=RDqT5$a0uS?Dizl{{$GU(%$Iy2yPl)bU zL!;kPE-Dj>k1vejhirJBSXb-rM?LGyM;Vi}*Q5*n&|Z@Y^5vy-<}J7PmsAWhTA4q7 z26t=hMNe!*tSqSirSu<_37xqw9-fXrHXyvF6~D!!W!R!9AH2xk zv@fF;fERQWkXOza2t-yYrtn32H z&Q@9YO}M(T_ilKkKYv;Lcy5FrAAui<^{Tv!@&oZ>6wXEP^U3<&OWzmAz?laRo~-Nw z^hG{p^CEHqjK}1{^?7hmUUDP6BunlAWXGSc{BF+egzt2}QZaTw_(tmuM31OEjYdaH zt`i3li*_cU4~OMfo^GUwHq%cZIeuPILcYt43ndOf)0>gWTad~5$mf#$<2sp~V%iy= zuh^TtC+@TN4jv)CdpUCVV|=(yVt%oHT!#6BXBXnXjRa@;nQwCsgZ8$`hsecGvvc)z z*I8Qr+4zsz=hMz!aBSw9_MyJXnnUE{tF^OFtBbvc=)tOX?!4ZJZ`ST?n=C)!Fuqlj zv$3EH7*$SkshAG?R@SI~lXFMG-8{QznC^}B74+Yqb>!YF*!N<`M6mnM7@nbla@`wb z#kHL5!5G9|jDGieZ#DwoKHXUR_4{1cmJ}qV+>tcMKiUAk8c&sU`TkvXDc2c4%KQfR za=pF#+`#bmx|DD7d*m(W1x3Fi9zZZ=J31Du)k62bY ze`1S!nEzbLH{nYx;rlu8ns27W%TJwy&fddalxuhvWDX#H$;GG8nJMCh3jCA88rp-_ zOK9(vc@-0vxD)+8+Ol-uT=s3^bN8}eMt99jSc$!zYx@Xg_z0Cw&OTr~I-IAHkCjS( z`zef5X1;I{e!}BN^j%>NkH?p-8in0lJi=JJ#E9D(kIky+-ZAk-qi2HlgkYlsjo3uy zxU*MKr_-6RRdgByFW%4pm$2nsjOAYR?{xI{x$IMv4L_HCihgWKBKs7VoB}ViPjNc- z;7Rr=>V2ZS=aRkf`jhPO-aFiA)tRzOq2q4OEZ6`ox1iU8J$*^>l?x{3x#RuW;9in$ z_oaT$i}aJoU9n;2A=(ew64TGpNIwZjc+b5l3HU#5{G#djMe4tic|tRGqh(S;{to!B z6n*H%uW+LeOVEefyRip4;zzC3Ip*$)%8B^1I%^|PYQ|f4qs^rZeBC{bxr-Zrp#=Oi zev^5=nEiDL*s^KBD%~1}Mtk6yE@wh$0`C*dc>iAK80&5nKW&E0I6pbAz4F(z@7z!Q z#C|Yu5X|(obW&pev%suzsEB-v*R&UHQDjW(I74s|{HF1BU}scT`*ii4fF9IcDEfUB z^@c}&%Wf+#QP1b|96e|K-s8;3mt0zP(P@f<==UCYa4~v{bBVYI*cF<~dUYq`S8b$) zbN=2pJ5O;2{3P+W;y2Q}v^mLzeN;c_H2-M!d*ti+AY)QokMfpXIL*@Uld@)DpOWwyfK~Iv-RO0_j|0xv_%D7J+=;*%=C^$D_(8_Pw|Mso z&-G3;({HUSM$3!Og=ZpV)%Ht~wqAnXI%7-Us&k0nU$mEb67i}6bQijSxwmQMqLmWE z=t16#u$k3^lGf12>~bS15a+vpmV2yce}&=Rs_}|ro~u2Z)5b1p$m013_T_D6zi7Sl zl&x7chGbiVAg8GW~WVL$j{9vbsD$5oznOY zdFC04Ynt#DvYb|C_}F8Rl@DUa>DyDa_H}$e6|ugtRvpH&dWex1UDs%HmcBicx}tw9 z?b8fjtBds{oq?W%9N6?v3!yu#{AYAbYit`)`j!Bj^iiP;nZk#5u@6uA|Jr+|wj@(} z*T#3e#|)G}hdqqxg2BZ-8zOr{nNJ3^*RzoQbec-kc<;>k|cmhA>S;dZB z$!+hr7G_=ROwXG2E4Pt0P`h)Hm;UVvo>jCNmuC7$e_FKNjXp2oUR}@9>K5j*+j16F zw`7}*1x0*v2iF!95B_-pdF$&$Gr=>Ld5OEp6BKT7PZkHO;T_3%DeZRg?J^f-LZ=BT z>jmBf@SO(yOIxdhJFux?%4{&>!n^sNXD;SrT%>vQ64rCW?2#*E&+u;OPy_5b7a&Rl zwNVHS+D<})k~r+`7xWd!-kp>6)eN8iqcq87j^$#F+KrwbaLn7FadgZdDEgO-`6u8T zKR~-D8*>MAb1{$7c;_435y;q=5UcW-I|_36XzZuM`{(kBj{lek%$}{H*LGBXx!4)2T7Izs&ctN~-Y<$epo@EyA$A>qK zEjip)Q*yW%p48ZApJdpQ19zBph}rUY`tyLHdDj4P@Lc?H9#{q*OX+6^y!Ih*Z4mF} zKAfrlyU>qY(e+VYP@m#E^;<-r{|qmzmi$ATlk$Quh8G6^Yk1-Q1bAV9c2C9&wb0s~ zWXu0*uQRub25%G}bQ_$vq5b4JgV6)%B8`3jwHiw|X0t9vj6^z?I?LbPZ~F@y z(YMmiTH}tc(KM8uJg-&tqwOVfUU?aQfJwV*^R-BuHMALBGw)#Uc(h%?hOd5`To4f7xw!`=Xy?nGErl*uM|E`PV#5k1`>C-C{07KO|Pl z#%A%(#yS3&{g?{g!qp4T{|K&5^n=FDmCm{B>=V*nN^9)Mb2k|`2jfRhUS#Y>GIklq z#*R2eWb8&Vc6x4)-LBZNL*FfeN9XbR|NGb_z=y1<_a=f<1^6uHcOzpMen>jg9sVHK zpQ-HWRbt)^~Q?$Tp+p#Gm6(_^xT96ta4J+>?+ z|BGbtUp$_1&@`SlUBvnlu$76jk(`a7n8I%SYRNk5kKwo(`ThjvX7Mw|_|L=tPB0Su ziAJJdw(A-`&+w_@v%?%7es=8QhAc;N_?fZ(hRU(a8?GL^v_a2r=Dk5oZYlpu#x7}i zn*Sd3cb_>d{G?TOX@d{l?iu8p?A5o+H`$}_1mEPigv)#4fi((aHDh@)_(lVt8~9v? z&Q0cg5n=Bmtk`8D{nQNW)ah>-l`%x6z;KYaW`&e@~KcvUI{#oJA5x`EuS( z&*nMhN}!u3%{N)FOvk@5csA8A1AC!c+}PR%=?~9H-{c~myJntG%m`dg^G(hH=V=p( zQ+FD55_mX^I>k|J1R46#(DnAJe7lesra^PZU( z`18~@G`os&3;0;LfMc{xALUCZZ?y?L36VBcPGhQe)UL{@y=>^-zb(P0_vg}=+O^sO zch-pUU4%zs*bN>Z(pEO*rqQ2t>k_rc_z4G%{|3f-FKsPIPV`sOx0kYE+G}AgPw-?z z?+)x|UUIzOH8bCjz31#ocesdWw(YRzPY%l_{x0}w%pRvc=g4h+#K`1>O*sR<3%^k| ztPuO?MxWL(FP@(XjnEr$oDp{OrSL>P{7=5N`rZTVw(f^lMKV!RiONc7(VEpO#s zy@m<+GF-KJMX$QS_Tj z-FE)JKK79Y-(XKmmXQ)32MpQ3@PSqL;f7Mf8QuvTyP#dE!w8QX;qZHaqkX8+V#Ik` zMvZX#=K;?chvE0oUcMvFUqzc!kd3SOZjX1ie28zb(e$(wQhx^W*JwJ!drY_g8ep4o z;Uf(t;}!#-5$^@oM82G%&r*O*3&%GH`?OOe~t zQsZ{Cl<~jDbhap-ycGIM_nvRKwran$)NXcroA&Pzb?z)D~c9>vs~Lwm7xMv<3* zf5*Hkk$amHLd50LcJg2PLONomqcHd(K6fQ^uRi)=K0CXcK5um-``KXXHyxf9kE67Q z*iFk!a1wsC_{W|h)h&Jai5_xub`cBo3^7_1bC_j1{l$)N^%OcB;o@3j+a_`_JVSoi zGS_EptM*ZTkkQgXeZerxaQgqooLP7@!oOO3tOZ`3tvMXMxexy{9(d=V58vZ=HZV5< z^DLtPUs)i}P}_(*rpfaki$Ha6@W|8RrBI0~-Uk=GJv-HrUd znx5>x&4CP?4!>`3&66#(t#bR2<-GJHzvw*rNZ-0Gj2E_cLN#MHjJ_AZXHD?_E42N= z_+^~UljtuohtY=7qB8QcMpLJh-y4y6WSV@VV&q)@<7Uf$bVht*?YZh;Oyu|IuII|v zjuLB052bIjb2(3@8|u#atZvZV;|oHb;QXf)C$Q)I(h2jQ`f53t%4L^g7dY0R2#`ZMQ^81A2s?x@Bm+?u?!;hK&3nvQt?n&f2--WNT=D)_b3 z!MRt|$ufqAm;cTal&ohj_5_n0b(y6+Q-9J8UifQ2za2}8gZs_p(g#)FPF{j-HL8+U zc!IYiFK#GFzQ4hr>~GM!CCPz?F)99rYGZJ?9U6C2x70`qr?h?5>cjDq%Y$R-Lo(TyT^k1MFj(rM{#;QXI=OlhOpIQS}Bmv;h0CG>1RgNb#pE^90XY?g`5FH=?Wl zqs9~5NxcogyBYl6<9D&~fCazWQ<+^FBYw|v_%b~KavEs&HQGHZWf}U8^{1bDg6E_> z(vXz$K*Ouxp!e!C`6W+qhcU?V!zmZuLmMxfOERCLPxNioe8cUZW6{7*JkZvs8jBrf zvQ?*jsM+EH&v}*)&w5|Hqotj4q2xyz;#2T7;5q3c>9n$lPLtj{nNHLBeqMO|f2-4q znP2zUX`+SKFnVWhx-3{+o7EGm)7pr+?n5_JIg-$6NtSLXuKi)q(rKH>|3L;tN9iol z+%Rf0jiY@R9n*36y~XIccO!bP37s&D^X9Htb{};a2dnPju*T>+jL{Tyz%M=-Izf65 zy|Jwkz2`+|l%X>O=WWIyzt#_8_1-FQ>aX{F@WT7(CSvrxDZjrg=&gcOL1x$cX4cC%%>gUHBfl@LhDF`ZIvl z3y$i4=uhsWjUnOp!L8IWH2inI|7bjvIRw0}1V;ni_zO!n4h{=fXx*at$+pkYjb3C| zdU4hZg3p#&crz@yN^%4;+j!2HRE?utF*3L-xv}Bh)Q2rzEWy4=k5)o|TgL2q(v_-P zh3=HjRDHwYdGw9sMGexM#n9!|bi<#=J7lt@le-N6FLda$+%J_)K5u^=n)#}&Lw}Dh zRX*~k@yboH_mKBZNjc_U)tzbU(CFS!%f}*jbTexmmGIzvcrlI`-h#97`|y3L@uB0H zS0uQ7v*aU+Ch`S8xH`0Q0e<7~uZ31Vg3S?KZiWBHQNIm;F_CX%(z1(=amyMu5LZ2q zyi^mP>O=e)`BEAa*`jvJT{!NMhAH%aIrSWTwv7!mWE*Xhr9V8t((d$4c2M^N>Rlta z&@b88>#_I@`yzgq^z!hM&`K|GU5HJdH#M}hGi8Rz6RzWx*wu{dEGO(F+;FQE>)7i5YgtFRE=TFrlq{ zx)f-gLz!{B??xXecSHK%4e6L+oYe|nwDEfsK84zrzdUx1v8`|pepM~Bs&(`{PMgZ5 zP4*hwr0dQH2kC6_>ook5P2knSvzr~m{KMhrefD~3S`p=i+gJnKe7*v$!zaxF_cHuP z>KgcmRnU1X_z4E-Jp;HqoGBqEaE}5%53tV!zbU|P=|B8B!`D`jL%C}3%cfih{S{Hp zLz(^PzIJq<@XQ8J6TLSo!S-Pt{N}FH+1b<`MZHqW0C<<3pSV&!;dJo55FVV#M|$rzz9m!AV;91AYw+1R#xJ$x&48YH;Itoq zuN_`2;oimRw37mj_u(HV1H^Ud2|mYz!sEk@s2dhR6i$U1Um`_ChtbjJ)3gpU9UC6Svay`MfX1uUU*Q z`858I{K(1?ap=S0{+IBVhSUCS$YmUD%4hd7HdA-H7QFP7AHU0 za9i^72E9KA{Icn9zU4d26nxGm@O%Z_#*Ikvv+m+Qe?*c$3Es&??0V`EXTL z(lY$d+J;#vb(W94iF!%A(|n(~cdrvTEWa6FSAAEr);@Z6 zGUncoThE*)%%L^!_IybnwYHy+s`YTqd1p>Ff-?;BggJLMIxXs-7Nf7T@rB!2gGfoZ zsb>mvSPwqcOvYyhW2N|qW89R(M(vct_;K1(Yt6%Fk)NhHt;+7iPINHGW33^>%ll&d zvQo1imKl#Q%($qnj;WaQzPY`#-_;mlo}YhCden z$e+z!0nS6iQ^56XaGguJ?|`dveoi*0zYu=i#>bk|!=Lh_nCDn}e3#X(5uU|+FP~kx zbCCey~v_`RpzbpF4Vjq9^Q$cD}T2Le^s(G79aLS zeAp7kq7vV?#;kc#F@kJ(+|4}0fDTjtk9^ra?pYYfm;K&W+m}s#MLC$pHaB?$%Ii;n z{`fc{t;M}Ytf8I!&o*pJcx++tD*n6p>}Bj4i90qCuX*AWa<`$C6&D~jwUeK zJmtyLMccm>>?KEH2YUBI>fr-nr+t%)nO|nZ8^aj0*I3`JCgxm=-EJFP-2y$k+jyVD znx*E_s`E7c{*`eBR%>5v6y8MOC8lTjRQQLX_zB#raH$!*Is&Wu)cS`HpDLV;PG>Cl z0P_dOU``Cxj{VwUU(MGJE4I|mn4pWod($Zc{2v0l+Vud>M&wbsXW8)nhrnnIvFBJx z2C+{*CNZ{g!`SbH4=cT1jgPTkdY%80$64^G%IaeDt8ACu$VSsk8Cg!u~|@ zW}MpKxjW6}$0usp&De-v#4EYzJ^7i1+|fAwK=rTkF060RI?X8h_*!rE_6){u5Bei6 zEzX~q7VrNWxIWECaeW6gSKTK3hc$_wUO)8}gP+0gr|=(^QQyOOtG#QkFoWbchaTtu zGl@p;C}iEcp`Ac17BsUC)h$9sZaB%b7T|k+?B1wqx@z(Yl!3of@73c&rtrV zNO{d=8}U69s~Jn&r~7c-ec@3DQ$K*>%&Q#aSoyHsaJW2i6O{2H1 z##itZb#LLf+A8C}V$Kd=e+AlZ;MpA7UpAOAADrkPcK6LaiFYsP*+Hyt6>X9e98iBx z^y_am?;^NcvipBM{;Oif|0!_3g^zf&l8&I%3n|zS(OyqhJ;v@xY8Lj`P5i(>|XEoY3(P;sNySF}aWEv(LyqXCFxB z#Ro=t7xM$(WV1Y2@Og5t98k&quq6E8q^{p{l1E5dQJeJ;-7?>WhD7(|}K zVD^{{;l2swW|*PE`;`l7go+s}%{7##`tdbirQ8*iznr}Qk^N4a&A`>YeHq-nl26|K zMF%)P{}6uF$kTkY<{q80z1m@R&zUiDWz88*|0?F|_4uNB$V~~KEbvmyGYh^wSw6)LkHxw$C?&bv!4rRpxp4J}t^s^$jU% zQ65SAP@^R&o%tf|vbQAkDt6Ss-e`<+&a7z&Gtb5LZ1W5*Z1D`<_+%-vCp#qjqFm7> z*rgI|A8SF|4Cqk_tg_eQam6l%(e^dGdxrKm0#BC1&3wVIeCCaOJ7_0okP*;cltLpl zyeh%Q>!%~+ALL-W(^yZ)V9jL&XY!|qE@eE0+bHl6ZnuMfEPgpoqvZ$WTpS|L`D5-; zsD6<9gULNpo~h_1pJN%c;^6VXBhc#M2(3m9K&u?!Q=YJBrThlbYHYg8KObE7M(C0a ztxDlh*TwY>d+;q;U)tt{R$geO^@;2lTFKAQni({+Xf+>NeSrLkR*KbJK-;(SPX1yC z@RTy2GKo8iZlakJTA{ZsTIoLfV*DZDv}lCx8Pz%ASt01L)uPV;_^hCua1u>+k%Rx^ zuD*-ENESXig8#w1-9Gok5&VR6rw)H_5Hd3u*@>Q?b)t;PrW|kgO6uq=_ODXs3gk?= z`JC_KbOi2+$l2UePLwlpoCE!3Z44{?L3R6P?gq1K;G}OLowXM(CnEA&Aw1h857a$JY-4h zd7_zoeZ{#;@sEAbOmiw9G}AiXhY|WTBC9SlB|@|0&{*VHG&`SmqBOhExuij|sI{;f zXoei)|D_+))eHgZ-izp=H{ZAt>nf>$@ zCvtaknN=>LEwj4EM0W8?Wc#?hetJuEFR&xI?rhK;NklIrIi8EoW;7#levl!#2mzLSl737HJj zm;5Qoz$S7`+K7#A;rUDG?|A7|bYwer!@$Sg9gh!+O>B3zP2L(uES*?n96pwTzq^H) z?JKM;yflJ2d+o-87issF9CO>v8`+mmy_Ys=u1ZWiJ9Fi9`kP7HvYD2>EF&Kt+iB2u z0{wbweqL}@O;(TRfsF;0jpe=UXf*aJKC^6T8Md?xTgrRi0Jije`kKKz%?Tv`uhC~4 zcnk-213Wb6YU2NE;LrvRwX`eSsymw`vvY@7`K-*9^TGc%+Lhh4 zZ0;QHNW%VR)9-oo>xJG6ptl!#TXq_n$rfvEA{%_7^lpLP&^oXrLht_ecsPAk@$L=! zlkU*^PCIy97-{Eq{muq_rGPX6z_>9ozBtLJy_zOR5O(v zQQzcz+Q_DjmuPc0G>_6}0&wNiZZ_@8Ugz%Wn|<+nlK(H3(YjCn?`-zS!?tfch5No^ zedB>_b}ccGCEz~QB_2q_NA>hhLkHyZzmc_q@I#s(AI}vX!@GY)uINCv{Mv{uAAjIP zTmC$Ed&-uFfpa(Toy&Nf&v;9JSh0Ed#)`}He+qtNzRS2ocF*=BZTlpfzQ(kD?k}`Y z^x>zkc5IYlpt2z;*qXhpwaMnl4#_6%V{NJ!9UwdQFz_q~t|n|*DYk6R*jb0YHM0)O zmMI1;%4yBDlzC?n zxXl?mllEsGHVxbNRL;ZPNc-B4E&QYNj;LSrG&G3XXvYA)r->~*Ht&E2QGZ%Aknh2v zx6r`$pGAAim(ltoG?4#uGq7u&MKss}&x{&ZLcF)+u;ptn3wQZRIegEFv@aT1`jmLRXz|bZ zKpFjfpgF*LMnr%7GycNr4*dIF|2uzSJ!cBb$3JrKuh|!G;ww1t)t&eNgP21Pw$8Ha zb0%z4eu3uE%H!MwoQ>QcoO_8e`^QI%wl`j8bpPpt`?goGU!42?xT{b0k-NuIwU;qH z#2sgkdC7}6of*uV9f244AIHAfQuago9V51GCWm1{$BaDmc zn1eSun1f$6a%EYjJqIs`pZ*d5!i#@Vo|5L*96ZiQwfqa^4hrshz!&@Od3dIaek$k9 z>nUXZJqKUai%u__x4WQ=@(D%8w#r)JZTlDE;dbn38TQ6IFI147xUpa!_1n?;qTPAi zk0_qbuGm{3pZ@zfCe^)iWQIt;0LPEIKusY2n&zVn4JY+!jUV z-YXpLt!48%3k3UdyqXJcPjUN256AAl+2f&y_)GWOB2%oBUxA!mj=W7l?#QjYMY)xK zXih~Y8_%%MoSBDSs)To3 zQ^iLy*=Nb8QG6jpzJT@xXdi%{--3+ZN|{&qzaP4`^IQIA2WuEh#x864fHe&dw5jEz zJS=-(ft`n-Tzkz&3k~whXP)OD1}xn($NH7S-(?PCy#gP|bopgppXQUoM{Cz}X=?{# z+m1~%>6dlb@UD2XW!$)$hGcSvnyIh%C6xUDU7+@Yw4F_R!)fDHGs7?bjsuUWo(4Xi#ll|$Xf)|{0rl`v8M`w6|+hkwIDAr@O*6-lA;{4f0+hh(P z3mEj>h|ipD)D&zK?9|nI+K;dil7GP|UGOCMBqdZtx@eUdev< zL-hL-_SKJEJuGnVy;t_$h`dbT)9diHmb1?z%N(h=?(7wHr-YunwlMhc%h59jPO;A% z__Xh$Ce9p);x#3LlL2pRwRGah(1XyCy^$Gp+Q07|j{lHl=}FNcn|HR}w9XX@>5fI6 z*>J<#M;{X%Hd9Z2i6_?xOzyJx`QJ$&OSwzni_ZM{Jldb~PtrcP`5&kKgny9s7k&Zl z--RE0K8_J-Qb)47yCO$&66F@jr`E}4}FPMs|+w`rZL zbG*^oJQo|W+6b_YK1+9<_iZ;Oz5HMHS>s#K4?hBjb&lbIdUQuQ>rNxE6_0biN&8&s z3?p#GOwNotmGdW^%<(zvL*<&m$sKdPd@iysJhhHo0Zg5&3v|+F*J|+0F#_M+VXV;J z>>oKeL&rM*;wEtW7QW4mL2$L!Wji^8r}-rBqO`de+Ng~>c=lxXT#HU_lQXWM^|VCe zq7AnjlbW5zBd#Jd;5xFSo-laKF^r}+j8xzLtZe67`ds(=SKc}}&)ah+ zxcrJb<>V{xK$n)GYppiU^0a0RHpXV7TfsH?3}~h2;^Vb^uh@BRApPI3=;hwHH6G>! zk8_Wl_Pt2=KF9BW-)V3Tbb8pz?MBxfoHxEFwx4!v<|^7DXG;1mU%Ys~RmZ5R0fsw= z*=LBT&h5*|*NwO7DgNi4`e1pnr>AeOF{yrtvCe{L6*)8Pe^ zRiJ+rpW^(2D(@y=LD{C7g7RX6=SFKgKF2_JR*PRPI+WS)nT)OY@i&5zJ6FoU>2YAo z0e%m0;OJc|xs2Dz+S|cd!q)g^Ck330Z`18Y;Pow>tD-UbXy^p(@3P)|#|L(Cwz_Pq za1hPKi^9Q24s#88!}ah+6aAB08@T;_Tle&!GgP)6UjA>MMc-FO-tXi6Q2IS`?{9*i ze$T}B<}BaqoXFjv$XqvWTL*HYIpt__Ek>i0^6-11+qP38F8rl>U&1`X8j)gq<>uZ zc^>VeT(B;)2iyHR2_4T&`sgbGojc^B{f(xvFK5enfu^~{t{j6}Ryp-OxTW1OgwIeu zE)6v%=WJ<@C%*nyeEnP_&|F#& z?3}CI^OXf7H(c4f?t7O7*OnFro99jreyg7IuQ^NI6?4C<^omP*C32^0bKPk@&A=s{ z5_Fz6amL?teykCAk^KWv_@jMT@Z(de?Px#QoOk3w$E8D?RdqM?lp3z?(a6=7Gw_k1 zjqIAvL~647JbusY7r}8paP&Fjn7>^P?@WP*n7e&XbGH|)eR23OkIZdPiuf>&ki&yt z4lnmPo#=U2@X&PKryU2+#lv%sfSa?>B-7$s#aZB!NmJ2jTZR}HX`j7|-<%~i;rr;P zW@KP0bi0#z;EN8=B>3=>3S#Eyscv}n5m!x8VE53*%uS4M@lP%YxY8T(ajEyjWx;y* zU^in{QZqT=dh5#GU5w3v)h6S?`M#WKojuqSocw~ZVh87SY<}O`* zMsP9r97xCA1J6mXX?&$`oO#BI`_VCt=!I*ND za5Uq|T|~gXrj7HivpAh>MG|#a9+ zkTbA*)TaUOJka3Ddx-P>HCI!=>QC=<9!V62_FU{7{jUaR`5I1OC?~hNi}R#uW0GWE zI%YJvR3Be6ow3sTbqQlTm33>UWxEb=ZspfM>RWp-S94hOPxJ5qF_2mE`^i-ZjlSPl zaTjoAGw;y2c0$jSwbjWOGHw&Bc9N0;H8JgspdDbksP7BvcYZMR64@^7p!8G#+uWvd&Tn|T5qNU7vF#v!PpIznp53cCL-B&kg1dL| zJ-#s5iBA+yop^9bL$AoDFxG1&XYk*eGGGwzX#HPzKX-Bt>OAgao=TmmF8kb?&Rp?) z9P<-*=&EJ#aJ;8gx&%HR>t&rlXKc1_ZY$W0k3()8=N=iYMw_o7?ljJ|O%h*~K>KNo z!BqI_N_-;wtU}GRytS@C4>};~2fv4H?BG1OPRp)jaHlN1`cdDyxE#){6#cnt2|k-0 zAMmVqC2~%@hjR)Oz%j{O?@BXTMZ+oJqVomwBV$*xzi-`t!XKldmtDx=+)9AZ|zQa?DNwP0pspk6K8Tb`W z&IY1?gEJ9E0fY40Zejt?(6{v5<8{V1^{@LX)bIE|+WpCY*IZY9TH^!0^`ktK7BBa# z_aomcG*~p#`r zjcNUj+;2lXC8FQ+58Pmq%YvPh?~robADxQN;HvQUNDp?whs2n+HAByKC;t3gBe0!& zbagJC{wvn}8vi??1>-eoD*ihBSJlpVP`|1JJ6FTpJ;!7FZu#xT3J+zrU^`jQcqGT0 z3Qp;vRjIyNvVqFyTa0}O(oe~vD|?&g8k1DEi+RTB-2bC8Zpv)n`69~kK9EPbmnhSD zjuA|wjE6Fhw)Z_Y)^Dshng6Hp&f~rMUb;~>(Wc)=(CQmpI7zam$|- zPdSZVXOU;ppesC+!0VBm>~!Ea0uO%ESdop7=xFu?UFe-Wc+`6DNweNhq0Z&h>B3*W zm$DsB*3UT$GYoyDA2lYUkavyekY0PP`#sVZiLJP@mYrNzgHf-~0XRi-H$v zpF$jGA+L4>E3iYC5ChU2MmAME(q30r(Dmm*!2=Hs4&G(i-n7ulHty}lM`909x5b~? zZg8z{=vZw8%3tArPkd?k0Xt2+%=qrl#UER+F$eq1;A4?C7So1g47pf7O*RU>7{p#4 zvy+VPPTE^NS9UTjH0f;HRySdbVr@z}_<`#&yVHz4!-s>Xj1=9;aOhgu&*|v7X+daa z&;79Zv2DadZx{47XlKg??y!se{%PB1@8VemPuqK4U7?GoO>gbGJ!{+shr4no{0=Sl z6=$CpU>=qXf8~#59?JZSZ4kukc_*4?6)l()26rz$lz8#R4K^LnV`~?2AG*qD9#*U0 zoNXD^!^)Gnn6mQI75lp5dLyXVgw~9Fv~AGu`0Ym@i|uC{{V>N{CmNSK4Dsc@9LgE! zqGiBl_Pg7!3I0$#<6z7g3(>Y?pZ{t5(Tr?h(71U2+nytd=VIHxbAa}}v@bjLG;l@R z{XH;$fjvi8UFa&&yMr|*`A*Te-gh`R z;xY7^{5X^IAY=oS-^iL{uj-XE@2rRKI;|EvFX^s!Dyy8lJuB=wXV4Sf_N zs(IVPogqfB6J61nO8ujQyE{4GSuqmzd64f7)J31!aT8$NG4JAu)xWl5Cg`EHjOimT zbGS8kQoFR%+Ua!Vm*blqfS1J&>zr9ValmWEQl_YFBe4G}WG20s{d09^tQX#%Yb;;8>SIUa_0Q2(^N`QZL7It)SE$cOj74)H#aR;nY^-P;VvKFa z$L^%Rj|CGlEWDJ*HS+uLK6-8=@3HgCIcIxYA$BD?w;3IoV@2DI#%>P1gKh@qS@61I zu#7+R9Q*9JPh-xGQ<-U$@jmDY-oF()gl;TDUb^57>`-?HbAWvGj@l?~F{+w~p_Hz( z@8r?{-5HIUdFhK9IO}<9))hu~G38~`Mic8QVU8>Q)}0TP8;zOOXK}tYx#H*E7q=4M zyt}4oj9>e8{s+E|@`lUIvUqCRhkdijEzFQUKN;+TYake$#A5`5`X6)OODp@F@!<~c z-x=dF{pSLUaH%AReF-p?nF&vXS@UWHwk`1b)`LSY5p04x2mX8(7*5u|%AD*Sd+_$% z|N0q$cE)s9oz0_#ufyBWyLnIF#W!2H*)k)$6_uHvP-fBJJVDXpOvKzGSR4+SXmv*M5#8_nO#R#k%mra*^wyM(@*y`@o@ybN}tVO(!e& zLBDdRk}oyTy}GHi?O{Djx^9|_GYs$p#N(U^yr#oRzLpces?Jv+zn}~MqzJhb&%N?* z#tO=G$D`Zo(NWaTmpn#&jJ@1{j_l_vcF_MmUDcCKdIEm(S^6zf&#IFiDq^g5lHV!3 z4aVol?~dC_^dCHLhFZVQ*{AIBMgN3G{bZ)K??ih;<9HuS6P**`g3m?29AABd?%}#v zIuxHsdy5U?QnB<5QNNTu#l?{}JFo>;s7=d%i#ZQ-3Tu;*wiV+S{fcN`{p_TU`XSFF zl&QX;jbsWvdhti}J08B;;YeqV(q1FqH10t(7k!+ik(S*$BMLnDwW<=3S!8Ti_a{2|m=tA43zZ>De|iD_`Y)8=*&R zo6-G!QMs^aaMj2_EoUNS{sT0)6B>N*=F!IzMFZzxXfTZ2gyG~UBr#`A#y?2`=Tzw8 z42|?vGd>^7Q@$Jer^4KX{th$<|dxt<7(9CMNW0 z`Gco-8NB#wc>dh1ktw04uH#&x>?-Czfxc>I@I_*s#JlmM6aBj{7#!?8XGqY+8gAcL zjNaaRjn=+gN3icvV`1MIb5c{CxlOUX6pyjudFS;L`-o>4*p}wG*WVq?y?%Xb>h%+U zQ+It&L5(Yfj_jQZ&$VZGdV4C(;HCI!x_8e(+t$6C$X#hXF#&8>=NX>mqqFY*)hR`% zg_>)xoYZ>Dr9t7@1fH6U=5W78@SvxcpvBtJ-O6_+HUa#<1J}++Jqzny#zlowimKF) za+BLv7YFN!JFu>@AAJ(aMqk98eLp^(5gOz1EF|Vp*~A(8#BREC+KfOM_RdWVI&ZIM zc_DXFtfmb&c_2AH);ZV%=caDHapT>krn}P1eHHme>G9vRr{~#v-emvw;aBBTH^*?2 zOTak}*sQ6*dI;a5zR$b;F~+Wx`&JI)Gxh$`nDpst$NJ7vW8o)rok8XRvseF(^XeY_ zs!eZV2k%1jmlE-rzIRXXH_jV+dMb;8TMqVF`+*CU>@D4FG@q>84hs*~@-4xu--4vP^G{XMNhj$cl4C*;@ zjKJTp!_^MO!mdT1%tEisM8C{H&s>A9{2H;=!PZTH`9 zdnZ9X@s>yV+|GKV!Fr?C@4bw@Nq)a0Ip9E-$NG1In>gnL z&1K4qjY(yUive$&$ecB2fEKrdPuVJbDYZ*1U%VZ=7Or=(YktvpXJX&__hn{tKNNla zm9~6F&-2l;qOELwUF7VSgE42nXuch_CmXqU`D1*mLgq7m;P4XbmOs_-G3$rK)tZ?r zwvn5(h523?Ik|I)r__+MTN~$_96N8e?WE%0gW$6P?!976FUHp^z`xRZy69T(Oy|Cj zbaMAk3+euogZv%^?QNT!YVFq)&7`Z#QdpM(mb;)GGTwR$_hvLtIfK~a{j+o@W*51x zC%@ms-j_O?{h!{*{U$YEaKFhx;=($Q!o~e2JGkGZJaWIui?v^TzlrA!^cMG}Zu!jp zCL_R?JvQhIwXge4w$R@3`%Ol0zX^D7=E_v|YaYMfWW?4iGn4yG8o1wNgmu5kz1Z_Y z;}q+DlZhApw4rIrP3y~?BghFJ87ls*orls6Ep@lZ2p@vY zW;o{{_UV)A?Y|z}W8E*>f*v}2oe`W*JH6I88PDimlnUk@pLZ|H0meb=Lf_pS%~7)N zMKK0(FG>u2nGyJu>n5G;)qI9~QEtRHwC+W@&J)!54g|aBaNjzu70X^|@2S3PEFG%5 zPSUq3PEZze*U1q63wOzg;!TP%$uIt*J5M$`(ycpB<^o&v&Xc#0b;-B%&*2^R9B(|h zl<+(nv+OC2L+2ikS;~Jqz#QQqJg>SA)^`5DJ=2QO%75L!Zy$GQMdK}sY2WtiqmOMN z)_0(kdzIpiUd4hsnbV6_dDKyyQ~3&g#D=@Dhs~?ACXV)S-`RBq!JzZ>iOqFccXy&I zo1EshbM7ThGS|MRjB%Kq#hiL0_N)^5%9Dd|%MDq^M^kdPg#5A`kz=nSb@e zihJD2BmJ(SM&OcvGlKO)3@aWM^L$P8`6}`O7ol@TC%ZU)p_;XY)R60Z+a?i-FFvqj)^pIKYuHn_1?P=k??`6-4p~9o zUN^j%fqS+ewbndr*%%QzlmGGwqOy|B|Hr9s*))8+rn(z@q`UOL9Y0dzaO!VtA3;8u zp5JL0>qH~P7lmhZtcHMxo@;(0dUP#XmbCx;Bvw*+S{HUdie&6_j^aP&U)z2R|Q?ngZgsa!M^IaU}trFu)Qvs zvWkz62t9fpWycp=>*e0@#_WTunG3BR9_X4<+-p4W)t1gnihB?L-5LDkn&RG1uPyHF z+%YJa!I*Y_rMS2EI^Nw-%-*Anmd;&{U}s@*??ouiGl zf9BJbJ2m*+2TR`A{@#)|CM;>$zrE$D{Xbsz%KnM(-Lw58|I7O`YK*{ySN87z)m3}; zYfWmT)9gOPe%-dSkz2=2>qj|VPs~B?+MTCD-_uwZ&*Z$&to*Z?j}~5TbPsZQ4tr(0 zJf2B$%&oIp@W0J(9QK&k9)291+gW_mdfjR2Ig|J?yd8HYV^Le$(^cr{6;65BhPqtc z9pDa6<-6-E8mFw#1oXg`dACo5k1x6%Kd6+wcoK4Av8I8yzc=(O?hx^$oTUwZFx#bB**N)2tA9T_9(E7@blBsO<$ zHFxGNIz@hLB(8gjqgQpi>XNP8xyRlzR{S1%9)iadtDbruzJ}Gd%`YGEI~CcgCPuBX z*E{8%ZC^{jhl-#<>)I0Ql z=jP-|(*K9XJ9@h+QzmtecQ1^>|I3BIvih1yuG5W~S$}m_F+LW~n@=-VNM3xvBVVv= zu(3AVl0Vxnox*S99O<2rmMqC%dwD&&lXZXths<6VYY`uRV)h>BGj01sUJS9VH0im{ zI+}iDhD6O8IgJEx=*=3ygfH+3vJ)~Y5J5O zet^$Gj3SHt8}Sgit5X3 z&d}=YE26EU-Y(Wt4lvdu$@N$?Z}h|->dR-4pW4h_c-5?tj6U6%y&XRAahHs3w^Kt8 z0H^Ak&QyN~b;NhFLFq9(+kvh4f3&@Oc+}O^_x+n0ATtTa1PBB)2?0qaSfnU08k$K0 z5`q?mSP*R!pwR!cu7?ne5)za%mrGKb0EILX&t!hUv<&XGp= zdceU+U=p6{v7-bEtR7>j;rWF4WP#h*$1$IRB}OQISW@lcTMX|KV3o{iuPyLu?sQPT zQD^T{u|LNxYWyS#9*53>iSE$ZWPU?$=`Nu${07g#j5*#jsn(Fm`!>?z_)IMH_GoSl z;Uhd%K9Xap{9%qiZb@Tc4>V4&hWwK&XSPqRoav8S+!$EJ|J2Hv$85JYwrB7?k@W1! znWt}?4Q^OZfaw_RbU@R0fU$cz<49-z5Pt*RD&k{h>H#)rKFVjEg^de-@UNfMcx-aZ z%;Y3^XbV0G>xVUtOtxqrW$Fu}tLBTJ_9ZvJZJrxM*45d_A(a!oO40Sjuh&4YFOsL) zgGF)tTyGpJMxcF|qxK|o(bQoW#~azk=F_ZqVQlW`7sWluQpr2Xz%QZSR4|{!54xMK zcR^Dv!@Js`#A}&+=^Vvy3GG4_hqmP`&E#TjMbLyF))yS>)EIl+l^>Y1ZTm-{Zo!$mn@qXT}j7 zFv<6m*FM`a935MF{2t){k=aI4M1Q5-4g9Y|?|p&&yKD~)@O2pZVD1%wUuV3h=~L<3 zNnZ+|Nq77+daNB^HZT0&j@$@bVc*6b)vkK>b4lp!N!;VlXWqri)R1pH;>Lbn0NnD8 z(AhxbJ+5V}+4(Igrc8bZc!je)qM^x`k+Yz$n{W^J{U65O*LP-;-Q8ToNBU|OHjH_E zwqr-u-8}Y7T_3y3UAwe#C6`n zy7rpE5l_C)Y8(vvP)_yJ&Fcr9jMNb2?WXDc)=gh%X^_#l5?yA|i#okzQxzu@&X z*%huwCg}c?Xvf~&lkiq}i}upDQ!bV^fHdtra)IS*!bf$)m-EyjJN)D@{W#_Ooe3QB z{SM>X`x9WMzB7#P?sw^H^Nzuuu>1sgTPB#%kN0dTn*ra3`+I}gM(8j1&j~5+dHX?~ zXC^tVM#F;Y=OeZn=>|jTdj#V?#5{<}+ob(5TgnLYzE>KtCnbk8M~5=59>x`z%$X(j z)Ku;??xdXnw$3u&Z47hcvo{x;MbuxiI^r*> z_AZq!ItXW$$`Pi0_RYUU@OB(tu?ugui0{@pA8h1~aP%9-2~RoQ)8xyW>Qwm)@Cgou zQ{gW>HG)IoN_b4O^t_PP&lnQJXE38bXYuHO(yPZ9&He=AQ_&(u1L64`j)zt(c)b^n zU!dQ^ArJ^T3n*mGQSiFVK1`d>G(Dd?7ti_Y4}U zXL=iU8f`%XezpwtG(7zkFFd?EeU{Br7iYA=|82$V@G${*UFby(>mOntYx)!T)|007 zU>|mhzPPT4;`%3yq0xN1;0xjkRL4jBKbI?pn9a$(FsE!#?@Tf_AF>v>*uzZ=;9tX> z+nv>Vna7I_M7AZ_G|)$zOEtf#yP0_ru9iRkrSDUlc4qAA5j}V~vE-7RN6;V8i<&dh z3$(Tb>8FAAg(Jzk2HH<&AF6mRfdYK(uXQ)S{AG8uKb2TEcfyaBAn)am$_R@-{Y#u(Qe`d6N-u)i_=(6Kxhy7Tly_|i9|82b(_f%o;j0GkcpZw@GM zZHqUiaZlq3`_5G^`EQo8PYgJWYex0q$s52c_U7m6u#?Ho!kRrL?KWbtV*|8LnPd9K zb034TCOfb19~omA_Xyg7H;u6!`LpIivU3UTigxy$*{ga6d)``80pja7Bk!TZ$k@0G*;2|+L&X}?^-YC|7qHd?H?Mm86!%p!C!{%{h&(arJ5VU3ihac!7B>d7*h##d(6psNV%mARh9y?=Du(fgHL*k0gnn#l7s(Ze0^c{r$u0$ zM?cbsW#>>G`-2tE_T?7zg=D7{oxTJ9+s_uAZAS*5x(%5x{$f3R*BRk2$krOB==#if&cFtz*U^V;4U^l_fC5WF(`@!Gk z)a6&?)DzQHxDHZJJTCs#nPiMdmg#qtH+Jy*6uv}bg$sD;E!Bzge+Twg@zNVgi_6}$$o4w$EmNsK>+hkV;Zm-g! za1-~exEr|r(7A#+Cx7f$xlb~h{~^}<9PC$;BeJDe*vvbB7s*}L({r*r4S5igjV18U zmB7)B4SO*>R3S~y^$C2Sx{RV*UxCHB8uayxu5h}um4x^|1|&6 zE1$@!pXiaV7W)S7n_TNTC>zN;{XKUu=YLUgIeRQ?+dJS=^Qb>D1HOulK86n~_?QAd zB6H8$_I=>4xWsyfefS*BDv_lcb8_Vb(|w_ zC9P0;9Q&GjXkSJ>hvX#sS@1Ta`B18{w>94#`YkpC|8k3G2YlH99`g3EW;-sXZSOI* z^PcLhk|EoS^6Ay1e66&9tMncCdhgNjmND>@@vpeavG%1y#o9N3cR%`8oh|w7wcr6< zUy^t2QNCS@{HiNh)D*#KvM(kFjsT~4p^|aN*0v?!>7w<5a{}?pQ23#F`CVBr*cSEl zu5)9rIx3g7#*)aH|0*}WbegMP`Vsx_<6CQfWef6vwqiW8R{IkCVSA9#&}521f8t}# z1Dxxv^0oO|Qzm1qqu)Bt{2w0(jKoh!&rJ0gV~s>B{*m8mQM~BLdHI+AD&2ULllbW8 z`Y5H3h4kTvhmE?45u1e3&tqY(%)$1f7jNdC%n$ zUNd&N(}6r@<$$Qr;|A$C!c1cq`r0&b`T2V86Fl|5woe zPWoR;+lKMc>*>$T35|e0ngjE}zjTu_e1?RdtImc^`L4NeO!tm#n!_V~M-0|y?i$oF z24wqjd&`am)vOy!rQ4q^J!*HfEIG`2>t(GCOdOacnAM(ihEJfE(cjqj5%3D`mA_)X z!(Wo)U;o`_ThheFT9}lnt1tfY@F`sIMv}G)%PhgkvfOTH~Hu1X`Tb%umHb>H%QCCW*g(lKT%e3zGASy2kheQ z`QX+pQ+k=_CC+k^dcp8}>bxG|Yj|w1=OF*%B0Ol`D|(YQa`+E@KZ^1obdT`hQ?!+a z9jSqNs{J`KXG$G}lOr3~%vdWY#NxqwyOL`ASz5}0N zx4u-o#rv_-xeRx+d~%kc7t0q-x-zz-CuF~CMlZN;0J;u+b6>@oM4N(%b+$N@-=o-b z7n=HWwq7qBr8*0udFTS{AJ`Kb#?>36Y3P2YJ|X>(I@i=8 zI|cW~NWahFt5hsIQz7<@zY-&2Op?9U$Y^b_55xY-{amee*dq6(+QxLrFN*!wYolJB z6Z$cJ>ka55qv0Fy!rd2R_>bvRcE}hn`GJS|Xv(Oq&|w~Bgs`;?K4F6&dq9kFqkFyBR)%PCXR zr%YNzKR3&KlQJVH(}I513O-%|uL~(ZvKJmI={LfEBYn3(%hBLVcKKrFeIj+|^l9I6 zuKWm}Y!<}8D%O0{{%4tfGe`7$IWn-6d9s>5nS=QM%|=G(jOcufNX3VgJ+pLxyLkoW zV(n}D;b;KoQ(a?;{Of3{!ie)-rzg|kK97sgV?w(XIz@o z>5NNx8tdq2+Sp!c!=q`*z0!t6(-M27#YfWy^-Aj>O|z3G+^Qc7dc4-<|9&mJMY4G` z>ule=E#$?Kmw1W1C&&}5ADDA8$a{9R5$Y)!0$qkemtoMwX40jDvjNpTX4AXf;tz|{PkcnbNek|I*ldkI z-W{QbU-U>q?qf>}Cb*kL8}7`M^Nx_tGRsSmKe@~|x4ucYAXDv@(O;ehA1#j3=sw1H zI!Yt0PZ3-tcVYiCag~~Fl#hfz+4B^OZxp9U{1S;nG~6kE*$=IZ%BmcL_ka!Fx3VLv zpWk_%_s%HI(C3O94wf_t?<;l~Sz0?c@H?4r=P={u5@4*NOmIlW0>@>erV_)$sdM!h zEUm~b@#puUm1vznUhe2f?65$1yLr}QziiY(vwe8OLH}hdvzj72dNa6|Y`KYfpgR{g za(A+y$CNEm8J`}VQ?aosM$bFFXsY=req&$pgwZckd!r}vORw-OHC15m0@Z7 zD>gFC!(og&ll0zq8*YGRqAT<0xpux=;Q4jwSmeW6HsIuQ3m>D-8D7*UkwI zhc5Q$UeyAg#D5m}_fqv^eE1&yp91Fa!N@(**xfzBkZuh6atCu=GDkM_Ft*C3C{NYV z{t0+Vwl4#kb6%vg$|Hkf78v6r{4;k)!>zA6VNxo+V+gRgXKXV^FwY-(});WsU?8yDh;oSYg zPZ}FYv3#rJzry|f^!RN>$4R$Xd3y%m>IuA)jz6CAGRd>I#lKFh|ft1JVLthB_}IxX>C`RO;DSq^P#xFhEz-)c8P_B7%#8b(O8 z(YO+TU3z5^^#tEx`1=N8h&+bh!?VN_`Xl3goAD}MQINOk#5ZO=VC-FQL#R(Z#?`l; z{pW@k-$S$S8vHU+zh@l3i?O7xHIAnNXX-=7ap^p|V`5=mZy=tDqv3p=)VnB$>`5&r zAADt25GzCH=T{baH}=z6dVJST^0OGf8C#rU;pnoQ3+p#uMa|~DN zYsQ+?CyXYC@O-Y#*(PlGigd|`P_R|-9fPfl{YG!tB-^sm=ANsQ0W7NHM<>%)cUvspu z-tKdzAKD~3r*hwN1H3`pfM&s`@f-uD&+8lhGhr65%v-TyhG3T8jLrl_gWmPxd*Lg- z7mpOew+*J8QhXfZzi`fMzG-K(cp^GjO+1rO|@b6+? zA1?*ptdkz;{<44O*~$ z%_F^rH>QI&{W{J1|t` ze>9yKh7n(&4$AFkPnJYGZtQ;G2^~Rxxv5SI^;ti5&!vsn`f+aUVBTr%NJ0*)ZS}t( z+CMUt{v&s@^k(2m{8n^Vk2eR+wQ>*pe8ntNo!QYk6`zH#z5t&si5K8=G5EZOe#|oW zL~-s`V0+}|VSkN7x}mgM_Crd~YU zcO`h5#TiL#Ej;g6mFi0&ua5oYC48qYi9^X~;cz85_CgPPrQs|38~Q5rz^}+#r#0*6 z37&jl&955=|8e(+z6F0FY0@VnYn0t*V+^93-tthI==*!@Y)UhLD_Jn3Z|QFSiTO6} zp>y}XpHZ%|gW&gEV*a)0dmu4NnWrUuFN62+d2+T^RG9a7egv(gyK618W6Q=S&@7u= zI`cvQSHGM8RU-+i?wu#PychOxj;yT@$A{K(MVS;Mvbx=8yC*eGO68VGF9 z0WhHBhfA1m^N!| zrjcBmI>k6Xj(4d>p&y7hW#>wnK5OmmHcM9q>3ZX<6F=0k)BCMGG>y+V-oN1E!e8_j zK89tq`M$^-&tRX<@8@adqdPT9f1R|Sa(4<}?zTePX#5!+U3uKc$ch`ydx`eiF&X#* z($;BoylZJ|1fQk06tle-sheDYE*x#I-I`MSJZUv?V{13WqthV6YOUBwsFQVN{I*)` zES#F$*js%2d9N~!cW*nBjF95*)!F_#CvA4i%*;XT(LChiKjx2X zndu{bP$xc$>Fg5%>@zjiwAmFi>umpE-!P)q=}}C(lAg{C*>R{dq-7>CeL@KVR)&K`?zD6PiNql@4-jKLui-ZZ<+ivLVF9}`@3kr`=O)asY=@jlg~aP ztp;4OCp(@D-;9BW&cKL=BK@pCeMuY-An!k=&HF=MqV~; zOd1qlJ9ol}ZL8Kk&|~mcmu$`(`!kpNV>g4J{p*d!af1pPv-8}c%skR)FRj0=(P0^B z%CFA*r}k`w=dy?K{`(-Kk#%lw4ZJ5A7J#SP`54Hr0CG(->{ejN9FkJ&H+e83!)_q0 zj5|*;8P@qZGHlWR2Qq9`jNW}@*d4L-J~FH%mLB7es0_PR{9)6&`S~(TYvgEnAk!Fa z%CIWdNAXNdh6Ryf*CNBJwQj~I)!N|+%{$R&AT-pRy$;%a0M5_ACk|*;m_BgZ`zycK zvuY=?Dxm$2aYow_OG2#^noV2&%(hZ!xC$Cx3H{EJ?|^Pr=x0E)1n6gn=7;m~$%Utk zL5^BKZ}@a2A=B2ndJYV>H3ogyB*s}yK3qm0|K$JI;9K#T!5UX_UqQeN+L@n0dZ z{wMtR|5gqi!H3{da_FU4dW_Gaa%ey4;R3xiG1`0fsDbTo4Epm}Y^(!3>DUtN26yoB z_3g&?*1@?(Vf4T5fNKB1{aWYpaCjeMbLvj#3G}&aH?n4z#kCK8{1pGCcNXzEHpE8E zMT?7g%xe`JN3z;a9RIHFydM90TVw6V7QPD_8;Au^^Ko8JU0jZNM}7}+0_qd1avH3* zo`!?gwz|a19AbRW@NG(+F?9t#9Ua7}KWKQa;&DUU$7{P~#tz#XIX+^%2NoLG z$QRGZrBC%!QS~x!1susaHnC!6OI3AFMb#@gtyK@_v{XHk^K#XI9?@O!`T6z(f9o;B z^C$d<-RxWI5?gcX6A$Fy2mC5L9QcqF0|d~MR{ zR3ADi{(|Tv|3T3S|83`K=)^g)C(B*2pcxvSIcW9F04Dib9(&y8J)L231&&x;!4BeQ z5i852(|UJfpgX^(?uVAfhL8L4oku(+OIyRovwHT<&+)zh&C-*qax!fZ8l@#w<~$GG znxNB*(CKhllujLn=Xd|({Pbc+LSN+$V`s%;s>=?;AbHu!@tN!1d zyy}N@+|`fd6jcxCF_^o;t-k$?A&<6KLMLpoSEnVl=A&(tv#9D1;AD^<9X;7OfZ^xE5)Vp)??R^zmM8xPe$x{cjL>@OXJtsfo$;g_**u> zuiRU6`7^lhfiGsD+v3vRQnnDC`S~-5LjxZRwqSRjOR@dy5+BZ~8_+WIw9V$KOME0p zV-7svZtmG>bA9r(&2?&*&DHR$0ZqaEHdhU?v&0KubcZ2aJ%G78935o@y2?m&mSl97 zQOx1dyd%)xGd+_#r|hG`yvg<09mu8e#N{2!+i%cL_oo%RHHjD&V~BZCWO?XU1ZN3# zPBcOtlZ}vU4Gm8AG}vMqoY3YUv%Fs7|A(z3yHtMAa?ZJFr~Uvs$KA$eukChoj@6Ui zK+J_q?#}tf5!aXc^U!;B->eG!6tb42r6+BZpT=Bt-{4Z>dk!Pc+Z{z~zs-FW8|fSI zuXormPVqKrXDM%F3`(fQ#@Ckh|KywHIHTPA7;A&#%YSKU67t^Rd-V}_PpXBty|?gA zdHVJxA6{l8hGoa5&&G7#_GRB9D9-JBw7G|VdgD;(e>fRF zhqu?ia~^N)iz9fe`8RmedA0Di;nwqb8wlP^TrK|p2UqwieJ-xzKL;;az3^gLa2_v{ zm|u5+m*!>Hdq2Tn$}+>)o5MSmXTYoMX5p}%SQi7l;YGypTW>ULF4a}TXYkq^6O7Ha zx6wJ)U+=2xu(bW0bo_38R}ni58Qj40<=$c`ck{MD z?$`OoUZC_m&e3Bq{R)^Gh?kbSk2tjW95+-i>8TO_&M8=%JHjaMAWm%{6`cb48klZ` z-el~#vyJli)yG`oYNEgAzQh?M{E~Z_rF<#pq{YMwinYI$_UjYw_iDUPUF#0*@ArGgesqm@PxY-mH9g_IVSHwD@354&p!elA6GMYINde+b$&a!H-g{B~ zbH-W68H3SAaZpRA2Af#Tf5;=rCsi zgSzS`8pW#~a%{6(2G*7^$HytYGxn)0YsKr@-#RaI?+FqsU;06+C2M6Ke)htJVx$J{ zL~gRT)n4Goz_poo2x73@{yDVX7mCo@H*u|bhciO&4O>qnZQLq*Wi{=j4#SquS~m&* z6xP|}E!>BY&At>mDzB3N$f%}my?DzV8~YH(j=ZQCOj&Ty{{FwmfhFxi9OM&Ex@&$E z2XVY54-O*Z&tAE7dSv`v^BMng#?RhGahh)d?%3Eb6D$+&_om)K95Hw>_3PYEU@Ufc z%2_?jl?q?kyRZpxX9=9Iv=7_9RrlQ&F`n4iZ;&3t*h`6Lps`oLXUenit+6Y{%M9Ua z1bx7JI{&DXykqQ@^d%X-Q0?`~Z;9r&NArux7r%>sCVxZE4BE`jp6?ZW#9->`y3Pn4 z%;r6coq5gQo>_SG%7;dTHPe3k&?hAg94bvAHL!}}j=YdM3No98gkXlgmHnOpDhtw=w-Nw(VnsrnZPGKHb`5#dlA4M7c}eNK7;ea;lX)E z=%3O5!t1ffZ(OO=@9jPI0mQAO%v+=f$^YMIzRJE4`Q4LA95ekt%Ku6IjhnBu3@mD7 z4cl*-v_6m4;pE#oWEnkuCobNcs!6(GTDx~ke z?O?aLWIL0P1%smP)JNM1rbpVzqn$9YHv(4}_;&HVA00@1xgR-FiA>lIUFPD4?tmuo z&?Etz*+q*eT|^T*G|8YX*{Tg)Y*COHV<>p>fg1gd&L2G zqj#GLv^gF=lsu|?lY5F5-ie^xcIfKBhn@e;oY$N7H_rHe^*8!g+0tVdmOUITE1r;_ z&nn6erd`!798mGPgL(Zjb(OFG3H%>G{_OiNtt)(!vlrHz8PY8)_Jxbl5jcZk&4aJZ z^-k*^>))^0qc^ZuFCqSyfeo?&zcUARrcV663o{(sg1k3$b~1M735K`x=|bS7KSH$`Dm95n+z6-?L;cmr!x?eH57@7NUis79gh+0xUM95D9yoC49Ix31?a2pF<4s_^KZ*Xf z^ZSqd&Qf{S?aWWt9PLD}4gyQor)5VwS+9#eEj$|74s5L1>MI6UaAEwK&ZPLJ0ByfP z8`)}u`BFw3E2z7YdMdB}yEHbn!MfMG-eKxR=g|VisTF=QZH7zp+n02E)2@QsnBAYj)X`{~eR!$oBd*Rp4y$1Bg z!zf@G4NNJ(7PZS9?C;4m6thHcZbV`;+Ts0*0q(|{FJtRxzt&(IW!g#b{cEfuCQaqY zmYH7Oz1mTIZDYM{OHMuUP>o9Vp&RoWEtSo#TnCc z@kOo&uh-Iks%1=9h%-e$^DJ{nL?&id8=ju^+f%&gI9c$~+g3-{0p@!ab2ZHG75vtm zO#{y=%N_er`XGF43`^Iz5wzn#4&bw6PeYcXOE=g?^fW9t+QduQTIZ2_sv~>Tu6&CN z|H@Fk?UtTa{E?1LbcZ@pc?$r0^I69GKfG~UQQeYLRo$B7qOZ#8-{#a*U*98KRKy#N z71b~0Yyd|!#G~5&{QUs0c}BP= z&YND@n)C55?cQ;f2XeYzaCkppj%8H-HYc-kc8_SBR(UXI8F6jP*tZtaPU(*RWoWHWeY`9CtgmogNuqJJ}Y0 z#Ibf-5ASz-o~Qf=;4I8sse@jR<0B^Dogi<<_kq1NRxVML8$s^j3Q%eK;voLK)0e+E0>4Zy134cKJsz`x=p z>CV)We|)y35?-@l6Euin<$1}B)qK2?xwPM9`?KugBd*pRt|Z^Do??8oA9?G-=Iv%) zetM8NXSCaH<7}n?{{!q|$81`=9Gt~7p7*)OlEIw>&Zp1E>M5Wu^o4KDvt9Isln=tm zUueIq9%079oW?tsSr4C=XNCA1%3k$9HfvkY!NHz01^qo<uN{P(eGk98 z1k$nw4Xnkc)0XvEB=1ISLeGMS_uz-g?N-lp=I2MIO=xJ>N9Yqn$8BFSRCd<^5u4B- z+2=fpP3Uw!J_PWUY(h6-=jg5?UMzONrSMBTxJ-j~Q^BKA-SfgJ{ui;op3A;w9QjjO zQ{4OyJXz8dJW$f4{Ju+q-JtCtYe$%yIZ8Yqb#zf$*1OM#>Lp{!w#^yb?#U?&<_x*+H#Xa|(3GeM3 zk{>#ikRR$yQB07LzPZq0)QFZDJ(iwn8@_GqJu;xhHEN8JcqpE;Sl$_0GkWyY1oFJA zI8#6uXFpng=sWm%TxWOA9b~i}8jLSou5oV*_dL%s4-Svwt^wtAH{t+k@~37mb36fh z)!A~eJO$!_%+b~9{L|dUMF^qEVWDj zoZl3$zSiN~NdGGa8J>9Zy5RHMXzPC3>VjsUK)d^`J=3-k`(zSrE@4dBw0RqCDt{~O zs>}_v{Q&WhvmI_%7H!=|n~K-Bj((JX2wTjO>k^#nzGB>4fvlOEYb54A!rMp8OD}I! ze*({HUi&+S61Q)trZI1MpV>U;lyA)e?Tw!A;b1=9QzMenAduk8&fbd;EW}DkMx3Ygv z9=`1P=n!u7iNL`Do({_Do{H|<5)03n1xz8^SfA#x=CkJYY03rOBsOcu^|HM_SNC;j z$UBbys#de__NoIpod?`a9aS&o1iP67RoC|%t2&5o;%f9){Wix-Y?xq&=0(4z9k$Im zb&p@$*z!-=RR59_c$0b2UCMpFZKemf5sP3^oB?XRjL z{r<)P=RKzqh^@$cYUeD-&pD94z~*TO#_8W>&fjWGf06eNMj{)W%yqph@ ze?MrmGzUq~IzGQia0Y=fojQ^&Dkpny68-5;i_%UZhoy_ICN1>t09SAPs=Vx_5m*y^ zg3rr&*ur!>@rK9xlpkcxz*egGI(zHj2lHJM+B)(U_fnx#_uEEjwaz=BNf0^&pn(Hi z2Z2?%4Iq~sl1bp{%pukTv8&cg8v?Ha_X0s zP&RX9Ap6iDF+kVBbKGZ0?0no}+O)Q_7v$Z8(0j=3Tx>uN>edlw(aXCpL#>Z*{m|ms ziVmY!kE}vvc^qtf!&+vZnph zXL$Elr>xn(Gi8P!T-C$R5AqITuwb?;FlV+amwkO6I)}G<$(qdm?2qyL%!D3+J;IT@ zX=;Yynw9{M?y)q_oys|naJnb2IdhX?-l@+T!hXbtEeRQ%iO*l)m#jA#?xwC^=C947 zE@#!Ij_0!_vL8xZb@w7fjW!_+Gw`-5M3*F{j;*}h=g!UaUj{oHVXGPdm59M>da@bOS zBe>#BWR>hsseiy`t?xEVd4>Gir&!JR|`5@KB zp6yrNyO6Ey7t}`{Hu+c^zmK)?J#3t^%c~7|=WlAmGG>+b7^#+|Rg%#w7$dfgRl2tn ztN%i*K7NXo=!s^1_(gi6)-*qF!wD9hA;#+XVs&O?OD5l}a~u0>t>fTMu>oRbx5vsZ zrG44$1s^>8T(2@ep-c?FXNBJn!Ec8pB;W8@JC9K&m~o3Y2z|6CuO}~s7`w6jM`Han zM`Gx!AL$i}wG+%3_d)<-tPSW1l^~8NfrUVYUn@cX(Rw4+##*1HsV_&)`$-lWdB$6W~1z@b+X_HkRdlSesZwpL4@Q zfvI`sdq4i^#8^K0gQJRfRSu2BHkTCFdm{0Ht2Pi< zxOHZG)yAAaDrvd!(R%y_!{B(L%4lz7L4LAUU&nu2ekiT-(H#G2^5-z;G78wAKAzK^ zLEd~m-N@E?e3$S6pJylYy_`>hJM_s^zVGBSAAh##e6J#I?oNE{7V>*}f-A7a-5j`+ zZ}=$iLjSeFoi}6!R`Wmmt69PId_Ta4ct4%@@x6}E-ITB9`_71-Y%t#&@CnmdO+9>9 zgUzAy#k^3qCEh%*3KrbxdVi5S^o)64HLxq4^Qvde^D6FsaHf?{eXWaEb_23YZ((=Qe&%dsG_;>L%xGi&Pw9|c<1SbW z@Em6ClK-`Xx`&8AF_-o;k>y!YnO*aGc;A0vLzbN`2|HN|Hh zN6!?Tt2S_!&_H0pEf% z=`#0|k5ca_H2gYgf#vSd)@b?3YchI-=Vkf?(hX%M^-PFS` zr@0V6q98J>j6JLq8fyM(oC|^dfBC+dZA z$29)nDx(>DPw~hozhqOk2z%Ts$dnY$d?xbW0q)TSnz4ah9lX=sEIE}0A9D8fWH6gM zA>b_mdUQ?Z?2`UZO^&>sAsCXW&pS#BM)3V!wmYP9VfLWmiPq5fksAs9X?u({bSl{z z>YBryW9oEe@Edu)4!jpapAEd1qqhy>vFX{#p9j8(jT+Lvw+q|X8Sr?+st0Dg0)IS! z9q|U+m|9{{n{h>T23^GeF57j!BKH5lTH8rq>T?_KVFgq3O&eY%aBKhkb7CC^v>)S) zC!YHQqi9ckI9C|n>m2C0$-HYaCO^~z{4XQNgP(-=<8vr_Bt+>ZyohExdvN2c5X=95FPf#3 z?mb5{M;AVTA?uV4TMvvi_UU{p6aV51Xvf}6F_8k3;VsUw?3}}P;u`}`9`B@X3TK>S z;4lB3Ei?6YL+~i{y~Z%l8~uyelLEi){rhiIepQaYMBmsPmg}4I7--1Zc!cKgMhxb6 zm>Z(`1pIt{4Ntzv*h-*zUI7hgU+{u8K#=%!U#kRv1)&sve z;XjQx4SqCn1e}@J1?87=igd{W-ETGJ0JxG|kguB7R`sjAQ?@ZalP7@x+#UotwP(>D zWaeLio3fG*fBJjeG-7LnYW}YIUvPf@Q|4Xff0#aWwzOLOMjicEA8KD`P^SHpd>d`| zV~)#j`CG(@c$T`tk>VPedEkfrsk4MUtxJMi=OIc9Gv_5^V`n9?JhdfRr&t=XvZBeQ z#$?x+7ze&(#|x9Ur)2V0Xz%eD&=xXLZ5AW#3c%SgbVJQ4P+MUj9wwersVJF%4~t&GViBZC}cE_ZEG~9yjv0VKctj!lptSn)+C} z+x4SC?zNAnyPN)yH^}(`y5-{&+^$En49_3x2l?Jl|HG#FSw@@oOWNaTkC&b0c1;@Q zUVCMKcXMrCg7X>bTzMt!)F=2RO))&$<7j*i#^X!>}R2i=s^D9euiscpS&8;=yUR( zKp*@FIop7qPv5S-Rg z?Ht29Xee*d)J1TfP;9K#`DO1|G%+6w#XU%eKDrlMM|^v1`sMBPU8r;1M($i&nBO7y z$LYYYI~Lqia+T#9F6rq3^z^a?20rO6iK7R?B$a!;j=y;+bSnugn-&yB)sCoMCvq%&7ox6b@oe{nBCF9A@A7IP};- zIrUe~J}s;{MSR?{G`H*Sf$p_zg4*s$jr`rw=(>2zCoVU={$MDkolwo|w9r&IIevIC2 zWupv_hq*R{{o}{XGxF~}W=Sm49LpzmhhQ*c8&*B>Nu(8%Y)MCmxjVDO?0QMQ(6N zFRHQh!v-;q`-btvT8c|IvHv5`9mmy}qehb;m*@{NAiC>Wp@;osw^~^>etJ zm$|Kx@-rDH>*lgLtFJokz@}M@QS|vKaHu~A@Txy6{Y{}C8~rVyuVwUQqc1!D0t@JG z8DqJZK5MDB0U0K{h*_U;|EKVFXw1O7fja*wyyNKiJJG(p;QBv-wJHj0DfMFW!^Lmn zTbO<{;I`S$9GmiG<}PKw7R@`1&e=Opeb0}!Av}l|WjFZrf&JVg!tUZHCQg8TK0asI zw#(&r&N^ORW{Hn4q%9}Dpz`TS@6X;3{W+C;r-z`K{BZOee~H=(#-#f??;*c#v{-5z zd5dvBcMI{KD3%|_Dn9I`+H#G7T^4k5wU^1;BI)E$fR>qt-k`E{H9*I?)Ytr1ehcyh zSdORZ8~aH*xX;GEr8j@I24=G!>9>5yB~S24uT4mc#C>uwXFq+Q)s)FjV2{VQfx2dF zDtx_)SJF-ib<^1^Y3~KNSP#rJ<9u2vY2kx<>&fmtIZytJ@zBZYvEzd{{081m7uwFvf6Y~vkJNNf;p{;zo;@{z38+Z5uU51OZ zaaZTNhN}|%?ZJ*C8a}#m)r{@HZGZ>SZJjm2wAIM>rVHPqP27KB%^5z4F&#noTnBxY zQuZ;c+qEBj3V#)xxvsJrUDeF#&BXrc#*ZVNb3NJ11WyUDXpb(KPG;1k4nAuJMDAJoX=j}^wX1=&G_yUu+iCCef}DT-OI^{s@lB|L7fw|tl5ld*#xung&VZo~v@YW> z4~qZDkH19z$sw2boS1L%zHS*p%n{;iK|9^!SUA);evo@MbFig<#5*(S*yREIxQ5+( z*}|*RDqk;QP7QU}eO#Pqc z-+T<;0pIX1D)$j(7e2IW+v-y~`%LOOfNVB#bLjlIr5B%!%QyK*JUBNy0`GI+z=V06 zQ9k90*HYS9bEX)>;-_R8(@gk{igWNU1peZoM%xpN;Wd9`4DHy)z6dty@*gvfzcL3) zfGyveRBMgVrRLJ`{ch|{N8=|svf?l2?cbL!aZ&2R)-az}Z_m z^qC_1Ap3?vpM{)#AZXgk8Q*izXv!6>TTAhy*iQYO=|=IL)Nfb)vD8Pe%DCsUh3Tq~ z-fmHS&fki!GTN3>zn=Q(;}32%hDPhBM(U%p+`LZpk(uA5eunB}zqhGAd;MZ~d^z=Z zQh)jQ-)?mSx7j~;bkzSl)Zec9$mmC?KVJ2j@BLJN0rhV(+P+EspHu(g@h@#Hg+69| z-qBF~e^P(H>cb;XQGbH!k2cEttNt?Tml|z%Q2!Uy|Hb&OtC@xR~d=8m3OpEn{@zgy!M{+VYJsk<5)#8LN7 z>O4)Ir{Q%wG5&pgt6m3wN>pPICTPt(V@rF6ij>kP|EKk%7!6rc8?qlY%z{1G}z z27Vr&E`m2T&wd>3vjCi_&z?tt`|#ioY5RAyEgoDNZTs)hb_-Y=)$U%}HTe;q+I+>)l#{}{ zb@)o@$`ffb#+&PD>-V(v?)aly7gQOZ{0gJ(=d=}IozVKB^(QX6F7}o?l6$?^(%x%! zVszc@T^{>`3;V)x=_A~8Ht#B@HnPS5zw{+Lc9z&Wr*y@g$QoX`wa~C1!QOM+%AAxw z-@u+an?3cn`&(|ltCjioPkas5rX*s28&+HSSjxJYMtKc7;L2g~iZ^z=9z9ETQPoww zuYUUA(YvVoZR$RYFL)z$)kY)rcU3-`vM!JK$k+;E^_NmV9@`N18dC?!>_;3w$~Rc- z@pkIise_%c{22PZ^c;=D98a`-EJo8O1$vj^;@EXbH@Usi<=WAA%c|V22I4BBzdCEU z_dr{rmB3zR#RmpE%Nbig-h=7y?7=p1+BSf9E#sUgv7;7)6Jvr~Z*9Jo{?=sl2lh3C zbU!ZlFKG$JbhTSX+-fiR*cLn+iE&v}ITQaS?B|@91+%%shQ6_bw_fsFCVGMkjkQL9 zx3{Cf@ci*-#%6rkrp*D4BH|>kK#m?zT+fQho}xd`38{?gtNcOA)^Ybn{c)e^>R<-% z`l_$?A!c7HHah6;56;K`qnr5KQYqe}ngv0?s-Y&Ew8Y z5qDXiUAt8FnBs4{;np4p|@iX`$8VPqhdeKO@7vAp> ze4o$`@YRb4Du#G;w$d99{xjsqYft0$8u(UrQrE%xkm70OvtMuL%*bC|nG@tpU3^o@ z`OiRZrn64^7vAFRfnN>sqMP+Go^yQhZDPM zGk1eVuvZPRuZ!$!M)=;@&N&48xkC0RnxiA&hv9#6bH;eS%$w0Nc4FM{e!ZEu?w&|J z(aWHHKlifQxr=qI`gb|OT8@%f?;cpOVBD~Z1vX%k zt}i?L0DgZHf9icdRm|6(=eO`}hhC|y^?u~D>d7WP1=vr_pT!up@62beL}Q}T&N1kI zskhiO!GYwSWQ1%K?0bF3ij2)VQxb0E+b$b`fh@3a253BHY}VTbdI#774=Dc3v8^|s zv5KagjW7N$Gv?Jk-4oT_gDtEJBl#U3$9q$>QMa7=QB|{5vBeY%xPtO6df$%oNBC~9 zaFp~KGLXA8BUt;O&CJxyNQ~6e%(33@u}AJUjPzNs-KXKNqj7Z0zF{?7T)5h+dsXsn zm3%q<8gL;m7K6LKv1yaQfp}mkckH@Jt2k zTNv7wMd{kkf6-O#ejZ&PhOW85RPkx}`Sd7T% zBfFp@Zy%5Hb$=`D&7^(NL2@uXjASx-%%gIb=1CSU#O~wBY}Qw|p0P zDnEGesQS`9I>mL?UAzc#@b2{8Ii?Kz zVJ>fGk274qpcWF9X^J;Tip&%e}UA{On_Je9Ppa;lAI)v%;Io zU`sM_w>648*=TgPPq=#q+zp|r0kY}4=zelfgd0A*FiZ{sfb z^O3XMpEl`xxX(wQt+dm^Cv20g(aIfC8|z{}aMB;X7yw_ynS5dAJjzepV(wD!y_I#x z?6-ipyu4K<|D0>mT_YTcD zAK`f`HiwbEm$sNOnxCrx$GW$+o$|vO!`;BDcT+NHPySbx;H0cN*%!XTC|4Qz%8ecN zldZwk%yW2U73+lwvn|;vKJlYx1i;r@&}bUGpx<_4P)csTVe-oe-!kTW7r!N&1Y|Jvf;SZf1d_SZwEr4X&GeH@YvbIb&oPp3(RyMPytA@8!{UzRDQx0N1g5 zld`+_eG4pX8*kp5JcB;@3G(orEyU{D&iXYqs*^32jX1@(6CO_CZA0|lv=NBpU))DRBtI?N?4$Cc`tgzcn z70auLz!fHM>N4?IiZ7qvUAu1frrOXwufxB7eyTUPdZG7|$Pp8zyFg7`07s_N&Z*8r-pE^3E%#%iOJ;O;0@!uAI49=bK)rS=Mn3`;p&<= zn!F-!=fo7!=6bs(j^TeH|Htxw4*xIn25mpx+L>{=*Pgt6tN2TI3QYQPU!aEh7fj~f z0AtbmZt@+va!_{+<{Qlhwwg~SyS0DVop18$Sd&+U+PD3|qT%j*lUK+3-n%H@gk>FzIn!biQSq^KEy&$*W_1`=afnoafcu`6jQ9^|eOx#H%MG{J1;cSAS^Z!1j z!K+8{AAWkaPIw?2j5;4Z#@so!(Fffx8@4h8qnHoP2ok^kBJf1B_9*3db=ev{vfRlFSN zu$p?0bj0AhR^)Bv%&I?rwq*xnb?-O!a+cz;PLwaA;k)X`_*G@(h0bn|=sCiN?%YYg z3)uR5v{oCIex9l0jm?#;GyZMdi(xK4$$HGW6z4MiJ=O{6y?KV`GTM^9rGC|?sT0%B zD-(^)u|7led8T@p=k!j8XBg$B^YYkCC~(>m%3M_74dZti{sdW&G0YnX^KAo%an{iH z&^5G{$$wIJy@ped^~}@WF@*TlysMF54e>6s=TvpPrz?Ai*LiT3YXs#38P-r| zm(lFZKsQTbOv(Ju80wX8y8Nl-XKaqgHq7&-(~M<#e#p(^( z^JaxM(pPZ)tWat?b~E5A;?3!6@8J%@p2Ft9oLQkaFCUe-&Ni^T08&8qG6Y?4RM)q*tdzVzc7gp9*i-uo@VuD|2Ba)u81G1F-z|k?*|s3lgJA*RVfzg#$9StBq#e;RvpGhX!+pt8<<~%({dl zT?~z z(+NGr%X#=4&#kof1gmGS6;GRSF*$2DV_0Ih&b=|>88%ocPF~L>e%k-#?%m^~tgik4=a~sGlaO#pxT8tH zOEO%%fIt+?B%qRjS|P1>6QHLi1ht44;w=fWHDSCBVyVy`5@|KhAl7P7a(a3Q(hG>S zQhRDWJuQj!G9d~gw;4d@`~EyLBSQ?d{=VmXzTflvV_x$-``LT#wbx#I?X}lld+kx= z>3YxGD!f0&K7@@RV_*zc4zNDH3LT&QV8IiNcI^CioJRHw1h*%c&$HK%2=jGdenzZd zwYl?|qN>dAqsLIEYtY)^LD3z4;{NvG!5v2H6!Lyb-cIrc83*~d62}I6Oa4C@`&GYu z%M_<$&n$fOXh+?X*t$pKxA*W{#5p9M%xY{j4=39(+lav#{XT-TG0v>Txu$E!P57g1 zVeONT57+i5*l!;}+`JTTRk0Hs@TDul-?TUrJ1V|(GdcSt-&Eldycu6^#(WUo(3-E4 zIqZ4p)R$-RP)a->YZyBpe4_Q9U3Nh%4IWY(HWnKDS0V?@eUXxNf(3k!eb-lVR}=e; zx!m=&hdERJ^2=CTT}C}OkuM)#vD|w3LDFBeUt^(`Z;Gdu2*ZRQ5@C#D4dx+V2{69N z|C-&s7>N>E1eGKrzSHOdB zfoCtjef-3$`+hyk+FOV3!g}_{*APQ&gfY3}SV6d81Z#Bki+uDd$=#1{My8;{i1*{q zdsrWj=~YbJ=;9rVu&MMVw&7ZHWLGXR4@;qS4nAfc(vvF|hDu3qg8!H|b}B~LGMS3$DJZ4SLS8Q+v#&h@64$;FeH4~poE zh3XG{F})WRJ+Pg<-OhflS?bFp$g~5D^%29W-^+EMHL&R1?W=Rb)hiUw6gm}G(7eKFx+1^~mT$fT_}jIP#ou!r{+{FA-qxAO zL*2!)9LStjLV!};KljJcpLX%Tik!^$u{QD z#(}{5gtT<_GH!^sk9IBLeu)^IJmBO_#)mx-&aQ57=(K31||F453@f9zh-cTuru8e_R}7%2{gVAA{W^|?&`$8S@2PmclAy3 zIz&E=v3;e)Hj9lj&f>QcA8FFH=$g_cHKw*b(toZSeKVP}pkuZ={n`sCs%=;;-MyID zp%oPsp$8aCd-A1kBzNt;S>w`R4o?n9=h-viE0)UWj9Cylari(|t8~Q!$fZ3C(A^iz z2!C`t|CNq#u`|`zbr;V|9O2IU9mKjy&ub^I^fmmSRuU^Byp8kJhd8f&lz0fzmD`Zp zn;paLIDn5rBhl)r7_CGnoikD_QqidbJ#SEqPKqC*_PHDbl7+AE$zfk`GqFB}Um^3) zH-R$`{bB@yoAdaeMtZ@nrWibXSYOo zs(o_Xv?b6+^jf*h*na>%(p_U4v9q)GoF>|t@VMP?&JiBPA9ZEn6N%rYxh_&-$PNrW zMI-U3Xkyd+I(QP8aoUR~#iOFNXd-xh>@(wvQ&AYsz;{0#ne}ba+4#mkYb`x|>d-B!e61W6`MHIl{L8@1~sAIgh=?{gYSQ>txR{baUM~t}^s_i_Xa^|DDiI zYiO+tM=(CLH=(tl&V|L(wIVq)}X*+hAEnSF*cvqwqS`bzYin;&R})0HdNU~)FE5NJa|IuI@K?JwAXXsp17XN*SBu#x$h_~ zVLdlV?^@4+VT(4IG1iL@$-OTIHd4z1EEM zvuHk0Y0$uxB3{Q9j~<{nM}0|uko5uU8~iFV14E%{dN%iWGOr%nZHA9_nc)L}GsB1X znc-b_dCur`j&irWwd`p;5K8RkpTLUBFX zh*!3SSY@&w?1848m4)G7&6%I`*b&*|xDd>;oy+mvHD~CZy;_f?XXJfuCt$qSqNn43>2K}= zAD`Vf``fV-oF%$UU_8uqBI{jUwa~SlK7WWfkD9To z@XwFA+qFdF;t#+SPTG^{L`QIcaQ}O%E1Pqp8{z-v|G1wqG&CT8hl~M(c8LE2+-WU4 z8v8UG#{pv%ajH~C`j+y3cC)ckeS2(oLHG!D6L-@eo^0axu!UH5ZvQdj)r8qQJdjTs zYu0?uN{41)_qx^$XAv(DoA1+w+xq(}fFt|$QS5sou=CmJ=cM}=klt}Sd!OK7r#;%= zznrwu)cNyX(K6aAk&QIApYn8J(g6QgWAJ1*O^?I#4)9+aOP77$w#y>(Y`Dfi|2=&B zW$e4X|MC=Tf5Bz__Pp?HOY>Lq{a+LF_+ojOt86$c2m0sp{p*Q&9*gCk?sA$MReo%hNKzeXpv9=lg{1f^9tymt}9JN29J(PC%T=wjn(07kcza>vG7HSwf!P{B;vj-yk zmC63YhtB%d*GVVFR;4+pjB(j1Id>UxFOgeWwZG(yG-uM`i=6#NC^~nh`rG>q<^1$^ zzP&Zo!I^pK*&ik&i-{j+aDLt#&^C4cfk&s79Z+24ty5(SG>MH^cHrAnbM|{f?b!F` z9e8YN_JCjT?g#}L`|YD^r{(T147HCYt`afk%9o^tDwZUL+Sk`iTmKYifZ^44#Tur) zhyKvVI{bc-bwGTEE_(1P_HKS%Y#li;kl!q87`#r*2z=*jR;WyNFUHseuk@0zOZ&96 zNAg_tg!^1+ zIr`PiDZ}?ANW7`}_`c+>>w{b#7Rc$xSiy!i%3U+5LvA!iBt)H)}3xS1O?Z|ANX zXzf{d6|u>9UY~AFU6)}6&%e%^{Nf;M!n(mcf5n>k;t=bSbwhc+)|&LqgrCEjf|1xSR2uGsfnT?jSv4D!!5FjDZ>ax1eJjWo=f#vwR_s zvgTs$$kSR?bEIVbg?u~up!&hcK>wLzVtYRdtwsBq<`b*Fsk|eI4NrN+@gGlq%}1Hn zbB`@HC&kZq8|l8joD1N-m%w^AF@%wm)#%52uuI4<@+H>VvNxm|slGO1;6CD)sH<{~Ghj;d`*n?ZvNc?*MQ-i}e9` z-sSA?Yv&%o_Fm?Or@`~npFd*tUnV>UbnUmx5A6Ds|BD7M|LEY zlbuZe!(;#2M>?Np@u<>o;8`+H&jx(5aYJO4+NXE5&Br{iwp|sgM{|2SxJ(c(#3|6) z*c}n_l>l^v0j0556@n?qM{! zTRaIr7$@7Nuj^DETa;v2-GxXJAS`T9*r*Fo9 zQ+tXxEI+w}*UqH9SJCb(@w2)DA3}#e=a=%c`eM3#tg z*R)o^7Nk6aKPc9g`=E7Xy6($M$44gvU!6hJIheYKK~L*1t8iCn)zBg~hv}bP@t*YN zC5*L=;8X+Mn;f@=7#l5(%Wkt_{0bON%SuBX*s>bYn+10pX$OFt80WK*xH9sM(^{*I z@Ac%BZ<#C0Xn79bO$$C1^&sZ(%XCtn`>XPR$5IGc4U^)^hNQB{pS?s;(O zL@w{4pJX#T4t<{C`}fid!oqip*5{Oq=U<22p#lBIO+Cve%%~c3$&4!PwH;>;=kA_L z&Xvd)wq*?V<;o_P{~^XC@B4k+!`j3*NC;47uFAu(NxqhnELOkHZE0BGxBSTLzpWEdI+vS$(nYifDu}FM4 z%8j9%Y(R5k_;$xbaYf~K9PGy#)x(jNLkqb#BNO`eBZh9e5tglgFm(GJwvBDQ2m7?{ z9ku{F%@8A;fjvHZUA3k4^!3DdDx6TiI+L;QddQ23K*50I80_cc~ zI~e;b=NtR;fTNge9q?87{6b6i3;knDn%IC1LVK{s82j7zM_OFm@r_M<=g4~w6lhp*R7ckFz45VBx;R2HPL=hTi4c@J&(1dOMyRLntZ=CD?4RPS#8_YMH>z44)n8^ zCoJVI+V4#*yP|E{2Ks&j?YI;D?H_Hbo=P0AvqFvFt9uQF@9p5LHU~$e>#r-wgLiiZkv~QB!%A$O?eIba zu_nx~)XdR1boDP_KbUj4CijYQUvv}W6Z!DtY~%p?;*cWR*VyL~?rQnd)aL3-t>>N3 zO>KPavJIAVM5ucmZtgvZm#^Uttd{qeTDFWpzG)o)4EXibO>BrC*LTog@S5aqAL?oE z$2vebBipmE5$3p=L$196xq3bF^=ru4uOe?tknMAk?Y-?gA{tHO6XR#ZHRY^(VOEkq z8$a*wy>P<57s(*aXlK8PcNesZ%lN<#qcNAF?}Gy`@GkvLewBmBr+?9NW8IKY4fD42 zG^PEP`7DmF+Ty1z()ILBx`6)0^Kn>$QTZBk{Z_;O0D6$-*uOD;Pf(w1sq{fhJT8KZ z_$>MGsLm%zw(4JZ2rT0M62&O0B)00yy;6N?RdowrK%FU zB>P72ck#K)R~IwB*-zW4{mT#OM>qUflkZe_ zM)vAjlP9v*V!GEmNH;I^hMiNi=5*=&uIKF=jFs_N2RaX)_^f&L@7*_EYMA>)tNE&v zb!j?~I*xqdG9NEZm?#s%< zj<$FUGN*a4H_y$U6?9xYlXr{wpL*_KpGJ8a`6my)5spATLo8o+ zJoeD|j=Q@zLF%L`nkC?RwUJN4QsX&xr@#+ryp|a(BqDYD+^` zL;LM!X=pifc^xu*Gi%zJ{9DA-oDIB5ORB9;_nFpZb+>KW?Q>X7b!Uazna5hVbKKy} ziAlX1c&=ft9MRusJ(dn1)BeHVB+ecG&Cbi2J2FGNeT9_0HI(lh-qlVUa??4V#(G2Z zR$?2S(9uO~uWiKKZ+6}m%5v_SI+eP2X`h2TyG;1Du}(VaU|TPAQhyTT>mBw^8tZNg zX-;nEY!!F5tkJzitR<(79ALBx2gRoRm(h<{?1AntWdF2%R)O`vD8st%Yo>M2e1~=a zoledxBw2T-^|J2i-P@XfsS(bj%zEgwo-u93=rsMW#EpPXF8)O)t>e_^d(hh!qO%3? z87byW!nO42=G1;&TmBp=d51mFy~Lu8IMTX`cuyOV=KD3g8cBs$cSlOLv=-pUYObHp z^KRrabN(qX7sg;-1I+5ww61LA`+8t@ai%ar+ztzQo-!iEcLy*x12dcVc^e9>`M`X$ zJqlB6N~5X38fY5pN0D}ndX5Am8!x4e;z^AQ#pV;g?}OK+bMAw8#ou0J*1KD7-IQ63 z-J3pom%1|@(HJl9LZ5dTFPXi)pU(SZq+};_TI(3l<>cA#7_cLg=k3UZKVw(f4DW7* zZtS6+QfCTvsy-L>F<$&-f8%~(>f2ltt?v%%+l+0*r0zSY?^o2fxtI6Tjt?RwciQ!( zb=C3wq$6#|W^5m-4;~t_>ucytPR@j}uP8bEu~YUTVogqRuX%~_mQ0;{u}3sAzwTfT zZX=%kY>h*Hvd64N2GyWnu19}JwE29<*lTpRP5TIVjn03bD%@ViF4TzaM|PlVm_G&A z3;$(-%Ts2mZ|@V@ze)2TAz}r5*XR2*$Z0-5@ z$@2=`&noErLZpw^KCkfk|MYY1ch?UJ@db4ve;k2{e|g}&V4Z1oyhzL}k6!+=o#r9Gpu3RI!B8iIJn*9H1{9JuA?A>zRSdP~ zynfdkD-|;($>lv-27RJy*g=7GANZs5)c1iGCG+t))1J`^U~glM{MNS~pR@g9@20@) zmzcAXD`&p++jAoXFi~LUh{(1wviOCKQjH?Guj@HAa8(GN{`+fCp7O@#eT&iuh! zWyGbB9x1sc`^Jxm>z;L}&m`$w&G6;3)V)#n#qnQH9ilsNhVd^Twh#WLo0)sI9f*_^ zF~*jk@6F4i-r@t9lUi8U?EaLt>&zm$L@m!Ix|(g*0gwB^LA)(IqH%XJJktFO2R6gX=0SjMPgGvzXureqabA8l(Oz6)#IVQybc zreQ~LcV*#wpg4J(i6tNznhFh4+22p2-FG1uw$jg=i7ivl|21Zsuir5ILb$hCemN2P zNqW`*Qm15 z&2jW!{=7M`%gOr|@Ip82T8F+h$}!MaXqdhkxaPfSCi&KxNnM5KtX;i(Lfz^t?8rYK zXZT+zFPrjh!??tNCmT7hD|;KUg6!A=ZTR~99ABTz5_55*G5Z15ASG@1{qWt66Sf(6 z74Td2h4Q_01U^QFIKbg`Xe{249@}OP^x69Cbe&@~(M=ubs5-yP8Qkf_GJ_{%<5B;Y zW239*-U8VqYtzvG6hqM--<+F<9;F{>Z^_%*%o+M1d^3i8Q?MUD3qKF0Pk+amwxOJH zi#Uze4&)u{3=eamrwcyH1eVU`ZR1&I(_F|^`2cj#rXBuM&!1&sCzlPxK9>kj$R~&T zC_}v|-d5TCg@<&7hEYatH9wuZJ&b>UsI(BByWsP)w=>D(svTnMsO7zhE9%f$)}He= za^BqdWR%un?-rJ2`LhSr%*m&I!4Q2+;Ap*thrPWH`XO!k*XOaLZB2r7e` zp+C3`fW`x%v6Fal1-2b+8|UupxwF0=JA#Wf1AG^LS8bsmjsm}SnQ7a_habcCOFM%5 z9)2~r7d!O?jl`TMet7xUMbPo7z2j;!ZUuV6kd1(;m zgwZ4U4dT5GS$M$N%Zf}e!s+Pp(Q&2ye%beWS?$wJ|H`$LyMX;Q>dkkuzc@r|T7x^E z*A7o-?hi(KTmMQdz#w-s2RT1`6uTFCR=AzG1L56$tnhBPrF(Yz+I?u>e`f2Lhqpw} zZ`3=Sc{tCr4vpfhb?836+4?AJ|6gMp)qIKJ3Nw^=jw=mcn$!ujMEVFnCx1LN8bTF(PvZXLBC{HO$kjQKKqcWtkAuOA{$?3 zyxib)uOEdk_=mJtu%!c>1D((1*8mLB*X1%cHlyo@==V(46DBy?Hfw0x!Tz)QWi&sP z+qRnf(4cQ<4E7)3pUgMHZf`u_zmf0AKF$e~??yXQ`A;skL$K9|G)E|g{`FVu}4_$M)f`M{afruO`uPH$#;$Sh{MSH z06gP*lf>fC7hduFV<);jk&p|;l!#{O;P-xqki$L*5%S+HJ&54 z_pw6L8S9GQG8cQP#`9#x_Ekps2xB(hPkV<($6z`7nEL6G?2I){v3_cd^^*gfnqvLb z80)9c+M@k*A2@JE9J$!@*?Gmfz$d*0og^>wr`)T<+;9WDx9Dt%t?%@T?wNduoV1?84$D6{AWt)}1sIdW=fbIX0!~^x>)s#rvA#tZvvGVI znfJ>$M`^T7@E3sBEtHeZTXnr|%RI#=jIOy~r44z&KZwlGbG&Wqc}8Ptk$$GRN48_h zSL(8kFb{kc{(FHulI^n1ujRSX-jm8;EV34*+?>av^Hw`+`1cgl| zV*VLe#u&QiYc8vryO^t4qgQ`wR4q+&=2aK-Jcr-KMl0#9Rmkh=nKYQ?})*;$;@LGqZ^_A-e#ZYm0VtwnuKD$_Qv*tG9p+S#luSr2q zV66o|efHp1JI?7BV0`5?Fuvw({dXHi@)ux4`P@YIIFLb3WKlw#i0SB9=M?3xW$$4M zu&07!amL!=2b@muba>FmFOsZRpA~S%X1>udTr`qAeJZAJ!G{y2Z`pbP`b!W!W-YOX zv#|f_l6IPoLc`a zvHHug->ZH%b$V!HK7QwWVr@caEUBh`>D5`Z3z_Lz3eAO+(xgw`!@F?v@_r_~#DA(s zd5?b0k$Y^uGuO@C)RCKv-0*p3F82A>&QVFZNWzl6npe1=^;Tfp^)nB$5AV5=_g21% zKI$LM^;;PG$|GNuINdsU7LKF9U-j8@HFPVYyylJ(@T%Zn5raRS^@ZRU@E(U@kf!-X z^~BT4Seu9jlgTIeqwV)Wo(a6Go_5wILGV)|mnFAfvX^)L)j1}UO zq@RpWas%;6hE$e?jxhGhS)a=Ot3D`4_ZMH*(EqMp#(?rPqg8W@;;|_HOQ=+_+ka+{ zpB!SoYrI{;|Hco;`Uijiim>E%4RE%guWH_tOnRT^>=)!~H@Hi3O*Ypdl@mkpdWHM^ zQOX~UEieCcx1Aq8gueG=9;tWbEK4P<3{}yxBB!0J@?zNlA zop$x$u$+B~J#)C9{5RgbhKspZE6JGT8;DJfJ)Dm?@AMQg>mI+@Y(acjA2Cs(Se@k2y4h2-~AQ{O;U_FK=mvft*M&#*vpsA@ z`ZT}PE;Iaxi9fW*Ni0M7dtL#yIOJvg+rETvwO*rS`FUQeK8-a$wx$L7oXdODn>V|9 zJ7+qvC0{|is@-c8|66zJEx7!Peb@;M^`Y)pEH?Z4bT(J=nZ8^6(m6)S-ixF6EBerx zwiM#41g&$Y8YMS!&PYDb5n@ud5o0uh&Bqk1ZamU3?bJS9G#o#l^P+v)1RUzwpG%%Z z9;t#yyucQZ&gOd%9vudc*3&QTc}B@1&LkDn_vM^VdYrxxZ9NWS<9|}d<`K?8T2o>? z0xwh%CweVu>eua);Su_|nEsCONUO>R^043abI&T~d|BYUw^rv9FCfKyZuK4TYCHFK z`|ur^&6zXNaT)K6%wg=Cx_md9eU}%TDa-LO%$Nu4ct3qBp`QvS&`+bh))VwoikaFq z`eOJR8mq7R#_s!UqHJHzTNGnQR~_6rQ8Er%jyF?_?Q@FexXK3LjH9pAPcPF?>NEAz zBF^*OdCtjxs^#8f^;73~>~Y{flfF73pU)V7-IjpcJn|&+*YyeCYR7fsud&_ui*{V| zIe(>fEf^E+|FMke#~IUW8PlD>5PxC++i^8EzrHd4S}Xp7_g~63ChEBf*b#8Hz^4^_ z)1YS@--{D)>d4`4wS@67CgGdOH_>z)=T3t3Wiov!I;%fV>A&|QCGr>P&a1hU7p`%6 zHXA!d^PniN7MvgD)$RPxMwW?R!F#`WvI4nRJ_w!~1h2un_3&i*Mc$=DGrg}SbN)r+ zxGl>lnM#`^gUVtusEP5l3;C0KF6GZfMxGm!k&@%rd6zEauDbZy>V?@S&sM8Xm(!oN zJVTDy@@!NT)^`5?QCK%}2JsurBZ8xtynV6C)>_`x&Rk$G0=GM^F#LCd zU-1=u2d}%pGcFSsAp`H&$sI?2?vY0(Ru+6wCJsX;mLn5Sp%3wm4}!}saA~AXyTC;< zC!4s>o2kEfTcl)b?0Z|{_p_tl8z?^uc|99heRXUujNgN#^OiSqS4$=3*8;zW^2me5 zCUJfuz;<&Eyu8ftyrrnD+1V9r>B~9Uj^g=lvx`C+7l_Ia&N2 zgr~*hl2<9nA<3-D?6+1gJol~DcT(re1C18pAbgl3?Y{G}cU127<}HG6i{W3%%~R;l zypMCLjjshS%m!EC1fA^iMZ|O#FBHQQ!q4XM>%5y1VKlmpk~&~Wei^`v$)3Dj_%m+? z4tiBQt@S@hQ@%FJFqh=XPNO`~Pw%a2+Zd<1WEu zy=B)AT*Y_Y#dt3wjr>(gPgffJW1&-@j`x&L{8tSB$&csvXM10Z%LHtNCFA%mU$0Ku ztFz5rqqeWE;C!>zUxlx-kHtLX;6Dewdm*}9`8N!vHf% zmCWc&_ioDN%$#^d>le+T9`Y8!cg4u&F~Hm7O!kxmb7r^3SNWY2d$-+sj2h;XW>Ts69}Xkq57Hq$u;{0wEl6^w!ccwk|rO0wLcE0 zope3V!be_psBJ~StH56&QCH@SLv-IDrfVnOdzIt$R=Ll6bPjt@m9yy=7k5Bq<~kOa zU&9@ToJ-0r7_T_Q{l`?5FD-Vb_`GK3?{W<6%;v+dW}eGl&eiy7GG=Iu%9}}_x3eZv{zm(o25a%1?kF`pO3p0NRyRK%WZizq9& zz{%D#a2lw)I~+R?dzBv{n~=}@??6Y&geMJjFwQ6MFSv@bE_{SeS61-RDFwf?2R!3A zNJms2`3{^8hef$j9NN&mghRV<0SC_Zo)!mt-Nu?l>z08IV`ZluYd0Mmo{23!BT!Ue zOmbxzoH0KQ-G#q5ire-c@K1z))a(KNS%URdCdg2LiE4VC* zn{@`dJszjqg;Ba?p)ZMU^Pr>p&H}gYG*86sXFcH7FgZrI)8PjE*~nS(%&*YL)L&() zpY$g5!qf3gV!bg{h-1TN`)Q@tJNvZt#z&gZk4BUQHX zylB}rPqeHF?q@7ZUv!Ko4?LXP1E0jI=R;buxdJqPG7SuV3E-?K-&&M7&<#122A#AG(OzUxvP81FVW559^pH&K(tNcMm+1ST?zbvPD_Zvduk|jkjC6q1s*1L%S2GJPOaugO;b|8F-~pJOiz>R9<{S-`X;^Cs_^t!J(lyPAkW$FWxup;z96`%|Li$ zqx0H${2e*+YESJS6m9>^9_X7$yLCO3tsv%x_<36oW#jxb7M;r;&(XEc{w#D#;arOj zE1b9YfV0L~oObQ#MxvcbIh(itPX030C%#6v|3~s0l97Uso@n>Y-+|vA%U_FLtNcCU z&-i^CT~IoM_oudB@P3nWIa&A)_tf{K_cR9x{!CZsjUK;oZ(>jB;tQMK#GlaObo`k^ z`P1Rz?tun&{+`moF~WA##4I^`7z$gXI!?cocf;pCVuLP`OIwmx)aKo z-2ZvXJNXNUNiG}&_e6?cCIee_NM$@+k1{>jZ=(MHI zAzkJC8)Ex#w|&-U za-(CUZyvDazxMkY_9Erm#~$`Wz<;Q>5!U)yYYgpilsnA4x43i8%U&((vyJ7w3-apm zk5?PDuDFjf4`O4>)f%F=nP-|Kb~NHQ(tRy){4VMDzp(d6;&&f>?srwk`QIFG>x2w! z>g{!N`p@uMPjLoh-*XrHiy!JgmpcPJcTY10b~^h9_BsayN*$?{+&NtGmENp7UgliD zOauQkqyMK>-&}f8@<3mfBlCCI*FAf$(tXJ8z|LOSkhv4N{|v+0@mXZjbtOj2#l70{ zDu)>ZFJ!I06WhV8;ln2A`)`i(eJpslJmyV`;_)(avI0D`*4BQ&SnT^BGuCvT;T`DXMj!V* z?3%k7c)J+;4bXfH^m!ZlC{GG^m#Hl)_&$EJ(J~ghF?T_FK8HacWWz7JkQdgKz{fUh z!`7YwaG!X%|J)Y^n>JdkjpA**2AGE_*S!rX32i7qSG8$L8>XlY)X@O_d#Xe2cocYh z2N^BIkqdZwK}&F(&3&UEU13bD1$9;sy=qjv2jFZ-)@yS8JyzX00pfNl@=Q2ytM<o;BTD(~OCF*4j{T-Haje?_0p#C!SIoHvH4^bXt;OkHgQ%+nvvJugEtOXxcH- zD0v%sw-34|<5L?%hU$(!pV`m%1U4T3XJ2_wz8SjPaEHgRmOos!Vmxj6w;o{MlmIsy z{a<~R2E87O;atHQ=>poZgLW*WE;r+SSvEX`-ktnI!>SL z?Lf9a1r7g3o+s0ZiP4R(C4syj8a|9BSoz3y3ZpogUdMP z-(2jyV+&FPDmR$?-RZwPf%XQpi2D`Pck?S~z{V-Rg5@#4f(HBw>K#4z!!`JR^fY(H z`#`uIfPNZ-Znq)7ofXO1+{Jo^e$8bqrg8BWa?zcGd`qCilgQ>>z>AOLT6jo(+Q9ng z%k-&i3Ju_vGRUZ!6~pg`{EtB{NfwQz&T-H_h59`)nN-Dh@m(%!rPJYeC-}V)%h%~T zLq6)>K)%9NQ0jkyZlm#c9}|D9=E<}$~hu0F}PyQok6kU0%Fz{=cdEY0*A zo8tU&I^Iwj>9m(oUa~*Pys;NL%1)ceBiF~iKS}}QYp&&cynof#wp^u;)36hC zIs1A(;wr!PDzKeBgXW9=>Ew)RSFUhGEey|gE>%^BpAd>ljlDWWwr7rlv~ zv*`!GjO$!AKZ%qG-gfkJ^DrXnmCWBU_#LQ@Qa8Ovbk;1>d5+_%5XaBaO2U#4Z^r z-y+%PIbXsVwuLXuuo|ax_b%%(&Ta1~f5Eg$7&rJBPswNh+F(5wNH5*AetPMq=DN~Q zpziEV>t8#2Q-HIp4G#HF-L|QoHQVxm#-@heMyu9s_^MWwKjO5i14dpoV+&tU_Nb!i z@igRL6@GFVf{6~>vTzi85%dFoO=0e_w&QQOh-F{Q8aFG~wMP9{?(v3OoM(pw-$h=* zD)-YaiRONUWESwfrmJMo|;0eWnv{0*ID0Y6aV)K&LlIx z1jZ9Tx}CZF*Tjs@M9Oo^tj*kR+WQy1#MVxt+BG)5xs9tZP1zd<{JF;?j`=o zK+|WkmRqL#Fu$AUcd}pB$Qk83ZsPvc!QQGp_#uAt%IQ`s<$o~HTlJ%9uDLI>hUs!< z*lX3p?C}K|+ocY(@*~!)%NYyWD~qpLgZD<)taFy0T(iEkMC(;ozz?00S+9QcN~hHY zoaL-p7lYG4)~v;>S^KkQ^`jFOvu2Iwxm)X2;%BEYZ}w+h=4V}|Z`F+T=6leY^HMy5 z^#OGI*j~G)+H2Pwd%fza)Lz-l*t5Bqv(Pb~)V-vy6c{T5@T2@Gn)ucV&nS+|+sN7h z=;*2TdNsAOo%O-E*mK97?D4a1?O+|YRyxWZoRekUx}0@ug~w^R(Ib-4BT~>IT&!D* z3fy+==AtI{J@KC#4BSK7Fcl{UV^Tlpc|+T_rjBP2tU=+7ddeVSZlQgs4z-~ zp-tBa%ZD3btxwmYlUN17friVWVHfB4YPlQ{3p3@S2_MC6DhN1f|;h;6r*TDgK^m+a01gcv&Os?)fx8iIg ztTw%;{IscJ!57;!8@O$JPo&RPiS(h4IDOs*Rp4!n)>%Ub=Cc+y=i(RPGOo*K9o#$zpC!gharja{^KbvFt16aO zx>J2+tb^_Tzx9jl_uQt5H*mz6jkrQ6>{96o&8;&bn;z7S^KTi$tDiz%C%3@W2z= z^DKoIJ|eAneANEZaY2+XiujkDRUa<_-q;yEk1N$HzL^al?#YbG$!CQhePLnCIg_l; zrOkZ{&J0)W*)t>xw;H&Kvfx<$Nm*i2S0ZmYu`!9CgkPM;#8WnZ0aN<8=-q(r^zX@X z;ZR21;*V_XfOh{*JW2b1WG-x^zV7pj&PmM%c9G~cjrsLrV6tXk&YE3$1vft4;&S$F z%4nWVX3j0DbqA7Jx65~WBzEGU^<8Ur_69$nA349- z)BN}VI49D3Z34Y(;g9(EdmKKB*QI(IiRBX0yIzUwU4M2C_w=Lg*%zO=zh6>+kB;`Y zQ@GMc74WC}Nb=T}vk5Xo^*xT<34yEn$7GMu?jLM6UuOJj{iXHD!kFxn-8L?xyM`Ec z%sa_w$vDZ2|0KW7Kj)kow?*XN8OxhH`qaFMys369qV2zkwL>-pwW9%f^kv$yGFI1i z_V>?3vyZ@QJ$Vz`+(BKRx0%?8!cT2hpQz1iW3W51Rh#`zN~M9nFePeU7p7%laJhL#72>i&O48FpJ-47-M$PB66dfi6JJL7XxmS} zxuOBQn$KGLTAJw&#OFB4>p1U*FH5r2X2oh!e=GnW$w973@SdX(t_G?~`kK5Df$Y%lLCE{PmM_)Q<_c=I=Ghu&>)!%?GL}LAk?QF+4S$UCw`6WGrg+svI*wWM(7`~pp6Iq@9blQHUAgT-tIj55xgLrDf1hZ z8PEMfaay(=ij?3lk+GdU6ZK=+%MrW(Dj1XT{u4a){Y%KAzek&Rzo;)BU~g6Hyi(4q zdEpCp=Fqv?@7!3(ntQZ!%`EiW&BU+p6b$if=S-X@)0ps$5lwYD5G2pvZZ$4|>uWABP^lq@2YW z_F#U^oE+1rI(RL2x?X=Z_J`TnA!cEZC}#h)h;!i4*lTY3ZQrrT5ZN)hSj+0{DSn}> z5i>oSKa{RME`lC6Fa~F#Zz}B={g(6n^7lGf)09tJ`kE`#`x^HP1ypy=9fsAO0P`5@ z-$UTFDZO;c`q8CJEo>Ol-##HtwjkB}G0zp$v6MP0sYCv&-SIf4_%)=5(!H(1LGiX0 zV!P{d4zc%QLnET;qFw0x==1ONJS6)3o}M$K&+qDaboBXGJa@s*f}e=PHqysa&mL!* zr!O?HW0TR}iedFU^OKWrN%Y$!5#+t!|Z<-LJyrw)j9C|H;F3(j`$KxJjnX+dxg1?(GF|# zy2~w<+gz6!;++3+`Eji6&G|-b03G16Zh^5A-vH0q=MXm;`GP$+T%%YZZNvP#9mcy0 zvWEKq%>Sb`6Y(K1{qyiSS?52o>NfBcoHF>e1%0k@iQ#Vo){lt4O?~?%lRHiat8K#P zu$~6iEMQ?H^EAfb2(RzQ;CTD!yibN_quNLP(9r+*UT8L*dx<#D+e8e9gXep7-)Kvt zvoxf50VBAdyMs7EN^6IPN*hdCJ7*C^i$-v60^SJfRodaXoQuB7+uB%1d`x&&d5)m# zG&;XOwHbSp==&`Bb+)sOd7{zzN78Q#-8aXuh&vcAUg-JzX=#;plI~YPt1MzSnBXmcl9bqgJievqll1+_Dvjl@k~ijK z=8t?&@ewjSU#AWFc1^;!5Z^?@0_=C9Srh;5(+&S0RVGF+`S3^v=2UUcG)Ch$xu4w5 zPn?Q4jekm-%D$6O){Fh_O1tmU14Ai)0d+^5y|A06A*cEwulggm1|Yu%LYs8XqdNVU zvWKL2VB9m>@`n$-)&zW));TXDPTBC#jgx0!%N!QK7xI$1-1{=K-w+=$f_Dxl4s86} zjgiR2oU8JD=){4N1%`hlw#VA%6&H6zU=Z_ygSfaif>S&$Zu1%9;(l)d_aV+Fb_!>B zJ2-=^x<6!$=Nyk*vW0oZ<}u)k#~!Br>zof-E_9}aE@F5X#Ex0$TI2Q;|B86A7ITPV z$6~(@n^)9KYjpl6d~_=+Y0Wg^k%VP$e}AFZioj=CjL-L&%kC@lTK51;=j0W4_;%_r zW<=val<}N(xzVy0oHXt~X5PuF=6vkLq^itY3~Tso(;8ehz{+@<*wxs2wSQ%?cO{$Y zHug&nUON}M&w>8e(GOptKdyymxo?@+=56?09idLi&HtpGoNu>zznK`i`4@0@bu@AB z#(AxD;4WZHLv+8nyrwI&B2CE4pc zdk^usdb#X<1(lshUk&E>8nI~)5Ie6P+JDHI;~LHZ{`o%QI!sUUH^kBm)9o|O4Bwwf zU+=srr1;UVfuHKg?PCnsP z@Tq8YaEZ~HLVHTU`#AlVZ6^EFHj_T8H&c8Kjuc-?uGh*Zj@A0QVWB-27~wIz@42A2 zHR>XxWH4<>cGmnR7dlTEGkA?B$@IB+t^xK}fg5}=$j+{J3^5kBLc3-3rTXl<^pWDrsBf!BME435n^pa{iC?^L zL)4|deK0fHx4-AxPw1N`=}&{$%8Iq}nEG~fv~OK~hSIlF>^S6#S*3ohryta(%b~ma z)J>mqR@FD@)@YyJc!$@T&l)QQI*q084$%)O)$7+3(}%Ca@6Q6`Aa;w1ynBJQXMxlD zF0k&KC)z81)nMP*(Ag{a&cVLJ*ld+PjI?HG_8t0GvD1$;UmYaIYIEsSYbNw*xqX_Y z^Dl!K>tWWVKUs3Qg}y+%z`;8dr*0lLpJVL9-ib*@rR7LPU=;>ZozWV z_Vsl)hV<?wU?w11X>CTYnyR%Ao@&_M!Hd|AVr@^RKj)(I(c?mWy_Ejv;PT z>Ik1J6+TZf!Upz}chxp{;21QOtPtPsLr3fN%w}|g5Rr&*_S@R3@6i1Zw853G1%m;$H6<22bkBTJXju*Eaoh) zZ|yL1|5E6<5SogXmm!&qwka~IET_^~Li1bc%& zmJG^-57Yab;bjAmuk`JG?5|f}oK&Uz2oH1it3c_rD?VrJg^t@8lOMbgrQ`1ho8j$g z(Y6uWF6^3+RQ1+*(Kc>hVTAACOr`qe2snxV?;UNesdlDUE~LFnSzo=0T(5A9*pbY5 z(SEVy%2E0z4E`nbv1E&Z+?H&)i$0JHX`qkwKJWbIHCcSCAYJ_-m>=6^hX;hS@=8XO zFKM>xB>scHHP$C~e_U{-^(_4@c@ysw)?D!Y@Eum#a9>l}P@jYP!_e_C_+-Hc*HD-0 z(LDMwIAl6Ay54wTN@zKKR``oZ$;=_@vvj-9eBJu&p@cru8C7)VR~FQG6w8D7*fn#O zvM$sdXs4Gs?R3#(8Q&K3Z9L!bS(sxor{77eeoy=z`LmFB?Zu*xzhcyLFBPy0y=i_E z9egfj>FX%mg!*nFU1tg7FmD9LAl}<4YhsA;5^2&I?L5@;72dV>vGW*C{||U~ z@ov}qgNvi}){OI;tbiz z%I87)(nWk1j3DvbR9@=<P^`4LD+WB?A&@j7gM>m{U6^-kp7=Y23SBFUJ9fRNKGk}`sb2wBcb$4YY5#xJsaszRy!nM|A3AcmL1SsRtzRXEM+KlXdFV z^g&NL^-9wJyL9R*>iR#WQ?Gmc^g8wT_?DU& z1x9zB`hC(8b?OIsPt>V@&HI^j>Qd@R)TuM^iCb|( z1AlM9m=n`G)A^6<)~r3F_c&oEiR;+2Dch5deI@CMI(DJzK(AIE>nWe8XJ4jo=-0sn zJ^NznjqAG7@z3QQd%As(kjf|O+G8j`M0*i2U3)ZnbBOmGFQ3WXQ-XnhPq~vi_gR$d zu5%A1Em7xAey(SoJB@ss!N&V2<9{h=g5i5L3Y{o#2#zP&Tk!`X8DnZcQMD`q`ww6OYpChc+& zFM1|4+C~idnY6K?F0sp~-&bB7Y5ga?cZ+J@iOqmFNddJ7$i`&J+yF z+OU8rSa!K>7uw|tDW|=W?y&v>EWr^x`i1?S8T7aIcjEE(e{P?172e{>c%1yD{%B0< zKR};P**lDE?ZckO25S>*fg?wL+M4o>1-Lw75yND4l(rdCRf8fg^hyE56BH2ihBC&&0luwG4amLGUS? zpX7h#JCRk@*smJlqj}h;mH+Fp{FG;$rw2^#T-N%2Gk4C%&#Y$!bU*0HctY~E7rgHQ z&qY zTlm>@Noap+_x2~i*Ggs!#<5_h0H_CLU@j>nZP=Cj1tbU8P z`TL9ie*3@GL;H8dV1C|yY+>Eozvdrk|F62?W+uS##oGT7<1W6JA1ZaTS9WqQUw72# zjGE}$;p40(=Q}F6yGrMJwC|vQ`HPnlgS`fOD>n80wX%_O5ASH~qZWHK_~zx=-|?|a zCYGF@v-FJrxt`lNOO~7(eJUaL+wK%KQvduw^Uh*cMdkA)!c{yIr z8*vUKcz}39jA!i)A0{?8cSq#?=E9R_waSR)LENpy+5C5oF0eB3`O5;onT%QaHxcvH zw-A4zCA9z!R)(U=cV_shoBf^`KGbI*j&4MAu&RwuXymE-|_Qr;cJ~z zsXK;d!6VAE$JwyDm_6pNk|zbYob%YP_;fDQwZmohX;>Y=Ca7nZ>07Nk4u_iT zTd-&7W?wXf{Vg5cqLzJ0e{NeiWy)ae(e>o}4z_w1 zFsh03T+a7;e1c?K(;dkMdn`Y{>%^*U^hp+Y){{5!KEuR)eBIj;VvnSau_SzI_WBYx&&^sRg2h$%sKgs$%cO57`Z=9xq!;y`OmvK6nL%1XALue;Hkez)u zG>qSg@w0D7^|hQNW8-1=QC8h#SP{q2t`7XPBnNeWt;Tr#o{OYqCsuujefbx)e`T#;h$HIK%J^_hD!~BE4xYd2$V6upl=&Xq(1|?mSxy z&Ke(XXrT2@4myIqX}_=?S_y6^xsQ*-IR7kyf1ZVZWH*=3SYp0JKAfMBPyS%1)5UYLqWcu+ zNou$JA9m(su@B81J6TEWr6ap@zEWVhC(fu^%ROSt7$=7rThhajFD=b!8SMKG4!GG9 z&0$ZDvu*p`u{(Sv`^HM%ox^^wVT31U6bvGPd#)p zxVINv?E9_Rw%(!qymFB)| z=XX~RW*_m7FG6ef^u?EJ9j?kCI4@(akPJE;j%;NA-LH8m=Q#K4^~GO_ajy93r_*VL z`az$XD$+Jq~JTVmXyrN11lyp;6P{AAT&l7PjBpoc=?x_y28PIH&N;yim&>@o`?5 zmt@~7{txrQjXm(f<2GOR%nRUh3NKs$FN}s4Zb;yTtin@x;U;L*d|u&yEiXKI5=WaC zZi?{&_zVG`)A7P&aH%KnDZD@#H)RreVI2AXE-xgM6E9>@2HE|Ic;UXsqP$?g!wYxE z-v76G;p@d`<^^-mNnYs4_;S2ZQTRo<@V&y*%LQ;bg%@6f7hZ%H$_r1*g|gyPc;S8S z&u;!k@qaBZ{PRg1ZC+Sd7?lg)GX#82#|tgsQcvDfc!4r*$|UkafP8e7rsxK zI4>Od_NlzEFZQ0e&XO)sfGkt|R6k{fQwMs?a-I#PC#);A{_Os)ee7D`M%M_jwLzUj z>%`Vk#3U&E{SfqS!|ybfan3*8^HhKIZl`TC`*D`^M|5rZ$p-N`arg4B@NkBTa{~M4 zhdG{(|B@?F+E*>*?N z2b6qvef5s?t`2-3wN7hCCy=iA*@MNO`=~YolR5I0?Tn|~rZm5+>MG`T&HwJMdDL^d zaw~c$mo7U{tZWmy!%XmT)usm$+c_Em8pbxgzOu^+lfe{`q;=$20Z5oZ!U{n2}9 zrPGNw4CB;0V{YNjm^IuPa~W$K==5%gJP`ifyxn8{!@2vV6koNnbB&S$)*P~3>Mp#S zd4B$J*1_DvC!aCh{j#QViq7GG(n(q4qPA(G&1F@+xX;9(|7xa*XXcTn`y(gNhwA5S z&PGgu&MtV17>|qFfs=Wov3Of5G&JhwxLu8R=bXE3_{>yeQkZ(Q7SR1ze~6VYfX{Nc zH&Sc84(Ja5y=%NO(O(d&XFvI9kH_XI>XPoN@;l=7(}%L#4aN67{_SU{ft|@+NR8-~ zy60!rq8R=?)cM-Lj<8>zByhY_?yJ<8r>-et!(mLW! zf>-%t+(8hzU=sd`v9=EL+!Dh{F$=oW_c?H)Uu^o$V?BHp@5fnJCE_;@T@RVCxCYu` z<6fLiyc~_?L_8ajbKi;9^JDKo?nK2O>JeZ`Rw$qGT(9uZ=dOT{qVi%b-y}n>QyIfp zEF6v!Bf-mggF&=Oyw+4_?0kNKv2%0v<<@TUHBQgm)Y#v+^u%j0(y;mn_rr%ALw5*|4=Gayp29uC`i(OW8`|rTU3JK=Iw$h0 zX4=nzDOu=Z{40Mu{h<7tW9dP3W$?{Vn_T|Nyn1YzIpoop$@P!%=X|!-B|h9Y!dUqn zanhsv2SWlcaCaa5=c%{hMe%SMO?T^F@}SSQF@NG3Z~=O-5&i*nCDz>-$H^HT->SF# zH1+PJ-qp}BUT;11#_LpDPFLq+szYt+7~>yoatC2ABiuaLmfP4N@_(0(|BU^8UXI`O z*{=HX6yuUh=|kC>umQYVXpBR)=+5bkz;}I;E5zbt3`FOr>yT+*LAG6sjJpO|cQx{T zwuS7o*PWTHWy{F>i%RA!WTNxY6RQ?L<67Czf7{1Dio3#}ooKAgeLS-2UhKZ){lvIu zl6{v{-nUMy`Z@E$53}<9O%tMVD(&~S%k6U?g^Hc=l}m-2NB6*=_wb2Tvw>~HcsE5p zWKVM+uKGO&#`#;#FDp5~BktRuAMC}-UwdWfF2tVL^iU&eY;X)AwlV%2N3^EhjwL^=l# za%O;jvls`>b!Sgu{t353Z=)>7zm@+5wEJhY|Izkw*iRIXEYou(@id={G>D1PS>u{(RKyXjl53g;xyW4-zQvW*!=VN?zHsM zX2TnuH}ZIH<1XtA-c5d@&%rwEN(J^oLpFTPgyNme)2RexKEptehiOkOTAyfH3 zNPX>gTcdu6al6Ac^ufXRy_^$?*8f{@ZRZY9?J?;tyRXBix<_p$IJoIgS1o7B@TYBw zwI`1A>F$L*wC)6OPPR>VTYkbF%ZGPymaeXCYOtR-T*keW*e6+&pZRV{(KN^y)i}|< z)j@PfJ^vP7Kg1qGVm|m{&70y+2Jd|I?S=HA z?j^j0vrP4zC9-wVblMCJ4C$iS4mW)p*>vp}VMq1EnicB%dCQ}Fet&|m579R!xJI0^ zV>+y3oXIS5?n>&tf;nLZ zbm0bXXVebKUZTDBnPc-uhJ9D|ES}XC%{K-xgXploJsxQZVndC<2hXuDud@`Y*G>Cw zvhC|5d=EmaeC|;mbfaMna?@7wmZ5(QdGLYI^|s&akbsNvBstJVJ1?>892AglHlF7) zY;aLq`5{3y%!{!vR?cUShxzc8!mM%H&mF+J z)AIyt9p>lCTK0J;^E7<$%5dnP`>U(MnFV*`MY^K&zl`yD8UEJyAlFLx=WgJD^2xS) z6gs%rYwV~i*pO4{TJvE_+q69ThXV~^v)0(a8hP41PkBQJU21Da;0MsShS*%0UTkbh z#!lG>XELUOj02s8@>JYErSwpA&qwFqBuD45=OY=YeQ2$J9F%M9Q#0qe#oYOgPl0Te zmn0efpK=#_J9*T=|j@Rd+Psr;C39}{vpnhUD@C? zCD!+cZgyCQxWiH7KzGg7h9V{MyJ-?UWVGsAEqM0+&`A3%iR!#M#!p`;e>loVx5G!T z&@Ra$`HP6IvdL->IZk8b*elue8FTr-AJShY@oHVhD|2#}n#4$QRc0|RMgIpD8kXpP z&nS(-V)&s5zL*JrTm_$8$r!xiJWmpe6y^D%VsnlLSk+Cd_?f3;{d}z2g^{cC%34Ey{ylpQsb;N$q3?N@HAK3l)(7ce4+W!}y1wHNW+ZWUJY(O+y5hw) zB3`{192c?PwtWeozWM=UVHV@{T=7(r0fp_sN^x2r~krGOz|C9K(z3PIl1n8=re*T1h>0H)9%B7Kp zp8Qya%a|k|4Z%B{IC;_){-ey6Wej-YG42U>X!Zl^T|96&b_4A7YLmR$>s(3OqRD2f z4`=0vcFL%q(iiW>mantO8f&e`Wyr#eML@RwQ{bStzY6F6L*l0!KaXzHe0Jz+AA2ee zUG;A7|5`96hb{tVPF(lntZT>S^|RJ8#X7X;sy-*6`cQ~AhS8Dg&?%V1dotfn4Giaf z==$iU3xV@#Cw*#aU>NU7q`%qE*eE(h56B_UMi1+@pD~yI4iB;P;|Bhhs=kusK>Zr* zE6MC5Txx6_Or3||ogHKFzgj=DJ2D2@Wt9@T#L8zJzL)|HiJ9)_Ca-Y zQw_hu)p6F#_*lIf`*nPiJQZPFqlGc~Y2Nvk>=a>cc-oibCKErY3|oWr_9|={$W!R8 z;{%!lj!e~;Jau^S*}2dvx!mYE7yhG>x$0(b$z#t2PrtKsfip$yJ7YhC2lpkj)U2~{ zl861T0^CVX()}PlY&r7VtK`?iK48WU=U$kZN_=VvgAwaZe)GTIW;dPa~0Y$7xlL6dhyZu>mO-aj{UIQPy0J!^-rLF&8<(+ zp%eo`zxw|--p}OWe>VB@`Be-E{Z8O{pZ9p59B3uyjZ@YdO`Jbtqb+as4<4T&xErXJovt*G*XGr)5@PlH?6X0EjfhnxX~B$18jWoiXeQjIe~B3=Z--8#p0v#r!kOE z-#Zbtd>pO%>c48$*TB3ep+3og$MI*h)*QDj_i27Dq^|lZV{bV!>!rL$ciMWQwWnNY z1SSr&`WjH)3Rm8{IafUOhnn3#{zK$qe=zaUrlaFNf8prtD}Hmd<~yxNbDsL^(cM>^ zJeqALzU48Kc)TGG>%9lLL^vy5TGpkz=Pg}wr>#p0f0Cao$v&6K{5~le4SDY5MrymzJd)hApL ze7P!PYNI=&tNHPasT!NBpgr-l-uaa`{Jh^fAFVRVuX0(b#BZRS>iq1oFWO8j8aIs^ z*$pQ2XD-C#gp_-%HGYEuyxx0HZ&Ey!GVET`f3gks0*+iBXuO#7!((Fq_fF@`GyJzW zJunpb+D6*?rq!3YGFnqr&XZDC+u^RPwaUDoj{k})7^#ZYRXLry^MV#12lgxQ(OCar zbkjTc!-Leu#t9aWUz9|=72py*zDeKK@jPqUCteSpL=K6+?>)j8$d07(^fblxFuMIm zGZ_QXh}oHRU%X@w-5Ia@;!o6}`_r~?d^YC4AR` z>uv6Y*SJqYwvZpmad7AOkqB?`_-bcO_#_TrD=d5^hZe=~rSB`>b?}ueAIt7fz}J8A z-87Odd=-JOHu~VHyflEVhd2vQ)K;X`)oM=4PI1N;B!-)d=@UMo={p&JA2Bw1;K6^H zg@>o^1`8?WM=@jn&!T@snhO+`8D8E*s?5b;g9iX8hrIAiuUs|D~_- zbv@ZfT~||=WZGA6mCo9e*T-UFeo4uJ6a1 z(BG12tvLbx6eSuHwJ)go)E7SJdrjGB{K^xINh=v|?Oivc)2?OR7r)8WngT8#M?Vp- zX~5q3N9<6;e-3|=jS~LUP8xc5PThQeZp9+$!t$$24Xw#xt%g?OWk2nmdaSgV>?a#t zAu>s!k!v6m3|wn$+=P$2Q+ESvR%Hy9KL`d7{-yA{Qea&PtnKgtGe(aC;&DZt!t<|= zIe5Mj{LDa~EwpV(Ri?eSz#Ql~p2?xNu=A~99M9n6q~oL5cO4bLHm-})|DjAtSHhGma8In>+*Z;=>uLvEQ%(}H zk3IiVLc4?$Xa2=#tCBuTMmcngLmx4^z=yJpI+rjvv#*?tjAZ)(BM$~|XUz_C*1)2h zd}L4da3ahRn~tD`DtxfOwZ=-e4*AJpZ|}Z$jHN5FCR_<#XRv2u<8?JQCfx-rd5N`n zPd4jic#9d-+T4MxxeFS~{!i&L?x0Wpq7~U%Gqx7AdIO51bv69K=hz7-vpQy@+(w^% z1CNb|6McM=#aByg8SV3Xx3f2H1ik|dpWnNqG5(u9U;4nuO+E3EBmaKz(FUB*rqzE> z{!D!P?7p)fVeNZgelau-ukPLd|7pL^>(%coV*S30em_dTqwWFB_mtqN7bH8RG2hea zYk$U)`<$y>f$JsXFJNBEM}xU`?0V$kI2-q2bg$$1%bu}uW9!rzr|^&~o7$6RHL<6` zZ@;E+!px=!^E;g2YC7O9YEpd%@ZaFP6_44pLv_d5zrWAkuqfSF$#jxs2Q|%^znYZ^T9p|5_zj-N17k z`){>lZ@HfF0zZcA;#IEPqZQJd`-VYtt<`nk|JjdBqIonN+ zE%%KF_Ix)iu3#=QChC9hc5eFN>GW3WH}J9fDdTk*{u8frKM4#P+XKjKXZNG=kE()) zBJ(3}7CYxWB-3UX$(_g+#aZ-U`_Ax#tH{K3)DgefNW?t|F3 zJ%)X5qLO`|MGyOfk{7hTKi3l71RrV^e=aJA2Oy(nu`X&n9Y5vX^}k8|kr(ob+s=LH zmEQ>9`?a;;kD~$ysRRDit?^3w+f2rqxL){h$MdvKc)9raEw^eO^@Llv>wz2ay<+sZ zGxCJ`y2i%&vPx)5?evh}Nz*>{nqmH2jdlE(SiIafE>LurO;7RVO2ElMR=4mHh(nkDz#x&upD8yuUTM72V(H_HYMD z^aXb?d~wC}NTIo^aSQ(Yg|0w8dj9Bzn(H?a4_;?p?78lZ^O@UBU%!r$)_~87Vyh@t z<&Rwh&PA)K-Pl5;4+rGs)%jIl2>ZukMCXJ*q#GyS;8W72Q@xJ@`@Wt>c^Kx%}_2 z`4+Dw{R8*7&_H6J-EMk!OYZ+UZBSu=vWQS#t7D#JWx&bk8K z+l`=N@_u_MbN6w7OHqO`#fmcq>GN{L7|C7GDSTj48 zmQBs0d;|NGoSVeY$-5idF#50j&b&3=*c~Hz@0iP$419FHVa~dz0z3+~U7tp|7J@s_ zC?^=tmP_rkTr1-sJek~;a-4n6;U7h}VWUjZd-ryKP&lurU2u9N3ArMitg{u#mXGM3 zXYj$GEdH90`96piFQlCEOD_5S?Zg$s9;)%$MLyXLG^gL-y=0luj2$c>`-yxr?p$P_ zvlgAIzIx*IrBauC9Mo<(b;P&(e`4((S9xSWbfXrdBU;=*8NEB{s>@H@G~wd}Fz**# z5I-bnjYZbiFg0bpTv>Ona@!B-Xp;_4Wa-Bhhh2_hSB!(!EMsu{GI35JkRkMS-#sdc^2~& z@T}wcJCBi7a{I6OeU2xS_r*K~JnMM=&SPZN^Sy-cC48^vdp+OlZ?7n;zrBIy0MAYy z-@5SCrRxq{tzYA{BR6}DTYl`phvaw23-{0u#$*pN&jv^4iT5vVzs{C zYivkonPht?y{x*HbqRU!tls(Qt;mLP_Y|LGoRPy@L}Q{Y(PZ%l2~%|TH&J5{ThH9Q zhp)HkOMFZ8BKk2&cWCU>T00N>8l0D@nEy6#Z(KVq&{#D+s69jRA;q0sF`9d47qK71 zp8xu#E4nm4#KR9DLgnp?B8flLu!7)z)X7vx85ur@*?_mBAi(7CchC&F4-f z)?PaD-0;w2rKdN&?i&8W>*N` zF4R_#vbn&CpQ=^Y4)HZ;Pi1e!eyMU^Xf4A$zZ1K8agcTn!4q?qOq=>Fyg}{L?rzl= zfv>U_Hn)(coptK~{?R`7`8Br!tLC=SEAh2$piG3dI}GpI#J*0%ZQZjyr6VCfch>;) zNZMl$qKChHydQal`BerDfX7_n(m+qyb)T^jzHy}Hn9ehvjmH*Xoa&%2$qViH5(*~A z7i>4N3|=QTuk5S#_Y4cS&i9>sZ+UNZaPyh(=l3`xsW?$8FC5ALM6#FW_SZz4;OHds zYTGxAT%8BgH~ddFY;&C#P@a2`A8)Mb$GHI`MQ#5QIi}^o)%c|mdl>i+GR9B5Vx3u> ztnUq5?7e5jDo@)S-SioBsd&L>ztJzg4dI*C%QCl-C>mf*O7!5fk(IQgR%3YnrO(#> z65iN9Si2p(kV-t9scQJzqfqKRQb;c7iZy5-<>f6__-%{*K?1|R~_9KVB;D1^X1V4 z{f~GrHC~fk5d7V9%e|)2(k2-hAEQW>8R(`S&Cke8{9P|(T?Os~l$Gzmm)GR?GnctMReK`?&hAmW8OsRmMz9~m&rSarBXDrF z);=@PAKdHL&F^#2U(4@T2SxiC?&QvD&C8voeKkhS{%Ye0ubs)PO8IHP3>4P^3 zzhL^2!);j$CmG9f+;z1YOWjYo5V>_YXV!Dw57nOFzv>Z;T1z|^V5cSCYc{ca9r!cc zX`O!Ld&v@(k5wf;Rlu8;IHvM%NQz@T~N$hz9w%cP8pvVy?} z3>6j*CF>49BkKxJ+iAB9IW&)YgiHMnH=n9?=F&#|vFhL}!etg~j`m_Qn&>xWGPc?G zK58E{kvOY3v~IhNwv|tL6x(22`qQ-!xjefUGUwl*4*B{e4kYFRF&4(}Fs`k;&{)>7 zscvM1HR>wH8~x>o=e|MDc=E^Bw6lJuQJ>1p;+yxrUC&_i=xnDRkJnS9Iy~KL_GMEK zJn@CYQ|diLZu4$(~w;?aqsc)=_ zb@a{0uV}?dKTLnFBG$p- zZdh!jSw32t|2i9UHSLd}zmlJ&TU>j$_6_WJt$|nZU-)p+X7OKZgZ@_nuky?w%@ddA z=f8NN@@Ow>8vlLlMI@6pj7Rm!o^o02SLtS48te3HmQM!0-`f_(;&{=H=AToCzV{xZ zQBT?93NLF8Gd{Y9NU+*27R3#IoZ3%RMkw=8selPwFy z(^ofXvL9;wv+Cttf<^kLCaOy4`qH|^zL#XA1x!qe%F|M;mNxbYwTqW`$; zXR?_a+RyISb3dE&MKx{gNtUty^bY%c`_XG@v->*MXnRajLs9fk-=MBn+xRXVVFO>b z1Dygp>BpH{ry+-@h5k4I`I&vZ_&QRN|HV&rX7e5Dm0kX_p`3MLFT+G8GMGchsN307 z(;lMo3_!jR46IMh^`rBH|4tks4}L4!50d|gB~QQyWn+60To~+eoA9aUc(34Wh-?kR zZ!n0TP%+=~2j@)533$(EjJ5o9zY$9p9Y$OuYTjiX%q=tmpCR85zs|JsPD6hxA=b@m z>GpALV^1K?x)Jh&AK8NUqd)4-k2<$;q#&WTR`83C-N4fint$UCd^s7@|Ay~r-4{O6 z^-XP4m%bf$a~|ntBlyE_+5Y(PzWAqp5Zgm_+kROd%C5QJ46Gb`HHFmjZm41^$KVcsIJKul76K|)+&Y<&KIifM0dAbD{>dmfKEgFqG!xisy zk&RrT{5@>ctS|BZt~hJh2C`Hi{>+NA zR^&z}g_h2_9&yh>gnA9_o9#4m#}XG$`#<=Teqo==Iw!MOaIt^Ti9F7oS03)aKE}OP zF6Uf1bunjhbhaD^jg90{EXJ37tkc$>CGxT2GseTT2%D#Pt?Wtdl*cZe)8kww_M;@m zRC`oD%FA!i;!}mTe~05!UP#|{ui4+A_fdBKmnz_$!b!=yMzD;2i53U)4Si!Pc7!wM z$y4L@s!U{=BFcCwQ_p=aJcc{rP566fm5n>W|FxUhBkBW(W8q&`EIaUcaEm?0D~a(W zTs9t!PHJL))dQE--VdQ)ogcC3KP6`*b&9?Z^V>Vtj-5v_YcfJxl@DGgK7K8Dp>6yprXEXiOUY_Qe`0Wjf8((Pqy_~q-P2IL1w|KAmITzRsA9I-eSMu)XJfHfS zLYj+5=lL8T!s+x$wnlhovA);E&h=F?#x~#VdDd~^&VWPZ(3*N&n>@q$Dcd($UNR$#C!Dr3+MKFwy8Zl2mi(3U-#3gZqrAcO5){f zerf+-_-4^;4#UqJ_&c7k>2Nl8v9qbc!QbK zPE09$6><(nE9bY^V{@q=U1lYIBAV-s1>Tj0>;{#74NxF&nhU+&&_ z*>6|wTSoibANklFBiTPoZ=K#)=?PtX^KFm)mhai-&`^LpOSv=6^KZqE9h&2B=@g&C zcetalAovkBh?2Dx)4<2)0M6kaPDOebFHtK6^G2>vbkDV8q$ z--X}Bc-nDjU?23FacY>nHj*w`%rzCS{bf=Ba6lU(glhgG8Iw z+Pv7voqjjI-m&@9*O^brPJ;OX&gDDpgt2*aknXE&ohG`~*!NX7y0fS6rFf%h)^irZ z6N(~B|Ma`|`dd$*IM>*M&E!%89;(=@dHuO>k@uC)8`;PXExr9vzYgx(dabuT){_`r zYAwOWWyvtW`GtC%G0tL)<%{c+FMa*R@+-sNqaW~MOJDifC~xa%du^6)hu|xW!FLe&-gffC-x;^ggTye| z=rU?VJDKpTx2WUiUjla+olbC<=oh$6?2mswzbD)k_*@C@Ozv&|N3gsUgQXSN{{oGO z?mQpN?9M{3bMOm)%83H^+Fo#vA+Cu7cLRLyS>Ueh1^0`Lfp8htFECZ6!z*Ir=G1#z ztlkppkgxC^z3O=tK5Q@N5#BCAv8)*U|;K%T{oh&VnM@ITsY9BNED&fRU|`6sFG_?Uj#YO{!QiK<_6or|-qBjLM}`F99*`su;HO8Ba$ zUvt#2e$WK*3?uXWtJIGM-rG*sd60>6=24ch-{Uc}Enn9*`omecX6D`2*WlN~7kdJ^ ztevm$t~lawNH?Rq3Hq*Smrz7s0zd(pS=RWKS?YPna@|K1fbI#+^1NC=c&A=U(s@U|no@ z(68hlmmGSKa{qxn#@`)nmVE#}+!QNM{Ws3!xt~0K#Y-`+2yCW&K}Bw$fwRE3lRlaJ z1=8DwPYOK8x95Dhf%cJ-vFunW9r)K?LY^|lS@K{Te35%+t#PKU4cU7~2MqDxiKFea zGmNv9e(4yswWq)j9E_)YfalYm?uzSTO#_}iM%fhsVBMp8e1F34x^OQ0JVsy#u>xLV zk5Ya?jigJjRok*_OTV?sJ~h2(+19C6*?h`wrmWB5^VD|>wt*zdze>7f(Fkc;PfJJ_ zUsPL47u|iwvLg_Sg>lqk5rvoQHqizp9AeC$ZUZ2(GydUnh8WZV_h~H0GV(w)L_SbeG|epViBT@?K%-W%98{ z{s6W&$*Z#mvw?RC-x@u}wld;fL-Q|*50w$8rxDuLeWgpWTX{cNy|1X!2xyM8cW}hB z!wA%~J||W#>{`fK`7-SCiOl6F_a(Y`PcavEy^WtzTIJ1M#~(Wj%ZI$Tein`C94Vor z#NEDyIwNi)kn$08`FhSm14AitT;qVVduv;m7uB*$#C&rb-N>MejlDtQ*9iAXKQuO` z#OiUy)uTNZWQ*?e_!eKE2V9JMps))& z-I2tmyr3ta*k|BMw5T)6x{rA!XR35Jb9gc5#ul5{7c{>srW50|^CahwOT{agr=Orx zdWc1^gmX_}ckA@(i#*Rfg#AzR3^=EU6I!Pm{rM#>g5j;0-hqDL9l@Q8iIUZ31-sD^ zPF!DvtT7Wggfse8wp~tr6n}b{aS_d54b4h^*$5ra;a%rDC-Ppz`%IqC?|nUZY9Y2p z_|dU>Zp(*0n&96Xg5}JcD_L z@ucyLNl8KPNDciJ zoJ;R`9$JX5N$Cz>MjewMn*L@1_NX!b+z$G42s)D-<1>pt^_vSm4I|G)ll*~b)mV6N zAvnShXQ6zw-?VIa+*yyFn;7i=j^_P9z7GP2gE@=i3M};4_l4|42W1W2D*K-7v~7#Z zb-%+z=E$;ke4VO4U9&H((s&8o&e~&jd_K-+{QTn44_kh@fgOzNO`P$F$?!)^=I%e# z6=HM3(iNB&moX=b=zrUyahC4qLq14@cHd=Qo(hOgY@fL)q{MtJ|LEhn)){I<8+;qv8!td}Ulc#pwt}(#g&Ufu4JKs($CpHLkmi^+r9rB%6 zGIgrzcojI+egycUz~;bo1Muu8W^)+csJc6VV>ji;$J$q0f)87-1zRU|rC<8+NcqV6 zVr6LSh}tARe9gOxInv+ARh!yR*ZOGV(cT|)@VJv02-*YHw;hy~9^-s_oo|vy)j!3Y z&q(D=d5pGpJ@D}S(1#Dt56*pYx!UvM+mXn8Z?oiqRx9>X2{E7^jMd$KOV7HWqwX;M zl;4pB7r2gZ```F($7N^i!EyYzC*!|Cmv6+u{I7gF!CZZhdQJM=4(wW&D`k_SJ=LT8 z=f!8AhbLs8Zw$+uZY*nL&Bz;MSoGSY_X~|>->0t{dqeT~(V6`Ey_C3y%*{gT7LDqR zq-kxbFIdiGInz}uiyKBq8Vhr_SL%NN+ z2Qkm;Omo>`){-W^Ey|pG4|W&U6v|aS!a3$73-&W@J>c(>{&wd7h5oxeJ^okREg9j= z_bV%2^IG-UIM900`wVj%y0&w!%u_nEyNq=i{d;RcIE(cx*|KpSz8=}8*EwV1)X-jF zLLYm9teYNuE5t|SU)%U2L%yzCJ(<}7uBno4KqJ8DSQ6XIBmi_-2(&)wdG(i+;}?*Ukw2PIzW6+`w;S z40a2AWTcDcE?}KwZj>=cw3o9Ln2vGYNPB_WJKOI{?QDZbHgf0odVF>q|DWxyv`!y9 z-=c5%nYTd0;5W0E?@c9THdD40IP*FGP{AHqwvkqoM?C(?^jCi7)k~L5&7iz|ZkQ7} zQ%UbY2A)CtlAko!PJrVMp5xednPW%tY5O7gX)fOuHZf0`mkaSzF_?S$e+b+W%VGCJ zX&Y?8zN@6@MG{j*Bm(eYMe7 zeOrKAzE7&_@cp~-KN;9*0@LBdbj$xla6OCj@_@&PMieyTs?<8af+n9RN}=^1GA$ZYVE30_8=N|-Vp zSndiL!7;!g8j0Xz)F+LQel8l>%Up=3k=Jg9M!NCmghq5{Lt2bR)^ip??a=??ycn(M zHCq|Tvg&PIleIHkCjr+(nV zKIldc>W8e@A6an#IwJPLogV8EF};iX3aKv-YwchCN z=0(OO1hano$o#g$P0PdMIE(bM(GuNZ#IOxCpr4<<){=28zTAxdtT>306?oq& zU+?NTJ#94eXE@*5FGepJgRkaJGqC)x$elIVXxKYGHb?pjcTfSJ*4c94i!bl!Wz~*e zc0GC-a9A=ZXD|cHjg8Fn#~K+!`i)%uO>l7~IGF)%3c-;bcOa55^OSIR{P&!v`&VR5 z+KaB~ABt_x*u@4(8{Xh1dD}x@sN8Aa)dUQSc zi_7mKT4;3t*_G1S#ykcGAO8Y8+{nJk#(T7{l**n)a>zw{bKs%cCp5vyI^NT<<5a`H zb#}&t2WG4>gFk|fd)RPJCrt5!KgHCplFtRSqH&cjDqm;rz7DR*!>`$uQo}qf{tkRf z^TrErl^o*m)-`VX43GR=O3CYIz8-Lo3LUzMSSj$<%i+KBAJKl1{MOgtw+=70#!fMX zU_089DpRYTGxU?CeXWgS}iZ-uVPjn5;C z-MNvS<|@f73*93+Pg1@*%fN3lefN6&l$~`fkNb~WFOFNsD$nBD@gbdzPk1F|MpG95 zr1}2}u6-97lOy??zjT9ES}@$M@gB{+iEbMPr>zm z40*tMl6zc=d~tX<0|WO!Wt+uq9Db;i=6r}g+GAHoKn2du)Q@Y~Hg&;gu+QMluBNZ^r*^_d(~3ga@^oVUFr-16Liqm$|*WXSu!B z+^UAIfxUZ;JF&ABJe|SEOSYeQyqtJJtgZ^lh;C2KB4V zfQwo3Yp1SuWPRHVP8>>%6Zm>NXUgQ86m_Ay zO803<2;?SL*JdT0wW~VoZ5A@s~;D|5J9+<_W73bqq(Pyu^^NG>u`0bcJ?rLS<*@xbven_qm z-ld<2jz{xf@2UrxDMdD3$tCi0)w-u|wmob_=-HLgO=+aPMSXU<8E8S*l5W!ktg-_r z{cO3jGD?BfroPp;6X)pO~dy+XHr@m@$sCOsCdT1 z?B$T2$uphz@OA7RUGlKS3zR?Ww@u4EhS4G(vx~a3se2av@|f6^sG}WPoXWY!;f3D_ zDF4@02YQ0)%jVr!p!nq07$Cpuyhw8B&+}tGbHKEce&OS>VR+T}fYxm7^Qf%Oz_y?h zK`YD5E2ZQ2jEhl98hT64h-81RYm9h=Y>gvBr?!~6>}f5Fa5ge>uz#@CuH#4ra9Ztb z86VI+KCSHI9|sPnoZ8zK%PZT5@c8HZ{g1Op!QI*81H&JDkdY^~vDfV#GD5bsy;Fzg_xv7>PJHYB`}RrC@>ZFFntNB_4wKZ`iS4C4 zv+hQ0m&4ZdXFZ1}%+559DCT!|19oZjYSrHXO()?$?Cb9jIxxw0*S%tP_7r^LFK2D6 zz=l=IBmHxC4Kejj*R{P*T!WE}-CO)OX4roCJN535og>TyA7w_+xQ}?(l=DpNw<=}s zfM9xYLU5GdihHub`^c5u*lGgaNBVbLb#jKN_jv4>W)$xLH<=a0%Vk_b!X@#^DyFhe zId4Am1Hoe{8(jkGdM3vS{v{9nRhRV8-qo>w@x;i4~>%D(~4c)ExOPg zpL3z-84tA8;m!$R&#v(@CfMGIZ{hFml*|S`%HS9158ZD}FpB3eHr4ko?GipJha}as za$fWly2PO|?qCt$bgt|4wd`jg$Eq#0)4J9SlufKfG2yvYdhfA<{pm3sG&r!}%)?FCnhsKbX`)vk7b>Iw?Z4(hbV zj5@Mbd~wX$1=CLpg39v^WUyKN88`CPyZ>uiy}Nc={Fto=mY{oKmxnbl=pNNood)hc zDh#H^XyVlMiYJs7ih^g!Izi5D*Sp`JrZkPeQ|^#^5O&m&*c{SBDyw!HQ;cTC?@6CD zV^zw88LPU`F{04q&f)l6rWwl)u@5XdLEp}iADY&^F#qdD!keHM@m5Qh=bK_wE8nNc zU(yjaUSM#2OvU^^@pL@Iyt~Gpg9mso8Ot4@26%FRBXmAvqA~v@fxCrQYmb}qGTb?24 z)zF1i|5{H#{J`Wc)ei7|D|KowXV+j)O(N;r@s-Gi|7BeY{kRjw8@pvUmAtO8)j0kq z^z|^h!%{hh8H+Q_6Xe?rCd zB5>@-S0Ru5f<-oG(c&S>R^O5Z9sX&WH+w)ZNIByx71O=&j?SO@gIPlc)|@P#5qv1Y z82SNneh{18`XnPz#~ph~L(T3S@asB^uARo+5$^MMCk{2Q9d~b87wbY`yy>Z_8(|(< z1x}WNAK~RXc#&vSb*etmu=r~VYtBUIT<=}bZ7OTd4RJJdE$`t}qgnP&{5@V0EoF#* zpJ#S&#!pVX1=v2$hQ~h+j0+jdy36{lnviJ(Gx@Imsc+&{cPqBs)>j1AJJT$E4};#d{$%jG`RbebzOT5pSV(X8}JE@*&=B1 z1mpkrYff+Sg4d;q$u%Cvt#WkG!tD#R?Hdz(rhsu14ZZ;Fy-!}<)2q6r-wQ^Ko5O1~ zJ{{@!l0Zk*iyEK7AlWTgK4H%{l`-~yU*om#%y=DOo<@Kv$o;toUU)9Ltr5$i1{CgXrtY@Yj;;s*hKtZyamGvC(o z?Xxld?%jN!%J&10{>v^&M(#bJoS!t+C2l4*MjM1d{b;Cr|uW1OKU3l*x0>H}_l%%qsvlBTz97Pz z>RQhKxB31ukMwOP9@A~4JMob)iT zC~qr!fx?IKI%)N^`HThIDC?ZeJO{Q|Kj-lOGuB-Pj(z0)cMDGKJKJRnR3-*ra}2(E z;IrBx{pY0h)xU{WTdAQZ$ZN-89}ql#x%{t)!Or{^zw&UuA8}JXjb3A-ub(kR{zu{1 zeTLvxW#Sd|%8R5ryPIme~%U z2r1vr3tkJ8?`77iX7VcEcVp#=$1|yfd0)xB<&P0BMD0n|m!Gl=jgD1 zPX{Qd3$`Xr(9gUC;4vi6Ko`Zr!MVHI{y#y|F`_tx>*mN z8nJarR&)OUg#Y3he}Nv3!9Q((wxr<6#mHoiFWXL6U@|_djCIbp(EHN&JT^Z)xqf*o zWi)?YhK^RSUTQ4ubA!pDul&H$XO9@rN`{fz8Rp*C{dw+ph#9g>dR{)`lX=I$z#@FT z#&Q=WzBWDLMZ0B(l8wNLLx!vfJ)QCGtRU8<{AH~9hwM;>u5YdX$PELLZ3l8DuG2j* z6ol7!1|`)X!|nATr|H{VVro9ZI;^-AgOTGCO;_OetIE5!yM}argpT?VI_gL0s8Mv( z9jvKM=&0q$GqW3P9aX+k#6Fl*WG?K=z)uXmSzJ_UzekV@)}V`u2iWs4B}eC;qRfSG zRrR!R)q^}g;(35)9Zwa{TAoUtifJz}SB}lQ8vNV-a-ZKj=Zvphk%>Pq@wkf2o4Xp} zlSSarQ<)aXhCjQ&#VmYNbvHpHv;hy@s@TiO&q46AM}D^1zqId=LGS)J^OSv0!2xhO z2>jmw|Fg#ACuxB_lx#OS;OEjBTPT>wBDC=OpXSF1|+w z=PQm^vurXZK6(}n#C(Fn;AJK-w=MN`#iwImwS0x9K(juNG3mXfle$*2k6Veq5M$h2 zjeZbiFBiG>NG0--WMo*nwYCC2+rC-QSQJdPsR`{|=906p5!^89a$htk~z)c~aIY z?WMl~5Be4K`#j?(ex-4+#&_VTz-D6bukR!(U>L6;1|f8+_r|e)NjPm*ND1wiu95i9U(pR}sl^bN2E02|{hDO#5K6klFl^bf8yD3&q@T$+f+fiS7 zr}cIAS!byP{~lsd9cO=OJGv2TO!0{Y`PTg>lHo*e`LXuy{F4#%LSI?~IBWM<7VUVU zwUtY=yUNfndY?hoSZJ--X2EHneU^Oq;g3X{$)S1Bw|z#?g*-$Iv$6glye^oH-Nfd9 z9xFDWY@1n{BgxhH#BitMPW#N9`uSIE(a%8x#XP<5_^^0DoNwR)c*IK1kX18x+u-r3 z^r4!$@q~0Yc!K6Bb9iqx{(kcP^JV&jb$rvDQJF^et}2+L6YNHst$_;Cf_YNitZbbLE4Ff?#yqkyQ2I5 zd|&q)^}I*qR{HGW4rTG^Z0=ajGO-`@H@by)cpUzJ0|N5%G1wP8la@cEM_SaHD{=n& zh3JRyUFpd95?J@P*!Q|{R?%(K|8d4{0qdaP)tbF61}}Si#e!9I)O=%p@MZX734Lt9 z&Z4|8#PZqmBQ<2QR)|-r%o1p#nb_-X;7_)lGK*&HI4R;I>}$vPRZ8e)^6rNZDP6q& zEH6t5UC*}(jHL}*a_G@GT&kV$7;GruQoQHqjMuLjM`v!t-|v>Z#~&0P>aH-3fY;)c z$O4j0%ZR~N#NJpY{xfwhv$GPr1PlvarnAhFYg0l)nJ;1X>4k?9*7W1^h-Hvq2ANwM zj2paxDrjhNy>&)u!@3e;(YcX(!9_K&_~5IOgRL^~*Dd*H?uf(YZ~Mabb$6S1n?28z z$G+Dy1zbp1x|4pkBSYuI*Ov1hVXkXlYA(t*Inm0O94hBq)NSwGoVb24`!z$X{TlF) z+c?JLu0u2Q3cuVpmit;ecY9TRr;9VYFIB+rTGzHt3u7yi&99Ad@y5lg)*iFgT+JUp$MNrq;Oiy+)aV{HHNrk-W7Vjx$QCou_Powg zn}Og2b8p-8+-0`d+#4*^yUVVla#A2V1>LD|cwjZ;1B z6C6WdgfASKK>a?R--_R(e~MQMr>}5#GGhf_o`*d@X2W;eHhkvPB=uGOv-)<;lz@x- zo~*tp?g@44jyTbi+L+5;)rZh}yOnN)@>ut4V(qD|XY95@r>=77*ku2}MZ2*W7lruv zL&sX{L?e*{oUh^xg7?y=YST>b?g;a3zsuG6&Sh(B4eY0iw>6!-B&*i`aX1^&c*SL%py3!{r@YP z$OJaEDcTOR_t+aYeVg?q?FUo-zqRf9C)&=TZH+NL(l22EV&RHZ^=rnhB^gQGLE9i*(>EZ9RL0m|GZepsjmoqcV z#_>D5vaU97gt4r1LQU;LlX&#lDGq_(g~+e0H^nXw`$IgUd7T}}?C0+`c)kLyOGXN( z3!ge)X2*!iquw&+uKfGj*(Zl4m*M+d+=hP!{adz!wD>xA(1v)t>b1t0-vQKZa6Td# zzHZaf+4_-TmX*k7V<=}?(ADGXDy5y?b$v1iTLb)WEpkB_kNTlGQHl&!!PCk2zWSm0 z65lW8S8g_s`qjJ7@nhlniN=n7J@nXTdX0r>Ib-f=*_k(Fw%YuMddcscQ#m_UDRce7 z8SB6UYmkY&GM#sH#b&2$%3Q@NI*S+Nt@$Q-C+SWPcTZhJI<4qQa`%7CG&Zu|@bQWR z*xdNvFh;SBlS4XJdL!T9nLX0wA6u++Z20l{m43C-2Y)fWh;-$BlHV`r1Qu*&@Hp$O zr7IQM{7vws?q&bXZ3KQyxs}l188}I6Sgf=6$ssOU=+;@XuaQUR%Ctu#eBT>u>o{wc zMgOd~l0Sv>l7-BdbG3V|-EPG{(eBV*?cVey?Pi_lZ&BN2M%$D!W6u;nFwDEvNclBS z?{C~ql5+p|@CE*tFMhj^w6~TRDek3T{ML_e&+~0RPwzTC)K^IQLLT8ZU;2ORz(4`K zxoI}|K>qmrUgA-KC!N_6-m$@KkPYSq_RkMsk16>2R|4fh+Xhp_zKnQ~{7j23^al?X z8i7v54TH{$uH}7;D>w-MXzOmR*xKqD>t~-2|3TdqOVedhV>Aygq(R+wr?=S6^trtUm^TL&6RQos=Dm+8M)%FHX)i8R zweU6h6|^TmG%}pLzBYdR6m#&*+EKw-CgZ##esEdv_3GNI;G0^XWnXXSx0Q8k4RU#J z-nf{2jqtnm(ia;B2Ry50cE`uep?{X`+9YO9U*#((Cto_xr!n5Wz{MkAq7!RXh z+cf*1gKPT#5?oVaaK*#HxOSW99a+dL4e+sK*0hNq-?z^VU2Pn#GyO(!$&dV9PhWw~ zZ5X+w=(Uoo|1bBR%cn4&uiJW?y@x2f?CadsaBkbB=37c=0{W)(RLPr?JGXP6P9}bR zOQu_TzGZV^Ui?A$+`*k1UuJ*1%aR@K{WHnwisQB&TiOzAgQ{Qt!J3CYGreYqD}DD@ zbvB)O7-6kqe(jO}uiCC0R$Z&MWXCxfv+-!|>Q419Yg;}R&ivlB#EzGFl6ySt^fasN zpBeAZ@BQ~+H+pLi9mI`o6Woi3NUm5omUDLCUA_>y&tW`p=vQpIJoZM)jQC=e5Cn#N@90)q;gls zl3|{jyN9`I)Lu3^Y)=1t`79i{4!xs+HL#L#sG z9*S7K_PFRi>(rX%v2oevN@HBoJD*@&kPVAVZ$w9+eVvQaxco3SE|S4yPwR@sfpB=I z^V?hJO@)7Xto7H{-yNN|jrsGk=Cws5$rdd0fThveN7Xz^4pm$8xwqb{xSUQOlh5?= zE#lFrf1*jny8ALXq<>pF?>ThuODs4n-8)p)Ck%b--a%lvGX{fn?=1SA&6=CNinA() zao)HkWm9Jpx7}}Myo>zb?HOwXwfEFKl1Oc}JK@}$)qfWD9ia z?=b4^pc>>E1y`Zq4`4 zzYPDm+$8EbmyM@4j-0vl&(QGdng28UQ5W$9deCqjjr?t~kus;$Ncjy9`%f=K?y4KP zkh8uW*Vm2AGCi}aInV9!KRv(HFq4W_{B$|72+DVYI|I4yd44^uKbqh8@N3I64NsEj z);UA$Lk2gtCRx5m2i*Na2ien)r#0yunX;o^Gf;Ol1{Oh2b7#m#WCUKtz8p_ib`19Z zL2SIp0twi>%s}Zd{61L&63e&`q|)BkmVGME@LKo8HDFg}ex_u?j}60H)6SVO-3R3@ zTQOC4W7Grt3iQN0?#4vEDqch0efXqWj9>>mY=yf0q zjDt=iq_3fkCBVCxvx0MVFC_Kl^F9umUjtq%ksVZSHh5D^fD+C_EF_PQU-|4sxCc`A zOcwEYTYvNi`Hp8Btw$X^X93&z3x87!f19jv7QKiDg%{yScYJpWN4&qndwg4pO|jHW z+}%n0<=ma3`(QQ$kNl`Z;QTgX4m{G|y!KJWr}&$(tZCKAVNKb_&=)Ho9a%@b8qYvt z2@XZSrCsgKwp(>dlGu(T`t*`e^W*N*~qU!>3p8d)Uie zHUFT^4>r|}q)jW11^2q-9Oh0+wQtgXnEp9!9O6EhY+@_S;clxOBXM`R>ETJP0Y`h+ z(9b!@a#}Y;ZyF;L_@pCT1UzMC@@~B&a|c4e^%i$iTd{M%#awW)5L}dli*j(WwDRGR z3&DlXk6br2^@T%IES%=#aCZ#lUvdN2Q0gqAJ*!UYR=xkIK8-8&35J)pP)B5oP zu;N8x9c*F0j6K2-w2%e8TQUsvKC8h%&Zu~-=u7Cmo%{#br<0A~6Z|ihujPJTV!`R` zeUNWHo$tKX3{JXA^7ctw@dKh zoBB=JxvS^1cf3JES2Xz@Cbi zwecFBX3B0{JDz*QM&)RiZ=aH;-@DbD9%6?*v?gbX7oa{G&SYDHP#ren$ukxHE zf7XNeb`3U~iO;ZMR?U^&N%(IeGi(zt46<%`1|^>H?b5e7=>E6i+a(@9h;KjZ&pBY% z;LZ=Bo!{Zt^*j8!_V*|L40QQ0I-};Y{Lv(Bdz31q+Ycn z|1qUiif@t@Su-G*1+8-qv)jUVl^M{v!0(krH@(1}Jx$y{_hQwzyFwS=6D%YDb`w34 zanat-&p5O30evWE4?^ok)1@<3{lLmIq`ZLo2gCDzb_w?-4H{I#TGYMo+HVHmx^O_y zsN6Aa$5&dX@5;wl7C(JYWkFX1{^afMs%dTcoUa&T$e(^gXoWvFc9v=#eeC_j?7q{L|+<5ewW**6%&Yo&1=~4W}p0v(X*|daRr`WQ=$Ien+FFN2XRf%}y zJmPuZD;vUm=E1d`r@DrHk*ir-?AYGOOxF3NwQ=W@uHX&_>ACg5CqMPqiN(^Jetq2a zdOh)-)9_QD$GAJ~`v(%=`8+!=+H=e;@w%(XXK_hYWOY=JiuO~ z&cOi7R^!b90Sgvizd$B_v}wTN!xuny9ratX+7sWi{T3>zU+I0}DLsZKY&?jX&!QM!YlFz%o4v~B+x@H5FxmDr0i{rv-O!YH#f(->yOPW*lm@@3VIX%Y6tHI8BY*%T9?gm`b#3BRm<@j%sCsqtrR zk}dAQeb#<@s8BFh$yVO;j&$)g*(<5D_@9OE%3kmZt`+bo2TqU2=uUYX+Y0Sx_>3TQ z)x8rq4E&$82lP7ce%4^8AIaAk!Bq0des&yu32(q^?|YD6et9oJ4;s_H`WTG$F_Atl zK%VxT88h47Cp^?!dGMP#g9l;&_wt!^?z2dH4}T%>xC?!{jehC8EqkuZrQe5!)zu=i z?A9J*2Xlq>|Aj*M%0FX+OeDt9XmoEYe!&>|Yh!N%Heye7UdV5Dt~1~DjP`}S1Ww(oDw_~&fQ zKVi+NW-Y?@z4d=+`?k)J|Cl)|`&w_9uAzR>?LV@?ul-+wV|5>JWaG2sF+P4Qhxs5p z$(EOmexHv%FMrq3msZzaMEio#V7$= zJf-*854F=1mEL1N)K2fG^kK*y&~C#R?YG+Lr}Icp`(k>R(iN|=SN@|)=N{7D>4!;I z+BayY_kL?{nwj@c((ZBc{cq6jNAMrfuFd!3e1xj-V~3}Enb-0O)m&yAizhSIS*5HC z{qWt7Qdy{l#qd7`VG3g6-mm^Nrx0mc8$b_riFPd&^wnk6-+DVjp*a zq5t@_4z`Yvf85r^_35&SZCzKA7C1nArN}g`#A~rhYoNFRCHd*$iNuI^z@CWBAQ7c|Z< z{RVte)5}Y`uBR_$@M+oc^Wn*5gE?cYJ1~epkDN3VT5i$ZevQZy;_KV^-F9GnAno6- zw8o|aU3-V-2mP1b|2^Kf6CXaqNaJo+?%qRAO)sB_pRV18flYdmEq~`^f-A*P_^Fkj z*rA-CxQnq`Nt<7MeEjPG#9HzUD#$;X(OoL^Q<5}nr_ z!TU^WyoPrwPQS+Y+vSt3@l_kW$5(e}eJwV=4fy%2Vhn!Bc*)mRbHVY0>+gcE+2guQ zG*`)-*S@00Mlz=}eyd~SC;43af*M0VJVR@^&JA=eEJWsx`L@O6pv~Be`x>KH8B2{( zm^dhkb<&#-_7=*{Y`3Ghsm>Tpjg8SS`EJXF|No4Uzt<0eg=T}N>e4P39kH+S5du%os_WapU5Su^O#KvY7 zWAlC=W3v?8s}057aK_~i|6oqQJ-EGjveV#k*xnujKnxWx+@W%ML<8iY$5t}<6 z?gtWYtj9j3x#JmP^G1z}X!J>b-(_6VnLAO|Ek8W8-Qtb9)0{X5yx(o%Ih{Cn&fJ+~ z#Y}MKj*B@VS{HBBUP?)9Ty{VUBi#7L)27zJDC<_&7DKv^<^8>4eVm_#_BR+`_liGlCavZ9eKa|F~KdJn7JulC%aMny_}Yw;|*<|&s30iKYmUo`0AvMHHsliT4#UzU32YYaGQjVBm1|Lr!ofX*QrasCCW37 zJlW>pnwOB5tGGkwFm~2~GmPNZ2OEfvw$;yk&O_$2&+rlbSij^4Rv&%9`w@N2Glrb$<45pqV7K}>IE-)0=h0^Q zkLQzjJ$+ML+*bA|MB|F@E<6l@4x{wnAWsy0wDVgN!vi#KjfeUuyg79=5Q}O(_z|x2 zdB2ozP8rc-O$>L!k%POQI2uwj@RvsL684j-m|qVIPZN#cH6JHT7CwFnY{G??K3qjy z=k@e6gzadz(z1+TJbm=0mnGy;n##uK8`9zr>f4YOBiPsX-?E3|wA0x~J1W<+KD(ay zG;1#HNs7rM9&3M2-|L`z^-FbFvTlio`Rau?nd~JPxzF?6O6Oi4`D@4)D}SSUC%rzQ zM|!O_kA4-@z0~q8mOVsrP8nk#FSpGl#-P$2*(d%!*$QNB$x?0bfcQNBov%6iML&_@ zwBgZNxxw;*mv4e;`N4aD!4KU(hUd(pljqksRc>!G(fu~erxJk~@K3FPtr|YQ8 zgAECvi5d@kXS&<0fxLpp8-oQpZ&n%NPT+%-7E%nTDDkojSbz1u%$3?XhB@f1%ICbU zxAP6;uu4~IjpB@F#oF=kO@0|`klh#YU2WQX<+fi&N1pn|{Vw=s=R@{L0@^IKL!@=ThcY7H5)Nz!c$|+E2tT>t+3s4Zx3% zFZh!=|ERKW0#g~b{5z>v@Cd#?GtVqKsPG292^|PePCqUpezC?!a;{==lW&830dIu% z>iBQbYq%eDtTUam>j0nD6V^8MRWzqQ&X3Vk?>hj}prTk{!mb8z$XGWm zN+eFtNTYkq|HIq+$46CNd;jOm0GUYwB#;mSXc7=jCh8TD{IFmq3DqQ^7L3TZ;MhbNb7C2_mV(c69Nc=n)ri! z-=BTXgh@o(d!Og^{UfiOIcNV^d+oK?T6^ua*5>Sq{coXYm_2TP#z)?|R`$VvV7!vA zdlT?QMyFIiHf7b%8sv7gpOv4`&-P*c96=9JKi_lvu=@rouYo4iN2@yyd%Wy#sgYFp z&rW>jH$ijgxP=+WWDDF$w}}4UrEhV3L%y1I4!?5NIcIeA{UUrY3gd!LfN?l3_JBXZ zD7hMok51(G6W}A--keWp&qfy?f?Hrvd-9));%U1 zHu6Dd2Pdu(Ox%ZJY(dCW&Kji`IQJHQ{LU=3?F+INSi7<{SxLKQ+ey1;`zIn#TeEb3gYIuG0JCr*#(>x>Jwb3c5DT=0e6r|0&a zI9=R#wAND?pq(U(fBa541h)IiQ@|5x%vpzPZ5g zBGG-^b?vQu=e~sg!=)W_79S>F`Qh2^t&RI;Z)*GvIliX!wXObqUsdDk*{zK)&))3a zID2v9L)^prt=ZL$qx!z`Hu8=800ooA^LR|($_?H|+v<0n`t^Rn^xK)nJ!M|q6Zvs` zYajVZ-_B=KW$!Z7Uh4h-b^p&Dxn}>r|8MU9**|Ak|Aqa(DUaXOC+z=`=_j+S#L$ra zfAP@$KYab9_Ww&LJGlQxuab`RU*7+J;qTo43+}&n|BsEwz{vjJ!06h5ev4jU_W$HR zIq&|TH9TbhzgYGk_W!&e-2X3e_y6v<{d{{YZK4*J}7qAboBYy6hT7!P3 zT>Iz~>$CPJ>P%q$L#>I5oe3?0Puw-^?yt#%dPUXSM< zw`D5}^jp3Oua7zRdlhT@^>OEZ+x#|b5j;7z*EAE)aSd@Otjq0X+?UY)@HfNq1!?=y zgW-D4mpkU@ zK8`oe+My|{$epsAyU^3{Ya$=N?%T*LuWz^i?VIg!Pu90v>!#g#MS8G3Yu(g4GpA(U zU~fP5rEL8FyV*bDlNQK)bn2bOk50SOnv!*cHRYlktY2gf{&{A5#w?m`-EE^=bW-2` z8|4;Lu9b4T==0OsLq=Ct181Md4k_p7Vfev&$kdeg=7A|$?^4r&0?I&C8 zcER@7G_U!uXTf(d`A^=yF4&I#8#vig-2UgZjyZo#D{VhAtz*t+?;mFGMaM90l@KdN zZf>=sHYQR}-<}^PNP#toXV%NMs+XBB?E`FnP zS%x2Q)>k^_XqmU>(J-fKS>Nm~{?6CF zPdQA0RWw{etn$QCVranUJ!j+cUSs@S8BX8Qi!@*7>;D(@AH}ccN^R{yr&T`nwx`Jn z;kWWzAAT^rnRU2r^{oYmvCS>A?A}InQeXAq0`7;IQNi;L;Fn+RmEa`E`W0NKyyGHT z#~$ctTuoKMtJ7cI@HVj3+ljqZtJZDEP&=&Asl-Jtw33>irp$<%B?a}?h}tgnD)foa zLe?Pm3jE{jUN8P%s(Y9{$T&N>xdA(w-h0@`Y^HAMvZ{hbmbX^>je6VarTzic*$8Y` zdd4>AT4VS9k#Px5-H-Szyk@r@-@FJI#VfWj#%H{@_0`*UZxwCVty*PZT*CN%y!zI@ zt@r@X@FeUT$2a*V%U@IDWFI>2%#HsDf4vz#TM56#KkKfA_+%|0pO52{l?9Iw-df=? ze#_GwusaIjgX7{8`tkzU5HnVJ>nc*MabETbxqg1{^48VzyEZ*O5T9&~dm0$BM-vOe zJ70Q2pv*sJW;y-8f={`|AUHIR&Cr1b4jZO-Y{(oCuI=8D&)X^pDQ0@q8iKL}sKbrv|=$2b70B9!1JFq z{f}*yKiCPcweLUq-wuxc0URY!XZ`g381fC*@8j)M2%1T!4ux3eX-;8LZh4MubKUYk>?E~LP^N@_~c15$u}HO zo$49!fhX{V&9I%g5$$Pwyw9+mxREk`%O0Ik?8J@uh#T?Po-I|tqPg<$KT6{-F>WKD zGe^Tu?8r|!FIWYPDp$ofm8-V;@^#ky0C4%3TRm%D@8fsWH{>)vO&_fmPx2s-h?yi8 zrO~f~+V`-2k&$M4q0r#E1ZW9X- z@Np058c#v~QE-3RrP#6Y%~)hRcYrR-dTB%EjQK0e$wfg-N~8xke82&n88|pQ31q8} z5mupK6O8Jsmi>G=YqS}BXQ!`tZjDutf4$!dy@c&fcV5)8R?1nk&E!7IPQTmvJ{mhd zZ7D9X7T?fv)^4-Lm0s(7PbQx_eskhOwb=d3lle}bjO_G(a=xd6GtT&R_eCv!x7Vk3 z6gH=GMwZU{f5aaNLtl?TZ`Y?Q285Vi%Hak_QjMfKg~kGx3VchdJ!%Kb?2L3?4& zQ7X8HxH-o<5CHE*AK7(5)&(}*S-y|_k(Yc83;&t2+#lE+cz}Bb&V%P}<}6ARIoZK* zh4f?24(2hxiKz_**|dA9-S@LZ=0!9VE$g;5NKad+mpihfunAgNE|@P!jLc{5n(e;) z%b2&zuE1A)SR10P1m<0|pl_P@W6XQ5zoXE!QAiuiZ!hD%jJZ8u8_S`C+|h3Nn<>wH zoAS){`N|VNuq`*)Eni1@=HHZO?$1|#IdqT<9T<2Xp*-|p%0mO^E3a6YTWexgm06Oq^!aTHsyjThS#f|0KT84>~H)ozZ0*kDfSQHbv2v(ku_N_e&>9BsEu-P z6@aGw@T^ZNQ{?i)05s%>C!VjI@(kw;gZXdBnf+OrvV)Oxf6o7VIn$-waOamZ`(4|> z{-|wW19#%Y%9(jza^#Ht8SY2oo7&MDjOHNVq7?M25$s<_ntY0qPm1TnM`1@Je5Pxr zRVZJE4CHyY8>897|7>D35A!{T7|rf{tMF#Z@9PtZ=RV4SYw>Cfiu&P2dms@KJHj zt#%g-#*7Czdt*F9b4*6hu~dE<{TN?w@^z@~odTcwGQBXo1(PNnUs=IMm-B|i;cIh)hy(PlS zk&FV-XSD9RVRfVQ8lhaH%Ad|C5WR+}A94GuA693`n6dd?sWE$p=6Q-;ds7*U)}F>* z<NF5chhCyHlf(WlIU#v*#hf{F7}0-nG%EU_!n#49r&u`rE+p! zkIey3nAKi4HV2#hGhW%d_m^bO4sBvT@e6YNs|~GT_VU}r1Jy={(?%p~IF5*Unju=m zhV^OpbW2`OCGNKD*aHUxnfRb!AIte@?m_&gO%9VSiu)-xHinS)&GHGVv@Al#@fkp_ zsD3%L_kC>OGn1_5i4EkP;{L~5f$d)4*L{$)dA^_L`&Z|YpVr9{C4Y>@E5Em454>Kn zA06mx#2<&T4Q*R<;(>$8?@~LJy$|qI08a(*EGMT*qLtWtM0x=*$;Y4*pE~Jyx~oXf zYdCv;8asCb?R|}$I9mJcv3vLMZW(+|ao6lmnwJsl^B>3x^)LUOYw~k%r%vo$pR?=EuzWQt-1hn9X>U3(d&^R!&{hW|iA!18uS&E98B&d>49v?m!Y< zV1F>SgQZ_|bg~~Xw&yq>`CBwl0?s!ue^HTg}%W2eVymmm`mMP zEgQ`?_8dW;qdwZZSPz=VPSFHy z%?j5H;yTUX+WIIiU$Ob}N4lNy3D@#1b^Jv?9haxMcTVNO(aELoiYPwc0T;rP&W$Fy zIJya#mI+6$to#H!&;Bghk^cR~tN-8Y-}X&^zyAGmV2#zkYj1PrI*V}>{2g;$JUG`j zep6Ot_*@sp%=K+CbDbG8*Vq0XbA6mL!{_<|=DMG`K7p;wSyvyQ>t4q2|6;CJ(oS@) zTU;Fd0hr$6Jb^WJ(&GLHTk$*S*3mtSo{?w!bBad~$w#hpzlvA1*6m1{`R|Lo{s-9x zB|qYk9|`PjJovgRml|1_A@umLoQShAMDBnu*Mf zX}_es-a}l>DCAZ`DSeEf&7f)XFl{gKcs|zlDsaOd<0Sj?PmM+NlZUVRV&xVc3LAQa ziAB8Moxg$AI%6M!J$)p0^-iJDhNq9w_ohJD)g4|6bphd6pj|KkBKIxl` zMe!q>zUG_(?kv4Mywgue&hNl6P9$R}_+ zzUeJzy>;3T^|RN^gcmhU8gY|s1M{)zcaL-I49cCMcbkaem+S@yTYX)5p$FhSAHZ+e z1m)K!Sf_p?-z4s%rtGQR#?MP_8sPH)!pRQxwqVQ=e! zr{`hw&qkMM20r<-(_WyO92Usw0|`UhO(rLfY1fAi$2mh?FE&XZeU@a+TiLyu`@rd! zIX+d!IDP-tv|(~;%Z7LGBkU*6;`^uL@;(DTW#7?yQm$gnugUq#f8Dt{L9q>#sUJ&z z191HPw=?oA?7s4;4brCkAzMq``QDTt@}&|7hm7xsb}Z;fcw05b8h5d9M4lAM{eI5l zqVF?#4d9PAUVR=!n~=Of-kXAYvf=vWJn#ql0HLEYGpRdauV@;XQc4}R~B%j@Hv zUwo}0&fM#+{qX(QolT4Gjql^Rv$VLsYhH1`2OP}2^K4oTWx5};Id4ehtRd0Z(ckor zjQo|g_!rh<7kozk41Y$y*nI=_(P4C->k;bT9^s4Cz`=NatB+E?Rk-c$;rTkAS8;Bq z^>+Uk2GZ~p6j^}XR|-_@Kt)36iS$P zHfmB_r(&raNt5N{{5WX%ta zgcAJpe7(Y-@qq`IJ>*^2@1C22{gX^=c#8QhoiCq0%(vCS{-b<9j!f-67~GMAE!w<) zudM$lvOG{zP%x1g#J&%F<{ZERR^j@a*8|`E&_Cn1ycx)Q;|Cq+{XD)5t1GvkWt>_E zRG+I`OtbN z&F|F6f?4t{9cn+##t%$7o#1g`k$tl0wNo>`fMY7Mx9`O=oEyigx&OJP5BA?oN*1XETbA8UP%R*%dt#4;MV28^hFTGv8 z_UPL_&T5U!_}18v?{l8;_=!VpsISk2SBg$)^C$D2{e<9i*2VY;_h~()c})biY1};! znXWUXG)afyJaFEDMFsVgX~mu~ zugiIEP#>q<_j-oss|Ly2Q^@d>N# zvftKzxW?{yJtw*2^>}}IbFuB)7tQCNFgo~nB0P$7?o-OA7ta~%YU~#4Y)|tYmQB9C z(Y=ZMuCNmJk%RDn>=qTPtk5xR9VMy3_En<~&AygAwj=1%iyxC`-Sj){br;|H$_`+kxeJY=rA1mXhl)#48Agx(V7HG%hu@UVF^5;iVt4~9oEu5#of zylY1khZ^Ul(@S#VFSElf>@|<0pNyVi<&2(g=e&o$%|3l=N$2R0d=JfYa14Aa9^U4U z{E4%uP4H;=WMM{~ZQ^4xT^*(<(K@vU73!u7 zH!sW}eo1qy`H#{;^lWu_o+bGPD{e;gR3aJSp?`EM^pi^6<@YpwwT*K6y`E>iQ@?Fv zoPM$OR<@0!-#5Qy`fcU6o-@uO>mS7@OV zqSLMA9;yU*aar=3Nf~xRZ6`3TK{qI}C-es4P2J1#3SPt4@Vm6x#CxAT!NjRmGv?F4 z(PY|o;84!;Ex@9F_Y$k-oGqUJUdb-TA=~3ytToHz1b?I`y{^m`N^(uv>0 zrzwvz-QKi-#`hZKqGQiv?G4v8qVtA*gBW^jAHFgQb>Od1^j6Ij%hb9rrPJ4>)ly2X~pW=~JPBs$x_PutYbwJPt!i>e{ephHh+F4IA87DVfW!z#$XE=i%lR2TUata{otJO zY3&B!T`d-Qdx?M7eIxJ>^hU4o_m6kyL3E(CB3e|AvOh5f;lBy|Uk={V!25uUcYLA) zYxx$%`+$pg`MMYIKEj{yu5w3Nd&2vB;9cKd;;;Kh>@VfFuo~P^kEFMxC9#QY?j zXA9@xHpk%n9GpMH#?khpYrRgA4xm~_l=e+PYHPDf1&sfYhU}9UE+VMUz**$C^(oSa1J>7@U60N>k;l8#2&Sa zem!H@%QKGm-Y5?@)t2`T`!tys}o>N(JMNq$&;Zl(>< zfIXXZ5Go!IyFnEpGFVX=ML)11Cm#`IN2Y^>VfL|cNxqRicVe*)ad_W9vkzHkH2 z@4evpPpVZ&fIt_IQs&)`#d}75=*65nB&?O15G3(+CYiwYT?8H9W z_tQ^2`oG8I^b!37ui=GJKIq6EM;_s~_QVVS7z}qY2185mfU6kycVCMOf9I_1%oVg* z4xDAc>Dbx^dE$>8o><3S;;Q?q;4rp)A7k`W&$a0TOPX1Oy2r#9>ASsR#bciW({+5S zrcB0V)~N~XHGIgYHe`g~>)R)JC0)9E3^|0~e(+$`;S_L^8j1tv=aCPx`PT3)o_dF{ ztLFT333<-qSLj@GW{sym3!N%1|p>I8#IRwwKbBGVUnZu}jxzNRB zJ-8I#YJ%>?*8)5z!n+coy}>)m7mo)fU*xyMQZ2GSen&Zb>&oaI(b;dt)$HaNdBMa_3 zPkG&+63uP@?k%I3M<=(v_WZLFukF(r*$ep9AHr6)8NM%mH=NfIF9R=>{lm$j6Pv4C zbGHgTsEIiKCdSsV#0q_%^7zC&-$Yw#SMX~M5I6Fak8!Or`Vw}ai+vl#PZmLE(mS7o z7szK?Yf$YNUV(fQpD6oaU|ZX2&T9B3T`|$nMn*un8w9i97!LP$=}-NhhW;IWHpWkI zkCWHSpIF@I*k61Rbn|VSv`*;5Ia^DOtbvccK>PHUqW{}~M`gq>R8G2+%64$)acg}M zJkC1s24$2-pmTM{>>hZOdrA>>nPqPw#sPl@$NwsNrYzk`d$uFk*jtgS0rHk+jP&=eVg7vNC2bjzbSfVD%9+)| zGyeJZnhogxz2jFo_k#^L9Np;W3jM7&m?E@@OS~mOb^d(vFw44 zjehIotCD}*GqmsP4_F%qyz#_)l)t|Qn#cyGdUT3Z+LvFzcfmjBNdei3F0K|$IQMYp zbEj|?zccx+b#-qewkLOu)xT;P_$KH(aVPh1SGU?lH^-;%<9Q(l_6)z@(`)WHYoP5O zuP0#5u+V>f5&M9(Lb&jd-`F3v3#AJizdh*Yxy#R{?I!-Kq%$k@W%%E}HYA0_H?>BH zrHWk0nv;C0{sg)_2~CMEuNPgGI`cPpZ|!O)pP6Vu`g#+0bhQE7*U6Pp4c*1Ar?a$E z4V|@m$ZgI3ggk`()`}NqPX#xU#~CTy`+$5u_!Vq>+*2>!`Wv1*;hQf2>*5k*J@4OL zZH2#iIcMU~i`G{+?P|VFyOisKjsyJf;rlZ1YNa;JSp_}oo>uYs-QpYLtY($>6C+f^ zoLVCr=A19*rLhUp=TD(AXMdX%+CmW&TgLN{7Gc4G4)Z@S_%EAuSPv%_P= zgRWux@+X|_ow%=R?4;(#(+?gj=3Q)=uiszLmrB{0ytfynZfrWT;$V>fp5!U}@^~lS z)S3HHD^F45O=EnKJK94a}FqU%0R0eHb7ak~eWGeqZ zb*vNIv5G!Uv(~kC{+)5Y!8ny4QR6Hk|HS*FtYnRqy^=dt^kHE10+Yu2p+{p)H)9>e zJ7=s;nR^*)3T68)W30EOn6YN^Ut|4%cSFW{n{VR@-kl*ItuMg}{T{ue1-}0xefQAE zrZMTwt>_(7IpaD;xgu{$?{g~)`(6hpOQ0R!(;n_UN!u6y&z=SQR#xjyvCyTques%4 zJ*U2oUa|}wMY_q~#GBEt;=t)J9Si!CmsfihXp$WL56Xe}2r-))Ci9P}io?%Tq9?G>W5 z@GkBAkazd7c4GCY_rK!K`#D`|{nB8vt4pO4lLtMqPQ**P(H%t7d*R95z#Uy9R+bg| z(?{Yb<0FO0(Wq|l+U=})_T?(~Tb`?^7u6xm8syu)YtNN`N#)U5ResO;%JXd}vC*mveBLI-T>)Z*vCT+(1d(%n(y!&cm+LcExavfbV9Ri zYSKq_2bb(&RR6JHFJm5uTac!`x_16yM>&J~Tcun|W$tz5a}~`hr=jAD1B=+)HZx@?z<= z>q}abLpHvX)$Tlf63_pdnWL1*e%6`nGVk8v-mT4C`oN{&h3?s#`^=pmsf^(T!5^^d z%Ah^bZ3MUlbH_inZ43{CTYJmVz`hq64YSs+`e#r7lh|^aTszLYx5yT}r%gCBaO1ma zDKF!PNg+8*~66Wo)#=b7hRvTc%1yN*H?6n3*}-@mOqjY zJwKSdX2W9aPcM>#nsuF*Nt?2(bSjq%ZB>!`DYg#>kJ2;#_BhF}_;-(U26v+Wz2p3! z{}%d(icjGf2l;y-!#3@IWb2#>Eu=y-&%clwTKhv{+wAz>M%J6Jrluex)vjxW7rx7y zY=ka1^Ix|2V(R1pLngjPl4YXNpC@3qM^hj*q2gqebzQd?*?9jUvv*GLd)&#$l zpU}tn8!%P_qx5=}Q|$8uV0;Z2BjU$gT>YT;dAAdZ@n-93Nxi3`wK)D|bUauV=GO+(}LP+CC zV;t`>j%DW=N09U10@ky5z+r>yG&7Dj%B?3F$ekcr;ZqJQm(MJQ$3Gos)?Z;0ch8&e z;4S)nxAU#P4F6iK^~JO+dM&08#RoKwV-81I`RdP^TL*7D>91;}=GO5aECp{3{Fd&1 zuB?O8ZIo>t;lPHU%Ym}~f#);ba^&1CFon~0;Si-#|XM`gm77GoEe zFPqvv125YRd@3W}l|vcB)2N?CS<@D6Sk!q^aH10kui|I&FTH0fdGy@5jM^Xi7P z>+taCd{#4`(k;$Me_A7dFa348=Q^8*;qeaO3%d08jDINY?fvi4-fG8(^#l0T`FvOn z4RV*0Xiz?Th6WF(IW!2}8Tx}JCo(4!@0zGspY?@@(hO}*;`!E%6V6YY-$O(Mru|e5e)EkMF95??MgqioJPTZ6p}hIcFoeJ94-auxOWEm&J2A z>#Bx1*o#ck+?R+h*~5#!$cAu^*3!-TO&31hb=-mB^yrYs^nh(h>A33Ja@ee1@ zo3Z1d8%UnU;>Vo1YkuH!;#r0_#qc@J*Kj^pt@&|zmTZ@?_&*|i?-^=uO2L0gbSGSA zBw79O(0T`Vz}_=Xd&6@!Cc)N;jmat-A1XC5CKE$*`TtPn_|Rtf@+SPMey%+8#(rdD zONhDGggG5gKjRq#x@*ADI=cLW*Bbt1^hFP}C|xmXqv>1Z_Fd}W1zs6ySSEWa&OpS@D_#neDKY?mY6el-0yK_J7_y| zX<4|1uGGHeEBzc^e;~xI!ActDcGskaT2aU1DxhK)w;USqc zG0M+a$7Y@eb^E#D_wbw49qxwCz~`asoONG9*>|DuuZyo~ZfB>2RK>yi>nE z-L(?1cKit4Ad~O!F;|DkN!o=Em+-Y)`io$GCN-pXBDB*$teV=W#TKvj@QawIHeMCo z{}<=y%l{Yi^RPQVqG$0w%}>#PVSc{h&X4GUwckHW`c=&Q+(r8qHt0;ookP3o=MwXO zd?X1QKWYs?nUQ_@^24oI|1Hb ze>41>b*;Ef$q31M^$E`lXrD0^y~&hA&hDZPIcM-+jPE5!PXA`U`E;M|XuGbM=WKGV zyaSG(|I&m|9&Je$=o}!0T*SJMbO_CH7g85xMl*JF>ij3kZFU1?yN_5#j(-S0dIuko zS=_(;?lfXVxWl#^8QGm|J^wpm{O-|uXHGisWwkz^Yiwsav+0NDa1Q`6EE$o#p0s^o z;!kwfZZ)ur_L1xOL1aV%+_$^vLAws!Z^(VSa{6>8F33>*NyMKrw_)O0`e|1_5RzNb=l9Hc$Ari>tu^*#f^k1Ew!(Y56B=Ia zf5RK@PUv|1oi|2=iPbi`$$M{%44d4oN9@ABH|@gh;7?;U^!vaBvsTs4{}B&HP7c9q z(}d7sVE7r|8jzWysTt4|!FYN(8)aLr2@K%+D{x?wc0xz?eB>R#=fDt zn`u+@E*+|db+v4BFq{D`h?a7=&%WsQ4lP0d+KU-IOm{I1fp5OUA0HEZF3o=$Jii|0 z>Q~<4^YZrd$?yAUyR|PaUoePwNN?Ib91d(^HhY&lC>wn5T-kcc{*1Cmj}#Za|Kqt~ z(PPh^Vsb&JM2=o!?dacvzH$1qVc#y^|JW1m#<$4$%5z5CP0p_Oh=*t)pENp1Ws7%0 z?-8Bd!++B8MQid-*jG-@zbHNGdq*ri+Ud2!>2U(IR7{;l>UMh51LWQjPs!U0J<3-= z`3ZlIU49yKp#F81v={p9N?;E87W9%YKAT+VoEnIVD0P+l2cOjIe~A& zTku$1zWUUD`-6Am@~#KJ?0W)=xB4D?tPl7tU!p?R)|tN!JbyL6qc*&#&ByrLpugag zB)wAm5wnj#Z^cGejE&B2*BN~oKNQ^~fsxfR=T25prTTa^pRhd5RF5Ir7~TMyq; zlMj(SNu_k)W8j`-?4(Y!QtGU+&6koJQFEgAS9%gmo|smIty7@ama!QGFgLMJry}|| zj1A=&cslVj;VH5AF~)In=K2lEHFX6)0arN3f-5JF;VJL5$m5K)lN^R6__@!G!Ot$n zW;ysdpuCa7&wP9egd1!j2L@uH8S(}68s9waU#2n# zS-d00@Kg!qTH?vmKh5}ttSH~N20OIBwwU_}*c(4y$r_kWyPQK8!s}OL6xwy&^O_fC z+GQ)*zsa9uuudnx)n=7*Pr#0z-sXkn`@(mty9c3vT0_&Ub**-zD8ePKDv~NKN3B6BL8G(c$XX_ z1NdkTOty9muvZ?)wRRlGhp=<3HLDZZ*vTAqHFMwdhh+yPYdpviKlxm9=(DKCIXmsX zq%@~9PWKi&ZNiiLyD#zPMBDo#?NyC-+GEXc>n;iw_8bh}*M*(yFuH}}UVFYnp1M`| z)%?eO&mCrOPJXOj&XJuQs;uYvJR=MCKh!bBlei?3e8D za|=D;aKLN)65pGr{dR?OuIHYm53l<+b8fSzQhO%HE;96`KM!onRooSKk95Izj(^ay z@Wj}LS1+~WoA(nJaNOH^x7Ok-V>{k{3%X767UZ3RPBR+4by_Fl*Kb3o8N0o83;Dfp zQgR@9oWrkAvTojj7ifJYO&S-tf_39(j+4htn)w!d;iNYiS8OZgb5|w@-hvNEmgpX{ z^kx*TXB7Jpm$#@=ARYggL8w87Yw_CHiLCA68CVA+)}0{7S| z_Lo$pgi3fe^U^wbP)6&FS9>+*{W;mKyacDoZ*SgPQ%u~7dG5Smux<%;#jnl#;Dv+l zm++oE*pVaA`oYPeeqc4_8qU>kq@3)j8J(l+mVl2;ersRbU7B%ahUKe0%)6)$#4^r@ zEz4RM^?UGllrGpi(6TVTp>%%lPgyG@%yUE zDqpEw!hUP6Dc{LGJ!TI*%%;cLTOi{Kt4?~SVqRL9AAC_OTk?pDfv3ohCjA523#^&< z(87xu{~2t$E%5YAeTQFazihtmdt{-`ja&YwbH7*1-FBVy5c93Md7*R$^PKSQh2k?! zH)M=^@pQ|=wT;LKr4)xia8cb6{kKj^zjxLgDt*PgW(WafNfWe#?{AUN~$ zzhP?m$~}x}t!M56m3fHY+OM=s@YHQ)Jlb16Cf`PENYbbDI|JJ-x>z3P^?_b!w00Bc z-yc;TTyw+c4z9aq{=r8soO{qmJR`iUa2e+gHP|GU*DNo9zpXf$VHK{uyzF4lG^=op z$GTGAiHE%P*d;Z^6;E#u);zE99CIV&Hc zTl%D1A{*wTlPvm+Rad)W(l{1U^0HOyCM_zq>d+l4i<52gYWPjeoN}rQR>2uvhe3FY zWu=F_to4L{35Me>#abows_uN|v;|vMp&YBOl(tTM%U*B@874k);(yo+#BWakyWVZq_j|1ccRl#>M)jfg z2fSl@wbt(X%gTejBNO)3+Lv!p-4@2ug$@B6h4O>+(T?iWv$s>dAUw8)--6cEl=|!(Cf3n`(weM&f3qZSP4y3;ujUd zhPK6~j|wZkIpDYY$J2i)&pR1&E&JeH-id!KA|}(vy053Nz2-ML7iBJg%LfwIE~)$Q7O3qnJe))>2il@N9RW7 zY{t&2UCeLn7$G0{fIdTOS=;Qr3!AVdVbdtA9%U7>Ul`;!^801)&Hbju;nUd5>C2qQ z^sv_}0hI;QuYU2G;d7??;;owJc;($(e@+QXhm18w8!4)K#`TO zWihlTzvj!pA@m)V+>{SqC-CdN>Pe;r*t3_SgQ~7*(8K@v^f8+E>+I3Z>rNdiXaTlo zfN3h_SwE+2)FCD($X@E0-GdFsP;j%)hKF5Dlv@KtoSLGuu`sVD<&^NX~BV(c8Fz1fqe}{p6%_MS& zjI@%A4#d@!gAc6%L(7zxy-_~u??C^tXOe3#=Z@A%){ffER#^VlvdK7paB<;xN>#^i ze4e%bbk@~He$=w**0!2xU%!v5Q+?^DG4|B`Rwq6e_+Nac7`=#`OoAnPW>O72ceC%0 z+We^h5bJFmu;%WwEp?-Fuc$<8mE zgw6*Nj@t|3;kmvCs}9!JSTlO-tA1KLy=B89cyQ{B<_*uMSy#24Ej##a=67ejUB^D7 za_1PkE|Z)HJ1<7}VGYVIO+JUfPIyX?^JQ#@#=l^M+~#-p7}NYGUR=4i8<##3QQH8H_G>=n`a+qj`Drc zc@^_$+Ir(1@;-5HF^cxp_FcgENn_d@Gp4dkXG|lBKe4cvYko8y)g8pua>u6rbB)93 zB(5I>xRs8gvYrdvF?XKB-Lo*%orJO4`cnL^VO$l zyWt(_N9WG(eyjFb&uVK4?I5G>_PPE;pM*o=j?$uS$Tt6fXyXia*lNxowh_C4&bKWJ z^Id`$8+bSGbidtV`ssYwYR^jLH|zZ$@B!&O8JAzh`-T*2+dcddFTW!@WqUq4!v3ru zq=lxkem*yi^MkkI@>QRh)KD^aZDsOX^-Q@|r`)xat2!|-D+EkYdQJf69oWMKU$*>T zVrVJL#pmAR1I7=mjPDvtv`kSHe)iJOpRGEmG59z?--%7XHLj@LW&JARrGL+ceewp!N0Qxwp#8PcP@3l;(j}GBv|O_ z8F)Sf?p4-*`Oy+H&|wYM>gvr1s8mhl|ae zsJ%equ|ohj|c?zIiykGI{E^>ZL{z_>3?F9gNwnwzUQ39R?=6i@i800bTeyi>RU5R?rn{cd*r`# zKuJ7bUYX5XxzUXYzaC*ITqS06WNcrH~l6&X;aK30U|LA6BERs#))yR-c z^*;&t(Mwdm<6YWEK9%r(2!5_Qho4<)AO8lG{|q`s=ZU!dBF@0FTzHl^en?xbf^&7Y zQK!}QAztG6!ECkMbA65R8GYw`@>2R$yBD1yHkdP8!KdHsX98b`<}2k#&>NSp9GLRy zgO_a6_s6gmw;pwQS#(Yl&-MR=+K2zORG?c9$J+zW{4`Uhq1(aROUS`Yo@05UXbBn} zVmIovd%+s%@epKVor(yh%9( zy#^=a{{mm!#5rA;tCN29G`ctU^Zkf2ZKY1m+`S*@F5N|aO&4;<6MBa9Cgo(T;5}!i zro{M=Fos!Gdq3LIwx%9)E4Xtw)YiND;Ia){-EpWf2?&#C_OQPd{l6An| zN@EvIY5ZShY{7o+w4>}*uAj>wPAbo}`5|}PbS63LYw&vv4?LsqW3Bu{pF)4VlxcO> zd35dH$$HNmW97%UypWe3*WanMMXC0h7vXj>@+|9dl^R{mMB*Wu=FD=lI0Xvf>k; zHqNgIKlnNg!Fi0FB8T7;LH2W+$6vwUJCJu5{0-bK)R@@A4QVq*)>P;_?WoNUxceHL zvMD>Y%bg!&MwB*-)IWQ>%1)<@r_(KiE*dS9r!t4)@;Y2So_s|GQ_*j_;YY52?AA}( zzeQs`RHqTV|2=Yl68$wG`-Jm)Y#fsNo1%M&H(dCF6GNNeV`C{>^?F<$v3+Sn@DU^H zMo}iym8CxeAIO+I-L;Op-R<|E9Qk~Ta#ifl z$QS1Nv&Q&XuXgBZOaHY+@W_l7UR z=bxazF3wS}BA!BgQ~4?~6U2EXDLxqnCHPA?twWgaqD z;Pqqv>c3!2*xwmDwovf?9q^t`e}dSl+(I$_9R1L*O~&3tybrxkj=dUpB9598y7mX&OHpj z4}ixroL%E^wyhi8q78nZ`cD1hWx(@YPipg-TdeTA_2luweka_oVei4&(hP803I1ou zo;idrTt0Fy@H%?FLzB{>F9Cj)ZH(^!2Is=bg%TT!^kwElWzS4I|9pV&kIl#Dg-fIF zpIZmQ>DM%`qFZRRnDewpxf24s8l0cQ>BnIZUNwH|1=cZlh2hu#MNt{lL^I-`8nQ{de&lO$M*TX9RS%Hr)3ys=NB(9Z|h? zGjtz7XIl4Z$sZ#Y%@NLgR#xA@vwhE$5FhmMNF7{DJ8>LWe5*s# zCjaQUm|M}h+CB`w>1NGgV>B_-*atr`X8HlmF*f@kZR#GkPU0vv7V)Sae#hog^|||i zwwQhX5_FIudJ6t-9pWool)itwv3A`|*$6VX}Dkn%rp zWbIXyhaVXEaFII?+zb0u19No-ygu>x#83_UhaA>Yl)f6##SI;#vUgxzsol+nccnxY zYwl>X!SJ~8kv8ecJnI}!a>Am>X4~sosjo*+j*nh=*Yg^jqgJJCpl&?$n@GoL_)Ba$_ zh=y?|(I3dkv*U#ut!=&dndv?Z6Q>Mbe#04*e8Z08f4Id{kavQ+BNmBcGt&_ zoVtbe_%zDr?&b8+fjjZBo5uLwqU>ApU*i37-oK2Go#x^WeC)&*+wrZ_x#>>MOwso^ zAD`IUFb|riUc!iu!g|j5?%=m_#4E3d^5vt~AJAP`-(yV2mi3u)z&qx%-<7{zoRbG} zXS-E*DfH5C9k$))t-7H8^KKV8WImwnW6Z_z2~TZE^d|0W`7C)q>6?32w&@J=S?>OL z1(*(T9$Qw%8U6!PHnKNAbp+qTEad%I;8wZ29{AHnmDSnos#2@c)L|T(uceO9+x!+j zl+@W$!Z+$4Sa){d`QLcF`@GPp&NS8MagVpTy{4~VC*z#YZ+zWTdU&Tej_=wN_g!UA zZ1%-l&)-a+!i`Pas-ybb_;wf?d&JXGsQNE|)~ZyUF4zB7<+FGv|Fz}lRmVmZ$IY)g2 zKanlW%N_V$9wpyh8ZeU6yil|uA5WbbUhmR{=%gKAZu?U@tk% zw0?v;MCU1e$nv3cPX_dsXhqSqhBR-r$O!=7)^mW|xbL56}~< zGw`1TAGvd^%0l8K6=TuoogC?B{(N3&Ed*U*2hPCfub6iFxmP|5+u>k;UrjgXIT-1C4lm`zCvR&3C}R{LB&ejt9gC%kbgWI7CakXahPNq}K}359hH1_^q$j z93(IYz&tNNd-2wR$>hoU0_~YPF3nvHozK=-sb|JR`CjH^GV^r+$r4>BC>^`1ma2`5JJ%iMj!Jg?x-Bdp*rn=qW`$>y+lT3!7Ai{A9QvOYfHQ zEec~i&oilC&Kj$YU%L>Wt6H6v==VII^Sd?r`=uuHy}|w6yvF%n<^Jw%a()M+-~G<- z%xHbLeT(0MCyVoi|75K6RR}GuFToB~M9g4NHXwb+CRE0KND1I?G=Dwb(UF~6ld_$% zCZC1B{f6pk9ry3a_FaZn(hWeZ|5%3;P2w{O2C1c!{zBA#Y&oI>|&C|<{7(dV)Pk5 z4S)2xGc6?inKIaThbeGF}*e#@6N#+(mPs|UvC5HoNs^|9F z?|)@3dRz3aUMB~M*{^nH0^daJBx&%i4E&PPBR?h=y5N+)yO(*%K-Z8io;bZ}l8154 z=NaC9QhQ(c)5)a`X`zc0=UW!j<5XQ0g%tx3+K*Ci(e){Q90@4m^ock9?q zR^h-+_I(43tU_!C#P~9Ie4CPm&%w;#j%w|lpDqoX`!=4o!)^Fo$gea2|0{$4z2rys zjX>7H{{oVI$W6bG-|#=l!T@qkvab|=;7c4T`}|2(=o-e`VtbA6tdH@})^FMpK8nG$ z)~$HyVSa0m=M#S5m-xftvl$<_T;IFAbyMLntN4C-U;V-3z#p*VjnARR_lzC<>5KSb zOLvTp^*Cc4jeheicrHa(xSsoR;tQ;>_}W&NCyK8*JZ-(h6B*kL#LWJdGW#iW2pmVt zoLIGf(jjmx9{Y0g=s>O8A2MyrBdJz_bdxYNAzAKE+IBUL)VTS+BoH4NYYYmNcZAyw=swWVYQ=SZ(#?N!|+Yx3kyPep7bKqf6NXU4LDF z=hL1r`@;3eij&sf$xa)YsiB?>{K|Rn>$CKtmGRY4O*0_6R?I|txjF(QbQa-x>=kO<+eWi!E?U=Jo;20?N9nfBqJ0K#CO)4Yc z2{tVJVLJIc$uYYPee&Jcg5mer;r`4s!?g%|wpZP^6ch~2Ek=l2i5`Oq!ItZ>irqv*Uh;y;;$oSflFz4F!K zkKWeZoZP1w_zQJP;IFGFa|!fbXpQRapZWa_MeJ=0A8y%DRLlRFk8Sul^R>uAUmqXV}+Y%h2q!y4C8Yt`YmQF+@QYr*~euD`^p!*8wfw%e=)D>H(R-&X%? zV*e)h+Im+XxADu~ky|#ihKszDdb7dDAMNyg??3QI%1r9*VxRNo1L+&H)>|Rzmdcgg zZdqFtgS!}5Tm8E>5JRwKGxGgn$lP89jP1T( zZg`qL@32NU@63ujHH~((C;yiRU)}hxAC(?l3YT(TXR%Q%0)ICrtWvYv7Dg{jy~ZcoRq z#JR@{vNuUbF*15Wq>A^@QNAh9*}1++&L}6v7x>m`&OTJ$$;VKscCf?iJGrd_pGI?9 zlcrtH*`n><0&Mjk+NYWr`x(a3bZu~Z>r6*)YAemy{&=4C1b52@GCvxa#W+vd;6b)w zqZ?Q$w389>K(DvKhYbJeAjdzy|DgK?j4#8-b3Ec-5b@~6&HU` zdPw(g=sfG`rPhH6x>fi1;10j#i}!;wgNt?Pj3Fh`h%cutT!LT4W$-^Q6P)VVXIrxt z$CsM^`X-WVC?;Q~aM~f>`KNS8U$Q2;`qFfBR?tN*(|g?e0bZnB&aNr+$M})cQ?dV< zcU@CL*(=(%XQ97#PNct!h`*tX;%?*@K6#dvl8vvYh5fhM+Kcbc{(KBmDGgTfelg`=n3_6WHW2zA=-X2&3eN4qOZ1`whSCYec^T1BAKRpz9dU-O0k~! z8tZBnZ9d1wif7F->;y~`F!68@$!p_A?b8ar%9d- z&3WkuPsF%yi%$vmCuBQ!ZgsM^vacXd|3$ioAt7=Zyy-e(4hF1*UVN}ClcdxAvN$|; zO0b_x^qRkg9y0FIVE^h5V;y&P$YihnykaO{i8)~r2 zAF`5qUu;;vVSxCQ6wjIsEgrk~$wntP!x}=quOl}1CgjF8Ps+Y~Cj<+=OI>pL)Mn|t zm$A#Y;Z6Dw4%CJp8||CkZ*Lf&-Oa!#-QzI(+?$rw6l|*|-;O7xb`^YLfH)4>PrcMD zg~n93oH2|^AOy=$wx2WnfFTBmP~*AZs51d{}o;xH^u6|#KqBS zV%3DVHO!;-(L0H=+DzF;YmOHj!cKV%{w=;*b&B+TXWdN zfV>0_j1C{X7~1lic#khACp<6+2SMP;U@X#8;&)hK&Z*K?0h`*{#5eZU`GUz`Z52vi z$hAGq$^ozjJ?$Cn1k2G|iqO}jj~t<|JJ4H-b~$ze**`BuPuBYy^cLyPIoLkeU?(^V z?-I^?WrLvIOVNLOu-&}`FB2@DHC9-;WWv}(*AC!&XM2qPELfy37ulKSx9}r;6%_~T zYIx3>wPr&A7*);_w!-lvtbWDq8h%equ5j>0jX!3c6Hi^Yb5F*0cy{F`_KT8XiX-hF z+c2)k%B<_*e|CyB6Fyy;^T4Q^+74zEXnp03oOES8<&N?__kpCF0+)`OnE?OIc_8to z2xXQ~rtL@u@f?${4AZXm=(!J!yD4XG;>-x|8}7unqPHh#?p&hGkT&x|lf$bw2ouI7hEGp-WmSZ7N3Wc4=j ztqZ;wjf*qos`5jcC-dBz$2{oH8(-u;;Cs_MDlm#z{W-FmS>Ao!z|_K>dz&r)+D*{1 zay@8$d%i#S=g8v9a`s6T@PZtBa@CAN&pl7xd-k%QpIsb&7g%Po#(n_btjTOx*nCxSPB&)` zJFn&p<#WM8E35o{t@Wpnd5kHr3f%QD&ev}bhJBoaT|vDQ;4sDVg(h3sp=9go(BsV0 z!fUg!)A*WyO+3V#*_ok({Quh38KDco12L$9jC-xRZrb&u<2*mkI=r&jjd_YLKN_@@6R*@epKd+jAJZs_&q2Z&1>S79e4z>Dhy-#2bMdLMi+ zIgNYl#f$X(-oRrEb6Ceepbq=>dAE!V=BwQ`v|B^Ffd`#-D`>a*l9x90cyj`^v|D5Q z6N=n+Yi{eg4|`g&-)&dVJ+xc#|FL)OVNqRI!#`&R|2HO|#(lCSMR!uQYUr^h;Dv1fPN&EKeSHVjH zjL~2&LZb5h)|nYYjA`>;zCXU_`TjW1nb~LG_u6Z(wf4FQABW7^FcdifWfkPGni`yO zUqNDGGxv@z)ix^@x%WC`s6&xO%Dl}lh)e8ZZvVpf?9hPjUC1kCo_AuO&z#}cNP+h< zSvyB7k;!U6VGi=nl4AbT zFRkC&Yw;~r1+4uD*8a$tL;N0{QE5z|zlW+qIfpc3sOKR3Ra-3ZhpoNEG&i`D`|)Jb zcX9sgB)%K+^g`muSqfd}IuCKUY4o=fft0>%D(hU0u%;lV*VORM+!1m%#sf|DNLV?~4zcQe6$( zA9MNl%yZ@6?b$W=dyQw|HT-*Ks4*D7Mc%z9dsE;i7Wg^M72*w3Ed=v8uc2KA0iGi}xMu+0;-6*`q&80K=NZx^1HNa#3Ti$_?w!8z{)C%f|UDa?XH>qyiY*!w#DHHf8Ca^k2 zaFu)Vtro$X!G`sjedv>Pj0=m%kK4)~ z#@-q|v8s6cM73W@=FE!Xb5g3tqJIWo=WZIK)sQ-doq#~fu@C;-Bu!5LD zgNZ|^IYvfbecI(4cQEHuT)b^t3V1hJQ3`T@p=24r{qZ8ZRt%mc{6A<=9BtUm4WwM% zzvU+R=NVkQuy;RE2Kx&GkX}nGFF-w9J{zmC&wk=Bt6uNn76yfzRr0)-rB%{*#3IWMe?SHuz8YF> zhU6_HPXcqQf~y3+>Y#z==b&pyKRT6~Re6E7-?)sUfEim-9s5f1>hSeJUsgGK8fp9& z*c3m4yXJnkacB91J$c4ak#g=pw|N4RY^x8R?#rs#wHkkwMo%uk-Nx_q>;?|giD8CA zI7c30H(_q_qgawFz1%Le0a|>`e<4fmsm%eE+g>IdQz^?LY)D_Y^&>- zlN5Dh%*Wh$$^NJ%J^6~UQ2RUmu3zJV*7Y=(ebGj$!`p_`FEom@A@!f3eBCR#NlR!i z;ctD@bsWK-GNES>ywPij01BE8)#=DHre=f(CJZaxw3DP>|5$O9{)yFgF15BG4`tcO4^D1 z?+(4CAzsRiZ1ixC(S>aLIr^dNa}{{YeE@7q*_L>-^$C*_A#mODZo&a+8=9bTm)t4P zH<<&6um5#FRrbkVJ9<3tK&Z0k?JePlE=nC4^y6-={o9}`XByvjkL zYaue0A-2TD=+QKKl3eNWN2@H%75Bbw^imd@OzNUwIU{7>GhfNjfU=M4UKx|*0{<+S z0iR=2rfWB`Ux7OVag()xnpJgO+5y3Pg7Ye)h;MaPxu@gV6j%EhX?Jv`%1^*z?Y8LqkB8ReG2(rMXwo~^i>LHNGyG5o~>l%a+X{h59@yI z%zOAoXP$Rlm5eJG7_hMZa);v@bE_oT%dle=q;tCu<356takAAf`% z-bcRqMAyj5e6&dXv&w0eRbr3*3hxJvO8w3P=whS6lgC@u;8oy3_WmhoOvd48$It37 zBZ)s{jB#u<#I#(Vj4rP19YajULDuhg?Ay=rt^Q81%_%tGGkm=E={TivvC+;{fBp@zDjV;(ry z7H)w)mc1x@-~;x8$dZm`ddmrp_LqHvd@I-s!ZWZRoF%@!P%eAn*urA>@rA`6{6)0` zllVDI7rdU0UdfV1?gyD;OJm=3fzt$j9~D>yXTC}tDZy|0_uuwScY{CM`6l{owJ-N& zy#qdXu_iU(aQKeyouiekQ@7Z9iF?7h0G$9FCqOgQ*B86?GJb)F2Bq}vC#a)AX}y{T-HP4jCBx8y;pl(;mOiM^KLU>eqjIKv zwlc-y)wPRq3w(ZrK82ip(84bWEj(Y=^6+;P-K>G&7u_=v2%&lkIxvsN8ukmu#x zE@{vpq;2WWPL%Wp&XaW3K+4FuQ^!7)^Q4?UEP&@Xk3CwoT+X#WueJ!?^)TyO&-d&U zrEssTuQ9e~2{zvK@XrO%7YFkT5|0S&KwpHG+zJ0K^zg;I=DD(=B@=GO-kClMEy-Ex zrzNeEZa_=M`e?}#U@rCswB(fQXvrgYduho@<9cSs_M;`Y%=6Nc;uA_>4;nHe7Mij_ z_9XPAejY-T3q2|4q6>QRA%Sz|RNz|9I-xChvaj_yolY!d_O{Gp!!;Pz3Iy(iE)|?6 z@>`i}Gcejv{@um}oZmwGC8%0PJM$^9)=frA>9(^6#esb_Lhchq=ml6A{TKT9+|jgtRu!vJ@{N7 zB4;j|>A19vJ4@MPa$YSKoND||j~f`R1gBG`JC*N(W8BP} z%s~x%KwwCClVrxC2`u?wCmGlgT`%!1dpEGNW%UO|Z$LjcKtCftho%eF`H)81N%q0c z6SVs#e8>mJiPn3@zFBpFv$c&gU-%IC&aAtO+KRRe>Y16GGRXrT()o~(Ta;PCzX>0b z?Bhc=d)M44>%ECN7hND(bKygNMSBA40^{%STl!n#d;grb*dho_q^l*{YDU|(Eu*e_ zaAbaQKE6aOo3gk!t27KUYwRUtl^)|vC94@;SjN77!#~h;qMX*ni-hX zWs7O|cNB#aTT%1zD)PGnUL_pAxfdw&mKvTR<9UJke4csC3$x8SPa99bTdtsuR$m(n zY2zY(IfakRqzyYf3UJ zdkC^dkr9(7XPu%c&gbw~xXaU*C9o-TCGffK)4nXxD-~K<&OCH$TF1lJxj2jbeBDO) zy8D>>bLi36!|&<)f3EERqwIgL9PuFg(7*p(YOuL(+^v?seBL)iXjrKiUf;_vLO1y9 z-9)`YC#SHcZ;)5czd8IDecAcUmB?f_GFRi7E6!A%f6QRM7L0AF61l9%Bau5?lgrM( zzf+e-`sK3V3tcX&+Z9koQ_UHIBLqLyQjfs*^Rz4Hne5BjU25yjT(xz#PyWDJW|e(= z3pPdE7p*FN1ba35Cuy~eSISiT%8Oh-jC}h@6L>A7{$I%WeR=Ch7g~QiY3Gp#B5SY; zpIraDB$t)3irjS*V->k8XV_8ryjXc&hmpM8-GO=u`Bj$(nw+?wgHFw9AW0a(7XxD#X z4rJebj_#-Mt@|09js4pHJfB9MJT=TAcv0Y@t|L@jNKd{Mr&^DPbi7aeEI{_i&GzJ5N2 z7LoXV5<6oAd0o)rk|zHpo&D@tf-ERO;+B0^DU1pFMNB05kmIsGsug;%QD~47qq0hH zvy^46t?;}V8SuQHanF<-nqF`Vb0sh`KQt=i7s&2ptaam8TZGT`(gyI|kvAnBIhJss zk~8zz-xChFIg1n2giS4{_8rNuLh-VjP zNiug$dAwcl0Z0CtcR>98#H&Lbb;ePRe}LwyGk&k}D7>tkok#wfaKJ>LIadlT$UPTF zZH%eo>~%r^RkQK zb9?+{Y9{hu><@;zOUVPR`a)`r)v2c8$CMZu`hR7-a|wO#<-gzsJqX;f>2P5kdz`6t?BNv@h#hHfeX+E+g$H$Lt3tnIO*Mv}|wJAV(QHe=R=k zwlM+3;HZwthQ_$W-F{b8Wy~_J#!!jDHRHWPCP z%R9=W@h&6oL#bhig?^I`IE5-=yw!^yylDbPX3Qvm|O# z7vECRy*NkSQqraTQu0>f3+ry`tK)8{628HK-dPEAf=-^qh|99@U$3A3Ngcc3*SN!e zK}m?0JM0%y<0d=X(cx}m{bdh`ev{;%PoIZUb~MkC0k&=6x$XtjSBbu*P4SLRXlq%w zUEFJV>zd|#%)6huRd7i#^O!C3!QEIIG=hIl4#|J1gKtaGi;+4+|5vZ0<`(C2>Imk( zQpz5JZk0PTf7x}c!+gpn^9?_3dfCz7C#ioUdMPqj8}k#fv7#*B#kNoCm)|m1m3-^r zJgj6r9o{C0!=gBtHxrBMz!93~dXMFQlI@zhBd%2Z# zecv~G_pWzuM!BEy=MNa;t*3OFb2ocfucHlpcv%Bwx>N9&p4RNk*EY@B)mQ1bCBsC)3FFnV-*9ogLLim&o9P zxqp)MVSHZ-9_rv8jCfK`^VYIVBd{3|p*oMenwxYY7u`H?T|0MK?~PM>d&e7G*x9ZM z{D>pQ>l^b~i8a~QL)**XM zjq9#OmePYx(j&C-Df>k3UVD%wM}zaF{JWO=9iM;$FN6D}uFsG~U0W$ewE_jabz^BhBj$Pz3ey`^z=*2>?1 z0mu%)DHnkeNpDA{*K1Oy3$8xGT#JlP{O_8$PbYTBw)wQR7n!4d^}~(m5NA|dl)^XT z--r?3)xLT{;|F8khv)MJy~4IeR8nHrpYRV&P+&I`uL=!gt77tFD38(3UW^d$ZQq|ABa zK&#NFVa4N%TBu)m#D+QQq6>VBHG~vMJLQ~dd#UpSH6&xAGBLR6idJ|8-1`A_N}ol3 zznOZ2izgHvhF6oll|W2Np^Gd1)MEB^6Ho**@NR4pMU;kEJA~Qe9wLC_iwZB zzs3H~0uL;Lrpx4>e}GQY>3Jw8?-a%;HZUCnZM}Lu{1?94|6jhxMkoh_?lC2#y0C3& zDS^*iKDDf>o^wxh`<60i!e5)im0pp(%DLw;n_A8VnOYiDwFMcl6FD(HoJ}6&8cuZk zTjf46g>}-;aLP&F_20B7d_eF}{BNUIgslm_-EF<;=r4-Q=x)jfPeo5iHMdBt)zG1Q zUuM;9^*_F+$<_J4*1etf;bYrE|FS7_*%J*HvB@b83BVS(>e$83_s(84tv$NKa%I7Z zo7RTz*m(KQ6J|A_C3$&Z_xu3cwx#sDoBjGBkKExPXR}`5xyXZ_xfPoy>v(kFzeE2f zg>BXG&g84a1W{gCrGkU*T^l<5P-uYtG*9=RH*LEJJ`&wB(NUk5m-}iib~7bPsHbAL zHaR~0p-FKSHFlYUo0@m77~8ya(q=W>Jb>Rp{4QVn+>T5`gd-JQ_&cuNd1Ckd753cA zD^Hxge}ld0;lS>PUl=&NB4FyuJv?Pk?OIX8a|l^_=FQ`%G<7%{d zu3|JVG(?y`q<_f%%_+#^ZeskmF!nQy{f@3`}^-8tzpQAnO1I&&`_wM-qm6a#P(}&$#&BMR1hGiUQPreusNS`Blg3VjC zaPzzY1I@@ycmgs)gRv9%y)D9hcXGrV=nQlMlL?${oJrkb+y^(PVP@oyX6}}vynUfsmsyFP&j`}iIMIxyHL?X-sq+1I+zOlu=LBT zuWq1ibk;eOEZBRAUa#K97&+tN2g){9nFQa&E8S82K25)rv9yH`C*<+O=u4rz->%ZD z%ih6UNE_RU`*Z!bzQYnbeSoVMeVm?b4V-8qPcZGPPKi^1KWFZp(LJGk@hy;xz50I6 z-2M0;t}D;(*~b}*jrzWq&D?Y9V;}66v)ssU{EzRu#Qann275j&S6zLgs{~(GKD*wqRea%j4!l7k3bU@wY=tGkdN)ojC-)VLJR1@!E?! zp0&9?#vZH_+b_MJhv9{Mef&gxTYeQr;B=XO4ExxDSg(vN8OowRo;7kzQ&;McysNIz_n9+c{;Cr!>X z(VeL#O{EX*fN(V z`o4kB*qUog#C}0_mjR#NSjJ!W{Wh4r5yjpp0X~{Jx1)jecd&CPF%0rl=g*Wo)vW#C zAF*{2x?TEmn)O;if8xN=vUdL1)>qb~x@g0DcK6G_RPyR$ym8+C`-`}Ftl?ny3EtzG z57|?!!G764@RZnKe7S!K+yi*~cH_GJ?WR$e&^)p?@8EZw68EPvYy$-csf@3bdWD{l z|NX~$3w;-y^c3?hI7#ju1rN&l^aaaa@#7xSbbNQHPS!vZ{1B&fn`Vn245dXWt>iu` zc#}!_@NEqm!6NrC;1ci!J~K6B1WF6^Qmd+(HuYaac#*Bjt2YZ@w>7mU(YXX(B;eXw z8eJcap+D?5jeOdYSg}3dYDvL%d6$yZ?!dO@S=AND7!>fP$hEE#YuzU6S>8Om-3R|l zfYP8rzliP4M`MU#;#)(7{DN;k8snWW8MEM^im_f?d=GQ-1Lo3euVrvug6F#Cf3{rW zOQ>#{Pr+f2pr`)xt0^bMp4N>$CiJ|}+Q>2`F4`bn-kuH*hF-FxfG3}G4kC3re2O!@ zFL~wy^cbqKJ2=RE@vT_$=y~D0Bo4)bKqd0-wd$huwT4BTp~2xpI1^*hyN-?AymsWG z0yWejx^(%JbEzh41~jQ-Cd%ii=92^!c6g3BeD&7i(c7a{OEid(DU2D%-L_4 zJJJ6W+$#INp8q-ceTgHVz}cte$dBMF<5{ctxLchvS7JLP@xX?K>GpOrWZnfI`f-lf zgk6F^{Pezg;1%`{IA7LZUw37k*oJM*ZJs5zVOuNOZq;L(9Sp|jmv2oZwxZwG{Ylc= z*)Q#aSAj)YFWIlc^Oxy;k8+;^zlt4}-~_*&m~-4$ZIhzF3m;SN7VKCCgA*R{m6x$E z=eLb?uYEoK=4dC5y|De;ifcP~^m_Xk!KYvM&AUIN+&T1J>>_H>$B(~1LANCvZ~kh( zzAFpW zW>i+?ftTdW6P=q5%H)BYq`aJa!V`n9l5VDqoCnYYR_~cN$X!30^~aV|@}I=dq2%8M z4czIoo$2Cj@!8H;v7Ip?3rrWg8S0a}1rOgRU^_D&+nIBUGQ*1PjF~*lZBjaRGm;NH z6FmXD87p=(X40_-NpfH_!+uUO^PPKBUH@Co)9c~j`ni!gBV=+dVrN^& z8216gG7dRIk?A=qS(|@Uw{I>uqrCR5dCbL6phX2n<*fJPs4v-(gQKP+n|=sh`4eDI z=p?~a2T}zVqxF3*_c1T>KS%7z`Ti5}Wk;P}yW2tTrSz-mcJ9mRv-lk9AVzu`vX^+| zj7`|piR?!FMTq{hzf5rizUh!}KeAYH{bYa&t>^XAE4JSKae&~DkN=nA`UIR3fq!tJ zNpd#$X_6b45xUMRV~ch>m;)VW>-~+^aY2bbwkUTS`?WufE$5if{>|XIXcb<7xw}Q^ z3&vHcuLHC<^o8JziHy0D?2yafd*XV0bGHxQ^ye%5 z^8dxY{t2F0B=b&xyzuSCGXrQ_&SRlPuIDNI`|vKt?YGlHzWkx!;sYVjRdbOG;@7)_ zdF>cya7`O?b;y-9zjIywp$c8sk1jGI*DsH3)s!Fn>wNkVe)YoJz^Jv)XzSg6J2HsaC^1dsX~nAWyuT zxvUoXh`iB@ny;c4&F_LY!WTjhWQuNs$Qd^yr&-2tku_}OEpmnhoI|08wznl;wi@aQ z*4p0QvZ0joZF7~BYY5yDvw6^FWF2O+uD60b%suONmz8rfo3`ten|Kr&&w|V+ z8JTAecOdAUY)#)E;cVeu@L~k~4X}KrtO+gfm6b9-j%nVnF6Mr06%sp*@_DqW8SjLOXhU{o0wI zvau@N*N(I?ix@&BYQ(BK>RG0iZp)uz+nzv-A#H8&4%3x8Pb7a}?A}TH8*f*lhFzI= zLhO*FExB)OXWihDr(*LIIr=`uIp&?G7UK&y>e)!SfBbe&8hf|K`0ngF<9$6f#$WOt z*|XF5zl*Y!K?PfwmztnQUAw7E___78AH_bEeKDE-%UJa>9Ed=NX)yX2BHxX5yLNfU zD)fNB>{#;F8P#k3k$ylgThC!9>~HIJp>+}1^2&cefK97q&NPBhl z-uC7GSI2x-*n#$0;a$1+c)jpNtO8aO~+iNSbRzJ-!~nP z8(bFFw4)sS^JAsB6WX5r_l@?BoL$nEl-2h_GIw&w9cQwi-U9xGZ%%*~YBM*_k$6!8 z|Huq_1>f|CZRwZ5HunMUPI!8$PkclOOo^VtHQ4sm|K4EbDcLu<;Kg;LZN+EEFMDTR zxOeXeZ7S#A_3+zCJ5pyyy}>2)c7M2&d60H1IU5CTU%qd)>zCMoe0cHngZ za^e7;R`#Cl{rtlm7JkS}cZs~j#Cf)nv1{(F<@a~>{^{E`mz)u&=Zb#-;e7|W6U@5*<(b06K;skJBs`R54c}Q{s`ggz(x>vnN?f4wWSI{ywZkG5g*2rVJa_gtjD;J0DC1cu8ur7N8Uw=X13B-jy%6*sAse#vTic4GF27D$*8w%G^x0i-gt-{YsJXE2_*fUn4 zx$B5?tSVo`XI{kK@XqBEUptOd*clX>EZc_+RuF0*xlH!ib9QN9$H*+NWaiOt+K z-W!+MK@8>u?v4C0nZ-v_20D)ORg=R_d_{ju=K34NWoBG@T;||#k8|x8bX^6}#`2}J)cZ-}%wVu8%Pu(SnAE8=@vKRZ$Q{IO` zGhgQ1SKwo0Z(T1J*WzqRGS?l*Ol1G%j264yFU!31Znh;oqVuxK%u0L!C!jM{I@;!{ z6&`?v3h9;Crc|d5&Jk?nq}Hej2bjZ*2n~F6CRB zv2;!cGP=GP@b2e*Gp^PBLZt3fQ0#%dKC6wc-Z3I?4@BNR0J!ZduF5&xRTu1PE z{u9O^W9j9d>|^fjU5v$okGqgB1~c|A%VN=&kaKgsE$Jxn zJCw1$dAuB@_f=r*TAB|I5_~Y$K_C5cTP5C$>%I3ea{pp*iq3|1FFMKCMlyE%>#IEI zei+gHFa|V;zE|uX;v=B@VAeFp0(`RR^T~NAHV_vX!`=As3F9vL0>4{LCI|Pn=xvzX zecVa6;WKgW>K`?B09zN(Q{gV&IThV9@!^A?oay3^?F`Q}HQW(ft8|}&*BY1q{l=T~ zzTb$v*jb?jIsOp7YsXFbV;kj8wBmBkiE1^_Q;Xb3e0SUg9Z?6~(R}4a7fbZUj-W#k zP&}nEjdr*TL;f5|Uk5Ue;i6;KnvD%jdRb-FpI8TdeBA$x*9?w2^twVndF+JHALrp4J?GCx^-nYDXdhAlGRBEIr%9p6O$)?emANXqU*=)eekPsdlCj?bCTW%xL8pnEBH z=T=j+L)IT0)G9i7%c;MKbvgtcG_m;M#=DCrG@b*l+wdHK^aD9uoQF{YQh5S%Yzli!`*AQt>96Jnca|r{&8c? zirpJa8n8cY-F5Kf>^%qnK09?@&5GQ4wJR!k@8P|4#7_IJ5xecXHYPVTlqs!GFpd&6 z#G^p35i8mes^L=r*cLw;PpGy7E8oa&d;u6~pfAVc--)>}_85B1;T~+?tWTf=CH^=L z0~^a$KhkL7`?A&38)Yu?GQILp|DCv;se<3W?pqD#hY}Diw7(7$wdgICv4)j~kgu$* znRI_XkkH zZRp7FtLpYb`j!Nu=j*+v(e=gVN3Y`-T%#_X% z9XHqHWS5Qhn|u3a)a-ifhUcjGVC-NI{WMic(qYXYaQKA}Xo*APKZ~!&55hx7F=w0+ z`+m%P-NJl`FD|(wjzt$Go^{ve!M}3m3w>9oCeEsaCep|Iaiml3g+^0G$_X7LuqpHi zV}f@WLlU;Tw%iI*-F-H98lH8g|ZGm*>5I>P77 z5?VA5I&LF7FjAhe zd_mjxc;Q6GE^Yg1Jki0=@zG#H!$O0#I{xW;5Bkb_LkCUQ>!s~~wC+D0?OpSK@zG#k z&|m%6{EItnKTUt6A5HJ4jbyI#m_L1OFMDZep{e|IR1R(R&)ffQLDoURH>F-SYn)Dp z%iW@^v%tf4V6v3*)p|d_xJS4i9tA$69^{GKIet;TguU9tyg_?lGipzai!^UH8@ zqyBu8K6q^%RCE(>VDB(j^Z)|U-Pe#6(Y&av(vu%=RW4STkc@~>QNnk9F<`>eL&5@WPy z4D{jwCuRCgVG6o|>hAEB%I@&Vqj$%x zjNctMIgs}N-nJ2A?4x;Vc@Fc~IJ?gduwBvdb&Aq`Gj;8z4$&`MPaPH1@hoMxQnrHk z9^UVdxY-^)>SlWd?>#(9#ryX7iZk}yiVy9n6=&@=;2!DU*AynGr*OF*a>GHA(p{qXd+p^zGP)pCo$9xk}kG|uUy1O-R0k89$=64U)xHf z7VKTi7`GNUVw$EJr`1I&2R`9Gp%k1}J^Ho+pXGuvbG&1Y7uwkev;D`~ z%$VBXDeHjW>-%T(@s-!xVf2S(!BL$-wgU-syzMWc?=q&3pfjYu*wtBQs3S7^_qi1R z#Q1ZK$kXR%hTdnPOT09M!Sy*hEjNm5H7LPWEO<@yD89vgZ_2$k*H6La59HwgW0B(O z%~f450|S@1k1sMR`)_9+at5F$$JzV2uJ7UHg(U`qe&-86yhieH9?6}z|I8>gLt6kB z?$Of=Wd9s7Xu+K&=n%3;1s*=RWmC)$VB%kZiAM0rQS?kVeN>%kF0alE8MPxbVGMfn z_!H{lsmD+9Io@(t6biq*#SmJrEm55N@KJ{SOSkN)(6a2JiST7jz-%pW6sgjerF=m^fHNhU&K!FpPehnF7d-IFzdC8 zGr*VC3jLzvb}#J#9iYov)-g60>7B%k4~9>u0}l5i-`u}C+Bqvect_(i&tyJ;Y<}@A z<(Z#zZX#ohmU`NO4H>`e|7PO)mSAJoVI1zR;fz`KcuJ8e7@0S4umoOD3$ztq4_nv6 zl?fOL21f9$Xbu5JM5jv5zI-(#_#AEZ-J&FsmfmMZUJT5DKO;sDs;)Av9Xi~)Hfqs) z{Obv?o>4n&cmc3I9(|H#?9ejS#thFUwod_e75T1V!_)aDGF0X^D;RyRf+yEyX5*tY zeXV(TI`vBaFXe5IO)SMPu=s+iA6k)_9q!K?{nfm!v59f`xYY1t)RkA7nH|l2_}bv% z8vT>~mT-{o*gIqoB^?}_@M3IY4LYCMBjB}aV-s!sPH-ZVWQ_;27l=dNjSg4VqrP>M zbK1Z?PAPCIaem)5sI4arYIg#7Sz}$)jp8Fa+AZrN>yql__Z*TVbd{ppXMO@LBRV<5be?92J5Ks~4gY?~=tv)&9Tk+S)8FPpyy$?Q3 z`kqJMbG-c8Tk-HOLcfdNmme2<`IcyF$ECg**Yd%)NIriaiOcej=6PtGcb-2n4q%Q0 zp&7lj<1n2+8v5Uu>!CNC>kfUcg@5s$^D@_QUz+QoH=OGn_!Iwpcm2cp7WxyJd%yXP z`v>za@w;WNkMy7G<}b^BB?fq%-fyqYvA{?v_Y}kEx6q=}H<4cpEV#)hGVFf5vJMko zSn%@7{rkrL{CZgUIOvP@ziwT{ZrrOst-3C8ulnhIM$Ym8m-wa^y)Z>zSMQpNeV^Di z`(dJOjPPNi*DSW$UVYFrtOwQ^9}Tkn##N}cebG4dy%;p z`HY55*AnWK-@<2k`Eb!4G`P3nJB|HG+93D+zC6+gO^wCZp%N{5q@48oUj7U35T|^> z+sqGGyDj;@$lHt!@Ya(IZ!_S3khdADdF%b3sMF8eWFuqq^EPt-TLw+#pPOLcJf&kN zBXeaz?l&HOrv!dSd#ADrIiAk%n4o<{cYQSLb_}{sVv8lJT1Gas!a33~R-BGcJ{^8V zPNMS|><=S2PIL%9M>kpYo&B+GB(6=468mm8bY2Sm83K(r=plF{bY%oaZUu&g*Ep!t zc!P8rkMw>t-YlK2^4^ijx#DY6&QBkWBzi7hx^31qx=r-zrc<}@1w)KOGW_)mFH?j5 z>lb{1xBjo|yU>KP|7CnKj@X)kg>{^P+SHpw_utwH?3E*{p5R%R`Qe+}GRGrRJ~#7~ zRiURUGFL#mJQ;$HVH7b;$S3)`jltG*Y_=YP7D_gR7hs#;nto;ZiOfHS4xf9wVNvd* zVZ(DDjaanm$iU&o2Xc$b+hU6{(Z7i-UR8wfyI6y^B_3blM&>SKj$zTmw1LiFYr)!( z;hSlz02(RZ6*4?sx8KY!vgTQey0H0dMy`aMa|ZVdEpe>9$iHN4>7lVgtF?;0J#=7F zLY^h@`aE3&hh6U6r7XYlx$62Hdz`QMhR9W%a!2WvwJGdH&7F%r-bdic)zE#aPX;1# zW8rf|r%`ORhL&xrN-Eo2wFW;6p zmPF!1Z;)4}B5@sV#ZOj39KJZ-4RAeuHPBW4nBw{wyhY_SrMNm@Njl4SX;*N=cwgJn z2cgH7F=qKT9vSp9Rcm>e|J7=gXD2kB!f)}fFq#;x=kgy;`~Y78)oN%#F7w|G9AlH< ztX55)b3-;&bp>sza;Rp9+-p8ey0<;EK32I~5S~f;D(S*IP4e|a`mmRB@@)&{_Nu`x z&+-2xeK0_$+W0MfFc25_d_I2u=)*~MU_qX*4=X5hlKYJFLmX9}AV(Gan%;-!NcXlE zs{7OupNP^2X7bg^Zm#E^=tYX>g#K$-dEyBe{Ek^(AQ>PUsESbUqi3;RsNs%_4Oou#6~?! z-e2F>&>Qx(2{@?ZzA+Cu(t&7nz=(--2zz{)Ygva|fWZ>vg79*>EKt%`GWT--eENO$ z9-(^-{0`z-7W~*YvEz;8=}J|cfxHLs6m+fG7QlNjPXtdm&k&wyo3`aqJlT8lLYwtF2u8tTuTW?=`%C^=$acUp^Z?Ih*%l-v9P&+{)iS z8#lRRUs7i+yD?N4{iVAG^nXjJ$s*ZT6nHmF=;$_E_GQJ#mvYo@Zm9 zwXfy-8ou9p*JpZr(w60}&TH*SKco-0-Szp3)YzT&6S2Rs-#@a^eku01_UR+{*?$(h z-yS~l1$#|wjlD4TMSDAY==7+U?2+_O^x2gW&)P?i+G$@uYLETbqke5yN7dRN8nxS= zJK`mK>WDq|cj#Gd69R{GnRCIXmP?Oq%Lk8p3}(kez#;L_|4VQv zeeDm23UDZGox{ftG=o#V`(bbiFeot91@9v`{Ab?_bZwue^ln~C9Je2+F2`dA*UxS? zx;8DA*l%}2M+lwR&*#qPDXz|4w&h(xcU=~`1U-taxoyeF3q+4Xd|XAj|9voao|0ZF z>0bHvA=0=X-+%rk*JM4`=rrW;5=S|nV^0-Ho z##x=lXsevfe^UN3ON*<`Oh?yxqd~LgTa{vTKce|P&7zv72~Cl&>xD%-(W^KhdNbw_ z+cxF$yb}{Rb263{h`*;+#~dZe*4MXRzMtis+zI`d`p{tF>JZzF{@ld>^O?23&qjy+hzuw@Us$A}=j5_TlcWA#sr!7rERs*fn2>%>|PKCs+k5Trn z5}G(Ka+pJ8gP#;&o5Wp)5^r+}4_-q#V$nN~V81Eyrw&b-{%u`>G>sUBCfYU8W|~2pdF*CeQJNuqW{dtFIxCWX zd^_A$bf5U>f}Z#5%SpXC+*N#vjkq3PJKWaz?P&D9uMV6gG>Ue0$SgTK)0Ckz|AV@Y zh+eglFf%XG>@Y{ziv45jB=%g_ZA!6>&6A9-<)0OOj7o3N%qZHy{>%4yiMG#5p%JvaQ;D(gWp8kQB>5g9y^Opf6YF5@;Co(BUN<;Jm%U@0 z(S=?=y6FKfiOG5j`NT(q&=tx%t=I7|XUbA^C;WB3PW^fCDET2ut0}J|F`0G@2eHec zon@?dUa9Cn5zDQjyh>sV97DR9&fC=YD1aFO8t> zG}`mmE$yYzUV6XwG}_CiTsn2l*V_yBSS!}y$Ial$K{jD#{KA(?d#U7+ywct&e0It2 z4u|S8I8#5{NPAveFwK3H_!9EX#yml1?emX!3pV5>q1@kCGGpiA4>buFKOSzxMaSU)4u2&E+Gv)Lw6T_C;sHMe&h#!xgV#g^G5wJ(*NuG zA@?j2<5=Pw%e{#Ix9C;oO%1koOjZsE4C(S!fgkR`G~lZ1H{h;c@jn-R(R!oH0*|KOGZ%RS?RmUoWPmgAG( z8H#??UTpTqD&I^Wtm?8S26HdG05Y(h_~-~(yL>~H{Y0^goLdwnI!dHzITbJ9uD z3f^&4AuH(?*nGm&FAblErSM-*g!W6r$8HJt(D(grErHc1=QFAaaoHS}@Qu;!#} zUmGI7lQDIUNn zxsS3{UN@(m8whHiNW${@3%q7rK5Y-!JpLM*U|8md?3@a)03edA{!-P`q*~ z&n2E8^PENAeg}2D%`=lcy@ACm-z48-yw3*F4|o$59wCmkNs4{NzJJuq_CxSOQ{i(i zj@oNq{rFk?gX>S*Ph$)ATgtpYBF?^+c266d=j2DxhXcz{vV{jMmz06&2xSa-zIQyHntW%DbB9)?Pc=3#_tOBH9j2Ghz>`bU1X5) zBV6{IH@s(GxZx9f?x=nCX|b=`_f&j}PRAL(O|rjM@wt86hWDvwl09J5B>TNQdn!Id z@8dmtDLNpA4b*}DM?CfnOXyRqA;P2bPT`41UtQCk%f51+#k1M z9#4G0>XoTHxs0oVN9U&)gS4B<PFvp>i(T!7r7A9@};j+2^uQ&5^*pd6zN4 zoqpx66KTWJ+}c59TO0d^Gokk+d(vaHx(oQ0KP=sy&bKt7-@xG(la#aQ<>n8Y@3!zw z^sY@3pMt$%;gSEH>>+KfX@}CB=J>jOmyApv`tVD5=L;S!@Jq4+D{^hWtiaN-W}9DD zUJWd##R|7Y?7!Es;W(`F$r;Q7zU3q)2RG6Rtp{603x{&ya2WRm@tywiF2@vf-2 z;*}R{xGM63$*)y(h^$~Tas!bUAcOFh&D_uhTwE4;!Q}XgK07jlmBWDUPEs1A##IOV6kRmq$OqVMFF|369$8RTF4x(1HAjNQm`IR_5`k8;il zzf4R{xcZWfaw^YOiL44!vBZS{}znO z{ZKP>?AO6)0P+NZPtnn~oLdX7Bi@!0?6?yc)&4KSsPs$dDS=Vb*W6Kcj!8jIka}6@ zJb^Zb~d4Tor!N4dS)$G zO&Lul+r?7B<9zSoOt8R@iyphcyu$2hQ^Tw|hUf}`CPvYB2 zz8UmyD-z|tGzXgQ_(&zHX}_^hmHO7JQ$OIoR&+Z3`Nh5_ERX!~D><~K=WjI@rcn0e zAIZ;~JGkgKOvAZ@H59f%V~T#_8!3i`O$U`i;WLu>|NMI6!oI0Wp+VYF)v5KoZy~+m zkH&>f^k42UG}@MLa+l%e`$X(b+MGz6O=_@z+^t)ngU~;GF@<*hW1SSIv@YnuqYGb^@S0ABV0&_32H|c|gKCqwt z<8F(z@TC=2?blK3aNrPYBsukab4i`Rkz(4*us}&n4 zWN)$TCHyL7HKDh*k@tN3l%&xQ;J!uJ|*BgH-&VmQ|dCp z`_|E}MjtJtm(h0RZlmk#>IBxdDu0_OwoY49OAXF=ByW0~0og5n6aDFr zgnQF%MrR87ZH0Oygi9be-tcoROZ619l zeo2*S1~%Zu%Nh$7qMNLk3p99L{EmM$4P5w@eBjNmq_H>WQ=dhz54(N*hRrATn`Pmk zMZ{pz$7~t9v1&YfL*nh0v?|?c*sp0B~Po6HsX%W7848JuOUn~`a6UOvIOMdDSdp=NMX0kEs%jAYA{?K7&;dE#66;)#0RN+(Y8?E<$daRajKGtRIsec9o`dH@-{6=G~8?r8QliY(u z%(w;vb&UQ0>UhWgC&r7d%|9Ej4)aI9*?3Kg?WF|9XNhS3t7VXqX$hR4p%IhHbdw&J z%As6IIU#tcuiSgbBzlCD>FpefF5Q*2$PbkLh8Xm@r`U?eQ^z@TX=EsVAH#mSiMW&I zj7i{^&1!JQ<~d5#=A_W!%cnh)*|6;SRh!pN+_B|K{`S3B7C1`R+O}7xsD+ip3_Pb| z$4K1z4}wY~H$QV%FaD!5&Kix@%@GQ+MqBR+WDW)R9N);D$L2w{Vq%5h&W!#O>v823 zy8}UStA`kmcd7%sIFqtgpdWqmkHj34`v-K5?C6(AtR?m|J}Hi`Cw^<~z`||l8ri60 z;WTGV*5~o^te5x}heA>2{5JN}?HZc(u^nWigVXJG#7Q7Cf(DlCICGk-&bi4bR zXDs_g>lf)9C>BJO}PQ?i}bmZZbHIgTGFK zlaA_gbg{oi|IwHKM{5HYMf_=NW+eJoQ<=w7`WW!mw#?Xfrxb>zRb~dDi*+x3j-bzq z?U~H;*i9JYewkUK1{AzaUfYkKUA61qYF7PWYDG-u%xy6xs!|Zd_;aR|#=JlMasC?% zO8K2RO@4eIRHoOq}>4%g{HVpK5jMzvvD-m&dlM_X*8%tV_(8?C2a2NFc z@wG4QAl5gu+(6IqE4e3fD5vZV8!qy#4&Xv-4}^9xT)Wr0M0^R+2lKB(I%^>7aHl#z z{M7dLU?;OV#LRvT zTJ*v}C1P@3T%w}i7h!u$e`MX?hmK-Qy??*19i1MqnVr!4AEqh+&N9x{*~I?Q(_iqV z=a3HWnn`*o_(tdoJ%4k6PFGAJy^ZopX`pi%zxDB_41l)q-sv?mc0GSwpfjHRwvYdQ zTaC}r`~8Yu8~ZkdeYPYXbK=+!MQaLyOjrwkTN+_{3ejvu#SZ#R}ib{)SlJ zKaqVM#{QPE=CjYvM=8bkFn4{>wa*X4kL*p9B^~?vUir3C{}!TuOBthNy{?94oChxq zq@Tytkc@C}!*TZ06kz&z%D~|(!3ocU6OON)yyH!9!g=;_GdSTSIN``R^qO>>@J7{f z@WET)fi`1EMg%q{lO8kenE2Sy9i~WI@wCT+cO1tb;N-`S?QpAMj+-7c?-0GVV8xX2 zIPt9oN6fuivOVsxmL21XZ)zHBD?a;Zz@mG(_bLQuR9r4O(ewvX;U+a=6Z4wYhYzJp z_R0Wo#aVDg=7#E+KJ+EGgDbMA>j%>|#B87WR7~GiweZ{g-ZN!;jMxU8eKc^<0QPJY zd-i6=_Y`>tvuB(BG^H?-Jv*2^`+f3nplmdI_9Asie06MoG-Ssq`nQ!B8k^MsM+9;2Yf3Xd6aNvZz`#Rl zu;V@Yc*YpevOdy)-feixF~vLHa>lz3A0)~2`7r0+ucH zlv&1q?6G>qCls{4%M>p@rqHv|Z3O!1*~nRm9)r*)=*GUar){>gHdpipP@d< zZAot?UB~N}!@cRv)Gv0@{`@!j^2cd<`aaUV_+E20%>s zd8JBR8UuRj{d@~D2Ytq{R-x67E>QM~9+&nPTZ#!{kHv;oD*%d z84S$eH!VYctN3JH?#nZzujFNP2s5M{>D#`ApT->g;*?mS$)3kfE9mxy9Q@-D2eL@?oW{}le;)^M1;lArE)yUu8AHG{+RqxM}iq5GZz?(Pcw zGcv9y_a?3FqR45rvG|dJzPGR^YKeuBT%&Z?%|>>K>|p{vm_#pl(z1se-zbw9@bga` zhkiVMCF8`TWfK~qdAr}(Z{DH8ql#~6T}A@km;nE*5zp#KiZb0_r*^B7wFKXh7V500 zUn0YkxIhmtTh_R#EZDukbfoIxWtK+tIJ-Bwv>n3t66><3{XrYPJ2g)Ne*a@@S!ed% zA#?-0>9M3&>*;l~u!W_5+nn*)(mNWafVg z|3e!7@Il|xdL45$y$+F4eai2)$D_UFAEf-_k>2vBADr z>+nq5GPgBBv-Pq&fn{mC7Fd?He@FhE!1Hd(yMYDIgDJh|nTK=6lwMi?4(45Wn&tSd zdlP&P?bYh!cO-8)j|9F;h^s2|RXP?tvYY!g-uN7IFB##5#WCc~d(?nUsuF=s#zkZ; zSplr8=y?SKH|q_`4E$y3@#qY)cS$dqA~94}|-;Yl4Z=|mu z9l*g^`tcsTMiBjY+1HQvjpOuw{JSAY@5gKO<5^!n4h~mlRw$#Ub!?JZU*=;S z{B z?C%uW-@wRDXsR;$A-F0qe_7+5JpO*jn#kVt_e17V`k~6crXK>6-hQap=0k9gJ|Ey8 znGeA~{pVwT6#WQLA_Pvw1|<%-s)San%a2P`yZZEg2)>ZBA%OXnK2*f*+);@Sw*cl; z#}yrYI<6>X4v+Zo1Zy?~e*_VJTygC0eOclIMdT^B(%iLW_FQpt9V&3F_jk~3Ng4d)@ znLojag69_i+sk>xu7|mmIH~$P${kLhAS#=<199YfJjoeb{kCi zq(w4rX#&4M+WwJ4OPx3*5I1d_|F6Hu&;~DTb_g-VM*r`*_q~~sEE|IK`zQa!FnaIZ zci&ylJ?GqW&OHYnW#suw`xo&3H2a(UoJT){|8!&wtYgNpf--L5T`T9(q)!Fh<0J1C z8NXL4tBbQH{t?C^DVsR54cnN{)6Qx8;PJXmcmnv2CCmOB-we<_v3q3i{7N6Zsr0vu zx{-t6$Ak0XlqqeQHPtAc#kKd`rTv0;Wn9s9Hc0uoT)(?{ap`a1t;J7AI%Uf@3a9Yh z3BHkh$KYAN_Bo^L&vK0pcprR+Cu0wM(1QQ)i1_SzQTY#lkJ#JHdBF$g`<{__N9YTm zvVGk-;nQ5iA4yFUIF$5HUzVPs((6fQ>|Qp$Iz{-sr^G*ACH&-VI{(X_im}3zTK2(H zx>xG_?-*K6@N!jSrs6Y866uQne7UM8Q}Ly9>~zhSzFcLFQTcC5q$|FZ{Nof~YTD_V zFQxo(iZ7j|(_c37lI?WU&Z(r+->Ep{)hon#9KW|F+ zSBcYL_uF8c9Q+;{khV`hzg)F*40TND{u0ku=U=D4(#N^tG4QZ!?$C0JA8-}9MeUu& z({mkT4u9`X_KoZzsz0UAhl=c?i~At!OL*5)*oB|=IG&phKYI+gJUic+|0w*d$bW(( z_Z<+v^-b1O-?uZn*76O0OO7AfwP)$f)}AGDw)D*W8Tqm%<(oOY7d%+8>kCUOc71W_ zZ)T@+|0VAC@EdugV%Ml66^@6NX2Lg{}dF%ml=t}`O zTxic(#&0dWgYeTrCywMidGHC~O?YTUe=J_Lry3eT-c?rHh2K+j4gUIADLe*cj-jo> z|G&XJtfR~?QzkUx+>i2)9Q*-ge%UJX2b3v%c`tmq(38y;PksuT!+z!I0?JWsW2`^2 zZIp9}^QAH`t2)?cAm5QOfR}l{cNF~b3z?m#7-!)n-s8UaOEZu3j)FHnEWGWEBd}54=6;N%R~|Vz zn(G9fr|c^K8D*ZitLps=z_N?jAn0y$MaE2(3D1^`99H5pKLLFeUiKt3{$Kbl zGe#$PSb-0*kG~JvE&DS$E7y;Aqrb1y9?rgWFNAlyVDXT@PFtvW$Z2WQ6b}h~J@;#y zhrHQXltG&`AB$}T>r;4;rkgbHb~AAMYk$kLGd8z8`@D08YRl^bc*s=V?PkXP6!Z2Q z+Cl%ir%5}U70`e#q)E6vX_8Qif8V}$@w}UvAjK{C7@i>{5qsHSgrz0^QC#~@q z4R5dHxs1mz;CT+Q$9WwbkdKaHFL4n2jBC!G+6g=YJ5`*;0Vh5!xa&0dsQ_z*E>?R<+OTZ*;=sSdH@-=q#U3n)k9GP3*nTP8j6C>)tTEKhaV8EWB#x1?J;*zCUf@^OLOmt9{=G4KV4)s=IG~_I3E< z*TK0|LcVFVQEV$AW8*oW^I1 z;Hi}TR~5RpqtaIL2z|?=&FmMhDqoTD6q&G$&0fy56<|-{g1>je-!q4={J@#M&xI{g zHS?G!d^~6299-r6T{=AaH11EorTKL6?R^@)?rZS$|DK6YUt|L!x07;T;>+)EM4yfHt6)HJdMy9fhCKdUaiZ2+2Z-1sxrqb9)4jWUGe-+RqY(F zc>W?gUGw};RrTup`H6JJ^Ha_w#q-a$(>2fkR8`MJ#q(dM)1NZ5ESkN^1SNBxK{`CU z>Zi@Ke`B=8#~+%I;O+lXrzP_Jxt+(5{>{+*KWY7a-8;I)-nt4|sRO^=!h7YRGVB3nwS z>lo`9*nIka>RL@*QpalSkffg48^1qG>i($uDp)6}^__5Dv&intG1gcP<;xzt;2*xe zQOfT|-)Hwj_TLZWbPw8(oxpFuayk21@620#!Z~koeOl?_ZyObhpdT;iv`y-4!v1K~ zDT!q=rTe>FQ~Rv)%$ulU2V8o+Z~v4Ji}ZIJCaM?{7fAccrNugJ=R_5&@>i1Y(lVX) zGT%bu-;#V6S1uNRlB|_}YhvKs&r8}3jC?8$orUDB1)rhE z<;-#oWow^Q5_`;!J9aNRy*E;a=*Oy6>hQ*&B+?M*R7T?@uZ73CfYNm~xe|IE6eUy;A(;2`)_I`}FS7 zz>!KnIjLW|!%pATKmE6(+q4WSx?G0Uow^iOk=?t zk3YOobf3ZUZtEN0i3;ZbUhD&^v0>=<%|2tvVZI4|ckf0QXCc>){f6~T^NB;q=kCSM z;Bn;W{l0mKda{iLbMVdN0=I}?Cf&Bs2?Jad!j|EA?3`Xh2h#7muMqF_dSk&jFR@+WPXrjV{h! z20c$$^+r#a%>6iQ8s5Z);a+20Ak7@)`-8@k}#3B$;3WlFNtwPdYbyyg{|uM zSljYGi*(U3NtxGhFJ<8GtzZo@#~@{$0pDbbEQor>0yiQ5=F;a+8e6{ng$HMa{RO3~ znftfchdj^vtftKIeWMr4{ci4&{r_DBe02`%M*O6R97*g0PR>TQe52U(?%a5m`0{68 zqwL}bW-W6VTi!}TUv_JG*BWAAKKYvQ%3IWXc=xpzp1k$8t~XvVI^U|sU(&71y1th2 zvqe8NdX_vo(or15kM>*tj%@^TeG@$O;}Jbe`bhuLPrrPi4k^9CG z<0^-{wz)#-3_b+~O^N_a`f#Dcdu zGx*Lum6Oh0s+@HGp6W?&|GIM0#GfylR2;o+(#EEFliof$d(uSbq|7?!giP%DS07nk zH|7XDoXGPZgq8^(DdTktKMXQ9-^bQt6>ITcWM8L7OYCucF=2m@OFO#o*(ScC_dC+p zh)nv2q-9_)&3>4rYfL@#g2d9C(p~Uhef!_7TMIr|1727Sey9asufmUDnz9X8X8CJ- z?>2n$HdS=}!t&QP6MF*jh5MN0lUMwBGS-WeesC8le{FU>6RD@sqG96a>6G%tyeM## zxe)sF3({88H-WpmxxdEp*@h2@HFh217vd0pCzC$gbR92~f3x*%fcyKc`&RC|t$QE$ zKd|nbxIbgvySN{RjnO!4i)0;&emWp$cKFF!4r50r`09s(uO2m)oW)P_o7sP{@d$QC zlK&0%ey$v3W3Tl!bTe1?8kvE|8SBmXkv#)06?`duT24Kg)_oQCa;_TvH8vgT-O#NW zz2JG{(B#g4B$mo_Cwhn7Nt@A3czNjTQ~CIsb7AkdoAUGEp;z6Lo;Bv4vGseft7`2^ z&r0XK5&G@zPrR1BUG{+8==emo(aQZ<@i&JY?<~K~Gw(eiG!s9)`?@D^#n(YCvCA+t zd1d6til2zj#T$Sn*#qVB6Fuf)_@`8Rz{}!1zg*EZ1zGbR{D!21C%W)CvPaGb6i!pT zFtiamo8EmN<(cp^(8B_Fngu4$19=mPy?1KHqqFunjV0eue2|U$y_c5NT}wH?aLzpYTizdo7tTw|I*y={oNpOJx_mc>?;hp59)VfD6Z;?G`6Z8SKQxCq zez3Tr>u(%0e((Ul@oD3aPe>bk99x6#HymTT@z;6(qiJ)_NZG47FC>0Av(X6vFWtS+ zD7$Tkc%Rxfk!iQ4;g=2{anq^$8`!w+p?~SLNyaUQXBl7V|F8Hx!5F5)C$$0_(*F1G z+p{cf*4aktSO^SAzW35*k#}YyZ#w>nWvq7rM>%t*bqeg*6eY-V3bTN_v{A7ar#()FI!V`^CJkfl7y$erQiJj>l z3kFZ&^H6y4iiDr!b$qjyZ`V-9YU;3X=WdNVpCC?3FEmQ|W=V5&VvG6J-_FL*MT9)!jzPwXgNs#D+>cvonW^?s*QR~AU*=N1BW`W-9rA15Jw{vL2hW|PpF-cnKEZXw*wV=Oxt?Kf zM|#Fce94oZ0nSzDxduGTck+##?aG`=tPUgN__F2bzxZ}9dq=^MqI+3~pS&YSjoY7x z$IKI4dC%B`?9a{xDhv1JFo%N6?7ZQ7et1ywqXR$dAg}rkeAYynV&5*d?R;`Yu3Q`wS#+;Gly>vf#=>S zgZJ^ZDY~39eA};YJ$ejYo2~ozD{%8kDL=J;#I+P(--GpUCpH|yhl?!4?)Q24zeZ@s zo)4Q&-2e5)6K)yvwbMGY3*qy#cW#ui=G^W34aIwAnY0<3ltn`G1}py*a+nNaX4VqR z)F74%2CA*VOZsDG=NZOz z_0elPowf1O%1_5HBVaV&@=y)7b7RWUWR1v!y}!qDWugqIclS z--Zr8v^o=kjda7Xrda2+@$JJZ;56j?`+V=O+0Gta(V{N)30;}@zA8jwkPQ0($) z!#wN##1i(fyVABygwGb%$MgQSwZX@BZ0Q zT*#WIA5$k}W=-zH7eEhr0Mw}ys+_m?9W}PhChtu=pTifxvJ$@ZG+KLdJwM z@W(R>jjnNdMpwRL%)a{_V~%I!J5*dj@Uc3VEwNrt%{DGw`s0!CXrtiOM#HnE!@Fg` z?-2tQTyrP#FNw!u$70z(?q`dhMUL>Paril(*t_`DZVgB8);YR%5Ffi2+SZc=J;$fZ zBH%~(wx03n>wEFrB>e5`Z-4{fA6;qK2moXGqyPNC682P5PSEr%l2O{zG_na*Y^@{4$w!YdQRs?k470(#;P?FM5bM5#Ibck#%gtPA~h@pI-rXgl;Bd6(nO!jb)td7-rKFF{U&u9zvFrlzS~yvbA^X zR(GA6U2*BXJ8QvbtH5h3!EY(*WOiqM@%^*R zJb30>f2rd6Dra;$KJC9y4Q+1}9-2LblC8P3>8GPR7u~#i-j$7Qz!T5rfU}R#pHz8V zhS7sx4TtzCe0Vi7Ig!N{Zrb~q=Q$58=eZ7-kEnlxxKa}LLEi1E`gOnQ>@NF0HA~sQnNhc0VfuD8pD6#o~%Z3Mt^?)qr6=>(a zCueX){&&CsmQsXm;JhR2jw^nB?nvx@180L>_0BZEr69XX`CHl zJ_ARmcFw^+O#Y>s6YChqpg*YmAU}y8W&FdqfVEL%r4fWUuglXMLC8 z-Bat!hcA%17P_0WXK6Sqd#r!r?C$4fkHvWIzR%jnU>7Ly zgpU29Z~yMe7C--aS+~gGDC>KqpSn32XXp$v_vGHAzc1-i*o4U5;QNN&8&EHMyD5qv z7M@mkb(J<~+@bzuT@?;?O&>BG>>n^MdTi05@76vQ1W&-@$r-kXSA$1#8N;e~mvjmq zIsLfNbq2grj6IL`->iKK3~2Lnt@K~wxh!KHi@lEM3(pbDMcIQ6h~@H57Jf+48Q{aO z;5_H(1H^Jkb7t;yIVS~_tt+uy8oxg-%emB9UqvjJDq^`*?3|G0{B+ZHVyt%<`28_D z8n-u2)UjMv=W})k`NT2Sr_hGg$StI7N$X_G}@S?_20`C03$S?kE+_Tdj3T{*E`(9^A=e8G3J&PF;ikK_MW z?e9h)FBWLhjo6PBW@N$H4eVFk)#~4gzPJ7jS9yvIh zv*6jreoO+#HYTrW#4t^;}{=enJyH)&1K&KhadHw)FHn3FQY*@ z$v$kq%GgOCj-?-4oR|6XVq!v_gU-Gqdb-Eqa|8MNesX5M(JAquj!*pS^Lxj(GJej8`p$o}Ls?*zU?S12$xcfee$Ih1;=IlNj-1mS)ACVQNOfE5D{s~ySEM_=3Ms$V5ADYrdnr0$KiMWY`UgR$O{Fx4bu}XC|ThGq&77oK**V5%FX5Me2C*qVNUj-QveaY^M9( z>zi?U<(`eFu{m+!GmUy1He;KThR+R^|J~`Go3T-mZ&vTrCRV{g8Dgcs9a1dy;h~I9_P>dDhaJKl$Oo*MM8-wOVHZ)|t?2XnA*knmsYG z&V**0LbI)P22ZZmnN6Ezon1zoX_wIETNG{16WZL(dUzVz{PgBexXv1S`@@F7wdiYI zwC5Y>*-i=E?M~VcfESm@+UgnuKDin`ML%KVQ^`B%`a;HF^X4V09Z8={sq-XzMB$qQ z;KP94Bb}SGY46V;WzMRZvk-Ik4r3#<_$B7-e)=G^_$B5{=<&q$M&}Xctdlv@Girg$-jdJ z)2;`YvtUPX`w!u#B#tHg)YEV9dw@9;ehNPy`vivupuzj#r}okQe0=E08t^$Xj|)5s zZWO;G_TK52@aCy&TKX*QvFA+oqJm%H85XHMz>`yzUS-^CeKQmeGG-_qKZED1&hM4z z-tRcS-&O5Rncr%s+RI;zj6l{f5}Cs&WT>N&p{A?-ef=Ma&fI}*rPiCjYw69EENz;x zSoA~f$l2a?8C@b5)AG$}*k;V^{x$shC}=lH+#QJ&xkp;ICzSjV-dLUAD!s1)SU{-d?&i`L+DwtEmeB)JO0nU{i2(G7Tt6! zzBFvTcn`X*RrL24#QMrZE+=|?k*POKJ*w1EkOYGrQY15Cthd#)O%unQ0VzXrs??-gy zuQ7IyiH@8-p}aHLi$ebv!4phpVi$B@ZAd+C=yuV|0N zi<;gY|Lvvy0)xW0h+X(8dmaO5lG5>tH!D zH(Ph7ZA(@;X6;L3j8og~$48ymxJe&WTXs(GtY$2FnImb>2HGR|@+50V{IqeFqkF=u zjE9n+IjGOcIzx_DZFJ86R7ID>csfaYerm0;L$r%^VDTi?l9QLw*5}l| zWQNgs)p^NJ_Wd+@6Mavem%?P8*1HXqlBLnK}MLsNe*Sa&n}+$Xsl4_n-XFn6t9Nt-x}T)1 zMe=2vX+O1pFSE~_xG}V(4Y^?Djzj6vo{85Q^%Xl@>Dk7_0PCQka>u0fPUI=_Zb1G$ zlJ*XEALQSYl7G+O`FoT3FMgc-hmKk0cT@f`%0Fwh=hzj>Pv*b)G4fx$Lirab4PO4m z-oeYiLVk0nS!$1u$#~9V9AB}clIXpUnKvQ~I0=Kc>;YJu3gjtK~P7?ExOhzmM{d zU8VfX^IyDL{{H<{)AmzN?8wpbOq2^=~cDoBrcy z`PTnDTK>&z7ZX#NyssQB-~ZF2lY9+Y4Q363wuG2MjJvq<#Dt40PI#{U=J@5jyT#e@NU`C6n<~o|boqj1yz2C%GbxijIj@TpFM#cHn!~uDP_p2QfyEmxoWa49t z>&8B+`#3iLV@Vt1Sn+N4*Tf9*J(4$a`?nr&{1Ne|GQ0CQPgF}D&JuMCjHNp&zm<1n zq_U%8@AXdlk>&5NV!tK0LTJ#3oMRQZ90y#=`N0Z!s7!%TuD$H(8=Y0(Ite{H^<$&! zPGqVda(Hb-M$Amzw>baE&8nY)|^GxCY+I?}TF;#}o2BkhCx*cTEfB9VUYJ&rA1q!G)cpdkJG z2iAN6L+@QZUkPS>|G>T@&aBnD)^<1E)A;Q%yldQ*anQeOY_}@+9$l_MxnifAW2F5& zBcsqdJ`9=7-9Cx43=lsQH z#`*h`_8vGh>HY)x89z9%ddK$<26to}T*EKK{aSoWcQ1VK0I?+K)7TFx@+&@Izb>(U z4r3$qO4x85IOG^}_NZfQzc!6SW+v?jf5CA(etOr4oJGd#%Q-(fu;GT64s7A~JZ<<9 z@G+4#&z=0`18=2`FZdC5MwqtVlBNHDw zP~;q^&KlmH`J)5BOe-qrqpXcPUO9Lp@pEf-Ogi|@8(usRT{Y=oG2eX&nExfu8@YeT z{PBSsCV%0;xhb6oHabUjpP%%|fw!kTbRgfEark~C^MJwmx*TKj2NRh8Vt#J}14WY` znsqPpf0{9t`OiSUIfnTMmJc^F_p2A)f1v+7jiHSG<8d~vxL}>t7nzG7_W{O#-=uv9 z)@2?)um-u}+8yH#2Ih=Am~D(@tjB!t4Pb?{cbt(LcUbBeXE+bfHPRUGv5fbapAQCe zFP!E4F@78*zrc%vl@!>t%QC1V$LL>1svcqr>ofhsw||oEn16pBXA<*xKFvNaKY#5b zUD&k;KB#yk<9+-^At&-ME}72WpR_tM&q|zy0-*~j7U zmeGD>H);*Z+DX3@KheO?;h4H+*$!=+neCW*mi@f)!F+1|eFt*zFOzPhALo?m2{~Vs zP2VKlAPs}WsYbf{dgGeL>utS<;D%*}W6P_o)pX|nG(K6D8F!ygzmeDo=FtOrllB}q zJ!LPke>rDPJvydNx{}3AHH@zc4I_T-S6iKxv>&@$!3Uf*l^6@)tToFVQv>wwp1c}I?0*vahLk`?;&r-juQvx5K|&^$FYOA z+%V-}#*Qfm@6S&^ICe++L1cZL7a#NU>^@_Q2iROg9EWTAjN7w+YiwCpn0{~#u^qZc zuHE+r`U^SBGKMi!a{zqo%{_Vm8<6{L*~N8 zusa%ccNF>K45<8Z`NLt~@QuK5!GWjM9c6antr73=45a{8I1+Y++k?SjnVT2|wz=aB z;V=Ot<;A0Je|&iQ3uLoI{Vm~;FB~714(kOUbazDBhg4t^aYWtQT*HYxnnUqnLqy^B z(6E)IagD6DVVMZWjBW!#=O+<~bVjhZV{*!G7&NbDKZj$_v(7;oZJy#Eiw=@%ET0Ej9CcvhvMs zt$t6d30m`e+!RONX171s9`y|m_3(gv<1UuF3`x%lm;dW)0i|CiLwyl?xvuxXiK}_6Oe?Vly3ujtRoC7+ZOT^ zfkooJ?W)k51wDc6;t_C2D6-XO`odfN(TJj~Aibzx8e%r~q&2eEKk@3e+d$#cm5D17 zwZ_`q+rnngRMQO_ZS@DeW^*K}kc-q;=!vukz2TeVW-~Zdp=1!A!oq^E+oF-JelN`r zyF)(n<{rn*MpjLGG!Pm0$#cAaq!1!F70vLHsm2qPOD(%;RO_-al>2U5LxIXX1E;D zhTLsMu!GSKX>pH}aUNyn-BMHtiOL_wQC3?FRRxnc90X6ej3UiyG)LgNqwEV=qMDXctLS{`$I9Jz*b4#{Kw*vyT0 zv`L=ap0T-PlI5eG#>ta!E5m4g(bLo8H1GN33qP?USk)o5p@^65m^Uae}(3XXBdrJGEs#Z71xx_+NF*D`^QXu_av#A6AGZb{epW%9pqy@F7$A1ViNYICzO5@KnSu^wZoeQUi`nLU7mK-De0)7U zGbLkcsaZm1+rx==>php=Mhye5kpXi>eX;hSLJ2vVuYg(b&@`F^!{*gYL=2+r4${vG zceJHlqzQ`sC@j=-lGtCh(ZYDg9N7`Zd~-~}U?yzJX2CjU-ZrK~h|H1!g+QaoWbe`3 z?eWMQ(;eLA?ugBiufrhPX!7fe@c5TD*jrCyT&l){DzcVT`vjJ{Tr-INGWHT&M70q+E z0Foea;RqEY7-?phgX%RS?FR58;a~^vBQam5d=z6V%e~4iyLS!L@_n;E$Q+yQD0}Mm z@LY2pFywE81GZl1%ITQ9BNOJa%^i#RymPE=|299h@>#NW^2LEZ7NR@cVbZQ-9kIEY znN=O8*VpW(4gn0S#dhUL5hy=Ox-U4_Y-pv!9(PzxzqHW?VM@#_EbKSx%|UmI-mKG@ zaALD==EZz*6M%*BEzTcMy);Y@jn#Ll-W_afbvOCq2s45}tQT5kZerXD3oZDtc#zEG z*L;OI*Bc4h`8O~`s@p9-WU-23<=S5hbmdY89q0#evKtZ3CebEJm4}$H`Kldy(&w7% zBXQc@>W(LiP!m3&KyO4LPj@qfhy6nUB>36$`91Eg~y;NHBuE5wLcCRmy zXk+**7N63RxtT_qSuVSZbyY>5Tze=jRl-VGUjzu*0Jqq+8teTOrH0Q4B zn#zXCyL!?-``O!!=}B&+Vb#Xvl{NL65itiaB5jeFKkjF1;zy>`0)ZGhOJWP^Xr`w< z8fDR{EqzvdIO_96TEhOj0Wga}Loh6Br^mR~A65!VRzwR7vMtlmd@sV5V)BUC#qr5! z0!nNth1nvs$Nj@3GS)1GFkxeJtz~U(zKyBu#zX!l7jjEijJ}7xyrHit8+e5$FW{t<{Xn zCkctvbPCauEd`Btt_(@V%y|{m-%G^Jj3Dl z7^7tJ06!M4F2PK^ADFge#TnVEHp0>Ir$LSj4VFhy8rj9DKsEfR5a zPsYu{jkA=z#?(riHH1+~%@Fd(VhoHieN!s2(K*UYrD2$~(T83}*!#ggOs~@v>E@p4 zH=8XG#;_Se><^0qvj;)*8Oa=3G$^82WteQ3X7&;Zp7j%x(S#h!;|m6hq{g90#rlaX z`xR#CX8oa{AIDh40*X&O zTy_T`>D>BQHy=BaS&9s}b zK^UK}cMk#JQ-p}ft7Tz)QX!>XzuJ)m>{*cQlWE>1((*{ikC~8Hlr7Cs5Gq#@+gLSf zAaQ%W(GsCyqQYN1w^#v+EwCSI%jV84;{PUy1O`S}y@}CYtRvJE3Bo59wFrC``GU>E zF$uAbSW&Z{2q|u<=G+Qp<4}m{)oqte1_mL5as|US2Ql*l&9Os)f_nlC5L{W2SZo;! zn=x`xk|QBMnzWT!=u{va92tjy6LAy5-#L~wMh_&}-2{SXRzCRzCzhI2JsgA7-0Uxk z#9WNcaFk{(Bma<00Bj((3WHIDD;P@a`t1aoFeebc5986WPgw&*W3B#S(U*K2*an(n zL$bE8N2ec(VZ{z*fb23;R!ouCKWs&|xq})V4j0f$JC#;0x{W~&G<7_!qV)jS^yTI|64tqJ8A@1Ey|!-orbb z4AQm-GewYcg?B^6#WIZjn%Cy{HVM&5+cyc*kg?`$?wc%N&mHw^jjkeh{(-tv1KQ7w6HG+n@4QU{ZwJ;+-WN*^WOk<|aWguH+BNNeVNGf+ZRZ z28w{U=)SunvHVt95aRYIy|o+d zgyQtqin*#;ACE*4i&zRGE%g?mwYymqm|iy0(RQWD84jdmaZXwsJ1jTJI=qSi7R#Hzjejt$Va}lVVj~CVX@_Qr?q3N9o&Q<-;+F_Rddc5iZ4^2Uau< z8!r1i4$ef1upA2ef{6OsI+A@Jj&Y=HrN>kp#&DoMnH<@;B3}w?2?+RjDye0;g!@ zrOTq#m(vC)Q&fJlrCs#QZQ7W|$WMM@5dc0Q6&GtVt{TW(K$=+>@-u||n6t6XY zvj2*Bhh>}vKQwz2h%e29U~7bV(@mz4R@jcn*&H(LDGa17(ew^ZZ)9XB9e4FOKWiF2 zj&i9e+nQ_JUadVGYgddwGa{o78S2~=3p#ChVsQbUAjVB>w-b5R_wUZuPfXQK`=Yu$3+|=WXvXX0?X{ z;Rpu>5{3gx0jytM=WWO6KyAVM7hwSfievpT6vY?a3Ty^_;g)!-{(6S}w&u>tjVnw< zb;9+^&}9wYFsoGT7j>x#-dyNA=7nN{ZZ*u#q7JyKlT-cP)2mo}o9JkSbG9g646&uu zJ~Q=Yq`6tBg#GRsyJgk2%Y_fOUNce?%S=m!VamJO8Y7W+Ma_V`)B5LaV;{?UHLkN; zjq!)jVA-og+Vl#OxtMBKjyagGInky>a+}}hfr44x07&&P!3_nqh!~=t!kkKxxpt8? zce<)qRH9i{o6xevppa=iXEWIJ2pZZ3Sv0P-3%1(Lz|%%H7JBTbV}GQkA~`<##bmoK zJ!m?S$xP0~;00cjnkDLMVX}R!FlqiYyD1{mRw_T8-fw3zI@ml(U$VntP{UsJQ5GkLwW zBvElP2ZNz=gxnrsm{K}DJ>?ZAB$MCFwi~1c3J6VvbR}1`u6x2JyY6ibZsxMbNJ**G zwkjzpYlYhO7~_*))hIHQl29ocMPp(z#|lhbkV1CQ;QVI4qm! zqIK$BAW_P@f~0O-KM~v~<{gvjCUrNI4kU|Wm!HsFaibMi!&7uqgoESBDg6ZFoNVSc zmWnMV7&2f=VZ+nKs5%hzNgI^xo=E+qMQ~5H9k3>kdDvg~%*anU^s%&D_*PTBz#wOO zY@54{Q!eVzZpt^)`+d{zlgTPKsOsZopVsJ(H<~ecYfcRar0cq`)~KNm2yQN9Jmhqq zAxd?%f;7&f5h6deKh+HTe3h8Uk`N!Du~1mrj{qT2r2ZD{|4G}Nzy)HY z;t#bIH3#D*-eGhXWS2{sC6uj?h7JXMlPyN{hN2RP8u_^AD$M1J3WfMM{yndKMb1LkRb0OP8?FIM5 z<}TlufPL%#*79zaM!+#hrAoz#rVYfQ>z70=Jpgk|Q{B>?SN-HD1148ceZHod`q3C< z3_gAx{dBO`r3@Wn2#_CmEir&Y$7FPclUu{Ggtgv}8I#hOQz0My2DMBFGz4IEuVD7AG0<-mT#2u?eqc3SF_+dSSAna^DUkO&RWRgUrg|!2V}k8d z4D`%lsiAo)Nnj!y|JxLmwFlqd(oxhB^~Z*!jsNH>yl7Rme}bDtwWhsAsScN!+C<;f z^JAfbyq;U~lV-f?yr+K!_JWQKDq{~g&Bg=K6$XE+!Zh7+Ghv6uqVAp?VG_WW%w zkzWeXr~Og;ANM%!5s_`1TbW679wQnNH$|F_lO;OjN=4OSZwo~C5n&50-l~w%e{%Il z5;*9R)=(z8HRKk{FmXtv4IgtU8_^zY2lW41{LguR)Buhf(FAbYGuq56HkU3Pre47^ zs6moxO2`h8MHH4I;=?N?w&H?_1Q}}s6C9=4h{Y)2ESz;##MIv?xJ-N~977`WXapVn zqYKrmDmSiN?pj;BuC~FtuWzW_*icp3z!C3HyEbgB<6QUh%4+-0eoz5~cbGx*8Tx|1Mw&?UH4c(uchK-%BqNll76^(L}yXtQ$DtE<=VGw|}?#F{onq zZ_R-E^!L)ekL=Z2<jQ4F?bX1iqi$nj%} zaUG!XWXgakpD5ac3+qRen+$|}jHv+!j%gB7p&ZVd-uGmAR%6RBv!|;zVrl~EdmXWJtJe$SYYR^aGcxyQ%aEL4HYq70;ZELgN%Q$`{ zIH(m-R+elzO#S~eT8>NHvNEmF*E=@)3YJSn;Elq$nYWc=BnvQw8~-q;0M4xJBT10n z_=8$fNsxAn^ao6d)mnXe?$* z$&nMRZtA#L(o04J+zaj=Y$Pp{$D>d_Esjrf;B`tRv^|(o7IEuLt&)3FupAU zN56p|F}@7r(HrD%;$QypiU0E5XMeLy_G9wzpH0JPBUp?g>1l>>f@i-&T}`>9( zenlOd80D-_tjk>NKoJeNJl55VwSqjiMnXQhio{v2h$5QUXuDmIoi;Zn9*^ZJ?r`KPt~k_{buU-k zC#rlLW>w!KO^8UkY+dCF`>vmxd@U*+>4?5Yf+4>r8gaF=xs186AIDH7=E4e5U7M^c z$GX*X8{E@UYlKAzg47_^>@{=B&B9_C&sN6K%&V_mSzDKXhisa$hOWWkjK{aW9!!p_ zM4z`o8IIR_S704o?NTK3JBKW1z`i{73;Al_Ip-%5mSW4uyY;b zEK*ZU(G^0}M`C>5`VI9PT`OvDSJ&E&`dV4lpq@9auTj_fJE|Kt)Yfd=w6V^*-icVG zZUY;Cef4e8*A}03jq59IRr!L}bJ(|h?M4@E@QpRbkDWMW+{DRK`ESa^DYIr>e|^?< z*IhSZ;-m@Vr%#^v$WPae`tdtEYYMBcJ^SjSS<8%_y^A|usI2yKNRYi$K;TCD^Y!av zMvSqY=kOb&fzP$pIj?!eCG!_7T(o$7B)&rSVGSJ4_SUt>Ypm}VFDWfvvZQ!kv76vE zrSlM9q{`XKdCnXlM=PI~Ih$=sNcMf1hL<{J{bcf!HK0sa@s=X86M?r4l?|)J`J|`a zTP$$zX>Tg_v^Vo>F9y7eIbH9Kg}k0v$R9=e5RZjokyxlb7;A~ni;|AjcQi^`lr)w> zaUkC033!6hKx;e}4F$Hx_yOL^iag|5C~(O8CdzS#kYa_l<7y%n3WlTn0*ER2HWKiJ zya5ku#1jSX!~B5%W?;IN^$}uigkmiLtRVtn(j#qQe(^wCbSuAjAj(?6S~U=Djs~J_ z!9X-dnlBb$u>@jm-T-49s9L@+0?Ztmhs6D+3!g-odXSu(75tuyMoMz{Xncm)8gC zEAI%@Z>R|XlU`3S;1TnAj@h+x+GU%BMe)VOz_$s>jR${Pp;$P$B^GN9;afh$Y4T7A zj1t<)j|t(|#IKoOdo0w_#4jFe@`qxcm^aEb%GDe3a0!QyWrW(Ax1*Wg8mHiJY@Rn3 zE9O_iZ@!n$JW=M3`Qq!AXmMSGzof1q8mnsv2kRP|xCSD14Iy`3!%EUu&gVy-l?(W> zm{zXkw~1d3zg7J1;8zW=66Dw36xm)3b_>Qq03cVoweAQX>nXAC^i?V;_AgQv~EJ;;xCN?Sd34eR)B zJ?&|W*EMX9uBXm=;8qB7SmcLTuHE*>2$A9wtkU-QcAL1xAfO7Py17 zI>dgQJP>9rChNTx0if`%l|19$Dk}JEfb98qkW4RL_>ns?d!s8NRnePp@`hfTLqn&QoGv0b(KVLK8dE$WQYX>|NV^96Fzt6It zog>mGniV@8*0u7r2XBAz*401#r!Vb3<9zSiF93(ah4>1!E2CHj-;Lr2DN_HMa4e$~ z3YJ4Xc@~ocrGen6xSSD2d=9k2c~g!TPnmJFHGnDyk+4=3V^~)chfD&BZVGSXvNFlK zQqS7-O}$s!1)9WRBIVP}dX*?o^H;hOIk}>rVbENgOi9k0lcdEi^?w7=U2h6@$Ksfv^j{Nas(S28fl+tN*9p7Q9rw@XLQ&{N~)R+iaL& z!mxpM-L5x0Yj)k? zlWy%|Fbq@lFX#G9o^vE|reSvLzF8`ACRaRBkEMKuf&gZEk zJ~h5Df(AK{0MA*u6+U0^LPI374$swfY%zr7#cIRYQNxGwSIy5at=^ost}-t2?+tzF z`>l1g^V#ozWu@ECr>$oj&)N9QejcZrpO~jv*0Y*x?(O`hm43Z-wZFIV=@cuS7^3Rm zZ0l;L+xhKzx7SU-d9JWN66mr&Fy^U58=lky?^4!d>MKH}+GWVqepO*7TA%Hd=R^@> z?YDMz3%q+2a9ag_u3+id-TPPnRqlZiS;kyP4*$(pf8|d9m&Nr~M^)n4P9OT!Fa|GU z@N_%XzUI)=uRCj;PdWdA-##0$zE3`X z&GEAVUms4%oh<#A&clv>Bj+xD52!CTjF7j79e$Pnu;US{9R2-YtCrO`=iXMi;P#tq zu31@j=QMXg@%61E*L-gA`1C-|UA`NlS?fV6Hayu=`SGvekIvfg|JVQf9sfZN=*va} zJ(73`8`P6j3NBCa<^}`c3$-cF>&m+jELQ}c%MLB4Jxz&v{Wh({EEC-;nggFZWDX3~ zq0dNfYFJTNnrt8RA&9mTC(u^oh*OxfM8Ql%OmW3AtcVM=Nk=j+BH=%jsZ~PTDT?A~ zutS->^lQZB)+Ko7n@ht{$(EO!Ui@LG2l^*_u#Y@LrF>E&uybKTRK3RKQqL-4M(z4L z>eehzO{rSGzIv5Q{ashNaZP=3B%u4RJ&Ss+!N`-d`y9>1Sj3VdbL>2GMIt^KJ7|+L z+sudZ4J)iJqDlNuLk^SYXV-69E6PWvKx|DEOD)oTVOnrTtt?XDqa{{^NW0~^tW5sG zdHKVN7xH_mwLc&vUZ%GL;%$9sHgo*8Jub3OBrW0zCPekOiIIqTMZQHdQ^O{QhIq~Zn07>T&AEK^Ls zWY4QF%KX*}N1`mC8t4ndhnz^AP=!CnGDQZ~%7lDhnN_im#nOd|mQfHrIRXw9TtYIh ztN(O;b=`VEfzw=^DU~5kvO==;SfOCFu1y(^Cnvw5cHMHH+!F(r~h4`6D8zR1URVI2)APFm5KmY$?BINTD44E9*Im;qZ_GZ>&p;`QK zmkNezio<4-y*`#(V)=?_L}|9I_u3BwIpi3kywr%Gf!WDb@ojP~)F8rm74ATUuSg0d zs*0M#urw8$EeklrH9z{U!p|^*)RD5g)MX+N9=;-FUaPT>t81ul*jTwCjQmu^-l!pP zlTRea1ZN@Ug4viFUZar{udSAgW7Q_>o@Pnplp*&s32as%pJ1d#fH@i9Bbj4;9j0~- zn>H>7C@hD7(M)8r{YHmnX%(B+1qp_P#h36 zNE|PZtK84T6Kl$B{EV8JkA=WCUGDo^ z2o^EPttKcHIvU)n;RA<&-N|d&^}-W2bC= zAc{XR5wPhLW}|q#k(y*~u_I8n12S<~ElTE?yh}b&75oe2Q#9xD#aetQTsb^Y%(q-n4hUYKDPia&6^l1! zL1I@)Q*fzd$JMLG#NFS*oXAE2bvnEW1E}#-S-*HS5{M2F9@fPwBLR>AEkSx31`R-l{FKeINTB(uIrG{(v+ zt=yr@*`abIJE%DnWi)?<-nC9a8cR#dp<1XT$(phZ3{qhyEtrK?eFKLk``l0QoD_QV|; zn4H4jHwwV^YsYDbpaF`_l7sqq=)Gokg3cT{eyt*lzRyd3{g z>s`{`In}6rYpW~Qs;5RBGRrRB4)mG~j+jJ`;HVHgbH5de15fXBTjrXL3+K(7Q!;Pf zyhe$ofjd0bKC%IDablm{filenO=du3U{7-QKC34}Pvr1XcrB-a3}VNRF`)Of`gw*p9%CUb1% zM~5#Y*Yc^#mR{}m#9ZVB(=hVJ8{^F|9!kMy$@tsK^m^*er)#h40Yh5xHDEZpMaV>n}l`<+! za6Uvm`z-%%O9Vo@ZRxV5sn5%JxM}wG%3BjT5;wdddSF?Z?9MrgBSMfA6sF^+!BXi% zII44mo!7y5)xKLX6lxf6ge73)K87%?Oz=0zR3Zw(40zGLii`x42EQf<;$4ebzH!6g z$t|1~lU2>+CK(k_2JMvCDoG_ToQXt1Ut5GYn?1WGb z!#&s^I856N&oR%wbI#kIy#17YD??;Kn{Xd$0QD%vY|A}YP3^||qV`y{2sLfgmk6Xj zmqAowq#{iF5~hMBGAVU;4D*~eFJeVejpGs#a}f-iC{xlQ&#_j{M!wFSfkD;f%6ZAfFhLGoZ32fLq;XI{v&fSpw|Rve9$L}T-@fAU~IgJTDdANsMdiN}KNp4N81 zIztvBdSf^y)>l#avmMJuPZWkfgvk;XOD&NtVzksIQ7j{E5z6q#)t|PQrwx-FEOO|j zXKOSDgoLn0YTgpVj0Z_322Ifr7EM?#@uw{mYY#;wPc(?R7q6r8hCgkgXnTke@)q-_ zN&Ym;pY~#$B#L(t57N(;rZ)ZraIgg6dC1 z{zm0bT>fn3PfQE+JcJ_fYYjgv6gx5;hxt(Qb9yz1*1v_7pkPYQ8;Ybjrk}{bRvQyJ zmoF037vhuz+e2Y>1R1ff81J#nlg1U6@rBqe$!l$jA=awatv1|K1{z`lVWnael_*hv zqC;X@uaz-_bgqlI+gNeg8@%rGFg?9SDd30fIGM%}`ufT&`lR zQBe0KDfjbJ?qyGH4TsEzbt^c66)Oh6Vt%5m1LOgjsAb+Ph)q(woO9%FQ2zS->H!c` zPm!2hn&+v%#rm&)C{Yjd)!zl`??Ux=k@~w>{Vi30m&jjTtY4KHRDW>}(4x$g+w2hB`;cd`0g zs{Ss~#@iTJY?E=bOouq^n_|?XVctqW%cbA7x7p@K(ooehWuYzYQjy85xztwQ2$RIr zN5K#al@(yks(!GhRXB#091<)uWI(%6)SJss7d1%AV=(y5|~DTx>T1m zCz2;YIb6g5HL{Hd7CG=~8(bv;Q$U`MCKj@SL7@`TXSV_dx0?SZ1q&XP;+Zd>A)r#g zCsyTZK)7LXwhC*toc;gwn-|zFEPI zW$n)H0beZAR^<&@-Dm`=Ze<6fz|S&@R}GUlmJmyBEt@#Gg^A%Hb-P*)YKA2dGbOQb z^QdxoV6IwqI{md%8Cx+|mfR3Ml{JRllWmKiY^IIhC9g!NQfC1KCs+-Q>2(ULp>kjY z=TYUx?DGOK`@jJ46!nP#HU?IWdi_D6tfLDly~Ms%pBISP2MS^-9Ty9&M9+DEFI3eh z4jTa6c4uSB6$~l2YPL@!K%bMn2Fipsi=i9}Rb&EEW^=KHVKif; z#TvmBYYbDYP)u`)g_(3ICAyRnT}p{Ar9_ufqDv{!rIhGWN^~jnbt&_8Df4wH^K~in zbt&_8Df4wH^K~inbt&_8DGPKd3v?+9bSVpTDGPKd3v?+9bSVpTDGPKd3v?+9btwyV zDGPNe3w0?AbtwyVDGPNe3w0?AbtwyVDT{O|i*zZAbSaDG3y%Q-*4&4-hC~&K-(|6u z)X#cd2}L6EG_P1mo`CR3;=@EeX!nEqUW2NISXv_B6^&zr0TNbNd{g{i)V&W}mF4~T zec$IC;BYwN(NIZIk4A<`dQem}DlBYRtSG6dXriLwB4t8FMH?+zY@?#Zii(PgHMXd+ zM#XnuT2$0*W91eV6%`dVTCCW{6y(7Dyg%1{{(+S4x8L)6UJrWC`&`%kx&D8y>%Q*m z-(A3YtrsW@6Bw~ z=LAf`ZKKdFA*3tD1?SydT5P2^1;w=6% z<#sU&w~OX8*msd~k@h{rYBzUq%8SJ!dndX!dvp`+LRYb>rdCFd84@ZA?fcM)mR|NG z+mHpX<^tYHiIB#6LWIR)VtmDB^0L~A;fuuN;7u13yTPWNbTZ9f5{~&xH9x+e!5gJ| zaNy=ZaqXrTB>S_ZgR10|(NuU@y{nAe^VBuw-DzRXqDMtUOW4O*vWJ_JS~`4~1pEG(Q@Ev|(DX;y zD~+CLLw%TFVd~=qD^aHj7Se#p;vB!NNKK&ldkl(ds-V9-fJ+QZ+gHrg+O$Jgf7bBNUh&0U^J6 zS9;JsNCD%y5+$u-4=44hnZC56iUclY+ISrb`& zT`_I))$+x(HKQ0;ew(+xTqzs>ig!zE&8+#Jy@lJ0WH^aowOu82DEOv1*>(+k1dF$^ zC2Yr@%Xb&Bp|XIReEzn4md(j7jAFJ6mX;J;cU2)z54+ilxP3R{b@FVqef#!p+jkTc z7Uyr>#n2xO8xx((VMsZ@#2|H%Rnv>LDc5S6tHqevrcN-I%pAN-%QG;U&#fb5_2$<( zmZyM1PA&dw*SU*xb91j^w#TEslhpT0^}R}cC#&z(>U)j)UYoluwY9$DZi4|D)|)ky-s~^Ro~mx_jdK2ufCQ4Ny`7V zJN=`yJ6UKZD_+(4apqJ+vAMc(YhZ%)hzmvvSfm!u1?Lm8)OKX$(_-Q&E75pYQ^;JEfAVDU^;n~4a=G{_rZN^M zx)(Eew7W!XX!y6wVD`0(-8VQ=PbYecqkYKFYJA~?yDs9qCETP>4p{u=~OKr$o6aJDZ}Tg*_*JCqdy;%dvWcgEULBp zwHvi2?FsFd+LPLCTC-N7t<=x6Jg8r%|3=@VKcQcsU#wT@=jm7LXGz)d_kH~l_QBkY zDNc)YT4&Ch9?J%?M(qa8tNm1~)IQOk)!x-E)V`?|YTLA>6aFpXoYAG4TiH$5PS<8B zvndmy|4~cS)@e^iL`3{tYvB8tXClHPnx9dIu!vtqL}+2Z32T52&;0yxO+NV-e){RB zmulbs^wZh=lfT*VpK71ZHh;s1KgqxF&YP+cx%uYVx6Q7*8N=+;Zqx3XJ^QBH9=xp@ zR^NQ*JvYfmbI+cqEu1x%eQbr6;J;aVq2)wP{)yec_)gbf_DO8x+C=H=u+b*CHzn3# z_YAower(nsL#oiS7jvrqhIS993W&LpZ}IICZI!w%zx|(AbpP%;uaYgah>t(iVo~{Wd10)EANWe%oDvUsAe$8TsuuiQRYfe`=>#Jo-~wzxJ9|Lpeti za+&@TnzLR%UN6>fLnoT_d|kE!pUu9aeflEp3@uk*slBQ{qwm%})c>vjR$F1&qFMEe zwC`!rmUFbv^qtz{a=K;UFJKD%@Lzh|8fvBatg%!7)m6WMpMIxP@o`N%-ttxd4!?G| z&1APC@l{fbCxv9n1;2xCC;KqjJUP^(HEY@e{g>7)`hM*geSk81NZX)W^{4e-?ImrG zo~=Kucj_MPRg~ZN^lm-Ea-5c~-L5B7QU~=4ZILC}@`iRUwR*L_Rl8fuuw0}q zwUi=#o8_zi9e(X_o5^lP;;W>7JSikoF8CdEJK2ZH=E`c)4;Sqo}u4Y$PZSFd}?`f{uloxVdu|>9YY>|U?uUAWaG8wWucklVKZDRd;F(hz} z9Z{#c)#1CGP{+KedWh#?JMnNQ%H?D?<`cbgaKquTkt5gqKFazT|M|X$ONQ<@Io0i% z@?j=0o@#dK=sL(S@@|d_WjA@qlMoM||9{<*CeMRrzl2RKb;}lcmDm;; zG*f+8HOnm8zQcEgc{thHCjHysH4YU}2MqXL6B;~mVEa`JBr(70yDrfWIgdbwazc%J zwof#vAW4XAi7#3_8!-wbtDx2LBXe!(>Fhyd z)6p3lPf413RjE9A_y!4CRV{;HDuWy=7#Lk~mARU3$|5;4@ZiF+5!bl0GdBlb=uTae zgoG4{JPoecS-fJq95GR}f+H9Xw@Jj7ZCfQ2pE~>e)G32yjF}Z`ejN%4VoK@ra91<(koVF4b?_wa^Brr+gk5Wk?g+>TZ5 zatGiMil=7Zt9N}>mWhTVXTsraFXNuF%|ji-DhI5cz42o9@SKx<;fBnR-0&~BlL%5| zJjMBw#wfqMJzU+FWSCPn?77dsCX=6s2gb07IT*SlKLKw_b}TKXzVk$=j=nD1mA{j! zE$JvSmz(f*9 zu&r{3e34|Fk;%bO8_#eTapv67w&UwrvB`8Ir#;Iy$BQ=RY|7r4?hjiO+86LSJWa3g zmz7zyAgtk>VCL9aOYa?@ZIFh&YEg-%s!SZc;I!| zyfcNP(NsT6oSRvL$P6`^diGe}#`?P;^`&>ywU#((dTG^%ZSR2lVt4?2J5 zwjpyv9pCHM(+abewRrt{naStgtIn-we0ro4knSMj8&}6bn~7T^FC{i$Vywvys|o2%$G7z zCR3Js4^@Y(5S3ERsA@Y^()V1ugX0~hxQ)wmi!7RxdpTjqh+{D6C|UvQmuZ`)e94CD zvY25@Nd-p9)gw^Iy=b!jhn}*rLUMnFJYlV1Qx(IU6D@+9ZkD?@6`L2`OsO-en}3?> zH2V9D{wgMCzq&)N6tT!SVR4AX;-zYlh}GTKujw4gFrf&hDEkWn$2lnHk{>J>K9VasAfluaAf2<& zK&WD>bGU`MS<|o39FRhjJ5h+8*+2K6kS3e$_T+O!+QiPfDYRUkIc32(t2F`*u&*nM z>ayx^s`pC=D+fBv`5AOec_L>cE+>iXDwbWnJV~;SlrAEo6t5~)_vKen+_XV#?>JmX z*t*Dzp?r1Dy(xXYn+aI8n3$e=0ZT?u%_+<9@CMd*&;&ZcD{y#}&DxZ;G1317yi~fo zflFI9o_~Hw$(Xgyd>Z$wf$yoDl#(637rUB0&f+QK z&n?XV^yc=jR|wp$)j2rkidSih{nK2&PF~3d^(uHZA?5E4QbCv_OvkEnc>4*|a74vSo43h0CI+$0Q^yn?Eo1*ctAqWy=;US{Cn`iNOECU;MIV z_DDzAvT*CNWf9Ak&9*GFEj!^D{ z9_?bP`=nEOIM~H~ULKQqP^1?jHIF;=4p}=Pi>!7ftxUO!&lTKz5|%GtDvKecPbIxy zdBwf-XrwbMO|?8$$j$gvccQ8MN3m7x4BvUPi`^&flQ+mSk<)^sI+>2y&3k_(((p@RLlGf~6wQ|>*WOa7`Rpi5(t5&VNY7Hwqd0N`J zYUR!~$%{C!C2hIczr0?oCzVoYA(@@3yv;~u%BfDgW9`b44f*V&ra;s!U#9PA@;E)* zFSF6#l&Nygl6@+he8Ehk&v*Ed-rnR9%?a)!mKTdjYwxUGNC zztDbYIYwV&{fo9yf6XR$OHn=FP0f6{t;U<#n8@>SBZzsZHbbx?{CPT9URw+sa()^SHc`x4JR;V$;KQyYAFodaUl&J$jm+XSxf= zr5(2pW+JCiC1)`DK1XA2QaestrmfOWW?pTxww*K3Z_*yoUerF;ED|XH4sQyGwJbA4 zc~|j*Ix(qJmCO^w4i`@_U)Je&vJ}k}H#~}L5e;GiVeu75<4-VH-o*Hy2AlMMA=s7w zd9W*wGT6g(>i?Z%;8d-WxBvP6w6FWKFondQ`Tcc*6R#beysz3#bZVNu=F(K+|GAwd z+!jGyi_tZGiDe~~bgg}!Ej&uoQ!P89Pqrjkrt5JQU5^S|9QJqJPFBsFvJq&d`IXGi_M*( z$JDS9ly_@$(tK~p(UJF>*9#nk+G36Dh?Ls zw=9luNBG?EizAkWo#|MpJ8e#zZavWw7L{n58y;mn)v`gKZ;3)C+Opi@jQOVis6Ev> z-Fj-+GE0doih2M|g_; zOuI9@Bx-^696idmNM9QnZE;vGkBxJL6PIx1XR7VHq)l_qu_ZAq^X(-O&akk#WRE)enMoP z9uqZFUva9^5MmY;esY*2+!m_y=oLCI8XXpa9)?9ZB%|~tx=9tY^R!J7jtlKM zi=!Rkk@`t`v}3;2O-+v8B>9zSmEa;tSvWkNPdKqpv^fqO8!K8^b(CsT8Xbi_j4VLMNEgfFm!JI-`C$o*Mi$7t(czBKGu%dv@P+tx+QNDGUK zh}4$S&(v(HbhPREe9{_i<80t7!lqdxBODgHHfGhr=S-h5eYs^$WJ&4)#|f4fW}SIm zj5Ef0QRK0=T%dnL+7xMVxSvZ++(6B;!#M8p=oOlyZqU=i=pZZ%4}T*;C5vlmrcGqhGf#QJrq9Dx{yVtN zU$_YZ^4W`}{E3vL%ccYz`V88cFnva{MN72hJEzlnPS=ljJdrYgTB0qIIv0MgW}RW3 zW_>4|x<1`$OCi@2>o(+-BNT0)2verk$M8zfv;9+TO%w{ z%2#RnrF|B2oMoNLA2XEsdIp(<=XA=>qrG+7x9OJ1sEAE8Qu4PpS{l=^C_O$>-zj}J ziu+iL&FZpE<7YS-Dc!y}s^kJonnjO5(W6ji{JYAsSif3sN!%>nB?81&Y8!Qdwp{l} z+iCMSY}O@_PK(=S2~Xh?&caf-@6=lLX>yYar!v2srs*#((yX`Yd2TIymj#V=xGh;U z@FYk-mYcxcmbo*h=}YX#M$wp9J!BA(u-f#W5yl1@@^6Np8I@$aUSBhPDQR_Blkhh}KgAj!rrS@Y8BgX8 zW#^u^#cJUuWzp}~t@c?ecg>QBrqwt@CzC`WtTqxCZMncMiJT*OZQ7$PpA%ymLd!h4 zitx8pF71$S>X8h z26`yLQ4OOk$zfsEIixv!y5{6LBkbe|O+Sr>B1-zu)Nn1#8g92+A{N+ctlC=JO1mDd z&z71SsI~ceJ#J31X|{38zr=g;UW>RgLdz>3)E<}YRQ3IWFfD9uSdvS!_e;HAKR1%+ z0y%RWk?`4iyPkwtmI#YjnC{T@5vwMeU;Te#|>B{@*vEj zoJ7acgap1X)0e-_LGE_?f`@ylcC2vluIKPeyZ4yyt9DPZ@(_?=^e&UJ?3^fDj?()OgERz6_)KY#CB8kB?w1L55cS zCJ_l70p<5pHqLF^%S*+Wqh!LA5iFT5_RoSdp}+zQMx=csVArT=;cYU(rF>&41fwhr zYqV6BFC)vS}u`}htrW8}kJ?JmR$Og1qXx~FIRDaB zMg8<|0b&@E~F?I?$-bd)G zCThNt@xQGM=K0QlB+s6AGxcew=+yX%GHF!xB+p9EDo?U!wP%fIE$?ZQH?}1utxQ^# zl$^9WX-(4FBwpjT(z7yY<;s<-Rwl1py>iXUwJTFru3P0dGwK{qA>eXvjuU(z8dfgh&nxr)= z*Q{ETyk_;9HEY(cNm;XQt!Hi0+LdcptxaCLdhMFEYuBdm202ekQp(DdRVm3St5epb ztW8NtS+|ZTt|Rz$c)bp1>#$mWDL0${PybYR?7#YwKEqf4O8PkI|J$4t+kV=`8=0!qNI0kU#7Y z@kMgu3~qcn{FcS_OFfryJKJ`G=LD_P%DZ90+*))*p1x_O+j{@(`h?jF>rXl{zJ6`V zvbd!f^vxA4ovyk#ePdYA2av_8?&=ssz|sC&6Kp10-! zqkhepzG$uP*t#};+M<-lq90nfB6iXGe=S&ac3Sd7n-^^TFnjRZTQ4|w&xaS>-?sHa z?X|5J?fCOU7isS-%F%isy7-BY7F{~f_hIh-{;ij}wJ*LksDE$Exx^tZJWty)qh4#7 z)2l_f-Fkjfy-s+R`nHEG#eaSXi7%|0YdOQPTWs`n{(jDQ(PKTMUCMlJPuuUjzrMye zFxsQc&Nr{v@Z}vB{%(f1PnlD_->ZIn;r?3_y#va8)4dlYCFGv>`kCG#Wq#_FM<2hv z={xst^?H^0k3at6xN8>I{q1_Mjkh&v+LfD2)9%>R`NUmbr!rsk>eH89J2UnBzxKu` z^V$nXzqzKy^}-*$amsvdZAQwTzui^+sW)Dkf4~0w$N%x>lDD+6L}ebj`?HyrZWHbPp{8C zA^p9VZXU~4=DS`w{(oP6@QOR@$8wdq;%D1#h%an<_qSvD%6#CREz@##KK95PV};7R zB@iJxp@mXbsVOzG{a{PFOGH>=gpPXm3y>i-k zl`>zjfH$Ok3kb;^8u>nE)bKKsF=w~sd{bIlDUm9K62 z#qcl2o0R#r`xuX+`HET7pBo;u|LBA9Hf4S~DQU+A$2b3JWV}O}o4$Ab z{+B;G?VcE;Q<=k0UUp)KE4OdC(WT6<|D<@=J@N6s++_49bMzmyoBsLKEo0k_K4t!V z?u?)0+dpahE^mO6^6ae4+7sE?R&|dtq|Avsj{PWS&NF}hjp0>hkt>0d+YjH zSAW0Zf7{Wvrk6f1ecY;!dk z<#%u2%>FC+D=7c$v4)htym6(Ltjrr1-#cf?3FkgoshO`*J7Kh=E9v3we}9<$A@bMw z_`a%?9q%_kt7R(lyE`76dz=0DH+5^-%6#wq@0>m||FVvMYPrgs^VfNgChq#?Pp0Yl z%KW!mi{|Ekp}%v2UZ~7xUGwF=_dIgt!>6mgM4EQ<6*sh8UmE$(T)j+Pzb5v*Yai;% zZn=hCDf0KiOZQ#+lOJ6AgP*9qE1Gu7Wq0j+Y4hrRO?r*G{+s%{59PL;_|xC(b;|tl zmil|1eelfpKhztP`S_}J?hJd||9);6Qs%jJX-|K7 z+hactw|bR1vErgfU0*KT@8$_zES4tS@!2O0uKP~5%AP(=yWd&&(CB@xzg}Qv*j%m; zoxJUBcS2K@*i8{mHDLinyhJki=O+LHBp&wH`2egqVDqB+O5gT zyrH$K?*2Key56y-D)TSqecbYscaMGG-_~?xe*2mae!Kg*yFQ<8%T(sW@0g9`0WWjsFIPfG3WZoMS1Wlrh=H>} z5_|!x<+HN0a;no+%>)FT(dWgPgcjn8M||@=dHJxPJC}=>VAlUY*V0a(u9e?at7#wp zJ52k-y(d4v=bt|~4w)OjY`Xc<4OgD@g^ib)E&TR#`F~H@{r9_n{?5?IX`flojn(v1 zf4V&Ko)4^JDYK@p=qovH;NP#kZtt;mywm)rwqw7y>QU>Gg`b_n^p0M(y!O&tUjNHd1=gB3_J70c0e$A};m%M&@YIL@-y}kH?&N*L> z?D%Iw+XV-I-&5Z6_uWIUe%Z6-;N~z{U$=a@CAaiNH}3<-?{kZNCt|6oDh~_G)Y5bw z0a)5e-lJqA-{ZgxXWHo|%XS(z_84~c6p21ojid~-7 zBC-j2Q+@)eFWAkCE|L+1(`5hw z4>qLxhYvOt2Y&}Fkx*0h=QDJ0?(V{C&kPI>1b=1n^uJji{(VuFD;b>YQpjw-daKhW zIg8BqJDs%_7s*5ZCFHMfz2%uKTOgp#zUTb&$THNk)l;*qk8`_dANy)#*6JxcMgA5;Em!-WRk8%A@`U-@|<-&-UUqPxdTJpa|9T z`~|`1`QX~NjeB;U%NrVZep4NMvT1h~2lmM`{JFB?>jL#Ib>D+IWqSs1XATVCm?Ky_ z^0h~JDdW%h{G1PCqtswcTQYyik|lA}cdubYj~T`?KIiedhR^+cSUuo!9W!%U^eku8 ztXb2ek6Ds(>>TDrTyvtMr^VRkMZ0Ftikdww>X?~P>Q#$&-jhCk<_u?~E6Op=G0iz+ zX7p@llsz&kYMRqI(;n@dehfdO94;60j+f_e;{+GZeN;P+SS`=d{+!6ChBk_q7qLZO z)8|=F&gSLLpD}2karXhECri_^Y7ZE7=h2tG_kht4Tk8%O$>(cY_X7uv%*!;*`5@*z zO{;;06}YQEU^GDYLkEmL*d^SG{KE%~{Ozo^c;tXl1M6W2v^5+shF}4V&!?{gv!Ul_ z2aF0>16$xQ?1L4L9x&WHG%fw-2aI&)8VX_euQY84X8c;yavBd9rLYY)!m?lB&(rt| z>!IhD2aM`NyyFnIVIGFvFz+$K!9MlZ2MqhDro}&Zz{r7RZ^BkhYk+mnYg!L%g~PB5 z#=LdF7=#{J{vz&S6$?`a;6B*cjep#hOWr1a(DM%I{XO{z2lr`OFD!Wlf8HhEU~JC; zqZg*Z;Xi0vIqdnPrqzl)-HCRX@6fg6_YWAJKau|*5HDfx0izSTVJ~d`ko2@ zs%wcu&hP;v0T#h@SPk=FGc1FHuol`z&{LQQ z%f6{=y|5d4;Sh{HM7+*HKVTxvg0^$<7Y;JD&DoS6NL^1DCB85R*1#Ir2>aoF zI0*9?%`ejq8a1%McF^!*Uj;icXNMg$;$anJQIqBgKWHSvBA5f)A`TjrFwTC^Xoh95 z8}>kLC9?#P2aQ;m1Jhu6)Ip;VHcdNd)Iw+UL8Bd}!hYBQU90eC`avTFdR+&N0_dD^ z&?twcuo3Qu`^A3dL1Pf69&^x$OGe*g4jLJ-1{T3qSPT1MJ4~H*(CC5Ha2WQ%*wy6I zY~lyAVF9d#)o=*5Lf0I^gY|G2c0l(U^m6V&BMVl+Qdm9jAfqGH%Z210ELcQ-!b%vk zmV8@GIIs>Dz!E8M@rEg|AGYF8#*%|Z9_B(=39Dg0_Sq*;o|sEvH>`&;$KC}Kpgntw=*DX<^rLFb8t3q7zIX2Wh+2IcXy5xP&|`ZB_U z*{~G0z>zM-NUVUssSXZ~zuTmxp|T^{@kal8}e(Fzz(&gDa7TgRls? zl93m#CLOSD4SECf))KEY^ZkT))`2n+_o$FmN9#)=x(8z?&OwtF3;6CX7Ch>%A(6y0#KZkOK z_2(Wm^5Gz?hB@aU57W;le$ca-d^=OqO0r20EVzL5zx_U_JC+O1{Ce%c!5Q9wuy} zU+^vDVK*#=iCd6|wQwKofP*k5k9OlM@*AeXdRPEGTS+%8fUR%<_QIHLBu7gVJ+su>kk^) znA>0%=BzU8G1tTR^C-s~$Um6(UEJ*@z2)fB`Q*d*vBw-=L4Aky&|XZw-AwtyemIES zzz?|ZVXnM|``Tvm1E#^!O3EMB{}4Ta18^9o{D^pGqyIl9-;43T3cW1B-&>K#z7Dp* zxZBWA?6Yqtzb>GhtI2oFo;%3ztI&_1P(QIxzmxod)vyBlgc|e=)LG5Mo5Bq*N zj5+1^2BOkvL$b zKM`+O4tt>MRs6k-e1hr7wfu$ph`9&WVy^2#4mQ7qKHezLBg6|1_L85l@+0iOPdyg*q~ASkcrbUvbT|ZapluOyHiNEVlKFK*yw;McMKc#*tbL15Ap9O!$vObsUh9iC*Cz|)M1Xhd)VlQ z8TSzX?f7#q>HHDp@X)YP28$jcJQ(*&{P{6*O~XbOEP8y{sDX{Y!arF48{$)i{Zr&O ztb1nI=!JQ|9X2v=rTu9mA7JcrgmW9|e;)VH_42UM2Xprg8>zQr{|Dj^Yr2Pxm}>HQ zl=6W?W5dQiSTsIt*zTZRix@EqVVq;cXoW4%3v;4IjI^InFP$Ss1#E}=U}yA*5q~HC zxkik<9pvkb5u+S)bIgd*31jAPy@vbI$=JiV)DfcvxnbA`+fNxW+ObbRZN!NADdBGz zF-l?M#u1|#W`ASE7>3$eBZg-u_2}#oBl|A=J$J;|4=b-4G2C}kUS%Uj3D*a17%_@4 z=iE4AbimZg5hJ#ieEi{vQOWg`+lVLTx`&AejQjbB!GUjDb>oPU0ZSemG5V3Kgmsu* zj}uRr&^%&f-b?%OB>BPh`rqKs<+MvJBStOu4RAk_2!M><<#2A9D|4V!ei2rkh z2QyzFox8}l7e@^HedPa3q!;GBGGgRn-v%pS2dstNunG3Ub~psPp#2ZT6Ba?&w~>P> z(Dg^+3FBcNOoSyc4OYSoSO>FUGt7YmePm1jgNu z{CmU~HoiY%G{C}M@*S3cgn!7j!uSW!^H0ft%vrD$bK~d4A13^L#IQX`K7UELu;m{k zMior{7w)b^zhEck+<%W4&U(^wfP8{k!=wW?ju9W2U{F2}X$;04GO}Td^^j2qlfw=f zDdG?8!JHd$$cTFwf9!{hT$trJWYj{>v_nQFaw#wm*1~@5E9M_EsxVi>dRPZrU?WU@ zgmPWLby#`aAtR@O{Ea_kRKxD$4;lMm|LQ}=KICK95I%In_@5Cz%!SQs4;eKuJ(X}D zrGA`3d|=I~hm3C6aLyrP5O-ZL@#pC8xrdB=n4d-ZU{?;;VZ+6TjD$w~zx0q%08_Ud zGHkm^A8f`Pmq&VF%{KDm7sPY>A)^THyW)`10c#5n8D1E(n{@q>{MdWQh{b(6tibFl zK4i4P%Bv0;&c~<^S06IcVZt@U3%NX)4NG7FOeiCtV0k(5+CzH*E0NFrKIwwPHytvv zuy3m%J+K2NG?5>-95TwF`-g{&PS_7ixjy)#Lq-#{{h06{r(Lf)WVB;0gV~sSZ#`sm zVIG40Fzq(VxrlT^56pwBm0d zdCC*!Ko{{Ygz?b+0`Ym8c)mz{F~`6>SOaTdZU=h7_4=0y{}BD9-ybp>N718y9x{fp z?;au_pCP{vV#eO>#UC%_qm3H%u*^DY48ff6QKK5UeUYO^4{VDXHPXh&2j{3!3R_`G zEBQKY)M&$;5It%%Vb-SO9+ttx{~>=|$iX&P2jgdq8Xd3*4!{bi{T4le32+#uLi@~7 zBMauhQrH0N;UKh)Q*VwLH8NpB%&1WT?XyM=S!H6MJ!q!$*zra7ZVD{PA$ zH3qp}aO|j&{J(??OJU($(g6pc)`s5C8#U5lEzE&EunM~4Mh&~hI4MknL$DIo&mT3~ zxt_X!^us2Y@GS9#S+H~==|-*-dSQZ_@L7gk2P?Vmh3&9q5%GiRi-{N4d!hSxl=E?; zMh2{c<**yJ!i4xyqZigf=X1D+ZrBNvVd?SYznEboj9W5l?1#xGj2g}OKLBm*)XRiX zBNoQNeu)=Mv@njk6k=ZrGvGd02K!+(tUr-_hYheF#w{Z}+zmkY^Z1)cx-hrGWHFya zdBAMQ`g$#SIq`mh^n1u>XiFjea0n*9NO_$yYNQ~iZ9so8J7FuVghR04bjqUxeLG{+ zh=T>`qlO1oZlwHR{+aj-TVN&g5GmiF95LH6C=WOYy|8@~;k`t?J!{k`gw z9P|Qr1?M9Nn=V2>S>#@mOZmXkZTN#+;&$o@9E5|gKc95HOgps$J%If?(HG(6qed?5 zEWkZXETlYO?;hg)dssYb)Ie7$>BRrS?~#w;y4GJ#zQX+Pqu2ZJ7v{svnAwQt2mi)lJ6ej$E_5h~CURVh2_mB?ggej4fKWxO@2HW61xF2@GKG*|e z{>XjnUh)?v!z|bdOK=~5AN3q_I;@BLVJ-Fvb@&In>PL-X>=Pd%ot?OcZdea9;UFx7 zxet#TEwCDT;V^VDuQc>C${VKtg8YbLp5RIJ6n6ZE{Q496(?a~k{1oL3y-!mwoajX> z>4oL55D(~mmGGyLfBPw4SoRmn;Z;r3UPn(b+q$SHn9JTEeAxIF^2|>;-=TiNcIbMI zcCQEbu=9P~!J-eb-%q?hMt|V&C&Wk0f2IDzmd_}MKa+n4$uC$5+wpIBlzi!;yuIYd z8{7wA3>z!bXM2rg%=TlwMh2{br7&iJ*QkQc3%$mE=w0MB>~B)9PxKnuFlL$8D1f=k zy+$+4TH!T@V7A9=b zkA2(K#0PWtHC`kAZQPf7jcUjPw6Pz0Z}l4cddRoi@DIk{?llHsW;NlzM|*m|*XV<3 zzaoD4m-i&`IY!r7o+V#k;w#w0>^~BZ_oe?%dRboN{1f?txdax9`Bkq`FJ{;*=GVMN zH|C1Jc#X6V(973J7i{e!zFhBkll+;bYx}#sMicCL$7{54-Se*37=~Rvl*4T5(Z^mR zr9qMgq&Z(%oamF!s5N#*7loIg7`P+&=07Y{guWFlN~2v(AXc2Nl?tC5{={ znBzTTMmy%Bq%or&bNk9MV-RyEw0%Om0b^n5nlU2>Hozg|3e(1nj0Nby8DmB@EXW!& zyfAa~n31`V@Gig}c3d=O)Zwl_XUyorTz>JG5#y#G0MlXnC1ZyBQ}h)k!vUB9wM)m0 za<1EQ$BZ`Ux{UORyKjvdDT}y2Zy7U6k#pyb8D*Gbw~iSduoik@%(gM3iR`!Z_FjQ(!yH zhF!1-_QMM3h5a!8GyH`q(0)1jB`hF(7z}SrB7b2jbPo`Jm<$VH25f~z zu;)932lMu#f3Oq|z;5XLyF6#%4tAEJuf)e&h8%3ZVa$j-j{5su?780Xy)k1yOex3R z7xH{XII!qO^ad7wpK^yoFb8-2HxWO~&I;o9CG7&tfZ4E->+wGrGqiZ_Tepy(Fz<(y z3oQRJ@kVanZKM-({O#!T@vO_ZgZcpz?nK|Y9(OnTI7s^MA$>3p)^ol5zA>W{b0zGB zUKsNa>gOY*o9o#PV@4k49#{g~9wmLSs}a42L%$d^5|_{}|C0R1+yM(Q$2Xy`m{VXk zOnaPs_$TRsZkYT8axeonBj5BZ>I3F>IE*>w*JDNv<^5%zK7>L9Pf6!&2z{7wrRd!)lleYhebghqh+gC(#BR>D452M1v@9EKgx{w(od#``{q#fwlw0 z2gbr-mzl+6`?|N#TiDf2eq!JKHu+9`?C+4@Fa_qpeXxn^W$%)IFuR9*$372M!Uos?8=)r& z_wV5j4#Gy*{XY2xTR%WwR?_e7C10WQL)^ihkI*w%`7!ZcMSMS@-ob*ul1{h}cEa|4 z@;jMu|3-a;!=Ix^uyYVSSxtZKALs{cKSaD>>L_{uE5^uASUHaVuHm`RARl17Hg06X zHdqSdtm8%lY=+%%Fml|8TT6PTkF(B(by>5Oqdz^JOKLg*C7VHo}h8<3=wmS~G6gS*BkB z-LM&^!Y-Htv(}Cq6)+dp!hNs_W~P!4uw*mwK81X|i1@;o+;O7|w!nUvu?7EGN0$d% zH;_M2JDqgpk^k6tz!c0w+i;ILaXaB*F3cw%Pb1zi9S*}j>??MR8?k55-@Sa?7{opU z+S1Sim;rlXF03vfzhKiY+`)uzV~@MkD{xOd8ej%&g}E@ckn(|vumPsQF67JiAfHbE za__j21#61Q2UrW6U|b1uFz*`TyAi#G9++G@ZWO_uYsZZin0+1j4HIt|H=JkU?tAzL zo#m7djDe*v^G4*M_I=_F({7?%zCm~}2ll`Um{5Tn%!9o!r*hm#%OE{J8fSe9;oM68 z!})24z|~i8;NJ52My?JChgi| zr0<)A{{;TQ+Gg?tmi(Ih5c8Ad>p8RsFc&&o@CR1G7TECAxUmnmJxzUqjn7byu(p-( z&m}yV4D;L22bl8$;lkKIQ4U$OpWW0u*!d3i0%pI9yYqOic@KFw_|dr035Q@Wtoa!I zJD>dRL*HQbr_=-3^%>zq=jVhEW1(#``37TQ3QU997)I>H=xdZ=q`?xH4f|mk%yk+@11y5Aun+da zoN0z(zl8Wh4@`?DKClIr!!}q0J76R1gl(`F?uTAzzm#}RH;fdR0SjO$EQgJ-7IwpC zXm=ULK9~Y~VF4V5wa}GIdj=EWFie99Gl&n&fkm(qR>D?T5Bp&&jGak*U0NieW-DEw&vPzRcCpxJ|B?04a(zR@ z0+CDPQ;(fT?D!MAG(K(Er9%0O=abE+54$X}*l^N@X!rfL2dt;xX*uWp_g;_22J09; zWqe$ntU-tVKk-xkBtCkX86WXe;*;OSx^cOf9_>nZ&UZavx!;!UT3!sWo0U#qv}q%#iFd@B+Zg0@w<|EbHJBofAJ(UgVLkjc;l|K zJrI7s-C-GYE_7!^Yd!D3mmVc~lf$R?HP&%kar63=ZmbEG5J$*MSk-)5UuPXYw0|CO zm*y&V=jEk2d4O2~k=<_F1NVAYX{RnShs8e|x|E4r zfhiZv->ixGn}}Q$a^0p}bs#;KcIQG&k{rpaY~)hen^A6-@BV#E%Au&=Z%eaK1OAe{l-v!n%;PwJ!8nPKEo zO}RjRh*8SGX(K+em&A;BNO{HTlgldw*;w|QB$%?zfizlO$YS)Bt;8>1{ADl7k;*~Z zfjH#EO=x*#PAab!ncasgH$0by0G;$n_%UuRs3sqxhsv3+P`3a(zePUlVc~dwFuraD)1g z9@Gb^w_V63BPVGgey(hvJ`G@>f_=4VU*wNZu-P%w7Zc8X2RTXWSCxaLDHXXoDvA^NArlAJd_?m8&ABD)bu?MNt^fwrOp1;{M!>>Wk$sQ$}say2{-Leeer>R<^ zuWiWnBUg!D`{L;f-dbc0+yZDjx!jLzI{TN7Ca#i)u@UGk`+-KlQepfOF>T3CHa@<(~}rG zw{qLm|U-`eEuCt zt_8W8qwuc>x$2{kvqjSW9)(;Ya+OCRmyMk4aXV5v%8=_l3b_X4dX7S_6S+F}(j6(> zA>?Y2OPEw1-0mi{OK}cUE_sq%?j*T%*Pe=3Xy9@PJTjLLn<{@7w^wWWLm`QxBtXl77)l*q^7Z=+xS&LhibBH!hg zzy8SbWymM6H#Lv2?Axc1rycXhN6NAZ`Fi9#ME>l6e3n1Gj$nt2mfieI1gX=|8}znP ztR_4k&=bg0cldz3lu04--N^gOCy+n1gA??p2DvWe@=SjN@sR$0R!G0E4LN5}9|L+j zxqkK`7mM82-FHP7oYY6DFD<_QpCx!dx7J7x&|==sMK%T5LH6YyNmkM-^|6$_xxRD< z;(1v6k6g(SDoXGYf+jW$(&grC!J;F!hZwj(?M{-@Kxj_0Y{lUbm{x5Q} zm$}N6E3#JT)Z4RPpXYF8wIvyvSu9gDOT$V9FMxnvs#o_9m)kHbQncxHlt&kG9t(SaCdoNy z7)&{{|K>%m5V^ETas_@d?8U$MncUBi^W}S>-6G#7v|E|TRUs$!E>zDa_bW<#a@D3> zP=6_Jf8~{M>yR4?hWnM}-GQ9o@%fX+QP{>Q^$?vXLt!-EFjI_UGnGt0ws#Y?mC?Qd&yP z!2uSWXnlOe+u5^e%Dd!#B)`4br(o~P$3T9o%4%l#g!^~mEc&^~`|>SN4;(1CSrO8s zL}#**>q5@g?v``kHrq{W3DLvod-fOEa%7Vo?AJ5n7K}r_zs$`jB>Vi$LAF5B|JP&v z>8}rzzcma0G5Y)$FD0E-gi#sAo?L$0Uktbl$>**>K8t)C^65^W{3Az}PoIsWY3v=8 z7=`xxEJMMJ;gbA`&Xgb@6V2XFu?QU(wDbgqnMAG*xnAT_Ou0aQGh7g8cPK)w1Gz!u z(o8uw+NtjSJ=n)iXK$-%U*s2INBTX_Cf9x%?!$Dk{mfPLC!^GZALEDlncZMbqvH?;@XRUiG&*~x_JAX!*$VN=?mzhudOJ= z?`r%Oy(g{#zbD@xYJ74Trd*!C+=_y?2c_3-zPQTxIQ}(HxPR_r6f)%gDPayEAAbz( zt0_Mis5cHv6?fY}vmvsv$I=&zVZX5%hd^9I+jhAm*VBL@_-CIS^%CT?6KE>3@_MlF3AY-64H2Z5!$hO1=>Tn=kA<0s6 zU1iAaM=l;e?Q>$pXH`E!{MPWh*{|@@b~NLr{aE(Bi<^jm8#iSU7}8C^ku(BoG7dM= z4)o(LcOLuo#hvd=Npy^Rur(@C%`!tMeB3qBFoICV&ZI^f>`QGW1{kr^gAvffcvx!6g#Lm8u zvL&2QyEyC;CfRwgiw&`p_8}cRdx)L*De=k0eu#9+Z(sSk?^N^JnPxIyfB!wXR)Jh8 z?nC13lk;hUPp-))C-rWkf4<%Va*~!Vztpb0uH~+ez&96 zc~A7cuKQ-z#XKVEs3cYTex_+?v6JRbkHvy**&9n4a;x!9Fq z*TV0K_0?gi4)HJW6R~VQHMkqboo{YED?2pZPL*m4dYQ%Zyh^r8wTuyjT1*sB?a=`K z?pw{-F%qBLVE>zEg$eC_42{yj5#%JEsmQt31fJXTa-37Bps9eC8v5?cp_;{n`bnG* zv%=3bJ~jATml7BU3Dzl8+pJZTp06IgX?hei`TXd{kM94%k97QrJDxGU^_*eEZ_00? z{`tzGB1DhOs07qX)m8B)AAe#`;S8Ox@u&U>{)j#`;ZGC(sPZ$PpWIR()JiGJUnyvp z84Le!(d$0kWv2!Dn4(Wvq4nq<*8?-}k1>5NIa?KgA87Lfpk|=8QdlPYSOjAWN2o zhAsE{2Hdsf(8o629hO!Z9}Fhdyf1a*#&t30JWY1vt3l-fCle}=${VVV7MsAm3HK83 ziE;I{7Tk)mxRi!m6>`4&=;6A>ZAi4sA#0=p4HP#ZGuVkX_?N+_20zm-;Y=)k6ZXV< z$F0q50UJYVjB5BNntgut;#W6*b$_j2brao+wgjp%e&bg1A#o{n`O^QaJr{TRxa-B; zelrde^rb1J-~&H>;nm@;9FY!eBI-F@JWi(MI9Qh&f3H)>+mWP|hJjRK{4S#8TO0ctUV;om3 zLi?*MPKW$T-%eY_f2;9#Ot7i^N>(|Ch$~p@_ z+&lmCwxJSt)wnB>zK*3(6`H?8N*71_e~5b@xLUKi-v8NuIOjQ>ea#qDJl6ibj1dnZBmlb6_t__m6DPixIomIJ z>qTz(yUpGa%1I5hp-XuW6W03kcKm0@&En;F<8UdzS@?S2##mT%ge${u30lvuVyCr_ z+<(+CK&IpSBYwN&DfvA7gSQ`TZ$l-HNffyjDa`1br0;Xm26@=VYqsA;H-F$s*E#AfH$hMV`Rq@|V*}T~7wFOVgpc-1wzlQpGWv-0Vq3o^5w!k+GUy^&ddiixg z8|8e=H6hA8aBs=MMn&8875AqrUUp-}12q-xH&?v8w&E4HR=l#VGLcQnY;mt0Dt_h5 zr`k^4f9hqY9yry0>gA_iaq5*S-;Ka864%)CI9ro@;`00JqEVtQtXA0eka6zjd`n$c zVzcyojpus|`Swbr)O#VG0W)PTIC9>6-HX0G9J(M!&Z+OC{Cl`358?JAY4MwjTJxHN zf9fjux8PsD3jQh#yPG+AdNT00Tm`?Jv)_Le{8R8xUj_df{2M3amv-m85F1Iqtn7Gn zOfB<$$`eq7YPw_ZL65OoknKixNpOnwkLD6ER^pD?N7@M@L&*0(%9*KAc~2LK(TFHF zSoU9so_%wu!bP9ty93$JAK1Buu>EZdDO8)p`y%rH1ukq@gt{j^*U3yo397`Iycavx%2}J-%O&kQ4Xy0~&P&yHqRjlNnO&9r zd2m>prKh*l`*B%t4rqV%3fLcuiTxTPtht>tR&|(fzr6a7)VGwy95N#>=e$tu{~YOK zR3;XAcPi@9PzR`DF;$m!6^&w`gY##<9sHj1`10L}?pOW4!(Rix^@sl7l#l2|<9G1; zVa^ZMdQrOkjasyb%MhLP1f!1MAUOF4IcJx9r_0w1GibAY?3Bcp<$PY@;}U(HhgJv8 z(^sLb7+R2T6IwdLw-=#FT9V(X`Fgn*JLdUDaj`!EAF2VqF7S@xleBcgHvylruRIG) zzWcElNz0I-1=B3w1(}QRO&eN}Zvomgd_meOG>JDz+k!Rd4x=pl;CO#}#1)>&}tem1PD0})yyeDa+d;WPeukuBtNmch!#@OV4O&)YJ>Nsm=2O*Iy!zowp7CYd zkR3KOd9Ral$R$dRN&Ws^UPgr{-1=lt(2w{USbdyx-KDX#KV#k{$DG+htxR+R<3G|+ z{0BAC@*k}mD7B%M7j2lb<*hHK?O%JjkRg6kk7~SRyeDhR`{0|Z=8R?GDHUGz<_1*|g_c4B3FyopP+Y*#|D4Z2!TAt~bt67PC}c7CRHhy@XckIw}a-TU-YdH+FVkvkGS{kLE&Ov$&T$zxn-D7(u=^l+(dRJ zgWY5A@hJE1Su5+8c^O|!WVeysL3UhZpZFSoxr)aDYWs_~>MwdKvbEoh&0&wQl6~yz zWV?}VK4WAbxjNYiWVbxoSDsw9gEX%r+s3}&PWm?Y7AuQaohq^LYxf@HXlNxoZ|!f^ ze-CBIep;Eka1Z}oN>J5Z(Hf=KZ>l}ifoVatkc4j^B>|J+Hv)badrpVAcOSL*;c?D@ zQEfGp0ZTP+J+5g*ZWRpKx2iB6vKYROX#MI7HA`UZaF0rtv{b)@IVf;eB`x<_oN!vi z>GI7FfvcNJ;OkjA#}72B-WwA|0`LgDg! z1#j)2@pT!MQDfOrFuK4PMAZGD#fa)JSqx}wM1Px*T?c0boO!`{t;GrJujjew=A+h4 zQ{P7(Zqe(XW#!n+8OVgPk*&zB*YXV^iSKnbzG%8-n{XiE{S@ZoWiX8%&jk=Alj48PBENNqUPE#}Waa(*czs}W{nOVGHUA`7 z2Vjjn$E4+pNtOU6fk*JBj<$lH}FW@~j$@d#= zn#$>}yjobKsG@7b;0&>!ZH9aI5sMQpKhML{eMR`GHC$jBa5lhP19Lzy?<<2zPZ+3! z;DgFv&C3wJ$JmjC#qjmb_-o8J2HLf+Y@)Z4zaDV1nR@+aS6 zTmn<&?`>r;kI0{-D-Xt=m#$Zq!3d`${8S=c9S@K-_5`YQoh^fTM7l(;#=)p(KU=%# z)n|94w4}Vs>XrD*-hf~BgW<*~KV}o0juyQy#eLM`g!QT?SnVMkQ)cXy(0BrVqN#c%V{znGxOYEc<%(=x`w=#;3E3`iHeaUq z8N2VXIN@>t#8+RvcTK*lx`kXu;(ucqxsI@$_!cY3bw9xOGezz-RxZ5P!}u16hkRtQ zg)V>kUcvVtSx<29wenj>&aYM`hS!QV2IwoCnv^aQ*(0qmp4-t7vtM0&-}OoQn@$k;;4 z@jVtZSGo+$Ms6w@hCniPzt9HWK6oAM^;7-Jt1VthyJ)tU#1FzYP}PuNWqtsxA2WXB zEhocTD}mJr)+kuruhsAUyH9OH;quzO?k8}eREtrW5t$`qMvxii-u-k*8LyQq8LHmd zN2dKvyw~I+^9d`nR=Pax{7986J05~XYV=g@Qe0^Hl|l7cM7i#xEwiZT~^_o$X-sfYC2@>IsXH ziv{hyyz`_thEIrv3Mm-*65SsMXX7uKe|rWvU$Qt;CCkb${OJ_BA1p2J-KkbEcfjn} zV6SWin0MKJq{ye9)a!AH9Y&76Q|*^~ym2hjHR6z*V2w0OzgZ0=B6fVv? z`c|;k!J2Uo)O*72J1kbeH8sZ2;C}T7awyBBfoJkZ5^8^#zTynuQSSL!0w5wg@d#er#v!}?*q4mwjj#(FQ zF&gmLILzTS#=>^YpQt0i6prdXl-_iKvCenMn?-NFZu8}@b$N4jiCPrLEO)8dIlaz5 zf&A)i2Zar3TYes1GoJS^ilHm1wUOSO?1MMw0XdKVpI0V zg-L22x2>CXwt7?MpR~Be`znlml|vIEJ02Rj0w8zmso~OIg0xVvil;3(32* z@I>`iMN@*V%ctr;(+@tE@4`#@ywS#0+DB3w(Tj^G>@_*g06NtR?hJciRURI+xNEVK zj5)-sgEO2HJ?2mosb#_>-Yw$YZ#+=%AG_bQ{ma;i;%!tVSSDVqCn%-TB>qX)kFs{m z{$1G%=)Tj&dyIdw#xSVZf5L@xX27evm+#k$4m@JU}T<1ah zu1;p$F*i- zT_5^WJP0Mt#l&Xf7$uInpE=O$w~t%98=ZR@^c+w1sy=c({8o_dLAFuGU+=N9OR=6~ z$Mh91ySd_l+RDT_sfE8-qh(XOOmMXkPvei#2mcq+sPxdrQ*7t#I=GyRAFyg_9bC_q z4T04?c%a^cb6;bz!gECA)}JE^O_ezr(z67{92nEwyLXhq5NECoh+rgtoO!P|9q2Kg z-&cjxam>798yGnc!=DR1F$ON1GYZBY7^%3_q1D+UA6>r(&FGm5^SdH7@vWAh zTWNr^zjvU{1$3XXWgG4@jk`30fe9v9{L*3M4`vPu!(xL!VdVp363+(5=loHK-fAyr zcX{Iy<32fAFgL)g|Hy%!>v;G`%-IM+Wqd74VSXSINfU9j`~>5JS@zKd)ms)iEkG;l%%5Ve8v4$mGN~MSm-j-FWCr? zw3kJ2I{y$H(Uq^-I$b(Z-@c)&UCI7?5bX3%Qm6B@7s=bB7CRT3yM3>&)I>d4gcxns zog<1EB}qX?xO$@`>AS{>yJg{^Fz{^R_UF!z>AQMsO7&fWw+&wQEBHXC!TY$ik;mW- z(?pAX5z*JC*HMT6d{F3<_U^CoEgj>J4gv^|-{!sf3BeozbM#*h3frf^{H9G~?wGOJ zPErc61g7ZOCYUvQ_yD43`qWMo@O*#K+*iMSc}eXR_8;|pOkFQ?VNYX|s=@iRP21iv zX&c0ufTuXWZKQ1w%&jx*&lmmrpv4@E70n%(0YfbfiBO9cXiv*vRiD44-pO{~XtAVz z!+W|tDAYAjvkFP-AoWx9%h#|6UE0&DEzZ(0`RtK;EtOBHgDjZa*Iv@+uH9lW*UIHn z-iIVGl+7M+X27YtaH)_Mo%`lfP`Fz8R?iXsmp-O5SOKbOsvWgqMbw4a24vMwGj6?( z{oPUqkJ)&}O7vxBPA5*YCfKG}&uxi)>;`N22I`o5*SEppIYoej^N1ohj@+sz_bQu) zBjx&#TSacM{*pQ$$-T?UEro4XrCD)OgImH(!d0>>scPynLTfLgkvHONc|CUE&P&=3 zeCD5_a8V~kW#!F&$)qBoUQy{N86iYzz=%Fhg5P>KdM$O~_u0pddmUmaIlh(p-2-#s z-b;lsu?hY<;Sv6;dYjdzSsw_(SOBaR{^Xl46*dLSUo*_wc{bZ^Gw8{NhE6QphY7g3 zAp)fmd{+CdXNhm6h4;&d+x6$DgZ-`SM6lg$2EHw1I$nIKkQW($F1hG^(uy{E+5vxI zRC-$JX9c6~XOPYCj)COUU(*x6H1tlY!r1YBQV*jAj9xJEU@Qy9TmK0Pm)_G+uh01T zfcyT+indgxIvm)zuo^++XOPc*5BvK?7hYrKORn$I^=3?Ie?eNd=+X}Iqr5wp75j0o zm7hAswoRf-@OT;w>?xaBCreC| z2Iwj$%9Kd_Ysk0#plRO^Sox)*a*zo)sKIh*M!u?_wMX8|>rwS!<%`lU{Zz0T#J02} zyYyPL+}eA3EO^Ika`eJZ05 z>Hm*)8Q%L-`tUw0f7SYs1*huAO+MaWaf-^boL)~MpM9;-hnHCS6YIkk^3A+2r|kVj zD}Q2rsQo#7Q{IzP`ta3%lv*fR4^e$+hpijgxz}APY>6&EZe@@0yZ5NjfOlqA8#=(7 z1#jl3FX=VU_gTDS#;`T=T66L672_^xbRJ^f=ofeoRrLKXTRu5oPi!Y9!n%J3d5KX%OeuX_$l5L{*T##iD~?tr;Jc1gd}@s!1kjD<$g6hAK_SNrqm z|GVh7xew@YL~b}FCwkC@T=lz+9(>ToM3vrI zs_V23oP|%BI=x_VB4rtFS5l`r3r0{>Iel7q%1}c#(93 z>ofzG`!Ih#%~)3MKeokRF62tCFUkkO`n^d4wJffDiFPm=KX<7x83W^yEd>Eau5?>B zYh*Hq4~Oal^@-1FR3l|G3Et4>4c@E2Jv^~RyWov);6F+GdH!*Do-L9gbvS`~hDWbk zW`<~!U!pBYo)gDmn!Tw9QA9nE6Tf{7y!OpYdjIv8o`AyT?eo)f>hzClbD;>SW2L?6 zb+J)v$nSmG_^XdcT}+K?TTuSKK*9;{wEC1JHIs?slzNdYrLzk%I7r} z=SZE9&9>nZ*X1L4bKvd$gTcG!+r!I%=lmk$lP67ipS5_Iz#2x`@)q$7zez9#o}!=U zp1Nv8HZ(6%sSYSne%0sH8jUyOCr^lE_-%nP_ibX-J%Wydfmtt;4&2jk5B z=aP5Ml%E&vs>uAHL6ABLJRn=eZwC23tZw0>v$@G_$uEwg!It z$c=vUY46-LT_;wDyMj8bbha+X9LOccTeL;uIdYtpm?<&LLQL#LA2@xdFBfLHFIk^i zn{T}chlL0ht9pE1S$#)K%Au4Emvb`hLL??!%DV@->2ExJ;xpdH;omqRzv$L7{3+^l ziThBwYTaU#7EmqOuVNpyZY6&S`D-rgZ)^Y|`j&=P2TjUKE}=C*%RsAE6vFwRF|i|^@)`8gJIb;sNSF;uLM46UVC@}TDojY8AF)_#rTqw- z=U(P&;R$#WfjsYk*_6kZh=cjs6JzE`Wl2owi&}=!jsL@0M1pzWahS&6$rK@~#xD6D z1+UAwtkZbmI6Ry0vT4kMxdrAhXW%%J#;^W86fQaM@|bnAaHb`R?7b-_sy#uizsz_B z+m?$F$A@ej)NXVE(ryZh*1p;mlqG3nV6AdKPq(D^^%iUCsPwAsZg2!@-Xyq~D0XO{ zIM%R_2i&{2**J2M_xp0vg#6hMn7L>&&gieOrpehot)x*d>FcJT?fvhk%~|Ftdv3CX zEfQAubz@iA@93SWEMXgj?Gh$!h%?abS%ft_?h}^Ye_Eeo7Ux6ct-h@ASHF$^>b}c` z5lKhcI<cbPnuU01bOdaqcp#7$chtXZ&@UU9k5Px~cZOGmG<$jm7$ zfy(pz>p{d1z}o))%W6L=SS9V~Ldb5SsjX209df|3>fQWdLT7|S>Skbse)Q(zGZCea z9V4uz^Rjxsl62{PUglk1Ix^$Ph==6+G}14uAUE)XmrI}XZ~BGF$of_25e$jH>Q~vf z_CuG|UcpHGWY$I>{!S8dZOFADx6M6rCD+fQ@#EfjjtT2&x<4HQXB(Vu!RhhO1;jt} zx>MEfF2I+5^<}NYgXQ_Q;Ol}<>}t7hzDV11-;T|NFW9%&7+Q}nYmTHhsbTYx$hN|_ zfUMlhCI7pi%?D_8(E6awLF*8vGT$W-zH#_A;5#bK!nX)t{ses6@HM~Yvi869cW;XF zkpVCD4t%Nqe1v~0CoKdW(BKpDg3$p+!;fOKxknFtKi`{{J;9h|9E@HthRb5ms*Xv= z1{gEnW;$v{(M2%E%ccV-`G|Cg9%R92deA>7tP$D(v}S0n8aLR+aZYU;v$rBMgG}31 z%4{ODk)_|4c-o5VLe}N|ghJyip&r)f&8eh$U7oeI!&=H zeeg}dhp7_X2#+B*HSpU+t_isXEf?;G;}d~y zSo5d;D{Tt?G0pG$<7ynCzC!J-sVE!N0{=qK2YaEr@7^)ju^~(TK(#yABRTN(9A9&5+&bzkS_QD{xjwgWWDe;c%# zpSrBiId1n|sz$mchOftE1{pgHc_o-{j?$@SS`pc|FmB=jnF!v4ZXgk&IYk8 ztjG)_GmcCL_a`YIDenbjoa?@!&)Z~TEa>~L6IMf*($z@V-VtG`cbArHBdU7BYWo;> zpUfv7D(4fAgEauwJm%*00oiu(LmZ+4~OcRQYk1>`f%t_pPNdt1(*#7z<#mh=NHQD>)vt{{Fn$ zeOkgvmpYyVYv#f?^qH@LVT!7M!n)slxhSj|xpl(Y2^-?x>XSFW=k*f$m=~r>l;}IhUM;}_p_qkni07z0`qUJ2B)Tj@c!Puu|F#`3Re5oBf;UcB`|sfI=y7iOJ)~VMmqi+3WnvH0 z6Q$~XwY%VU{w`&qadV~Hrk+m%nsC2?s}ELICYp=p|5VGz%^=wM@i=yzx?cKyNM;xuQ%*!4b$B}sun^T-Ou*GMrnxId7#>v0mlYzhaPfLt< zw7-LWF&(m$Q{?+S;)CUoA4I;Fduyxocpq8o+bVsUg?Ap_jPUmRnl8^Pgth4s8_Ah6r zeUCQLTTE1Ka0txxzce4Z_&Js5ADc>g&+7h&0_?5lkS!nCf z)>eF6q3u9h3D6|2JhVpYTI#`+y>cVuIsIFd$KRf;URvR8gEu2MfqJRqF-uqCVt;Tt&tpX6iC@QM1%CA4K|?q__TPG}p@4v04;Ok6_Sg|-)=Iq#!vpt*`m*FiP3 z3}u=T8a}-DJp@T-BVkMMi=7J2rY`wUOF40^+DT&hN{9)yjCTPfL=c& zKvG9j$V`y8T9FB)rJ2V?!X^nDHDPLP)$8XmFS_MP!;3RU@}4yAfyh>ULyuh|dR9G& z4y|*By5Mq2e(Is6Kj)W|)MpE{6tq5JVh&NouiCDx?PtXgQ{^Fcr5E0Icsn$&9@ltj z#TWCv5bcvsz~AR%2ggA{yWn{;medg3rOm32>$MWw*F>tZQMGt%4{<7Q@*O{Yot6E zkd=0C`qs!AF4Y}yZl(Tq;BTg_S4-NsGW_y}Hlh3H^8IW=HZvFZsZz$74^U3{s9B9s zVjejmZ~F%(h0z1#29cYurtGzxU%qCXOvMH}jyQ`3!ROGEl;sL|D~UsI&Md57v@2h~ zWEI(elCnvD5MQa{urMfT;SyRJ+Tf`}wFV`$9)1nbX3id}^U~1wNSV{%{4$qF0#CA2 zHte0djePuQ<$x554~@@AD6Bi^NjC8(%hk zSQt7d59g0dM`Z1Wi0*67+CYSA8OmH=l{)IHA-X5mumm;9ngM6FN<`MNhV*T zC}&u9;BR0~;H3QN-=Xim3jQ|u$FG8a5dM{`;Gcm%e-->2@Hbw6^8C6VqK#h#e>40O zSHa&4|LRrnPr&b7JbC(8;BUGL{sZ`XPslHNSwD@x0KeR$Z?>reFewY@E-6<5L-t!gX|Lbq&3>N zV2cA?Nn}4v9U|4gOWS((q35e~_< zyI^eGf2hv70i$>?ubd|r-p;J-Tix&B%RYeJcS%TMId;CbRhdb*GQOy+m`u}6+B+$r z_0K^|3?$8MpZYK5(+}3ts}A)X1JAAehM`S&9(v!LE7Px-eIBbty^8XRGT^KV&Z`d# z``nkxUv#f8E}WlRL>l>3eV93du0wDBCb9;i4oxsylC(1>wzU)i5yl_aA!Si!M2Tr<$2JP&3~^ z3BKd2NxCkw;EjI3=zDaZg*~!zG~gktecxony37!?U7H1~`GbdrM&{(GS1H%ExVYxsyK~6@wfk5XfK{G#qAk-Rcf5%o=_Q3x>nOPpCp5`i{_3nDgXMVa#v!iH^5y z%EDpYA2!cm^)c*aiiZ+=S}PN~r*Il*kPuH~H_31-Ylpv|TcLzFJLPN^fg#VH3GUd zpQx{3_Y=+7K2L3$CtmTNH;_vo9)|Z3c=K&pzk%pj{bt;aZ1o)e1G3XP&ZWryO(KLO z#Uhm=Jw@P}8u_ zL91iVPVSAaDA`6HR|)GmpI7I75w@1l6QYw^l@hLajtNP)nbW8dj6Y<&eNA4km4|eX z@*}vG@YVChYp~LGS|qG0@4XKozDQuck0z--DOF1Twl8@^i@Y#!B<&+$4OHj7_05Q1 zqmG}mewnPv$v9;ZnbsTgdJjNgk1J}VJu`R)_-(^CR+BHx7wM39G&;SJL03gnDTnff z2{t#M(;U=Qh#?~2)Dv&_M=771@_H|0sB9~}E=LN(k$M<`HxI9>lMt_-FPtI#fN)3W zA!}cE*7JoZUDn&1gifmYXa8A-MK)O5jsx&!Zq9pq+d?)cV+~E~*?xPk^`Y@&%v+@J z-AOY#X4eL)cx)%E<8EJHrH-=DTG1cFZ^z9f*w#@7z7hDAn$TVDjb591AM1Q6Jt8Nn zvC0EwZtf95Y!!^od-H`Yjp6H%wG}yAYsyy01_@J(p6w8v>Ur>5@}AAvDsh(nJg*T4 z&(g;T1fIc=w$lyH;JMhWO!1zVV1~kJr1>gMlkD}?+P;6hmAh*EakijZ3tSl zuCZmdpOL&JKd$+Dw2o<8;PsUe_g^fdYIJ4}i#iKDdaY9hnrNh()amIXp24A`(<6Bq zH+(MlatUn)S|76FR>~!`MQA+%S_8B-Xx-4-gvqm`US4{FiDfJ(JPYU}{F9nLG;`zZ;q5oPVH+$ir)h*_Skl)Gf8Cq&c79AS`k|$vofKmV{x0}czks~p&O^&a zXe-cqBeYFu{Sn$8w8021`6tY+LGyH0bSG_SO84>OJs(ASmX5%CZ5j9$1G197PDAtR zssmatw0ZbU|5eUr$ZKoDfd)1Zd=hqytf7&PG5M2S=sJA_y=oRZr z1oO*p@$)3uRO7BfeWP^Q_n$l5qm_xSatv+585aZCQpqcV0wtteZpaDdt zk>4tES>%Sfr%sFIO4vdnexJ zceXrA9_k5SCcIg~19>p&seBG;OC9j7!?#$W_;UUpYwf5elNdg2lCfo&Q6`ZSW%aZ5%KcgJpZFMr-&W>oKt;qHu z+mGxV_onOu<0N!B(7;0`CD7MOUo{KP1~_?*6Yi@v{k}@ly8-Xad#*mcO8-BJjT$#T zU!bnrbxKuT3ttO-+b7`bfp6;=z9H~N;d3YHyR~0w{k=-c*PCjuOvvko_K7~%-s5lB zd)(-f_d$NLH^UH#{_Ir76&p=84 zJ^W2^J-$qI2koVz`ToV{zo49ase>i($7YVMt19?*;G6x3Z)Y2!<)KYM8{j_Jre(=R zcb2VZgYCNww#GlFE|D2Ktz?cntGI)2+>|`4Skg2E*6v4*j`A$DF=*M3F@}@AgiC1C z&>H>-d&L~STtZub*7$MCHNdwDE%_&Y9))iUTGyZYzD*;veM56aHfU$f-kTNMyXcNj zaO(epG!sQaa4QRvg)e3=2XuHs|pA5@7kK+$4`#zNLRYU8C=87Pf(CVS}Kuao$rnNxp zgjOA(br@Puwg*~U1aA;pOMoVQ<0!O7Xf?WCm^O<}Ct6}y zZ4X)>G|x8`zT`4_i}0nPWuXQ0)?jEgf&6Bm^+sen4PG#gUTEEsc!!{MMrdQu+9UB! z8(OeT7ocS#va8UVp#}N2pfx~~e99&N_o3BCWZl2S*NxC>prs?UMrc(LS}Qa+LhFL& zL}-1`4n7r5Uk=(Xv|w3{L)(T{FA3(7G|w2>Am1XiJ@|rkv}S0*GT1hFLD~VdjYzzy zzrt2W@M=rYnhY(7*9L7lBHLYp)?b1)Y-qtcnt-+hUXV5mZ81Vyg0=uHn3i>Dvk|@> zXfqL79@=z7w(3*NX+&ss&_*M)W@xzxUOTj*2wxUje}r!UT3>`V0x}gPiV*^?zG|$JCa@>X16`?sR#j@4VL^g<553M~yYk}4Z%@rl(k~lh`HAnb* zpfyHlgV5?Dv{7ia5!w{Abc8k!ttvuWf#ybNo6wvHZ4cVPUxo9Q{A=0~G*>dgCHY80 z+m6s0plw8CGtkx|v`%O%5!qg7i_n5?b*KbwtORYk1Z|-NZPn0%^|l3VK9atDXtR;D zxPMbjt1+}7UL&;W2wp3+$p~JTp?R_`lxa1zwpGqf=H9*K*ZIp8^grx8H9n?4&XeU1 zr!mpPErM5bc&U(-GW^nSK;dG) z*fINBWOn6FT_JG;O89bwLFdw#9Auir0sFIcSb*x8IV z^kS-BH*P_07P(O{+=6W0#(iHI95X2J63~uZ8*-D#ZE^2DWaYx+ zNOQgm!w$O&q^4FP&w@7rUc>eJokaJ=N8>RUaU@;_ydCf|7xg)g9vFzq+`j;qOIa1dNBk3-*^j!u`I0&jj=Zw3OuQKt9EdDz?l`CTgu{y76wI? z-i;tTd$ZBIufGQbuIRc*MDHZeOUSJtCvzC?r_0Di@|=P90J&Y{21M>-Cy@*5gT&qP zKj|N8^|>YP!xlrzEii9}w(HzVsj0jUAUAZoJ|Dq-rIpL+airN$lauKi*#x1)97*#m zat+Vc?{K>JS-EiihRP_MW~u8vFnYn5=ia?wF`{)X%O`<40w?e!_P6=(n3Jj3?_IlJ zeK!=Yl0JgXzdrlZhf+(~;%^Lt)$$yZrjH$k#nhQehqn$c^{|Y5`<=%2ywl2u%f+7D zqSpLW|CvW_=sWehqV6MB?znzwy!^3-97DS9C9HnX?x~KDEv~&gV@0pSALS z+K-685&3qJ{~moFlKWvRuj@gpwbvV}OiP?OWJd1iycY2{-fLxieLUrT?qq}4p-tth zX-0Gg`6c9gUQQi~5A?dDAK@x+sTY|FSYMT9V!Y?O4)GDc{!S=drQ3sgb26|FgPX+)YlwiJA>Q}} z{&2p?6byElqSpH-h^OyuysIsB_D&NIijlD| zzNt9Wn%6SCxe@)ovinw>CcW;W+MV`Scj2r0Rmwv!UuXH&N~S$U+6f7yU5&9CH}DnS zjvpl$_gaiVd;JYFJH6#K7-RRTp@{7ke7A?I+ukm|p7!>57lf z6qD%hBV0rtrbye*6Zh&njlc5ve}%#o+zV`S!rDT}R@cJT{P(2$UHaW?_q|qDY@J#& z6`pQ*8sBYr9xmn)9nHbh1JA73p9d|^($VAKLJ-zZ4 zr>}kLRf|g8#s~>qX_L!fw|sy$c?RqcTkK=T{c}W7%8oW+tBdJU1`S_gUhc#CY!&yj zHlDDqnlhLUD4aC>M&$dDPyW8~_x*Hge^K|37z5QvRR1V8aSE9&WSXVDebB}^<*!%U zPI*Iy%i1RDU4yL=g5+Tjyp@lb_WO{@9(ru7uDi4WZJW$fwWoLMAs-Dl&PVt^BV;m14Dq!X1R)QirzF=`Ap0#*}e zj3fo?qQ%m>;q42HQ={63s+7`0`A?)rY}y();-UC*d7$hnE{_X~~ zMkf5) z?AwRjF}_`1MwcauPK{5ix9GQcxBcK*@zt-*>3L759YI;c{&c z2O-+_iYUt0oCY`lQr=TMMS9-n;~t|QZ5Z;BbCRMT2VgG0;;_&xnD4QeOGlSmr;M=9rKKC|>4g1qFHV9Kp+2+BmLv*12u z+vIV+mmCvS*7xdo5=`FR9Q+T$l=_+mv-OX7-%ZNdPiMJ3k%+7%d-<;p^*7{SW3i`>u@%d>Q}7hq3Mq@)e`a0se{fEM*fk%stgVn?W5ia- zn`aS{wrxXT*L=qGT^DV7w@Q!qA*&>sVIPe;L?Ny1{MNv1UOQC#vfQt}0}5B!?}ccG zlJbf4hRVb#`&;`I{__8%FBCodxW$`0x}Qim_$Kfam#MVp0Wka45A`=GK4dYA^viP_ z$3nWRy~a9l7Qw0eynbiheS^V4N%hB<)ZkUk4{XCXzCmB7>aa9lJ$R`d=1abKSXdRl zmm0nja}J_w&G0sE9;)|!+?Uuql=O#&BmR(_MPf(ex~NrIkAk`VWzKSv_@8MpBR(({ z?|Z$nh81{1)BGoexdrCpSMa+8^NF{UaxOhj9hjr{+*-aK;C$f!_WIM z?Ru8VEa^Cl?P>oi^TF?A|aU&VDQ`CA2V;4FLk zC4cX*cvHoD0!+u`3`N;A2LoGk(()pGA}2V-M9iaPGA@$BkL!n_VTwEPTC{ zkKvXGP;F$1xO!fYFKkPi@3L`?`SV$N5;xI6Vxg+~k#dxtBlt|ZGyJN)hHbcq_eTWp zti{_4&mWoj{XSLap0QH3BN&}v?7on9y9DE@5hz?`)*WOX#`XoKer3L40*usm0@Adhu?J{%W zSZ8CWbYexe71_b>WY4we_ZzLO=(n+ZW=()@*6a60T4Zy`X6|F3pUA$}%KG^<@lM)Y z>UgD$7Ll!Lp}mXjeO5L{j~$6P(M&Nz*R4|!GRWtVAN{Vp9uHlx^5Tb;7_*O&0*J`t ze+jv9a8 z5byRT!Mw*}E|naAOq$7DdHEU+Yd62B{o4$9li=0-2Z`ZY?HeB+?=Q}cDo_CeDo9<#E(ZrZ7L6g$`k zWyT@+Oq8Ac`oNpcqHj{>4_Ul$KVbc!UB9uILrtg%ulaaKSE+t|8Qhv5&#QAl-1{u9 z^rPYNs{Q<&@=5)^N_~A!`v_GSfPvRycO;FcPl6>KU$8&cI^9NoyTMxO(eD(wpZ#Sh zTrw6bpRfUGr-5EnrK}OL8L&2gN}o~Te%xZ|IRm?PqGlab0y4;KB9r`S`dzWZ@3bP-QuO13l@Hqu zPlppS>tZd1DpT<@2EZ75TVBr-KRFDAtLU8Gd6Nx2CbI`E>+L(r_AY|6G?FiLN?IN( zhl8)}y&O+@$m)*&sBJ{@o7D5$O+WCk#o3Cy*Mt8GNeq>#Td{c=WLJ<)ijKX?${y*5 zRjN~Nqh6TaBRmG)(5Qa5*1h9|cyh*HF`U{TvjtwqubKKje5)#ITh_YWl+_o>U+n>F zDagyazV4G&{^6dKJZ4yxj+dQ%k`AzfB{iKz(@1a6g-xO)O zOnWB|84r1v$hSl9U3t1tE5yKgCGyz^?@d$T8-Q=-edb;2PXASFyb#R>kMK>xw{Q$! zJ$%dXt-#lgE`{QiL5UYJK~?H;DqOm$wQ#J zm7Ul|?f@(`hxsup7q(f(hRZi{ryV}lM1J)E%dn?$ajcWrmI z+}e?CSw6`Ghwty{1G)ugTk-~u%u^EoF*pY$ailC8uFx;cmMC)?lc#3F*9qUsF?>?* zL-4J^H^#kOLK}m&@!$OSlcX(93m-Jei__<~7vWzb?0~SOgmH-t5ZpTSa)^8Po=1c% z`HtG5{*Wz!rH$VLvOUNSP*(0~E4x%Qm*Y($r%`M|y>i!Hw#G{SAAIEZW77od)4wRG zkaSEHVfphy4rqjXXn(KZIuPpsYvBX>+z$7B7HjU9wc0xH;IVjL^z3^JnA2b;r}X>R z?(2L^e~fP(aR&9?$$WZ;u++si^6kir-Er@=@=M_{z2~&Z*Qp5dl45Ltq@$rgo&Am( z=U!uRa>ePu$$+XMG=RwWA)n#B{blamC*I6ouHab2W`nLL8xq&{4|x|GA$sV3%*u!7 z+-%R|$GeSO7P)%K|2wQ)iE+$I(YQr)t{5t|beJLAi{R(?Ja#HV0$YrsAGWI3SxzcSfAxm9^g&Uv5>m*{%-eY<1?CuijrN|@kO5HUh z*E7dCR^kImU6dcKohUH$A0m3&<^VPDB3rJhrfqx(V1I=H0E{ZlDo{Ez87NjutM!ty#J2f9VL&=}l(*BNg=vGka z4AmYcpwS_M9DUWH+Qm5dTby?xa|PrroYpe<;-LiKoGhZ7;K&&n>q&*PRl1JsCM~HY zcsw2P_I*{#$6HF!wI7zLpe#ON{WRT%RhvP|v>54=d*NGwZ~BZ%OQer5PU1Kd(ta1A zDz6UqX=EG!()fpgb3WTVw*wyW;a1@rhObTQWLPKUI{^WusE0of|0M56i~XgXt)H*X zWcM5zg2d0LKNWKZR`T{cjw0Uzt>v%tg(dF8^3z_*)VhN3_QJac?}X;nX*0G3_erHp zGAz8~g8Mh+b1Y}9|mjU z3-qHpJz1L^4=b@Q?_dNvZrQUOd?p(tV-=jiFH>%&-4x-7CI{yaD`k^=kbv(8PBoL7 zV_#uxuFJoSP1PTe4w+Q7J0)Uf`7%Ta)2sVG;%VPvJ*TYg>j{<>h1n5)Cat0a%iyH{ zL9c^RpMf^05+wbmR3=5vL zk#=Ys(E7QTOY|fQt!bOKi|&!O+%Yz2jF-mYiP|8IKMPjR*ZeaCq^&F&+O)2#e(ybj z%7lwK>o+SgT7DPa&Hv?}D?@)S>4TZla5*f>X7|$rhWG1%Kpe zy$)mR#`-ubrdIUKmz3cc{5{_=XQT2gv}tHV(5ktYOK1y*R--7IwhC=H!nb8;Zh&te z+F*pwJzGqxiO|FrHyWDMtdGBI?W>}4o{l|eiUR{ix?3T--F#rxar+R28r*XYnj*^p^48g9H&=+YRu zW5&E&F?Jf9sV4jtatcNo&JNGe`~%b=Cs{@ z#XG}U_aDli@i@QoWhvV%eC_aMCBAq-*?E+_a}XqxL)B#i8&^0PSBMV7diAC{UnO6hZ*oww_ed}HV^su{(QFj+Ea&EGf7s4G;Jc&gUlRy z=DxToy!qLw>ln%~0eT_b>UsrCU(--0X9&6vlM>EHTcJ$>iX;Mz(kdpH2S{&i?bR*g5YyuvegMLsNFD z%othHum@k}`B(IJyyJU;k1(47BYgl1&~CMRoqMx;Q+$ZA!z$f~HyCU`tJBbY9k%lB zEB4H=7O>l)H9}kEo^r{A=JRMLeqW@&Am;awdR#h+{37x*x_okF+l##KN2u-LO?wfW zz6M5ZlWzM?HuxltI+d!?|a z)8^YrU2KVdk`%C5fo?!2PHa^l7|YFIa4(n8a?qx}!!O5XXyeevp_%oX(&JC{IhW8_ zQ_X>_Av^Y+SJXUkv0W9<)}~VVCC)|K%6;0th%T3$KTOEJmT+8RA6t-3x8Rp?k8HpP zkWs%WYkP#O{uZ{B`v7vy$ca8$Ib%-)PKV0R6uceqwrk!-S7*9xo5?AiQSmcg))Rbg8`nm5Et#TZ^4dvEAM2m{XO(_tE;rxHeVdOfo%3O0y%} zYV;SsBx+Ncke;EKE?MKZDEQxV#rsb3k?Bt~l}INT(r%MC(3V^2&yQ($7^9+AC-SYx z_k1ty=ooowt4Ln-Xc+nY_gztYXQ;Ch^PMu+Vr{W%zn#b}A~*WdD}~OZ#_nbcI!pFL zLv$(M_*4;5+GO>OjM-myB{b%$hn9o3PqAK+R_#G-&paon#x3ji@sz-6@9Ah)cvPkrsw{~z}$KjeXB$gnb($S8LjSL z`Le{n2yag(ZC#i95o0Scn9;FSdz7U5&Z=k9w|wMj@B8s~U#s-14TPm0eOi}G#OH1! zZ1e}|+jL#nc^HxFA#4lGM(!m~o|%=foQbbg*d$>t@zrR#(D-`6gc-Zg5yV_4tQoo5 z5^{TlwGbw8N5>l}9A}Yhmau@|AiVX24HLFhBCM6L`q|Q$S;FcFs};FOnGG2^73O6C zHi>V7um~nJeslcpb{9lwMWcTt2Mfh_JRYVUpe+!rFSS z*t2{y&<3HkLhI3OH;~>1KfSWw)tl~@ftYuXyx=Z^(eo2m3OVlO651NHHE47V>Jr*E zwDq41(+;3*yzYwLTLO>prEZBibG^QQBK1%Ut(AJHQ_sRa{Jyb& z0ox&UT~F9JVRA2555E>@4Wz;N#KmJNJ-m$*m+iba)N9kSkP#)xDno=u92tJo#54bn zD}`BI|7*qL0y~N5Q2m7a-K$i;q# z=JS0}Z+$8#u6BrBYi=$7OP%!4ZD3Pjl!7&XW>1hMYpf5(mtiPri&KA z;g^P9n2_=~0B7$v%ou}bp`~tPEHDxFdupMjpsBvb@HH7)if6e*whdYmKDn1`fL}K> z2U@dEYe#52C_|ABI|8g!>(~AxhA$9^a~yc;4L!-mV?R)(Wu17Jev5W)Y@qlEJJ9+j zuM|484$Z}I*0ROUntpqtq76rF9#w)ZTzK+Im!AKu1Mj!ZShwHrXJwR`F$)}07FqbJ ze#f_wby7Ca>Yynb9nd?qylWIs_$J}2{t*2l_aXi4^7T{rmf`C@hELM63*R(+4T8s| z>gsmJ+|blLG_gz7&=#QW3RA=ft(2V&jkcn!=8IDnCJH z=dBM3-V}VR@HO~0QuQY|D7FwJ?JMxD{jQ%6k=+zoX#F}4zkLVFPh^wNrksC|c~jxz z64^Ag8fZ&Q+1zL|6P#^v68CCx?R(3|aPX5ItwNq(yC zpzR>r#=X?NS1;0?c=aM>)(qbkeA~juCA4;Ea~}!Yk1VvA08M;_0ccauOxfBvShJ;Y zZB|rzrbPZx#<&tmpso0B0(}!fEAZ{ZmnkYMnN#yIC7t{5x_{ue1If2rA9HG<9Tkt1 zNh5rH@MX148#~(mCS-p-JYgduYcZ0JA+U0DzHQAy8#6RjuJpl0dl=9wGa0A*b?P(p z90^Cg{9y1~`0W#i^M}lPaL?MLZ5Q&LC9QuQ%=!iVFdC(&mr_cy)K;zklrbG`GVy zegeJ$__mJWle|sB*D&wT4GL`zT5W{346Qms+koam^U6hJcMZ+6bG6WBpbbsxvAO&4 zS3%(l_k;V-ffp zx%V!iPeR`-L7#^%d5iL|LYK0M(zl^Em!RjNca)%4)7kWvpf^CzL6>}am!z{5`a}tO zH}ts@^a1EACFrBjw@T2bp&vl^`fZ6@;$4JZ^+(0@b?Ega=)2G}CFsfL#+ zCFo7iM@rD!p-+{d_Xz(2{esv`bBP@qf~Z`NPLw)*=v?OM~CZ!x`(-s5q4M%0kd_%KBx zFHC+{>|U|3EpDgN=H|}CdQzQEKh144*4gQ7#R!^=uO&O3@zcrq)6V8;H|ILrr)S9j zBRch9+#mZMdF!`5HBGG`J5alkbS7f%QZlv`OXX=2>i!_M&QM}CDfH2bTE4e9S0VQc z74y(NIPk8ET`McDjkQ^)(_M_ky8FD~!sNBFR65z<)Vdw9`X-Nz%(prwt1A|=bK{JI}4QRLM*w8KNEBFvDiwi!-oaqrr1R%g9GPwIqmL5 zJT^2O>vD4OP~6Xr-QmoFGaIYYI4gWoa)XM!Scj=Tfe@ZAjr_$UHpKgaf?z*$CGvx|>-A!}9 z=ep1jT&$T_Uxk=km+EyIyU^uAEG3I1-KjpOmza2VYZ?ZeHdOP3_jk6Z=aNnjUK6Lg z%qOXgPFg~|h1-?*AYFGt!Z#8_qWTiwM7(-2fj*%4Dvo-prqRS#=XN^{RA6E9xv^(g zr-q!`G)|~H;ABWFtz#t?qv)i@<#vh0d%4nBZSJ7cAy}f5?JiAjEtbs1W9u<$z{|gc zwYgKO-u7d$`WDTOVdhBIk>gtyI$q>1+hD;Ttd>MzSiw=>fJV{v6<0?J3xzYXJfI^m^&AXjmK)e z!~jy5d|~W)4elg6FWqsby9J>Y>2@TM8k3@uQksdOO|iL%EM?H|PT+ONTwbi0lR~xm zcuwrLU93+6Wp+oB&St{RC1ZPu)NnF3o^)4|vB{)goT{v^C4sC@EKAA7R(&vKmnhFo z-1e9|6OZNnijeZW&Ot4P+zn=4Vk8E;DNJ5@Qy0|v4&5TzkH>Psx*%*!)f09SkpsUH z(XsP#U_htKZqe>-Rp8LBb=KH;ymrc+sB|_?ZR$=+?C^^+lyF6FinELcC(rBYvpHZa~MHy3~DTt-BO=>ZL2<8Jl=hqdS*y5ZOpb z-186LTCTSB2T)}h%LWf zMuGKiAKDJ!02zu6kvpTgk_Pm;D%tK-lfXWAC^nF?p@qrYW4Bb%XQ%0Io3O3AC@|1N z(pZKA-4-~s^;!~HKT5jiRU26h6oiSV&Z(}J^fbpBgYifjFt};L8r+sxQ#kCJb~27$ z&l_9jC0@#}#*r_2ZB|ropLX^t4v2f_w0m$mws$&+aeu6pGUjcfm^++s74qRu|ZB@L_cXDJQS5dvNel zs*=OkI=fZPDvoQNiDyt2&q$74a3-IjoJfgB>V{64Uo{&EwqlcHJfBF7C1WGWKw07& zHscTAwa46vB#9iHl76rcB}&vo*iTfAC!OhJY9kq&OHv;zNtfHT5XN(2w>l)IIfah5 zVrd8DCKsz^o=o)vGXYJMv|+E6!K<2KG~FZ>nc{9*1hB~KVv+qMD*mg(|Bl!_@&;v7 ziaswxfp%j*upKEXXeL%CW#Vqf=wnGUjea_g^MvaAdOe(o?Zro9)k|rbQfeXXY{arF zY4mi3g32XRqiJU}3GPg?n%mW6iomG~clLT`tD-u8oilgJ74}muq7$c6I~ObVPd87c zomm&571!l<*R5W==!~9mN3VCr&n$tvUg@r-osqLO^B0|svvR+6mQry>&SkjIpA)e8 zYidZ-<~6C|w6k-~An%b^rB-f46-a*W+PaY&otbM>b2m5(*H-Oa=d94<;0-<_y>ijn zdj|O)zu+!i?@V4mw^lB=Yu7nz7ZBXOP&avlGg_S{n%U|k|9Ld(y{5Z+~?E6IeY^N z&EMccn7A>ubYpDpMtAwf*usre_r{HB$a^=Yb@3PXPm>F=*hiV8WN9+Fn?gt2iE}t^ znBjb^jW7SL#NCB+&RTqmoYLd3okPi|xu-g~pQ)g!?N+46&pX4XggkRPwRp~%KaKpb z>vB6PcG(#_lbnaUvJuAJ%DUWnXZ&mx_X}t1cF$1{?c6V>szF>&RU@^NBJCsR-0^d< znRCtC=VE*3-2A!N@OgLad~EbQwQQ6L8}R;^xGcBenzzo~;_Spnu{QgsQN_&ZgNJ_lA+NX+>x7{wHuSOH#zxdrq^nmshZTz&CXm+ibO2c zNW`l(Q_883ekz@oa;SH+%FD)1PsC!poayHSJ8@SdO{Jn4KjU-{ej4i*L^Ps2%CR`D zektzm#A8&*uJ(vxZzeo&6(%ReC7d~3J$IY4t}-M`hDy44-N%~`%RHGaFZcB>e|$vStYE;e1)h3eF~Be%yUZ?9u|e(82U zmtvb3ihdlggVAzo_l%RR+CC%pd-e>LJ)Jv4Z{tFqj5p;f(K_i5wi9mtw6mL_qsQ6Y zJ?+dU-D%fhWQ6yYbm7}bR*8HvId;a$CtddCOq@zhRx&868mV-aPu0OQb~?@3e#@uR zd#8@19<|YNCSSc~zi(cPnBW71+UDsx} z@C%&HbCkouIhWhq`PAgy6|>h=t=^4VL*BTincK0dUBSALUVeeIUhU32-`T9jLXBMK z4mUbu*GZ(4*9`+WbuqQu=*(V3a^<4S?Z(BbwHH)uq}}DlirE`dOLsd9HxRgfLo>Id zH_8UO;hWP7FL1VQb`PHC?A%PJFnNnR*2vnuz|L{bAU5<_lJfCeQ=50AHhq-tZSLOP z&iHMkWNWI<9lpnzsgqu#&YdNNbrke&ojd&kXYKat@fSGTw_}_(pY6`Q$QixE%|G86 zzXOihJKXiVotgU7PP4O6Up0TPvs$0ZzsT8y@*-!a9=n^buUmSNGyR-21-$;8^ze(= zMcxF;!kwv&7dk6bCB2{x5s)1K!qADlS_UFnSN40rExCU<&0yPWi{(C{v2c9%QZ;G}j_?Bs4YQ}4{|c9CjsH-^cc zdI}EIyEFC9FqZUtoNPVFlYTIMh1Y$BGjv51ER$DI9*I3CAZ2=wJGaM)?{x?EI*Gkd zzIQKWF|^k$G&m!EcyuqSdeRS0?}fl*195aWppHiyNRxa6y3SmK*L$VEfO_=Yg7tc} zhlOMcnEpUgrbE?9N{$1RGRUMGrizg8cui^o9dHD<2HgaM=ZHvxF0)UZ)d5jHR@7X-t&UVV5FSdaPLvApJ#eRRVfo8SHe2~_<*f8{j+)T*nJy)Se(ubU@Q8>SV z-4&)c0K-fN!U)d$2W7pX81BVnL&!K@LFAqaFH_;rVQ>WwgS~~B$LgPewh1SMB}vku z`D8Q@!Wt@lB?Sj*0MZi$nbz%)lzz8w{@kV_{V*2)VEq-(beWXx}qo7lgTRKe+qW zdYLLSu(l3-YrS|C-cE^5Z^8~CoPm%?Y9}obkEGGV9tz<>1ypw;-ObcFa|f`{X{2y4GJr6~!Tr<&=l7dLg*&(3%vQKl`_04> zcN}!roj-`dFPv^R@nxv5`DO0(K@5OLCl8rEkMPYs0$*zqI?lAd^AgZ& zr#H9H4D56f9^L7|X5%|OaL(+EaA5$i#A3&CWHtU%>{V+qjASQ=h?vZ*u`LoU+ImlHd8C3qpMBVs_4{eZ0o#{)#!^9 z&aH|j5WC8YuQqe5yy>t>ggqSb^n|^!u*rm@`LG!YlSXiSrbCUa*9lLC@bqd3Ed(q2 zIUIJMGr&L(eRjXBI&{*+;v(k}XN5uMsl~Bg{bSQA@j^BZ)aU3?l0U90yc)9#%`A}= zR!zYPj2+B}vLf6S7!G7Z7-TWv5IsE@bRG)&Cq*^WhoHYVc9^D^0@_@K*LN7(^=RU- z8D8e*LGi-z!`N=SQ-{pp%JAqRlU(IZqMxopzv~W%3+RHY-S|N>yP7)j&>D9FO?nND zU(*qH_<+f;bB7M#A8vc#?8b4#VUynEBHM{g-pmm*QRO8Mnz7Ap_dzp$L3OU#q^cw6 z*8SCyc(ch>hbIr3o-IEAmT0!wbX^$fZ^rdR;?KssJ~Z`M-4wdaMFS3^=eD_d3 zhXCd;_Yl*&bCP1Z>SM?*e???wzvvR8+p_xRNw^q#uf8#!d=uJ-ycufro(md>v+PuVk^^mNGiSqLYAbJzIK zR<6ZkVYllD_Q^CY_hO#E4j(m&#jnF@Qh4ZEoD=7$V^(^-*P5XfZv0v^hc6!>dL-;l zA2p-lFcM6My`F2S3HKf`y=chS(V^Y=AspJ#Zg}uKZ|JBQ@wdI%^%Tz2ws(4ced?&` z#XNb$4A4B8k9wJ-W+IAtvTK7khjzWO+Zj7*W;QiLNL6_L5ZbuccMVQOPGzq#<6EeY z4#pz!Ys_eD2;ulP58>oC)aSsq`jI1M?h+#OU*?V;Hu3Gzv1T*8J&ai)x1Cm)@yop~ z^u)`N-{9q5;*go&=}jFriCr{C&F!XeS3PF_bbam6VKZ7E?nU#Z?cQ8H^}wzx!sxtL zc*BR$BfNNv$?gdij+(B$9(s9-c5wsP@*Ofmd%-ul*G(QWg}v?!ve|nKHp@4V%_bY% zIP`3E=bBC8iPH|mOzaC4u0gveG5t*tlWQU|V@(h<+2r;>Kof~c?{~pHus<|)oyqPG z<*zfj{oW92Z+{iU#}9;3*O{IJRFl~Qv6*Yl^Z~DcN;>GxTw{6;#>S4A?7>*iVKaNs z%R}L255v+xvp00eWShMdCi!M}w%M6Dly+uXoVmlN&5!9Fq&+JdhF3aME8N)?4vw4?E1lFzH@(v7U+IEAxN>1aG=3j$q?apN ztFc=?<6+~Rzy<*Qj#jeW!3I1T$_23-kB4gTm}W2(!9UK%W0*n(9>dU7iZS3g7FHN} z&@FMMx`dYKndMO&@Mf1syP?wZdeD0+i84@$TFqB_anjVA!4|aA154cV#ypeqysSqD zhy&QHda_3kV}e0@aysyIG#x;LMSd}C2{2RyyEpXIn9TW+`E7VJB{H|o3|}zhrfYBo z6IBMj%`hvO)|+r~;FW@$@g-4&XO^VUXQ=G{^A*k@G9JKy*aN1CfH#AC73b^>Aq!VN zLRjo!Q&&MpXnDF31FQJ?RN+7y>utxwb0j>4hM-KqL$@3Z9OL*JFI55C&R~ay?OO#N zZAbblaL`SQddS8)0)c@*%86s14Yre5qI>*2xzn6Lzb=Bq*yLeX;7{&DbkN|-8b_JO zalE(0Ojgi#jAU^+!8{O*jAN@4^vfCYX6P*OY-?C#k2$gj)xvzhO$ME=v)dWS+v#kt zrQPu<7b#wQp2aAb;}nrw_hp}CFG%=xB!Qxp%91~x@g z=bPN7X!3kBwJDl7-*i<)yYR57Dw@ZrR~5}|Hse)MJV=_Yie{nJW;8j!J#6r=zQnM= z?sYI?qykX{Gt>w;9;*Pqy?C=O4mGLeW`Zul1Ju79F2E^Tc;;jrb5l$}a4`KAi(^t?}xOi;Vy)rU)Swy`ZpAyQ1W~a%B9cuK*^j>%TV7&EB*PnuHV7acWbv5{uy)~ z2G>rXM18wlSO53a)h!so(RhMkoCV~A^9bjbM!InWWvQ15qwAnKW!)x3b}hqrG`-A2 zaBf)=d|fMCaCEOA%G8QcXE==K$RUtsX%9{9pm;x%Wz7iPDytt2U~6UjSrs}g`cs96 zC!VtvZFtnSB#hvseJ)b0{o&CXnrF8Y!BIel?pn@s5RV!|IQ5KSRgt8Jr+Dmv;Bau5 z8Z&e&)^5wTINOJp9rLodenvG3dtsf1!(JLo1CcH)1?Vt35yXPWNu9I&`kVN0w+r$n z0uih`X980QkK-(~!YQ0{de8Zeqss8m49nMPni?kjD*%({EQ9@gH&%twvTD&t@k9lo zN8eKO>mBC|$O_M(k-J@EJeua0_pi%$6LkA;)DL|CeQPQhO)kZf5M`#l0m3s&ntc5q zpsPl-Fza!8sP;aOLd#4DN zy4czju*eNo_2X%%f8Lj=@UTf5teAva)L&~1PIB;bF=0;l_i=FD4C~Ki$eRe!J%SPR ze_H8+_d=`?&W_K(uA2;IX{ zy|mBFICY@WdkHPkli1RnaXMYQWnr7(ObRQENV?KYxn8o;D2yE9#NNb~C!g?b?o4&E5ER+-->T>@dSy-P!GC=3;N8 z*34gw6JA^>2l%j%Ngxy;K#TCJC=HOX2mx~RPV1wgF*(Cz}^ z$s1u(m}?>sLa-1({~is7`)`0b-R>JqDuicacybc%y~zx^wOHP`wdtF%t{Oybjd+pF zO?X-oCCbVwuw+)o!Jb(Y%HL@E*NkGenOz_1zRAq3$9Bm~ZwU|FWahWzQSjVW3U|dq z@taH{hMnR_%mtwkn+*)!WX85lll;p<`I}7kvL+O;HtrzYwY>_xXnO>~!R=$nDzVEK zvpYQ2YQ}d{cxrbH3#@udnW~=$OY%z6Aay0JZ+or^7og=;osiNPLU6dzn{73tjkFP; zyZR^!GO`cNF4+`9Fx7l8>4upn}nZ$uS5{@@}^Q|~N zsz(Ng!U$##LCx%;5Q3wJ=1|&^BNWaa0lDA@Cy(HmZT?6E)YMT>`;QXCz|ju$foq}& z4qijk!O(S)@te%Vbre~+4t(=|Fy3+;xwg1@}{iz@0UEPVHe^jJ*LyvA&Rp9o|3)3ukKIxDUtow((j6UV3Oi>06>5cx`GyOS*5p zCgL48)v#kHHZE8qosA_K7dcz}g*Uxs&QDjxeQy8$IW8tl&^t^h^TOF7z4rl=x6}J1IsYxA zv2)|`oGOC*_EoUIFQ=C7{^8C--oYCfXo8QHgQ0JXEm!dAvW*S=EI16MCEwUUEs( z^$;-`^uap?Tv2DxcChko3MFYb7DFZVISb94&&x1V;G6uFW2S=KwfI31`3Pup_2~Hyn2YYYuyHgd$_&9ZdL8lGD?f}{=$Gi36Q>Gba zr}iSt%_=M}X8F!PwNacz;zzeJ`slTw@qmZRvbNcX?$h9z4!C$;H5)*IvccxuQcR-- z%w&&#t%CAe*6bxPyqvIlgght#y7fIgW5w$W!+4&>bx;LynDatmoW=L}Cz(^Yl*Q;9x9@qvZYy zS}Tt7Nix#^qtg)XU4yRHP!z&OomEk-dHLs{9kt>FfXkw*{*kqVE`@>;`N5Uj# zGVDR{Y}f-mzM3XRzf3FrGc9`CtIO&1X*6yhi_PJSs7`E>4wTVYrW_Zq-QoQ@41ir{ zTPuw{>l02GHQVFg2j~mX8+ce{E_7eiE8dO33#M2&Wohr*6Nq94qDy#qSIRnKM$XEu|Txy=+i1ezqB7@ZmqV8$>`1*=gI zy7cK3&SgZO3Cq|N#fUgfO_AQ^ZZH@-Pgdu!VZpplTbS?=T|XoBCW6%n$3qm`7b5y_ zsHzZh#uq)S!Cc!O#U}L(s_D^_9`ap{<6S!9rJ*w&z(ElA4csg5#p4qfFYmhDcpA1e zjEnAFuGiz56y8t6!70vh9W>8{()#Zc-{;`r9@^Cs54XK%mX107yD)X6F%T72&;{h_ z71MN6rpg`Mg$vl>iQTwc;U;$C!jm_-(~Ml)I=$0O*LZPIF6~V3#Pub*{5@C~MQpw< z3i?zXt<7h4x%0c6xn0e$$!>Rix0Bsng*1hClR?}z%P#9h zp(ZQcp(Zn3S%-DK=S?=53|(0^BP(27rJ7w8#x9Gbt{1zS=U`0~ccf-(LZkc4 zTunDxYO*#ovk$k+&mg!xG>zbPG{U*1zmsWv~Iu1Z=k= zxIx!{HQf>$zS>JQncUUknX66VYA?ReB=@;J`<%=^cW|FGyf2j9=M?t2sr}CEJ~!Fq z%a!oLh7+KX<*d)49xF6nVoTbIPCf zop1(2QUG%&Jt|Fw=yjOB5MHH4a=Xw$-&@~G`!3w0!w{9msS-|W{KG}*gBc{xIN@nL zxO8H8);;TZlQ={Q#1a*BSX02wTa+k7H;ZPmuETYR3cShU_EtEV3OBt3H;hl6&5qVB z*PDse=n<}|q!2!zT8ZR9&vunb$YfS>v;#y3U z-o#pySz9%_7SGt|87bArIWh~r%wAvSpf9t}mkA1F_WLp+88YX5nUD;bg|jm06oh2b zTB0`)#_DV!KohUE-9iiDcCeZENZ2RxJM>4XvgvK$q_bdU==Z4mf9hTS$Gg|JI6i%Y zgYJwK_-gm3$UFP;xR%Di;dvz~Q`jkp^;+aZ!!V3<1F|dq#P$!M&4=XDARy0uw z7rzY#9oJAR#q~+20q1sIfe{qiw)K95aX6VrM{~Ln#{27whfAu1bvSWCz2adXJem}=Vi{LrkxtEU&WYeyAQ>1!I2&})>~cYpR|w%QIxf-B<%)TFiaS~1%~sgf4cYXh z1w%rRTIWPC=GXa3zqxlz(V`F|n@__!8RDR7AE}BJB=75Z^>$(yhpX5zW3Uc}@P!U& zu+TF3d;rym4Fzpr610ICccNW1LD1X#-E?Dxdkyr#A{VS(PGp`o>Qn@qe-dHrP0?g} zozuQfn7ctsIn6kx9&@np^m+Y#avvi4oG46$kym5Jm!{=!Q61z{_xhYA;Ri z6^k4U(8CTH;L#g7--a~J;64kgaZt`&M;yExKYB4bb;r;}cn};;Tx8-)!d(}c)Do;! z%;<6#^l@DGya;z@sR|3>QD^WXGrgvD>LN3|mPmu=#pYvX_PhvQ6ilq6v9ND_Bf{~h zHx)D4XcXbhhA>_xoZ1-9#?17l@JP(0tB->|u*F5%p)FK`i3^V-JhC;KN0wW|D8|-s z0XKxUdJ{2|jD^Qym@vIu%nZalung0qZ?@&Lu&v{9${nD4xL7V=uubA|7QXEeqOVYN zq1go^7^ZLrj`Po+kca;k*cYlrqYAl`v>wB`6y4g5S2&3Z7m+D{41-B*wNo3fLIXnU zbP|D)5O%WYQ@DVD;4tlB$;Rzb|6$MZIGx}(Q2V3xCx%M=JREYR@m4Z2K4*8w>7Xy^ z97SuMZ(YtUzV*1FlXPVd@<-b(sxcP_zy zX+WWAXmt3MW^AhO^MepNf?<`nVgN!iyakNilpyqSH~+ zeO?t_(#W0Xjp5Dj^XOIcX`hl@7oA*hGV8pF^=4=ty-l89=jGPp`hYit_r2Fu_pdjp z^=Ka4X6YUMet`a9G*^KWfG8F#y`f&1b%gHZ;pI={XsZNepl_X=#6F|BKZKQTEna2m z4n+_aeeupwJ!WDIS~$d^WwASsQJh}Xp@%P+{D*_|UfXmqng}^v)bLUD!!|k9sb}hp zsS9`dpUR={DY;Rv5#Px;=le83?_j&kpF+_ILX&t4n4WX9|_^*Nu zIbij5N1r%`v&<7bH+cTN3w-wHIAQGXHkwn$oQlNpi3=x;%B#I4IW@!ypYWbyY&bT2 z>=1+Q!`LKJIy}OKNNja6#ZEe12u}s@Wev2&6Mp<^oOof3b;4NM%=nv0mcKwp^Oe?up#o4@8c z^k2(IZ3gocg9Y$>5I@+l?urbzw7&S0= z=K{SbEsihoEu*1hjU5)2uZch?6~I~&n zOu3c)hWGG)e{XAMNuKzrc8+h6^bg{n#}noILVuFbkHe4QS9=HZZxH&k;S)vEZwIZu z2)!M&D~ix>1C1pkP};@cIK*7lisy!pFV;&%o6TZCYbT4hXhy+ZFMK?UFG?7HuH?UQ z5!sl>(`%j5%i>K+j)jeXqh4L2*M(Mq_>ZU8MM^J=w<=H)(1IszoffP_(lT8th0&W+x}<$=3Xm-nJnGRJHT_$l z6aN>Kp8p0p)E72BhhmMf_5K3bllp>nR&VR~OTFEx(p`sXic7>PEx+|!BwZ=Fv;WAc zOWEPWlAo=YBm_|k&Hq8v`)->Fe4D@ZtsXkviz-x0)z)y@nwp`Xf{xS}2dLHX*+w1ixALXz0 z`dilxQP_hd>v461&|mx|&KK3-yO#=vS$wC{)gvA0rRf)x$fvpleM<@YB_-reF^x{1RhoY??-c? ze5VW;;LuEcXWLW$O-#?5Y4|0beiuqo$9$5Jpy_${m(DKe_;uFwb1VtyV?2ngHo&@2Tz)!PHY zZ}$aMH{>+`KVyI-o?{LS3`cFBoXx-E&6du7n4=fsqw_q%wtb!TJoydwA5wa~9rd&t zY3oY#_q514Bk2ocETZeWE#}wteGKaZRH2y{eQbSG+aNn>I*l9a@zZv? z3)#_}dE#!i_c4UYk-ey1b$dDvb6HywpLH;U?_zIZd!BjDVn0M38_wF_`V-wu*W=9j z(5o8!n*JHElfK31*MR={Q(TUt2$MUFAJxz97ckw<-=k2N#y3s>BIqRlHH!bUpwSqo z>7NIU`d2YJ>7(hRpiy0DdJeQ=a)@3`&X+~bw?)rZU~>0E|LyQ=UdZKbBO=__K;HrX zexcv!Q;m6+*g?~82c6dMPnY&}lkl&APve6gPqr^*Js%eN&l3Jskh2wGO}`j)YNy5M zyNb|he|uFC`sJWgJ!(0$m?inToh%2P*7fNMZg)|H$=UWe{V?}Goh}G6^#@HqP=fv? zh%c7^DCouJZK6}V(DI3XOA-3pL8mho%}>jV3qha$9SRDk$F)Y}*OZn?00~_D_>BZ@_{3(&&MhgWvo6hp$YSMrcM19jLBAY7 zE$4pWPrR7(eF`wSaTJgCbw3okwfm1jqyDVvKNGqh-)X;h5I;Ts(*B6_*Ypf%#ptvi z+K%6=U&4Am4PkQB=c%4{3*D~gE`|JJ^cv8s!LR4{Jj}nf2>qjImXzbH$hYl@*32|M z9_nO$Y`(7mjn-5zcp1}aj3L*Fd})1@okIX_$p3q`y^pTt`ahLpzy8bYj|jbn6ohLO zde7GwH+$@NtYlxe*C!$kty@ZszxC4p!&^E1xU7Hl`jXBqmf)x9G!Ig{()4G6cC-k+ z4YX^D&~FEg>Rrnr`qf3~w}G~&1fAq>x`*vgVR98PJJ~_gX?`k3_e#*u1D)ExmP6;~ zL~oV)O(0Cpjti+*vVDg?&VJ$(?1w+Xen#j`gN*YZU_bd$_RXi*um4YE1V?*Fa$0`N zTbVC&v!s6w`&B|8?qXaMXWzSz{b&#S?X=;9)ALyW^&CGe{@9Zl_a0%twuSxb!|abX zvEO+O`|UTdKQ8iX{9MlJKYs_uyLYl55r06+VasFv(HogB{1o=9h5rtz_v=Ldbtn$i zZ}Jb1ZMPn&A1goK!E)P#ZUlEco9V50vETkG_J?T$5BDg$T_y42k3Erb?-lIVK7sw} zdiFRQvYCFq|8o!0%D|LY~_1<)@n!cXU88;a1YOVFub z)3~kk{bdRMsS@;`m7vr4NHKkA*;I@^Q-V&*j$-`OJc`k2n_P_k7vcZ>4A)~2WheKe z3C4)_-4`VuVtkiV{9jf0>w>Azk<;=t{M|GA2v zR(L}3|5oB{`3oxkVU>PT#m`B+weO!4{)>`te#`c^_N!2MO37cY;-^*oDv7uGq{ zD*ddAU$4^7srY$?H!A+kO1{Z)`E31NsNzE^-c{IBIIQqCC9hVnt-l?Dtvz-sT(9t6 zg`XhU%8RJ{8x{XP#lK(0M^*YNg`0)nwg=sRG`{9<AXr{aH(!X1Kbf4fV?zd+$yWxvxZ{Yw=8 z%M?DN@T(NQSFr7m_bHrI{BKbBO$uxKyhX+LDExMXA5i#Rf~~#Yqwsqbe@4Z>U&Vh= z#XqFtwf=R={yP4{D*Z>4{C`vUV}h-{wY>kgxL)b|DWzY7ivM>N->BlVO5ZOi+@$!M z6+Wu)S5$gk-WJ8*s<7rirs9t)d_v(ih1(U@@=vPx4uwxCd|Kg7h0iD)SGY^zgu+RM zQwn!0+@o+>;a-LN6wWBzuke7vg9;BRJgo4D!dZn!70xNF>r3Oj;vZ9ZT;U0Y3kpvv zJf*PCe_F-QC_Jn1oWk=8n=db3Z-x|h74{SkD;!Zcs&JLU)e3(@jUT%FWxRO*S++;n z9(0ai&m;c7ZVxflpTDK%=Nc9NJr%F@uT}ho$KluM>s0!ms`T|LUZ?+`hJPvg+x6?T z!vCSl_priqg6;mTL6yHz;Xf(<@vVlAKzEFTk-cOoL0D3;XZ{k3im7At@OQL;Rh6c zkHP~=UPj_^J;!$sDgHr)hvXireUA4riMQ?b(~3W<@D~&wRQe1l{1wIj4TZ-Q{=ULL zR`}_4|{caX)<=?L2 zPb>Ty!M1+y7i{hI0l{loJ@YBW|8>E(yx$jW?-Bn_rLXusm*3jET9vm>;nl)#_1P-e z>a$15J0RHBSCf)=o#MYm;oB9yTghux@;W8nmjBg)t^MC9*w)trO5TGCkE;GTAn|rQ z{iwou#Xl_Zw*P%b;V&xuH6`!c3V&bmPpbG|DmWfioaULUo6JqVU@lewV@zDm*y{5UC9g@PKdNx6 z!p9ZBj_Hdl!_xohtn=B;MBFACx@jk8EjcFHhn1f~~#dO8yqb zpHT5Bg=-c6Uct6J2L;>uZ&CcW2)6ZgtHO5(w)*y{{CgF?Tlj5$oeJNh@aq(QvtX;w z+f@8}1l#gGq~eDZ{&&UyWx=-ozAM<4Z&IcIwMzd-#UK1HE~!nwLa;6WI>A=o3swAO zf~`Icf^GTtDSTMr>lHqx@Y5B(P2uM#d`jV$D*Q@?U!!nR;WsP%Hih4%a7N*W6#l5f zpHw)j@Rt<+y29U4xS;UQ75)!}f3NVo!ofdrOR)91T;bIUM-{$6;fodCu5i7=S1Eiz z;cFCbRrnT#Z&mmXg*y~}k-{%m_#TB53co?&e^U4z3im1e0fj#-*!H(i3%2{`UbPAOwhZF!#{*xI9@_@6BC zwmi2Bw*BGx3cpmawfDV(ZTT~5ee!0BxBb6Yu+{$|g=dt0pHk_+r1-xp*yjH$!Pef+ zBWyogzVifIdt55mw$}#1)_&Isw)#Fzu&s{{mHy>|tv>%C*yjIE!B)SIsPvx^Z0-9M z6+a=^_RpURw)OQJ!PegMir;*LQ`+;{u)-DoR}J``I%@AL1zUYK2)6Ps7Hr4sT?${V z@U;p*MX;53yTa8R^F2Y+xGJ`h3{1Og$fU-@n%Tj5rsz; zex=B>{pmi1^NK$u@wR=vU9c@rM#V?f`e9j!?Pc#zR0_81iLi=aBiNqboTuW~E4)GR zSE>A~6#w}OS8J@&|M&-NDZ75UQ24F>+XP#EFIDlED_pPel?q?2@Ik>={$Yi$QT$I* z@i!lA;z!VL;HD%_-Sv%)7OKij^Zt8j;4Yrne{ zKC0xsP^CYu@Jkf#RQTlzpHcXg3da?`SFqK$Md|k%#ebh*+a8lD{`Cr{6n>Lnn_sKS z@0h~J6@H7PxB0(K$$N*2f47Q%ufiFHPpJIb6#jsc->%{Zl)RHFzC&UCy#ADmKdo@5 z!e4hXQsEBy69DSif}*zagjJGk*xYBu~>H0FA!aHN153dI2QgjjrV$Yv*~J zPt_oYO2zj-#POETe3@s`zVEVr1LAj!Z|VJl^>=HYhh#CU6nv|P_3NeFuXni|Rl;lM zcl|x63B;2;{r#*_&}a>!>102WtiL-p4ZWydH9ZBhksM8@?;=qj)bvjZy|Vy;aQZ#{ z)62N$_KKgZWbAqDYkAMaKtW~K^p}>P$3dsHqvn4V=of;1-QQE|;cuFee+cdN0Q~QT z+1~Wc9dZ}5xA522kM$D;*7wAh*uP)o+IIhAq_k^R5@OR?e@4=0CB4na##_JcM=U>- zW4{#Nap9}^GV|&0dwc_AD!Hb=R_OYBa6gA#?3$nQwE9^8#E0w&AZujl)*q1gpNU;+ zn9V$ZdZ0GF>UCTXtwP^}c7rU9=6AtQ?NaMK1KRaP=#TeZIxXk9$d@$H{nUQ14pF|d z<+A>G|6`Yb`h$-h-~BlD%ayR-r?Bv$HmdEE1fBK>tJZKkzL3@S`vL{^ci5Jqd(yXK zb_zdzn}ys(>^+L#e&0@_O_KzyJ>t^-roYDR@8wdzcDzp^KeBUc2glp}u}$CnAls|) zVfNFr?CbH)e*dnNoJfY#4M_T--!m@7-y{4v;p_jyWAmr}nd3)Qhzh~*1;!L#|N#BQGvy=iPFcNq4L!N2wOTpl~#H-bi# zRc~N=2g2m)@T2d{X*!K(G_T*Wp2s(P9%Ijmwx(FlG4POk9)8XEY5Ma)qi>#d3jcA0 z$-MzTYG0bZ5(Om5)o*0^mOq4YQM=ak6{x>@(3eM9PpV^bw!dorw;+znpy{MfF?w$a z`nyWd-&TVDcAi@ z==+ry!`Jk7&@U-M|0-zI&vm^Y!aPYfe5uHP3c}=Q-?bgS9{=8oc1Yvyf7f^6f|BnM z$`o!d+VeK}noi$KrSfWe73j4^=$DC{KZty?2|4OtG(PBhDfKsogSGk#qGwv9xxP z^lS(s+w1k@t>`B-CusW5&>!e*{5>M)Mv>DDpXy1|n@Z4+3cb{P-zDqap6_t^!0x*S z@vWWfr2Ip|*Yj2`-wncd70O4H;04@1X>UZ%wogXq)SD3MMXvij&fof__?v`3BK+3h zF8R`$j+`COuIynwZ(_1}9ekSCH2saB(LPbr{|Pjj$2I+(pixOR{Z*h9lk+^#x8gS> zdLBghT&Mf*FJRA0z%U~E+jd0#lz#Wz&*eHMLcRx|`dzQAyGO*YdKI_#wzo0A9S3d) zFSVmD3H?@t$vqkEj^;H@r_WN8d`&-Af_|g~{beXuF*z?RK~I2A_SE_Q60~A+ekSzQ zvQD$>eH!D5Kk@d(?V%qunosXcGrbjIax~7=_#yrimay#)?ehPyEQ_UEe_Zwh_q}7W z-Jg#Hq=$8u-}+OsU)Jjus%M%{;}5XBCy9Wsz|QCPF)PRVr)B^Ckl5pylI}C`sgIW0 z&$LSU+D{iBckMZ1sd#3!-w93hczXhLns+t*#u9Wo&m(@V&(A@>ya>Heg8og=i|P3*(6<%Q=LqHt%GWwu z&(`mJob#ATX%G6G$e!owbBYOM6vOXL@8otzI+1$-KkAp7o&k;c^*Uk(wCjt|gD4M4 z*8KMTOVjQ7f?hw`@3a?0&r2bboc(_$n*ITdD>VLT`K8ut51^kF)6=exw49F%zs@&{ zeCv_#=ikTnw0hnLe$vM}Tc6hNk^iTo)6xEl#wuN(_S{eNzYe@K{%HCM=tc9Krtbuu z#z#%3|Ce(F^yfav<*@VZZJ^N^oTfhqG+NJS2Qj`2zrvQ3v87x8*vl8kOEP2QC&fON zZhad+SHgbhgMr2aUA|9)PV=m$)Bh=kAPN8AL=*P6yg5|(5RhgIYg&A z)AV#6ra5UnJ3UcB5FdHF@`?-u`<&}+rNL42G4e!-36x2X6% zf)9(oSL9wL{wl?9PU~1rZv&m?B~5=;3Ht4z(|D`-+d;p$1ihGi`doQ2 zexeuSr_Y6NEh3-jG~Q}`PL+`Jf)ex>f?lj#L@%b#OG?mRT7uqLLO#)p$$uH>#q2}$ zV*K3aY^fC^Vk2U@axEy%02QR=dZ`t*oT?!{tNplxtC%3 zS)ZdwOYV&r52(FZ$J}Sf>Au~}KPc&L5ypQ&I?7)^SD8d1Xy2~s1<&4gg^>5&% zd^LTj1pQ+|f8W2d{&v6q6!Zs@vt*Fzw;@dK2Pkhb`VULc3nl2Uf-I^pE&mF%H`_STE+A~P=fwY3Hcuq`g5dS zZU-i}LhP*R^qhdotLdaqG5TrHV@3Gs9F5kOTFz@hr+%U7CqbuiL(`uG`h`X4t3fZO zPq+lV6EceNzpMoPL|7XyOl`8~2i`n5!3Hh%Ox^B`jZ~_56}N zusA*}=`CMM=AW96GGB|tPr<(gKTRJ8?IQg2|8agzCEG*OUk^I1OPjvQ@^8I@_XBr} z-zxMr@$V6OxA^Z9|6j%bck#a^{*T4~wfKQOEPuWD7mHsn{t@w?FaCSQ|CIRO6#qx! z|5E%@qTj6G%DtS=2JyFvUoU>O{GWxRf^Qc;E^=NV_&)K|;y)<(NO8oy4 zzoLQlXqEg1ME++FG5uTO|5W^0@s}JHJBhzl{1HiCD|oN?`^A5f_{YV+L;M$rf3Nu6 z;=fz`A@Q@~9~XVUDtKJ{pNRh-;{QqfrAJubwc@u)`ilhb7Jr}kPZIxG;&+JuQt`XQ ze}njWDR)8q`-T31__n|Okp@3Fi0uBZ5N9;uHVT}&Sk`rM^?s_}Z*__z#X=Xml?_SNgRo0_jQoiFiGJSDs{2wskw{&|S^rYx#XV6B6 zXNclj*wl~j+xZRii0>XhMVX^)fwBe47ARYwY=N=`$`&YF;D3Sz>^jb_2gyJ8XV;CE z&+6R#ZeHIeW_UkvYn|a)8+(J;8P2iPB9)i+042pWn(dWl<~BvTxek&6V*eq0w1+SQGTN@{}VU| zV)-FD>WAy||Kf?K;3mP^s|6uozKVlkPCq65qK0NSoZp9W=+6B0t(;$*aCR%K1#o`f z!X*YyKl@kl_uv4E`H$Ve`H`Grj^%#@=TOWa7yi~_%%E|3UW5ZV=Fba%jT|-2(G?lE zT6^^5qqBcFzo+0~0H=>VWwAZZa&n0Iw{5fhUBX|d83i-{ARZF1{6XPA`I!8_#X}?J zueycxk%Ov7rDFMikB9TjKP&ukPF8|{$rCOA>8CQIeX^hvuzf$dz~3eO9l|*BGS3fH z>x6i)P2VN_byB{;r*oi2@C03vgtPVGJ%h3B zkACDif7pIa*I4;=!f(@`!|Dgm;BUb6UTgW=g#VZ@4l3;4%K6*)Ic2Zt%NN_PL1CSL z($D{GH`@G9i2P<@>=jJqB9~L~;b*cwWLI(>f~y#sgy59mXxn4kkM()RGps(1!rv+c}Vs@iRrQ;K14Rh37D?_Nn;uT*gxtE58FcU@p1W>N6$$ z)*g}Pae9lJ6z)*CM{teG$3l`5Fi(H2&2L2b+bx6gt6-Oa`C-EH$2wRaTOV5$WP`F>=yuz~zNAF_!R$imRZGzjRTq7!eQepRQmX{U&oM0<|MzFPS^c3^g z3xA8kalx2SeAg{_RPc!xaQbS&X9U~&&IoRj_$kHj(km`-HvdM!R^K*-yA;kSoD*!z zH!IlcBWf&o+@8MnUfVuwM92n|&-@p@&+=Eli1kMUE9#iv>$m(F;kV_}iCRDin7{`u ze@^(@gt3;cKEPRf)xE;=ajEeW8AIOlaZ!Wkt2Rk*$A@vjgDlo0U%~;4n>ra=ecKgI z2woV!=n4j$#TkVgUcNZqXFlgo2f_jKwoh1nM}@yvcuu~8(^s=+5@#e}@!xSEDLC>f z4rI^Cw?+ge5bNGi=pY~UCe)ZDed+uXQH%7_L{{v&H z7jh@XuZ8?;jWIFXRQUAEAJ`bK2(LDEzj+whOlQm{sy(uU{C@M;kXnD0Dn^)+wc{M!~d76%%A{!h(X{x;!{3tK^OdU3J^ zMD&1p!<V$&1e80jA`8-~p6n?rXR?KmJ727O-{(feh7Uto%Eyh{~=XdjEHhqRZfdePoUd?Vu zZorH!@K2|i9}UiTg?BJ+5FCAgag*TVf-{26J2^fnIH_>7m*YuKa?OJ6_;gxuwI6Ft zw_rOzjS7yk7*lu`2bzUH_HM>x$6}7{`?M!p`?Lwa?cWK(M~fu}tp&e;`RolgeO~zc zM4!_Vhzp*5F9+;+p~aFN0_N4vwDO|wV}9Fzit~RRt8Q*jY2jb!e<}f&cU6bwZ+(#E z9}~XR-+6whTBWnF1A&0)yUV8Ulk`L<7tXLeDhIhn!L~mn6`l}Ws(wfg#w*OiT)rrM zG6c?!-#$3!5A)Nk%={VQx9#;DRzJx6zggff2!APipFygC*@{_}<+s0|^|9?GDY#lP z>KB|9eCh+tUnMvxxPi$g+s}b!iPzo<5CZ1rzqk3jA7uWRq_6!D2gU@O0S+VtC;o*4 zy&)}`F_+K9D8c2c`6%E&QJ3uhTSw5HP<*shEH86U=DKGpDdl&-v{@DLH+kOq?B> zQ84EhhZRIW;qR09mQQm2B)6F3{FZ}@({~DgDf>0zA|Hat$_YT1}-h5_pe5+s^KdIvDvy0`A3byh` zu+k*wGMkBhmIEgQpZ**N+L_Ia3#Kn9kQ*)do@o;7{)DkTZ>;+%<7$bo{Q}FAX~*Qg z$T%M%1pzH3}YNXl4Z0a55A9 zItN-LzFpw~!BZ07^Nq#%r)40Sllbm$a(rBHoKK%oZxA_U#*#0o~LyjM{`~}8I z_Ds!>InW~Q;q)ZqN#Q^KQ^vKz-}SS_c;M%Zt^P-+7}pE`nO|^VMC3L8lChO{@>h&c zN_<{$lcW#-ngjK6CAIl?j5ESNE%=n+6AyEIo1`C}UCiJ9d&Z}Qe_XJ&Z}Ja|`Ro64 zF@Hv|&A)YSaeP*ALgcsnk>dvh&kL>>T=!obZ~J@0pBP*FSIsjX7yennR^QB@Io`I1 z&c85jVb5eA;XsGY52Go$R>5(>*8Wq1E&nlx`K|t;0OO;Q-g=`pAbzv>EyCX|xF9$w zxFg8&qJp~wpAh{E^l3S`VTn&yEao2+Y};SU5{~a>&rG`^mCS!!aHn9aPunVvxAid~*y?vY%<*waKQ7qX zvtu>Kk4k*~8pc+?8NrPbUn^gJZW7!f*y^jl{9KUuA>r?duzah3{5;0iK68Ro!k?f| zOv2gnwEmnU7TW)Mj;A*z$<=ORT*J^D6FegI6WPr15#g_?W=vmHB{xK$N`M;>eDV^; zJ(AvB#@P0c7J2Yy$D_{e9B*;o4#sW1R6G~BoN<@ngkU?~4cBq}q{R2`WSkS+v5Rp+ z^qJi)Sjux|FXJkS@1a-Z;H*7su4HW6+n`{2bCX=pK8|k}JTD8KK9}QrujhDqA=I?o z!q|={<~U<{!QXUAeI+=#Io-zb(hP)(=fR?H;x>+tON@IvV>`Zu+Zo4%zy1!!HG)sv z$@sLS&kH`mWaCY7gw=OM@VM~z{9sfoBk$9`0jek6B z^|Ab;66m8(EW;(_6^`0Y#osu`G*BehuSBd2`9z!nk=WXWjZM z_S;31L4~8!i{sBITu`|A*Ngcx3VZ*tIKH)+^`YmiilOQ5Vmu-E)N2@z3T}8U<1T4W!}l?^^F{P^ zj9sypBwLsds$b9X{kA>6L9mps^G%Xo;%)kqg5z)Icu(Xf-^%#(#lrb1#!WJwH+`0| zo&VZD$GC$%lm7w-CZ&H5<{39jesf=AZ2N=NXG8)6-{Sc82ooao2{^d8=-2%l#&*5S zTxZu8E`3S^Zc@r`w{Qrgx45F_EABFUH4&fA}{n zxJKkhWnpOZPYaGqeAS;hy~Sz4Et21q%w$Ie*M-;+7I)I8|KTiub~)p?=;KG?kJYDH zD%zf>bgX24TYuxL8K01Z_CJ`of*a*Onpk_;|LkJpkE{3&h2w%xio7X>Ti0^_4HBOa zZ1YPCw)yp`_#qWPqVTA~dBKyC-(ZCEpA!3}*D*dV^&Q^8*tY-rjf`d4Y}%_BrzHLC zX2y1XkiUSj)qh;DU9X3>aJ4yZ@3$|Xn1g0f^ z-bRRT6t?5*WDU!+>wEi#nr%;YmvMZX z^rzGg#x{Ob@NrrHRm+2N+n;)`VE%E@w{tJ!L1`bgPhf2M`>$eb*XP56qryM-M2@%Z zrF|dcc9B2Y#CSoU!;G!`UiuU!+=4#WFkaAyK0yU%`TOb9Q*aCV2wu?V$sE6+&kc+h z^tq97LG;hu%-FW)0l{s4F35*Ig#>5U`}6ckDL7ky?wyNqMB!?|En<(3XET2{d$?f7 zfxIu(n7m+H-d6e~64P=3n(z#y0;J!8X5W565REzX`#oM8TGKalFm1>fMabh<$qA!&rtPGx#85J6?hIrw)tm1&e-NZEZF9s_yot>{LQBr zXC;5@HAx^V@wUJBewz7h{~Z@>^P3hdYMb6H^B-r=%#Cuu%5V4rsT||1 z=u`h?#YS;02`__r3v zcL;8g{DuTa1?L4@{l``MX~9SuBNEMj>w8T%! zdM+V2^dpY%l>+wtmT^JqPuPq;ALy3(r0ZekA6EQ!|7p`_r2RDgp80#EyyJ6>wfr98 zKm8|;xAJ=byqLdHzKzi@8BGbc^LceZ6cu?^pQB3OS&66)F~7B^a$o34W8+wnBIl(F3(*<>|JzOAp08Rlpg#=OGyzvYM#iMRjJu2!)9&vtc!^YWkW zZ2z1ToR;|ba@KF||Do=E;Hxal|MB}d=h->W{-3e4F<`&}2AhgH6?H1=s%dDHE2mCH zor*dYKCD`{3SrtZDw2_;q*$n^q^M|ARFtbQNoTF1PxJ{3i;9X24U11<{@&MpUDw$; z7fk5${k&ek@Ar9a=f1D|`g>pZ|NFV0=MlI^zKEyv`h~n+!RwduwEakOHrS4n=rNZ* zwOH!^R{64Ciz%OW;ixHR{p!EHlD}5Lx4g;`^@l1_{93{H3Zelg;pm#hf$I{uQ{Zg^ zZ)de`!+$ekm+TL&N^-%p%bQR3sC`E=-Ii0Caf#%o^>l_ce=E*pSgMI=#r~e?TiL=9 z-T&%5hvB8hKWn6Z?0pC0w+nvgT?}_g`)Ym=kNMy6{XUD$_S)P`6&qWL`R7MGVB-|_bodR!@iRB!DXI#wu zTE3F(+B71e!mEUSwxsV8So5=AV6AT*m$3Yl(03WQS75D=LjtS(+Dn;V;ZA`yKeOjB zUhP{g@D9<}dl}=M)Q8^78CLst3OrBfTd!dJR)Om}7)}ZQj4K&lC-@x#_X^x`732E^ z-YKx+=gJpdRtVmH;UzVf`CA0vX5f_qYxxP8>yXG^!E5=?yC&b>nrj(eCG;Bv?h?4} zx_tee0xuE#9)Z>0EuBoS_N*4TTjvbBauCVSN|wp>zl#@ z8j-Nqz*-;mi*IWG3i$$Djnv0=3)mmlpBa&-UxZWp>=))#t=6{{@&&nGL8RZy@;3;) zMPTh8_Xw=^sk}%d5^DKqvi09eMM$Xiq4|BRKO^ZK0&9M2-p}|@@U;S~z7+xw30{MR z60u+MSlFGP-^LFxto3Q3z?$Ddfp>_!+KF5+O#-(Gto5tQz)KChj$ZMKYm1Cm-5+8& zlKt3)y$tUW`SWfOdUb%L%eK8h!~2CZ=##OyEPk6AKTCvjt=Ok<&nK8);XZ-YpBsdJ zz3|V?us^gsGm{t|6#Q<16)(xz^Aiedc`LkCBNFx-c#FcHWP5iAyhGr$w4c47V*GB! z(<>`+iP_HG&hU1@Zx>k0fBG`U_X%Eu^&)@0;5EMs`}6g6E@ya17@I!Ba47sU?_gN@ zBUxB%lJeSoC*yVfz2k0%TR3oat2weu>PN|g4DS^Do<}&M?R(9m3~v^DI@dA0PvDNn z7~U`NoW~hfdpn+BSjXFr^$hP6`j#gd*8R9O8yMao^Rf1g!Y}r=Z(_Jn;3WeL&yn`9 zb}~Ck$0t$adc=Pg-uW!^>;6T@_w(WH&oSI9jQWM>PJ#7{&&>jBFi#@-rRO@MU)O*7 zh38h&-Wr9!=ZCCc_n#Z_qyk+Hg4bZBMkMSz1g!M>h2K7-U+)j7m_D(;Yb)Dh`-`PY zD536$YjU&6f0NPIBKQqIWBDb@D519JcFF$?NgojUto)xdzvfTHXnRuF_7~d}A)$_^ znrvZPo+?kjAeRwF{Zd@3#%ioco#Zu>RYzczt2P(pk+YfQ()OSUC8j-C*xF~?*MJI=(B{|L zx8xA?);~)Py_IL}TVv=g-r8s3L-AYvhk~uYY<{f%T4VnTQ$C8X`6Cx>tI&4}JWt@Q z0?!aQHI)Tv{}BC&`8$n1o1Z>|xAYqX-zD<)oAJ!%&-%l{{YIa~TiDj8L&3H_D1DRo zr`!0u$=IXtyqCrQGC$k&3d4PJey91L4DUDm9R}9&(qx6#O=Cm0DMG?pW8W5ox8vJZ z!Oz^!_UsXO`s)nq{!L%K2$rBvHj>jqzf$0pf^R&MBOLCd*FBnHZ7%-o$ z%x~+z+N=Cp9zEljJ`{NiYZ=!0#NP1?>-`AjUt{!Z|B%Tr{ak5JeUlm9AdDNQGo#)A zSA9)Wm|n|ksE%QsKeyB~yhh~jI)dShz|nMuJ5765d20U*jY!yQ;Ou-<(zX9mGWBoQ zF|1GJM~w{E8T}>7C}D}g+xFnXd!ts4C{G8P1gFV@SsK{95Aq@xA1O5KgZP1jRtS!Tm6>a=09cRtup%b z{DzWQ{uPRlQ0tE-E1W)-1P=7#PzIYxQPR| z{&Arf{yk4J+-~ISd`hj-`l_%kKMSu>LJ4;oSo<$cUZD{QZGE%$s6Um>)>kXf;%#{! z${x$#uZktK{;~eFu+~Qv(`Edx^;_`@&-xOVmyTy^HZWWx^GBtxG4i$vV$;`{zQeS) z7G;#MMEGkrF}>nD46JNj5?Leoej|UT$eZyr%d`EX`lEG#@%u#HtZy^isGoF^bj^Ry zAmjJTdZqh68P@WTewGif64;j4Y_V_iHm2WR!irY^mf;$Co=DVZ&pVC;&ryVg+J9~Q zJIl*R`%^M4Uxn4*3Tu1b_7CQ76Msxk@J4x$@sH+T`L+J8sbG4$AG}@&2P+xBOBp59 z{z#L%P5rX4?JsS8RDYTlm@me*s-E8Bkyf1B|~uaVy)@cywZPwRh8EyLD7eL~+jp7FXqm^m>Yo;4{SZkxhz zoA9p{Sl4H>r!sygS0mRro#76l?-zKpXUa$ADVy3~a|+Yf3Zhly ztGpS4mtw6S07$Go>N zy{>oFpeBhdeH-Jq2|TxjVa2Z!ct9B?Txax^90J~C__aULWIMiXF!EXq++$!_#=ALZ zvHc4Te(4DCwzHYu>a+P2>}vcP{H&EBqzbGQ3{=wW*Wg8YAD@ujSn&6KTq-u>2Z> zPZ@ZZ*wZTXS{`!^tZa%`SRR8`&^4R-)tSX`q~E}{KJGDi&95fg{?FQ@^}R>z4~;z= zRFH%#4goJQcnkL#`8y4~$?&%rSo61V4VRbgAFaJwUsTK*jYw$qS$d1t{!YbMSnI3e zZTVRLssEMU`fI%+Bup7t{h`SjiRhQ}HW~j$N+_YyYjT@LB-H*&lQR<8WBSXMhq*k4 z1fO2Zu-dm$;C+Jc|03gi1x`J}@Irw#P=1BCYD7ZYpEMf$dXsi|t-o#gT6znwF!Is{9yIWN12-K4w*F`{^tSx9{FKbvXW_L$$$9L|(tZ%|^bK_mJRQ4c_|8+H3utG3A{yu-$K{GkD8y z*W*^+JY%okZ`5S42w;LQRrQ9%;+8QA(~ujFU;Q>@S0r~Xzky&93Q z##BJHPsyx4TOJ#fP(tfpJ71e_{C}wOsWtRgo}C{y8GCGhu*b;TXW$aUZ`|W{90ITX zuadQEM8akR?=*0Sfma^FzCp8I?>F=t4Q%sg;SENf&4078&%$#JeyPzf+lpB#+x~UD zQGz`Z*(mkfwntmuR)4n&l2GZle2Lr3YO$~L%M5G%?-Drc&#&g=BZ0NO_6yu+^lAIo zWTjWw+S{xM3FjGjje%|bN*Vl81GgC1^4t0_QhBJDjT(`#*}&UP`CEUiHuNKvmy%ig ztbgqIVC(;S6(nJJ2w44<-pJ+OBlJrJZZQ05fj0=g#^9~L=NSB<;6w2r3UB3G`I*`%|A+RI*tbkwbpiZ4i9J_ZW^$ zeM<|z@ma?27Pxi`!!|!Ef4$(<-%a0Vdh73Ak+)0m`vum(*3apRkg(OjwFXxEcRt7Z z)gC3Y^~tW+Q%WeI=1-IRH6r0E3Fke}`t};U)*nsI7=LSdE51)7659M(d3y}KwO8Se zLAFofJ_D~cu!>cBh39HS!qC8WzNz!OL6JWs`Cs(|wr7pN%|i?;{k9)5Z0~mLhS|2GT&zMrr@d%kmyk-tLvujZdIz2a91tk2`_{yF0}nDXuud8>cHczJDTR*TJ_ zwZFv3v+aMbNLukLmT&!csP?nf$hYmm+S8V`oFjGrOYnx5*nTTd>x-(1%y^>pNAY$% zRQhcq-}*=U59OaNk$t~mdu;z|{okOB659E^mA}gPyT|Zbd7BO1(%bUL+P9PK?>78e zp521)XR<6+b2i42=>`tN`~+*C%2z7e9(olaq2j~cY{(W#9~5|rz-@nK{0{NojK45k zBlThSD-3IUpZ99MycvIGI1+g?1?~`etM)Qp%WJ*BI$mykEgwH9@D7o;?r-_{0fFa= z{JMSl_$Gnd1i$pZ^YJ3qDPCcWImz^Z?bz^Z?*z^XsOd(P-m`jmld44e_z?!Q>L zPctr|g;k%DDLgaaSht)%SRXR1`5O?pL>LDRe!IY$zdZu$e4wNtUtX=iDzCx7GYs4; zu*z=}SnZ!<=sOL*OJL1Uuc2RR;FShmW8n1$-elk{0;_#P2ESe42JvT@U`q8zN?^@z zoxmz@x`AgH{$|79X5dc4-(~Q923~34bpoqDHyL=)z}p2@`*#}rUIT|ord0Y81E&pK zE3n!#-QZ^!e2anG3_RDs-3IOxSj($l;2J5SGSncaDaHpZ~HgK<@Um~#9_f-a7Z{W=a-e%z40;@mv88|Ft z%T-^Efu{?s{7nXKG4$;Qul=ir^AL%-Inzd?AAbYaT%lho1bYP5`B7&P+qYWqQ8B}v z0(S|l^-Yqq!8*iZ?uSRKzWs}sf4WdKEM~Ya%fS&l|7kS%egj+m-3Gtb!0K<4?C8Bl zF}LmnwRhlQw$I8}xN#;=JXGH{q3;pFt!tTomQggz%)e(E{CWfH`N@?cZ;ipPGUxTS z2t4zPtlz#LXr;=)GVN2)HS#nl3vV^gLo0rbl$WkQmBG$W?DO2Kl~6(pt3Q-X>w`O;GrCq05{3q5 zFAgWJ8;<(ee&a7&e}@cxqoG%QYoxrI4Zg(i4+y+-9a}ct;5!Vw$J8I4Z>1h%ex+Y4 z{?PU_Ah70VtH2qN-zEG@jlKqf_X>TZz;$PE#uTq?3hx(r>rMVMN+_Y)qsg2Lg%14Q zRa)e&dx9w~f9Ei(kIgl(t$*r2C9Bhjgtk0XzmnPZWBILp-O4DT#anolq3;!VK=N<% zWBZ2{Bk;GZKe+!k34OcZ*Bkzg0!L3Wf31{9-BS!NH1z7f)q+?5ZTJ$?cM5&&28M0^ zt-QUaJ`EauJ*ItZG5GBUw)(maex`xv8rbsB6Z;w^KOI8fKSKU&{b4T*D+!0AKBoHC zj37_R=S-&Y_2*5ruB7>grq^mjLWO5bdsbM<^nG|6MPIWS|7IF^iTHotS2-trraXEL zoH6{h2G;KrsXbO-lcAqs;2Kk3WLQ#hPT_@LXM6h%{mv{UN3=d`@<`=VvWewcyso#E zzs=~={h^fb_X*(&fwer8zf&U;E-~^J8vF(W&oTb8?a8+PHHP2%!?vFmQ{JnLeM3gx zMq^K-!K=P)PqTmdcbG{1r6~vaUl9^odv!lIwdj-God6sASt$%hKdA-J8`wV`UfqM=C zTYjxS%2uKg2?q?k*1#68`3DuwOMdnxf5`ncoDqEAue`Ne)^E2DfrngEjJe9-5KTVO4}_O~9qe6+l}4}tGL1YXoSmACHSq__P4ufeH*vj0T?N7CTQTtYFM8ZSaTO<9GvJDxy(ZKymDB*SkuMoIzKl`^=;P7>Zt-e~3 zU;D3vK=hL7|@7>`wjtbGvklG4{eLIhm7#20@l}H&d*khzD-kEp1h_%n`^7TSumYPF}!E%}OZYK0|NqwftKRzqNn0sgPFQ zOv7*Gbr^eA8+u#5T?RkHz}9}7e`~+e_npAySz_dM8~c>LU+6WzYewMTV&o~mCM&#N z__hB~ysck@ijYwCYjTOHFFlGFf!_9i2h7h6zr1!Zo7!#4|A6t$wAY1#jzz^-o$2k#L`Zm4Btkv*jBadu{uOjJ!GnTY8JPu(fxl zk*EIBK_O{XBcN;QYLA+cGW6;{#an-MC_+MQ-ke^BK1ET3?ml;?s&S_G}h;n%~_c$hYNbp(u>{YQ={$6R{TSfk%_^rOf&Cjkw*gxljgXd4}9U1|z_Ox7haQ>0}yYVpT zRlfD#^ov-Z%AaB2nFdyV>)%ij5?Wa4TST71%4WwS#kUK+9iNns+HK5@TQOG7mvjrg z`djHVWcq2)*rZ^XXr_Q|b2{7X1Y)l|IwXOv z@=MHkuVq2=n3y~38%jUtN~Ui&^UV?iD?gWC@)_mda24~9)E;Jgee1IHDrb*IB@tqhRI$a}N?q;EPoVi)nQ* zAV|P2zB8BrWb4&PccohdLv@cHF94?F5n?+MhwcXjIg8kS8p%u!fmc}m#}95d<*0-q~~d3I}O$- z(z6u@wPZjW(hFY%MIQ3lV+r>gBtzzIkx3JbL6S+8=>-ycHA*EH8OCFa?a(8bk6LYBE6~*3brlE7HO9tJHRDH zx?_z-1a=zHbp1YM-F8g&JSJWO24smk2gns?`2`v66gW zt`x}@>}sLU7A(>Gk|nZi(JEQCXo-x=c8y5Lz*Jwh=pzfU(#^x7bO|ZN zyH>>J6z|ZYZ1Lt|eese~Dc_+*+45DgZ21ydwt#c7RKSEZCG5&}W0w_Z&^}in}#FGhXp9;`G_9 z$i*wZs_~k5#di>=-YIHCZxnmS)%7lR7vy=Bqv$)JpTmD_W^i{2v{ur=$LXIv3IwAr zMnCy8{Ktv{>bQbRIz&gf^jUA4>SD#7!^P&KG)CQopTFThc3a{NFXylLCECi9a73sL zkt$+}fPWkq(*NpN6)v@7jdL|YRB`OtrS1+k9w>rR-+s`eDr?ahvNahH1Vqs*d)W|e!E|2&YT`c}A>y4>)-kn)(#VwQ9Ccyt1zNdx~-cF212 zW6f9oiD(eG*jEvz_CAE4SacABAG&#}`=Dsw3etKMNk9hwxFB$;c$C#v@dU7^PrL#l^s$yl;KI)+bD#Gb!)lvFu7Gfw9!<1d+NmCZ=iKY)&a zr>Zipii$Xj|0W151!lEI{3H6e&%Pa`Qt#T46& zpVV&rM8R;OSIm9JxsQ8k@rrC&NAby>NK4|s=t3&rY$fr9M$(=qY5#iZAkuDwnWzJk zKaU)cgu=g&{Co+c|AMlIfI47OFlRqNYBb)Qh+4mcPh<31L*MYn%tMSmqE#@FYRzz| zd9n0$!m(5c1rw?I(-r$Eep1^h8U$J0E;R;Jl%iG4rA~wiltK8S>ES*>t^_GA@A@)u7+F8Dps{0%=!^ZW|9iR>W;?X8s{Ba7$-$GsZ=YdK6^cv?H$qWG89O4xxW76%#A}>!M-N!L%d;@XP9oAt4 zL7ZAc&=9Yn6Ic(CiVX5t^Lh(1iZb=tjC~ro*v&l|m)dgy=k5JSsk#fJz{$w<%K$3K zAC!trR%}Q&I6?&S8lgH;(umeZ*VIucqbOPTc{p_EgPb<$bWHD)Zo(VTEe&_jyECuh zQdOjD!Ym9(Ba-=!Vw&$L+=R2x<$;oF0re}=Pf)KCq+A^dln>^*`h92{!___8XQio2 z2=5Q6y`-rd2_Fc_m+23Fuf&CGe4M+h@WIe`S3b9;P(fFH1RW!ZMQ7~Ak|@MUEQ#onp9Z$?NKfF^G@YB~JS2 z^KgGyzXn9ZeZW{!UicSM?doZrX;4KMGHM-pGv8kr!3dZ3BgK1SWUplGJu&a2Nhuhe zj3!NgrQW4NZz=cfp__EhI@C4{pQzrU`xjLf{3APV4>ETm3UmR1a?!#!r|X`ELiYV& zyb-a*1f3Os6*NKkP5e@~RY3ipq!zsP!3K)|$!kEeAI;7jnCN1k1#cpJ7b7s4!NiSv zCMvJs<~NX>j4f-};vtfs#G;AT7s1T}b<*e-+)R?AR{RvkmSu)O5CykVTG1I0*&*Ul zon7#;3B{1uj%YDfL~{``(Y!pOV}a}tq8?A=e9mYl<|9kbB+l69$^5m#+KiN7{%O$u zIGK2tnD4y#cO~FxWG3?FE0pr)-$kaD42c+sL^hW9+*dpW*;wVB7VZRvS56UD4zf}n zg3BI3{1-q(u8i7__eSK(iI%Pu55XxOf-9(qK0#Ut#nqsKNVV&5zXA}tpTl6S6I+p? zIcT|5Cw7UQG#BSO;fwR#c!zhq2wmF`*C$;tbv&>^cmqD95lnrI9|r63j7zysONWcfvr-5mHA&OvM-Q6S+~Y@^Yk-W6%$c3My&U+rf}4ClnacWQ!ls z%L$#EQbj!{p(F3`*f&2>@2YPC(Vh5@KNo}#`;dwl*Jy6lqq#UobK%xBUk{m4B~^tV zd(D+p6@y$W1-%wHx7lNk%~N&1q=+3`4zLM7@fps)(CQA^E7%`>2qNC5ae6iu5#Cl0E9ah4j#^$njANETFxc z1;Fghu~eGDHAlGEhd~n0#Rc>Gkn|GSK4BS2M9!&i3?|&kP|wRAM^M2M{Lt*tM|k!J zU-7t+;G#=D0GbbAB%v$136Ur)r20d=p(~^rx{tzcnq7vj1f98-@?Pko!$g#Wws&(O zPDh+`Fy#&`OmCWYl#72BOMTSP@aRFjtpa$=+X~Ld*J8hf|KMV3If-wgiI9oOoroaQ zX9I=BSo9$Pg_whZr>OWy%s5sd0ZuEy4jD+k&yb4~3iKh(FX{Oes0fez%xE``SfhA2 z3BxqXj zE(n6?{m@)U&x8e(+1X&di|j|%gW~f5eTyL%Clna6QWHC%>;4%~++W!zJg@w~5`1fK+%bd}RQf1LoSYau2{g^)ZG$*zoBK>04j zKO;0{Tno#psv9tlRrDY^6%~^^=oD+@imyQmJur@@H0lWf5?aAn*wt4Be1l=K!JMft#0!L7B}4{o4_3TmyyO)B)+yK<_B{~_&RbZ(sv7Q7Q; zYGTW4{0MR})zfUPb?)oFag0T4)5h@(G!IGR=#|FN3K85m{Cb2on&STi-8g(&G8T*d z8E_E#xlSyGWeQ!x%iDt80I^<6q=E5mh5U5~CAxT$mbV-6A5dNP3)li#z~PW6l~;^Dz}eZ@Z6cYU4$?Ud}JtC$PE_`v@1KqkM3 zGrTbouY*TEhX44|;5CQ~vi2ot&FCjDS}uMl&$VfMa7D)Atr zv|AS655g-jip0;3=WAl(v*S$6Lsw~bmx;E;3S?XCK;x3sUmWA>Sv*P8GVdEd0@C(W zfoxA5gpoFbt`x>>nS!xEwx_cBPd))bSK}YuTaqU2)NZLJkT1v&)Yb9MUV-fFCCO;Y zB*@CY24bQJaB ztE7@+P;kLNPpAaNxg>Ga*FdpOkm$OBJfjlh)=!X}UTSdhx5vu}NmGsVc^26dc;x$# zUKLxuU=c!6@CY)xAy4}#Xiu$2vcIaSM(Tug^ph|@2s4<{A?uv$Ulma&el8{~+Io+! za_$AB($J}ay2=vau#pdAE_by&+Zo&m6w z4s33F4?CAiGj>Bb!9^XQg1(C`*gQl$FE~E!%PKV+x!-}`&NTUpgE;W#o1Opb|{@ZdGaAwwk&4Nch zT+lof2Fw!!7JCCI_qfU;3=8|DWHU$zrk2sVaWaxC(&9amK{uCBTrOK_z?lN3rf5{cV(i)Zo#S$HT6* z`@|2^eXVUmjr|(c+IQvG+IRU{yPC`7Joe@Jq$l!e@nwUKYse*3P!Yy+D)lS4)Gs5# zqMtxPE7F6@i{R`legn`~@0SsAQ7>~W;0k(GmSc!Gu7=B(FA$FPi zuth!aGkt6&xb8S#_diZ`-ycG}Fu(3&O-1?!a`oL6OXaFrd)ONi*nVS$Lf3zc96u&j-XD1B$iSJ_KOKBkz8>Gyx%D4 z86*Vfr>XL{Be`OBj>&Yq5{0>O@imCh=#wKWr}CfgEB^v0&n19R?9*-%7AhDNYjZ7{ zOBA#{0m}fMM_>@NQ7Pv%6m)O25|8~Cdhzrw{LHCF(u`tyy*6qCg9KZAq`n_wUIPn}or52S#M6_g-%)M0xCpwDKM738LP1(eeW3NpVK z?c7WZX0dBSO3U(1BF{|vu6IbR7)0qR{A|L1?5)Mr5d@XgtbHEd1m$Xe2o@}^#;*j= z1Y!v_1DlY7$~`ggE~2;aQ&pE7gu~*!iy$JK;&b3dh15i1om9f}@Kf_m@fN*;SG+ZL z+=r0ZfCT)(bzcQ^9z(8%P~;n`gtT$yvwu0tCC^DU#4|D(G5f-T6}N)2=9BnAkxmAj zz1dMCH#=(SW(U3D1ar1py4m6Lk&4&S%?=+=$7|_k$2h=pvx8=aehE;cx1eYtLawi+ zCBtt3&`iEY76_HpBC^;mXv~C#h@ax8MsFciAf0RAEf_JV25Q>?CG9ayS&wQF2d?0{ zAU7^Mv~;tfCV?i1oq%DDWGcOnf^v5gc)j*0UQ1KoqbWx;iN(~3*Nb($qVRK_ooK2a z#mmyEcR-46qfet8)o;fSN^%s+JW3wHyXoT|K~mteIF1&8MHG$Pk-k#^s*V>cNAPlS z1nQ%X^z~Wu&wazSA@>cmwI9Vged|D1@n)d=UlUZv+d1AbI?3}ioUPZvZ3|~H2%=Nl#`*CVZt@# zoS{C^+6XRQTl(#&tz)snl8X!G*VST|4FADrV_hJ|8L%jE-8FPl?NhO1XA zQ3+Z^eF~|*k@&q#T?ZylM^&RXlH3eDEm9M-4*W3_C&($^M|Gy-5o}bHT~2-#G`cE0 zh!|QG;&=k-OiFOx+)x2NDYdnV^|d?XjNE=ED#s13dt&NP=)M7E^1=&H_&y5 zA!|T_x{Hrmy$9?t@S5E22tmu zyAxRju0S@XlJA2Y-yI!fzasGvc)h2&AETl#Xn4$bJ&BuOm1c>``95|QZ&en^R%L>= zDsN!dxdPdYEczf4ifIuQxsp=aalG48xH4BtTZ+#zPmA|xGxATwigKaN z$iE@om!ND$dYkDLySy10$!27MY(^$%GcpN&Y)1M@HAVd>n~@3HjI2T;HX{?X^z^bq zR2OVU`ncbWq%Fn**j3bZ{4{tHLa#E`OR3Ue{h1`!9%Jw1&&2BH!tH!e1i>CspAh%XT$gV5@ z9pvi1Vj+(cg}kpgiuV;usTRM*`>T}p72i$K$W@+8?6}ghuXs6N-B+xl>Qj9qQ6$`G z9zx2=Cf!ghr&_%jb;)0+fG~7_puq%&(T&fif!ZUT(FW;^I)ULPw*^$O{)zQ2{+}c> zLk5Mffh7oo@j!*8_zlA`_>ID;_)Ue!;WrbWj^F0+9Q^hM;RO`L!Ye5V!}%0M;fE<(hKnQ?g`bjGDqJbCO!%P0n!~3g1~E@- zG!_nOGz@>P(J1_#MpNNmHJYIrBw61JPn2pD(#B@vv9LZH55pPRcoe=R8&8Gr%EmL{ zrP+9M*qMz(=m&;@Sa|C&5QfW!fhb%x45Y#@4g;C+E5kr@_}yUuvWIe@SomTN6o!Ay zfuiv5IZ!GLQNeuv@skNFa-imLLJri6M*I${jM!y91>q?UaqdHD{D|#3quv#K=sVnz z(k|=Xoq8yW|5%NOwTQxql>7Obg{v;hK z0D|H!u?&=NA;DxwE^%QKe<3E0# z7b{X2NYV{UL6DWS7B7;!;>8o7v!ovqBUkFmXf6C!q@a{bgECqN(|VuEyh!efhosQY z3W^9#_Z&5)oaO`s$X)U6N{k9()p8 zW@i==8}`DRUPcl?a&`snXTF*P<7B(9y=NAY{t6u8rz0$&CUDdlu&Y_hm?}p2=p<}j zH(i9NOScGO4FK$TxBm@dqwvuzqvAc|x=EF!nmK0)=W6EcaCOAF&U4<4G{w(EJbY{l z_S|PZiYUTXnu}J`Tv%ynltXDwB%)h{=+g+@=((aL1K_*($NjMdn>p&F@Bpoj#KI%>Bc%hF+@n{i<7XRu<7eAF!3U9y>f*O1U}RA2oB6Gt&Bgu!zM}XvVct&6Lb07GAcY!ZLKG2^G$I3E4K00Dv_`Z7c!GZJ z{d1Ko`U7NKHy!0OSE>hudG$n+n9Dk2e(zI$H8GG_ZKjO+UwOJRXDtncfzZ0 z$A8JfFu$HK@xqXbgG_mzl*!&->L+4Cl9oZg0)QF<&F>Lmg&Pt^7>#8}WnsfRk_p+7 zOwx|zw?Z!`Njs9SLnfA`N!pS0@pL>%JCf(YJF+85i}{4?BxXw+Y1Q0EkEV)y9SWMR zntuF9S9IzPkTE7r)gp?Y&hD<}?%DJFCwxd(&4c(6p5uteji%pgMPwyU$jWI#Mz1-+ zaYmYukvuE}$G$=gxT<+qr1Hm9j(h#VPgvDz7tkIA!lqHm3{3(XvCRSUUH76q!&;(>GHrgQNcVwg{B8DYeu6)hr120 z5v`M2fE!&7Lfsf0M_CmWmAoT5f>Ww_C0%pUK_rijd&!l{sSjd5osm4itNOmh3ArO> zF#j3kXd*qUM-4Lg0d+%}0oX}j6$!ScC%uID5dJeCgsFRw82?yi5@2^T;K_}sV@nX3 zg4;WvK$xK=N*5C2XHKm{+PMIy&|Rbpi3KOuabL*Q%A>zDXNZ5aV(PCE=B5>`8a0Wk z#;e5eD5T~h<1dy?dOr|PBAu?xG@n z%5){d`s-mwjUZcrM5&L5!Lx2fIt*@2d>WCde}YdMgsKr#!7nl(>Yz0V5syV!zroNH ze>^spkS`#ei0M7CQ?CYPqXFb+I9vu{TBe!onu|>`#-`~^%YYRE_MWb7SF{63r;}NGW-W0Et*Rr zW6R4CeF9Om#&@g-0;k*_0&qM2vB!P}Z zQjXaVf&IH;cAuC%4~)E)^K-^qHew@a0NMPHX}B*GgxkU5!e8Sz2>*iLSolx;#=|RD z1lSZx9tV3|!CCc0%eGS^z=Izo)o<`sgCrIN$Qbc1hXGUA~PzLFNG|0e!}yF+3A*LBq@m3D*MnRTGCH@*sF@GFjii8AE(o~r=(!B2LV zWk4z>Lx>2M9#-T5qlkKi-+CL0Xux=xq7e>;Af!tU99Gb(RJ+(wgsj9_3|wj1IOLvo z1Zi>>vMRT71oP=mWp!-%HNKa;J0ynz?4)DPa4T6ndPr&$b!%97U3cSYmaHDp&#>qi-FuAB5*k|92J@$#prQ)8@|j&eGa524{&J6X z{{}MTF4SKsin~y|Sy#5%^ZdmvLDv}i&122_GH5527v2-Lk6s)d1ms~ z8P4s-f5E00*$_meo#w!WR2wC=$Uk=hfRO(q9$p?5%hPoc%~NQCUqZ`Y$`u|n2pD#9M<9ML)V3NUBK!N0dm7jIQCw zH;d)*&1j6Eqh*ND^7d`Uzx0wxE42irg3~K)S~Aif1f$ zV>LPZ)(0}x+(b1f8bRbZ5~;WJ#>z8-ls0|}+>Oq00*{4bxtkr!i@0&r63&HV=$hCk zU34Q7gZWGFo1!zXzPUXRqZXV=6*)JFX4}+OqF}A>pfasK6|;W-UW4GV80B;F3y|3h zOz?QD&d<<)kA%9y<;{0QAiX1^K7BH6zYZ8va3H1rQIu>G&_#{vKGjWcOaS8Gub`N zhJGQUL3$D<*T}o*;3wY-u|y~<#97cqEcs~q#VH~xB6W}9%5l`sa zD#>M(Q0p!SYBUbQ&@`k`I(KT5JPjFcxhp^y+cT@w#R_m#Id)$!wpw%PgZY02#nGq2 zm)&t3b16qaOUnYxMUJJ(S9g3J#Nq`@F5wsY!TaN{At6br)Quzw`mGv@c(5$53MTlEn|>C%W-rOah}qTG*TclcH4N0FuX2lqAHS1616N9Uc`gB{J6M-Cjnveg$cCl@a2<1Spmz&fol8CxtRs|IR?(@w%|QX0nF7Kif9l`S8HdKh@zd+)>P7&Hi#`a5| zqF+MEW#|CjRMC1c<|tw?U(q)~;uTSZ6-AdLlejoXUxbp_nTJp`4Svm0#9+Rn6F}k> zQN%0Cc|IuG4SjUphI~y_g|y+fg^RgxE09OPE5pwRMgMx~AoO%Dm)1y;p5CcMwa7}_ z4ZR+#r+0pytBbx!l(W8ywvz|23{`_%BrC4uapEx->3N=CN!H+~6EpbvJ6I!m{>VR( zxMNs24e3z>{D}d%=niD6xZT@T;!0`Y{t8l1>B~xKNZ-PMi)2Vo{uv3B9~a48iEN5j zLulFlr(;8r92+X6V?*jos${<-zv3j6f5I`6V?%{>Y^VVa^FBIz6b)ce(TPY93(Nem zp>|J!$NK!SA>Qu>j~p8+q}@)M3_wUB9UHoWG8e@Q>DbUakxs{kA~`nXZT6}iT>Eo5gx#kiT>D7D?={QV?$a=lB-||?J9VyDV4ShB0V;Aa5CZ{GF)=Q zykZf04SJHcFuqULv7@Lvcpk}_sFc>RToX%a9qa2&R7&euA5E3gI@S}^xJuHg>(~@` zaB{o9l6s8~kN1k*aF-i>YY{qHv@)&2N!nZhXl1-Urc4#{+5Kk~C98KTT_iJTx|}kj96VTy+cSc;F3E&**puWyIGl zc{(lIZim^eBHyE+I`R?8NS-q*q_K%-*+4lt4pB-|hVy*)Qc9aCTrjSRyh2wcMRW#P zF&={ey&I@7Ui0A+j~_){?;IrmyJbwh@)v*RWwEzpJzg$>| zWLLQcupV|x@hP`-&MCLYL4+*PDYtJ?6x05%5+AObn~{hkE6)SgQ*L9Z490FFii8`- z6LjAs^psmA_5RsYZck%^7`jptvd^y@cYK)kNk}&uAG@w_C+-DwIJT#3>(_+ zB1^)rKri*g;Y*0nY5MR<>HJJ2d)W`QCQT_OUx>%4eS=#)XM3m2T@;$bc9`3A*AGN1-Jxl&s+OWM95lK zUem$FR|g=NKcNlxOR3Nnd=d&^8*j!e_%fhjF$ZKCozUzHVLc869e}H%1puXD9bJ{J zIOx>Uvh)rQu*$Pgsig>3P(9usr_u;~ z7JQo^5?U#YCVHR=2LEa)`RU#p-#^E>|2!A$VEH7* zb}+97KKu@p^lb3YkB>odP%$G)Y~!}W>t6MIue$Qx!0pbdcCin}N0+5Q=|5XOtcvXP z1qV^tosgP&QVD9w>;((ojgj>|*r0lAK)RsXn=PQ1r5hnR^V{@pFk^ljqF3|%MitF( zA4k2yuvbOH-YbX?&u`N*zs;v#gZc}4mwswHOZzTLo;>cQU5aW%SQ!6P{G>qxkwI33 zOD{Z|Q?&DPX_?bziw?XeX7EEZ*tE=Gs~!SS4!cD-&0y2ADOGzZTG{wNdAlaKB+XM7 zAqB(}lQc!`X8=PN0II>%TKo()Ei>3`idREun!#ogXJO5Sf81`ShMcAH5n4TjuRk(O^3-wbyX0&iCXB*4gFyC{G8c4^9R_Wi)&pD~~OdJCB2fj?!F zmTl6o69qt?}f)>tFOQhJw#vq7{JD}F>0L{kd`2v1$zJf0`1~PQ?mn_+&`L@7Kjs( zzxb61)I+8B|Cx(FTVZJ~Crhp>dUncJ-iVt*9EHb^dblaXp!1&u&wuTGHW&T%1^FMk4u&?d@5t+2i zfwL_s+y^}Qp^BCz319o;{`Hbl06Ci*3EtfBZ#?)BQKpi#YjLmF0DJuAMp`yEyo_Ip zJR)Q3;p4PS-G>`Oo(&5QVgt{^C+E$>(=t6iNLFZie2R$4c1Sdm$UJ;F(^PhgsOv&` z(LDTklo!0^-?&CrQQ4(s!haBZt)i-IaZjN_(pmIWUeWmXgvYMIvrn-dO@W*CnfF0E zjjB_q3g~9f6mpbq_Dtpd*(p>>|4a=PSN-FpW!M^G&g({eO_tuqk6VaSt!_9%54sQw zCTtI#g{mCoR6?$#7onZdHI=uIkEE;|MK8?5taCcgPN(wrapGaj0Wjrz2smH9&R65# zk{2U4ZJM|`L(anG|;h*MwIlZeGu6Oxt0yz1e5prZBv+?Hh3Jm+q~e^w&p zeZ<_cWBh3DU$u3* zkUAO+?W4HXx>1c_s-&0EpW&;_?|hXR)xoLz7x4>7ejiXZt&PPo`6ea54>IFhknK@O z7ClW-sAN0{;bP; zkxDM7MDKHY4Sv??>w)!7r?Z&e>-6TCtlM`0?cGjyw=%f2jC>`M*&bT8_|p$aq7-p^2vEv*Ijpn3ld=tWg;Xj zQ*zv;+znjryU0EBpId-K>QCZjOy%L8%?Da4>Y6umG9x~yViKhKf zT>Ci6JcSAzREKq_Ma^3Pqf&QlMOgcJXd|TWFo1jlFd@Hzu$CT7B4q5F0oCf}xrB_C zx8ja##~slEKx12h{s<+b(l4Zhek3g6gpN-jtZ9Kwvm{g8anwRq&*fa_+i##cU$X#B zVXF{v|4|8|&QBm&t~6eu>SpK22`a-1RNVphZ6L_H7ZBp|3zpQZQz6U-(Mok1@#(VL6KLVz|jS9O(_@@(pTdWKZ>D2xh>GZJ9FA=8SKo_}7 zh^R{{`&Dcdtmr8B!+IKb``lhZQ?4Q72%x*}^2RR9Jm8H@g;%qgbWSdqU;ZD?U5SH$ z!AY@~FkggSGrgH7v6+-vZ|6zg&IvPwc?;y@VXH=3$KlOVu~(6&^q*klEFr+~=Ymsw zCT8yUnONQ&Wk56z#?BEUa=T=r^(fB7WYmzkY9Yxum1HdMI3DRP-3JMsf^8)1X&&44 zFw)br46@>+cp>HgVx;3GC0m8B0RSrOVp@&{K~s#Tr*sv_x;8=i^^F9_Cuv>lCzm)r zN$Xl4uZbsUT}$PNb#210Yas;d+64K7e1UcC#FMXa&MR5!`^rpd9jWmT7^Nbr*SMC% zK|B8gD1@?K;AZVp!3u72yLh;MiihiF^c(1VH!h-=m_@;t1DZmO>gUwoFFG4W2EoRF zax!X&Q+F+*CoQ7jYuryaVhh{%-mRdGf)_Zoi&NJw=5g@{UT*D&Ct>UWCir1+GP=Nx zi|<83b>Xso82Rbg!xsV8{szNiW)$>&JRm2}D!^TA(DcTbCIxWZ6#R67rn0h}NKR*x zB|>rzfoMY)BCPF$F9wM8E^hXp1&pzwKlT`TsTQ9cb9)GVPf$amo(7Dt`KKfU^O)EU zMCeMN5yQ{5>|Cl|!4HBaKO~$7gsUA-W>@-T8a|dRZbm|N5r#F`y%Og-W&@ry7t)BT zq&Zd|^JKVJo=5gcuNb4kU5HE~>svuz`R*V?^eY#W{fO*YbRQyaGTGlPR#DTeydW6Q zf_+Yi@HvoxWA$Rn6Rz}FRF}2FL#-Xf(TvV#SC9c>^5zWA(&tFTo%ou>l=&vLqBLHJ zb!hDt6j?82`-eeo?J=0V5-|GM!cu5lySNnz@Mim^2*<7m=2}FkcfAf_&7JT_({=1o zvc05QEzSn_vu%{xgHBR>NEmOR#KAiM5GGQys<9Cm&Q;pkktZy|y7KaEf> z?~aYv44V-Q1`Szvu(x*SqF)G38AgjcQ@I2igW1CbwU;0_JIF&Di4QZ-n~{LXwnd*n zr1n)53ZYBz$be5RCfkXwnr-wIuU%TTuXtY$>OrvQ=HZG*CqK}5k4JLvBB^-&zT)AB zDqbyBJffnC=c%OLWvR5@Euwmt?t^oBL?(AD{MOyoQkLC5CuwZy4yT`y?8-oBH)adOw87)Nu=Im=1l_7$Dwk5~OSY!M>;CJtR zIhX+alpg%#QjlD<%v8Tr6JEdk5gj5egMz!v`sFU^5aFYo^~*}%ou%hM&<>SDHs2@f zmz#jViiDOfei3sk;B*RgDVI8->z51=xztAi9=%KC&VpQedeg68uqEV1*LLC;7ff<3 z9)ntO79usXFqKd&xn0*USJ4!srUJ8&jPO#0(e=wwNQb9a1}D=rg|1OuKD*iV%YD3l z=|?UaMSw4&`(^!dA7$^Gs85Qe?!&KN-iun2o9NdsKZc&%L|MPAW*_VN<^JLI%Pdja z2C=~dIqR1pupF>{aTwS~TE8>`H{$xG1IUA}U;2SK*!rbY>>^(uuznc;Q_lM30UiPO zaH_6f*6{k}ofwrfD5PKw)-NA~H!nlNxLe@7R-_gli586Y%l&};`sHy%#`Xg94WELQ z$|gi=KLDonNX@Qa=)B6fstl(V9(fv?;J!su0gqdR84{7|`sH@OeEIcD=6bGT?MMqA z4jREI>z5s1I&l4xLWz#NerX2wP1Y}6K##C~Sq+qTyj{OEi-92w^qxO({gT=tG#Sdo zf_AJj{KWDOzeR;r1>cXnBj9OL!51)p#%Yndg{V_g6Yyu`rn!1L_VzeZ&|szo^>={g zhXTdw?*_DoAy+_zVD}Fw`AJ(8B)$aAJekAWI*9kp2pUKn2+%Muz8r;k6@I>r|MH=} zDab%Ey~(hA=zhfC6bJU~N%8_YNnS=q&`I(H-Hq`UVU1cxne1ie9B@CF z%(LQ@8p)Y>aqUCbVgZuT0pM47$lCj5_^cUxTdBtIYyy7rR zlXwDnl7-tl3A#<^72};bTao@#OuKNa=g4;<4-ezN_;T8-&GKfbIz#$Hu(9Y#{JaH* zByZ0zY5(SdOXfmwOugw7PuKHEi;=};{OR#zZHiwz?XE&{Z_KlR75%;zw&%T}= zT(Zqq*kNAZ=#c6iksR4{c~{}R$ac~BSK_NB_^%vAT*I=mneZNCY>Cf9^k+99cY!3&_3bL9)($*6ArV@lqYH+JGhdibxd^qX(P^?^D?y7%W@SzYN3qBr?e zSMpsZU-PQd-XD-ipEfGej~P|pH7tzo0DGy_l^4^;jjHb=GmGigmqbZc^<5;Yw1iG{ zM%AlCE+qdH4k?f~l9o}u^CuAav7drDm!fA3!n%K;nN6kJ4WT=VJ|SICM=3Jc+`&kH zL_M9NIGzDlKp3!;(YA;eRY2(66hF@RT@xBW3gmsYb(cee=)jOgVs2%b7~x68id+qN zJ8c=+Gpr#u#cS~9hV)fEWa6!J;J@2Ky33N~^CnUmlubcF(lL(AyP+dPkegVDTP}~` ze`K!z$m1dixsk&kb<@%OhvzGln^vUIaLl>6iXrwFjk8oD!@0>#$(_7F!?vq=yubYb z4NtO6E|Bk91ysfI6=+J{z|SvoPSVG?evAL28O6)dKm$KZAa>uOAe9Bes*7Vrp08mV zQlNpI-o(IgsnP~wzSA|uE8zV_v;r5STlW5K#zjj|SAUKF(y`P?f@0cIFCFV|_PrSn zPlJNVPY{Y;7ZGKz42vjx#fwP3av2sG_^5W-i!nci^#uxmC?ndmMBLA351;bQ^ikgE zCCz*i;EpTA#N=1=v#5E%PqEq# z4B%7ob1VKUPKV z?a!d#snf>s4_8kI^D~pYMQG1-{{HO@W#0ra+|y%Z9)1&mmk`LSKJg}i5y*&#&+PMS zknZe2u2TzD8tC7m6nSz1fJbH-l>E6W`fN*bEArn6J)@5)gm7}xgF<7j-%|-b?mR2 zpfYOY<543}H}dn5BVdmwS$ZzV1Nym159~xIMOPz#T4w|yC)}AoVLPEB5B536Jl4-P z@4<9Y;_i5w=$(VPBV8w)co3$00zYLumXD$z%Lj0dr+dBEWBBoGPoaVbZ!-FL?HF!o z5GtgjqX7UKj7kgX_)mxp1mA*j9PV;7m1u-KHlFup*kk$pPvbdgej|!Fp1Of^QPt?0 zL5@H14WwYSKZ#Ef9m}Vij;DJ?cVl`)*9pgA1eF_+Oa1 zm`W2U7wypI8~{sWw=cd+?gmZ=O)!5sxcnQitAm>$2oE+!L3OYZDqVpb6^mU!Z0Q@l z9>;9^lX$SC2S3hZqf=_}d3kJ~QnsnXY*T|@5nFtv zP$K8pli8+}4Z(7tL+r@}4gVi|Umhn_aqVAq@9pl}-Lv%UD>FSXz`(G~ASk$wktiCC znCC)NbP~xEmyyw+prVXR3`P`5Tu?EhA`*jw%X42)iK1e}Eut|7x2TB=Cio;u^!NRq zs(btPEdKJ|`~5zj_ebuhZ{Mm@r%s(Zb!y-AhsKzJ@$WFk*H}M3{NN%`S=vv$W(YNm zcMyF@@DV_zNcDe*c%!OKo(Ey179)q?E-^ML`4q5ZmCGfOUIi>zUji1xkj+r6p_jl1 ztJI=j!NAQ>#ndQ5*;* zVy+8aXBY!1+Je1NVK_zqfIG|aVy+8K8{)<60!Tnu7O7;xX-VFiOy91_?1v1OO73pS zgu(zB->%8b1xSe|$er&Nb4}*yhSOrkgIt;Qw009(DWx5h=fRdSQ$G~2Hd!q}7O*xE z#&O^Peefwg27kj(5wj}28sB3A%k!|@5Lk|Yl-~&~oDpyNvGo&&k_Ib!%gl;iIgfz) zj1qZ=f%BTR|0)@F$r@NV>7R^PFQEImm#ilDnz z!0Y(g5X*<08$vdlDO2tra#97hZhF=qQukuj07)~FltaahTg8oWQ!bt5gt~`;1AUSU z?cRntr?_#upg$k~TL>VcxbfHF2aS&q2_c4x8@Gxp>qg0^Rb1c5P_`xRC}6qy@>Ci# zd7HGku)}WB7XR3!ZMPKnyGfhcAGg{sx366q1S;Aex2vK zucOlcbyT`iJ?6>&FFYzeQ8E|x?WxE**YYTEe=>?+7H!FAI3k-^57#)F1OCHwP5uhN zMgM0BR=DQZ3ujNmyEukLadDAk2mALtgd;d`Ka42m9~3w{_)Rn(J*z8t8-E{vk@ z$M!eZt{L>#^UzNN>gGK342nPheKZUW`ou27jA>F={0t=_-W)T7h|cv527QJ_Zx-Gs zUxaAm&7qGZdPzuqK_2>6pq{lbioO%kxf$3{UzLac5>USlx#y@)CORrai@q@r{W>T} zdQlXe9YMd+>WeDcnuop%P!G5?ip~L4e|9Ly>XVFQOjP$-j9nT(kD{MI^s9;jW_Ha( zUvZgIpI;tDpG))?L-bCI-YJ6YxDtm3RIr#is%bNCD_3Aqj-#DmH;)0Mr4ln zr-?qj%JMFKgqUq=`8QawOo9Dp0*IKEdI`i%KsvFh6+8G$0Flvyl1nhhvCOiViwtJs zCLsR$3?%v$r;<*9FF|k&DtR5ic=G1p@)=kIZX|!(;~M?Fs}0u@==Wd|*JY=}(3>S@ zX}D(o3P-Y$QjWx@FCSzcjnKE}87e_(C}F%~!pOXFHkJ#I29;}cRSob0q~u)V+3pq% z#VVdR5fd!DaURxlo-dxiA}El3@;}5g1~qXsbjg16v9BL*(K)vRN&7e9pV}p1b-+QD zTmmB1KOxEzq_z&zTqp{PO1%oqaXoPTi)ZKrtlAmYv1lcw4g$hTyv3Kt^O0n}22e&) zrj1}`k*QM67ht6?UI0wsK`a6wM~}^jQQ-|9W>f;{V46V*H!M2ePwr4Qk1&e5XD5v1_Z0Cz)J`GxX@zI$_V=??! z-jGj&Yx^R6(AtZ}`FS*sf&ahsqA?_&2Ccnltjwda%Z*C?25&Sj+o1~)K4|SlW2>bx zN@V)pa;%WQi8NTU8lUe9A57tYz~s{^G-#7_RnB&3PT2-a$!JIeDBXb9S+Oqgzv3OE zA)d411yWiSP@>IHQVwrrh}l~iWR(lE3Ua)xauG91Opwi;_0Quxi+Z|^YLqh?v(8OJ5=^&TOfu)xbc5r=KSm>&l-K-v#Z&)002D!|&HdU;KaRMYL06=X*7Jr|U zr0qdVTC>dp(d9QS^GCj6MyA1LIA)`j4nd!g|BDkjI>klB5aFOX+G_$kJA} z&E$bj3NA-cOF8hQ;BFM^!AZelwkHLj1&a2hVD4th9kM9Lh*V8qiPG0El7>oo;ktOv6b3u3}vuW+ynV5mm=*J8gs(Lf%ZIbF9#)mf$6jfVgHRV|5 z8ti$(>)s4yHHT}!D9y@1GySU(MUGLZqW$yy-VT3Lpy>I2rHvV=QqHdmZaWN>GjtQx z-v!M~rsqpwRN5vOv5jAtLVEdl zDrUENhv9@2`v^i57*V0YbErV+zpjAa_fyN5HS=PL69LrAJW$|Z-DfO zbI}ed64H5Xnb2pl85sgk-%BystAX(0#F5?LQp|qRmm|CEMlhu~vMa>|(pUEJ%g&OD zBcrwRtg*kul->nZs-R(zEkO$!*oH<7?XG{J^D4_f&|Eta!%{4%Svtq;uK#|-OFr0H z>cvN93>t$M;4O6gyvATo2J9mup7-@26U?)$N~JYxEm&m+D%2%GX%8du8@lngz#_AV zD2$hsHX=R?=(t~R$CzK*(A*2?w&?QgSJ2jgj0ZCAQBG0za$qjBv@Ng!ZVfY%m|MsX zvk_rt&ITOc>Yw4;utzVX;w$3G>^86%^{GGfVh~k7rNBx{zXJj4u1RP~u@n#im=Xj= z_L3V8!{6F`W*~q?{wzAS_jaTJ(!GEpfOiqsfJ`WWdB9xPj{vSj+&1Qi2>~q1@MtA` zs~@5OKI?@f0^o(pQJ+Qt4ZKQuQzRgy;&@21`%fgq<7uOg#WQ0};_hL9LwJ3%A=LAx z^JF@yubW)+$x|NP{*L*_;+P=~b+ zljHE;$%a6~h4_miB@>!z+_ngC5bC_z5E|TUwS;WreBYV8J<&(h*|2GN1W4l}Y82a; z7>AIuuK+%LMK8znRV|Lg?BRMv}r{1iMs5grqS_uwD{Ou_m^=5{zm3{5oPc^s*xzZ|NEi+v=4F_pB8d8 zPGSv>1~B6f5UqN%q+#@Sgr?hs;=@94bOY$=HnNsnN=O?SOD+|*Mjr@$%Ed-1Fv_)QZ$IB6p5o>kZ1;^Ox_8iIR&MLjpp4bKYK-!e)x#azk^;WpOzDl=qB$aR4a8T zI6o({;)07Ab};KG>_<6Vg%Mh~wj#7}#YA9Hv6FwTREO)wFGl?#J(-Da{OZ8Gs~f)= zX4AxXdi8ts;cGaZ+UXMC7mJ1*1zKW8-V#tAcslO zlPYuakDwBOuv^(|Tw?{=%AxK*1{Ny&p=iPKX{AbB5?I)J{9|C@5Oc-N7LEO{Jpv1d zhj&fg$P$xu}-wXV4v?pan)ymO?iPoYH zKp38yiS}49>;7Fhz1ODl*6BcMcE& z0)p`576-T=!iquP*Q5H4_%j8zu@X+1n0hvKMae60Vvm=U7Z{cIwGRTK#6MyN<>KKijeT9 z93Cp|+C!yNJXAV}5>I{`$LOKcB8N)5_E6~*50(DXL|nOhLp^b*wCAHbv&rZucye?r z07yloDUXJpW^Br}hf1e7EjZN>N%2tWTNyjwP4Q6ap90S5eV0CYVl@-?4Ir39r3>w$ z(yl#JI>ke!xnK!nEGhP(-nEi)?V-{s9xD9`fH+h-#Y3fqmWO1+q0(Xw50!T9q0-(a z#)oN*W0_Q9e?&+sY(z*WxQ>bo3Bjgv>4deVVOk|;!6%1BglncCSDw1|Q0W2C=*KKj zRYhi4q}a5L5-cE2k>rx2?D*tLs1 zLd8kG2PY1oi10o$$SHP&B{U?2X^$6%?eTjMvTGN4^h1V2g)Jrxayf(PX%~6q0%WWF z9lT6=j+ZRA8o}m2xspX5qNC1BI4(}kZ}R;_3G($q2>^ao355}~2us$2L_OU3*qfkA zmdDr}P;JzJPjI@HD`xWDe}XdB*l4tRi!)aj`kpSk?sbG(()~%VUkp zXheb(;LE4X9BW*Xg`HIdpK{8vVl*-0siWa20>oS0X(Z2MA`ae@Xni~l{^++RP0FEJO^@H1x7si>Aex(hd87`)z=apLSl|b2y*ZDI} zEESq8<1EK*U08iX5(v!>cs|y37){8{UKGirZdY+8VrE1N0!1k1JTipHJ z1&vBS%}x7ooBv`bY|mg67^Ct4Ny#oPCwJ6rNd?6y>aM3iznpY_`*VQh)(-~an@|#~ z0Gw()P}lWu5Ai|$4uF#@dbzoOMT;gETFNxh(j-TYQLZDd{oG4fU~e##1{FC*$yf9s zgjIcgt#ZESkg?*c@Mj*m)iBS`NbBd|HA;OCe%w!e@ncW=Rd%&w#fRk?9nN)sENufwqWsp~!pXT}eS&3AdeXsE}`e}B|#F$C* ze11WwPJazbV$7zK@GZs`Rl;Nm%9s38`%9+4DbS><%;WGB^Gl`!I07lfr-Wh(N-s46 z{CR3=Q%==(2Hu;P#hGQ$<$p8WO$L(_7%>8(+-~62;=&@Q+nWx%z2m#vD@U3En-dJN zlkP`KVEgudjaq$(COM9!l>_1>0?g3ZW#h3=v)>bBW%)pt2D$T2w@{qS-& z%kWIatM74!AjBzq174IxTjwAciZRskJ$BfqfY4L`y-5u~nsykh_;wzHZ^;A*s|Ri| zoC*iv%mZP^x0^@Ymb?ewYv35isc-Q{g1B=g|hy=x^v9eTE#yzX#qBu}d`@AvkTx#D9%Iak#^|B9;3EDgujY&t8L} zJqx*U0FTk0!=X9x|9~W(?4>N;;?SZ$5_B?B~N1&nh{I-AXc>qOPqGPUBe46pOsV1I)c;5{P-kIeCAr@{GbYY z#mCsA@tJ5@H1RnEae0FA=@W>=XGy>D87Pokd}8BJ=(Mp50( zoEl*zJRNf8%7@^*m9p0%{qjcyqe4}7?Sh&?s#jH^D|u$hSZi}Cc|gikW6M8pMJjB2 z-ioQbF(ei}Z>2wkDw0E$uky~}3s~hje1?=chfmD*96r`1!8v??gGMo$JcsWcygG}h zZH16=UVcs>?4^r9Fu)O8PH|}?E@lr|pdqaU8B5BYBOe@4k!B!l+s4scRA-`2r7(Fw z#TU$V_JE3Rast815fs(I5ft7f#rK*>5jLq&yg|g5$Bghi z19NaJM@U81)JfpVtq+dnh@IuqZ-_LAbmdRg2gh>6uF<<7=zJW6;K->i*TUom=cKrg zN;#Bc-IJE0NeP_PF)gop~zS@wWqZpuvP zG@9w1EYm+|2a6@xgW6<uHsJlKz?YqV&NoGTg(JvZMDHU=?=s75pN+-QcL8_nQNli8bD$s8eh zN>!q;2sj=}j)#3p-<}#?it^P@WSKQ;kZ1U0S$s?jEbGDHMS+C{Qm_M59Rd=bnll6h zq>#qp@xZ%oQm@Vesxi}9kUjC?u6B35Eee+*G~E?@1RY`Ym-HT96X`mO=SgFJ53hxs zx--u-6+sps-E}0e-^CSB`U?FHSVD)^To`nPCN`|~A3Tjia<9-+Akx@A7}4lKyG-At z>v4tRylJo}&J>*5*W>bLPQT0E2S;(ncSxs6UQ6~X|ec6|Vf z`=2SL?@Ash$#>~)xN`BXBm-8?Z0yW0=y(vaza1^f6}phF{t7zEt&{!=dMf-0FpN<- z#4IYGZ$e!MBD(8GYn8eQ;Mg81M6G}{U?dS(Gb~bIB_Yazw5+wb2eQu;@QmHXGx2u+ z!soE6@2?sQDHX4wE5u-7%b^w}x8rYr8#n}ug@6@t3Wc*N*cUqOaePrNzk$z*77aZN zK|^QdBf6HvL41LAtTv7gXw0l&LYC0$GV98!79n_W?H0eVD|dkvxss&56-iAo>KBv8 zOB5F$DI$JRL<~LYb$x-myxkR*t_sMOhmb{%T0(9;=*l|5&_Bnh@({aq0M7$;*W1w?~0!*BC ze9PYhViYGS3rI?bgQE`XRm5?7lKk!fFuih60ru{F4ddUeZWRkGp@qQKv%Iw!SA7JT zWLpwq8YNQO7D%m4q_!K!Uht%gW>LB9G#!1iFhqRL_Hs)=uhBqF!pCAE{Ra z>Kf=q1N~V2L7<+2W}sl8@QHd`pfQ1#O@TC)MTDi3sZvC-jR#^u#U`GFuM(`~QLN$! zmb-YZ%Au{aXw+9-kz(=~VmQe&g0?0~Et5xWV+8H4C|YG6wM`MUd!vC?<)LjhXy67p z@y9OOQ;>uHyC~wQ^I_*>){r_VJyL1>-M2%Fa(X0Bn=1&crIH8#c-BI1vImuQckQoL5C_|9t@-Ryfx%NXU zII=9Y((NZE7Fx-J-h5aB2A=_gHDA4nY_0t;?zNm2dkkT5BCEzub5#vR?M9Bpm2Eh2 zwbNWxzq9cX*FRXlh&<<}YV6!pE$61rgJ!~KEep)+Y4+W^1&H*!cne;2-T|ooEKsk% zlI5V8wyLqyR<)eAIt_e)z@X}fm70UM@cZPfiZLR4_0QAapv7?cD_y(9mF2v+{#~fn z>|Kgt+w~jHgV)CAgxzct=e1S{bv0NuapLMlV^cMD%Bz+&*CRFtobviJz4~q~r@S^G z@3384W2d}oSAdanglDF_hS({u8aw4x%PFriR=gRvmQ!AJKoyzVDX&^ic}WEYadFB^ zXnAfer@X`*PI=YXDX)fq1HDG7jb;?~M}+2(D|8Qa6Q{3!X>6*7PMEC<)2-^yh$^SN zglncI=dx<-l-B@g^kb&Hq#;^kCoijxK};xZM4b)M8hf5q!>8oV#0cE6AzFceP)~q6 zHbjRTo2sD`POqv8l1BZvZ!q~YoTz+SXc%#=*hZR$-LNEdGvg27b0|FojoxO{cx*N^Awz!$r(jTd=;yGk zg4p@A?hR4YQ#?L-#C>t_g1TOSrTwI(G!bDFq z`xgA(VJ59)w)P8NbC_+_PzL=oo0zpo28qa4IWc{|=>s?l6pesmLD5q$4uyj#NIlN8M8fZG z8rGX^<7o>#n}+pzV3|GPbx_FGED3s_A!gdsgW%*9;@|UX@b7y=@t^V9@t^f}!+(dj zFaCS9cL+a@_XB=BuZthwJAvNeIBu8JP!&c1o4;kN+Pxn zuOVXV!2r>BsEnBMnS?JohEhpDBc*Y%4)$OtX5xo2Q|`*BM;#hTs7_?JLkG;cbuGat zNk0B@@XYE5lwT&%*YStaJwyV0LKc6Y(=obb=QQKd|KKsDu6SyM^2QL7~q37iJ+d1dI}+CbYn~c3)B{}!7ANKT1}%oqZaCP%s@mv!HII!4jJ8u-JvJ&hA%a{N>i~(e1dQaAKPD1-(|Wd2G@T1}!w{JvQk_09BZ?O?r<_dgel`V)NLf z7h0a{u}LpxY0^u{&ZbeY0!}P91;UB2ti>CD$TRo>UnT6J z0!q9RjtIegGUBNm1fYV2m-*2MQvD@h{F#C_8(+EZCs@LlI2v%DfnYVLO0m;&9x;)c zI>o-pEykvTC46CI3;zzP?9WJ&Q3UL$1WWh^K%<{Xem93p)J zi4dFj37mDvOEJ?RHjhalhnvObNeY0)u-*<AO^>F+^k`(E>^>5DianacXvB=b zRC>o#WqLGaGhh#TG}Yv`W-c+}s?^jnwWfO}tOfm>GUkP#e^X&*9a6Udb`QMbWG#02 zB;p8?Q+#zfGk^^5HA+-Y}v7j1*aY>cXGwz9uIfj zdFtCE;3)B|SM^U|{qUZqP9ugq!@%exFy*bmmO72AH%XT%_fwitr+p73R^!#LN=n8b zKw~QYGX6k-dntnD*;29n3{Z+Ntk|w3U|XC}Q?YSEDOa(*XKeWu+f9}WR%~SmD5%&t zn%<{kn+$|+sMt8l9#m}KAuTc&t=M)0FsRssmaW*tjEc>93vn-e(CXWz;8%B@@f3EO z;MLUyoE<1aYeS~2stYz@d;r$DvP87Hpbf-}y>pGy?Fzq`b!pifm87q03Yrnk;%4iJ z1_c}!cXES(uWJi#1tHjHvIGz8sHMm)QN$pnyK+7vgZo=LZNlL1!QAMl!QjNSVQ`UI zwKg&Rp)k0}7X~M$$zVwn(nDw!&dZ~+6DVBNFO|RdL*;(J43$-RRIUMq5BsGOPf=L; zyfA_`UQLKZ;p>*x0*TjWpd;~`WCg#dO$0xw;D?9<1V2^cHAw_tdOrkJeV!3M3n9I8 z!BF0E_!8iy7s5l-F^0Qe>9_>&@_Orw(!3D>-hs9p-%4%<4jf@rPG=)MB@lO2S&si^ z>l0}U$BDd3a}*uRTdYGfENcwO?%PR{tK-H;ea-kbe03ipB)g$r6Z0Iu3&*)X0~p_F zSKy1A5+eu?3F$>G50g7Sxztt8ccFRi5AlbE86L}UCO>l*n=e!pR^x!34PU@Jt5KTdfo_vI&%YvB2}zz+j{&3b+pj{B_#Rx^et;5k5w zQ+HJC2sk}TXXTJu=q!OS&&BBrPj3FA>LuXM$7&^SrAvNv$dF7*TWGahRI}2hP{n^< zW09RIpJEIfi?1^w!p5R2PGF1eNVM}pv6XxUVqs&E!i+W+2SB5rej1A<*Mm37cJAj- z2ujHz5Q;$T%oEq~x_|h{$Tui4JXTDU7L|odW+b7YP^`}OB1;*!CIz|A!0Q~<2}md+ zBX)qV1#Z=8KzI;utk@$~FG&I_i3H4A)9S0E$bwd1t!eeunpR&OWf8Rc>avMktFN9q z9%uCur{=4=IOH6(`f5$9uhz8s21QzZ#{n0uFt++mr_ompr_ei|M$_u6!{|lW>dP{r zo318CT-BOZ-(b_~6V`%OUmdeY(CVw>cG~-&AWfyRjGUoRuIk=`%eugYevFG&LzdZE ziIs<*buj)>1S)n;^=l66X;@|AU;o~b8hcQrZMg>$*tRVtyCYT%mQNkiH`ul-mXlBr z@ELT{lh6QP8izf73D6&(=F34Tmxe=S?(!%m;jrN{_3j+aFozAN{`gKORcP37Dv<9S zLmlFiCAAijqZ;(&H-+0gh*>7gvDFtHSIS&2D$~4co?DokC5IxfOQx1M2AhtX*#ylo zZxFB1QpAjCeM+fQk=2v#7Q3W>B&+JF0B8Lam-L&GEX5H%Bi9rM|HPO{aY?@*RI55x zb-`T1pwl2=-;=FdrbUwC6oD8$RmZyZRGS2~W|*pu8MMp7>e%j>9azZFIo6`;IgPA# zZv}a&3G3BQ;Z!g=zfIUP3K>z_gcGtbopUd?&$OaoJ82je=4kJAR)pSL_}AVi_;R}t73OP2WPEiC~p$*u_XWlFApKKLJWE6q47x|sQq#HXTg$`g*rpy zv?s6FxrG=VFsFuWARU9Q98Q!cE&wR=3`S`$DmV!!TJ8*jMf}P{xiPE66TsY)nC-Ez zAqI=t9?M}Gb2`OUtDZMbTvFfO3tbgh$fEitHsJ>QuS^#LJXv9^U?_t<`t;(K4D{-DB zz>>k+A(p}Yf``2ZIG&Txd+m+nJ zpm#D7;Xb^M5s1R1#AM;^2LK?9X*{V@80fqJCOlLR?-rK~9BmmO3-AlMbQ(F;1#+5c z&}wa-I(zp{>7M*}t1g9ET8Wu8f3mgPRM% zn2{^*25P|)n;IOh(Os|Sa^8})ks1yV%{rPetvdd zuAFMnR%+%-AGwk|W-7qQGPu}Wx$W$+07y341qS5Gug~V_lPcOM-my(4FIO`A=4H$C zft+mxJ;|1fR_eD($5Ye;j}XQd$(BC2V6qf#G|(PR?e~%YB<_C#!R#pTD)|pg zd=3Dz*i`O!m5^5ctZ)9aA%W^t`2D};zm44QhG)S1_x^&s{MXYv|GkK8w$-XlB>!zv z>{CO@x8?F5mFS!DpWFkNSPMlgfv3bT)3-wfOs;_CIaria`!+g<+bz>40)iu@o>%IT zVzeM8S7G@lFNup!CUO}YN#$Johli<|I4o9I0+e3^UKRs0S?fs3iR;oVzh+eej$&tu zb)sCbbGLhWCC-|O@6A$!k4%Lo#)8e0Sn1uG{v8}_2EH|YEG#Cg8?$oupFToW&a^aV zfPn7$Wj}1wBO+1FVj@17wz6+Ss6Eyr(qDO7T#3Y?fFm;1(wwWf_y5A~$KbSJgGK6) z@Lg0_&7+HzKFZX&5R4R!M@i9G_5u2~??*?(>rn&-Tzj~Jbxc5Rea45eG5JYW|Zr~_CSLQIHa`7J)ErsngCF|*< z5Lr(zq&x^CHXUl5(SF@Y41xLC4?_4kAc(gOfva;U1qlHjeBw$MQ$mPBBm^NPjaG*z zfh69;Um==Gu|H^$O_Gf^Yxy6!+AG2yh2|idbvq(o)Kl)7@nE<6a6pWtIDK=Dv8foH zqH^(%L+sci&l6CLeg;OPAL)_Dra-?a$GiK4{~91hjLqa@@8PDe1uq9fcyIq|W3Dzf))0NiU>0gzF)2KHkl zy*y=iDP%orj4wz=Uvfx3x_w9o>&n6n{nC(xuNuux=Il9828$%DVl;}#IkQ0l7{ta5Q0^Pq+$JpbXQrwyTUJ)!&l3Yv{O^C zCwgrD+DN>xHrxzJJGBK|@D95fl7{^dP(sW&T*cwB*ivK>8$}LDyIU1o0JLw&_>sS+<^yW+v z<_6@785iC;b{V&G0|`!gKMGbbH;~|@_jZ^a59xtPZ!vRjfCH*=JD{3i(uj1CYHZpu znn<+-2UOQXNK9D{sNM`pc0l!+AB`~E&eI3mXm&!Rx~uAUxQ`95+dyQT8X<}4HZUz= zoBMHwZnX6${>Un;_=li};nbK5TqaI}bMZysjNCw9)IsEulh_VeQ{mM@dH7tQFT$~> zzvr};)I?JuI*?$WF8(_HP^5)HVu$hG2116dhD^u;ovX6)t|GEx0$5HAj@uQ&g}(yz zBC@1avC6RAdzNgY)u%Ho61N(giqi?DY3xzRn<2&i1SA&8a?=r^ENkOZj1GuKKT;%v z4O!O3gKeZU@7`sT$>jO_kwlfRKu1nVQ~85D4=Xc8?6x4f?(5cDGv&}V6kg8B^JWgi zJ|+DlXkm<6XQY&OW0IK*Mnv;(ObQYzwgnt;c2d;&D_EGq9BdPzhO?8Yyu)<&516Dc%8p2;=qf2k2{{i0lWxmAl)!jOS)}X? zaMsUo=8Zbp5}%|L4zB_E7d+Ntym>#i>h zZPs0ewyI>D`IMV_ujEJIWDsCUcS^t@%ebZg4F;(Wi;={9${0>CR9#^Xrp&B>16joh zK6k=Me4Knz#uDqWz3B`7XbEnWD&Kb%QeIb)R`vb(Lo?8fDVLk-y24yLC2{=|+(9@R zSJL8B_A!(@81gR9Tj7vlW^R&lFvGF#b{etbU?jeLYV8V#-C;iw?tH2!%R1pFj!U=^ z$fZH7nb}tc7V2~`)~VivM^EiD2IRpE8e3{T@Sfb?R=JA=s5;?ds$A+Q0tE2ZOefuO>J1xIG2tao|5{+c_>@zW9Ie8AU8X}qHUgN;FcqVhbvBg&PALX;*kB=NOo!D%V zmI8t!gII42HA?bMfx6t=m%jm4MSbS`<5Dktg2LO)mG@j*_ZnmB#!_zSUpgNp617H7n z{7sGo=T+}U&s-2|9QV(2Jb)C9<6NK|Y#k9d>9}5IuRj^)=*_65`YW|u=QX7+!s|Th ztSEx?yu*@OqEww~zsI&d<`f@@?xil)S{7 zqGmky{=G>b?<|*0he+v1k=pX7Qg586 zk(7T#U}pf^J3HPCb?>DNfVKh}lbmkM%DoWs`4VsFB}l!PlnrOc8Ogwljqrbf8T9WfyFC zbEDd{2jtH{?%He@NSxqQxu6mtrD?Mwi7cpaAF(^?vhJjX3{BlCYYqu zV=Kk6w-*DAaSaAAL?8bfp%gT~0~N*_Yup2J)*jWLBK4@|6h|SGA=daBRjptnFq@%? zu?vW&LrsL6=_Cc$3g=vW+ez};(X=9ET-HkAYeD10z<^8EZ+f4)Co-x#w<%YMuwwr5=fBgK8`m)mZ*3sHSRi z{}bf^hqcoO#h9UWf->{1jiQmk6}gh;JfvvRJXhVIunevqPH4k2u?|-8 z_)hs5zLmQpQXAnI!wGEwPDSZ1jx+GBQe#0EhH6e3oeTq>nu(xUz8Jyc@%ZA1HVJhf zIN&L1gD>#iCRM*K622=rBhv{7Tvy&)fkpFz%=^cT#0 zlgDevL*X!8Bh5;E?Lwx;rEoxiYt?Gt`uchn4<&02aJ`a+A^HXuDz&d$qKw!eBZQlo zb`6|FiaP4mdt*AIjZ;Vv>HhG$g`wZq0&%WYN5Br}iO1rLCoGs${rw?`oX&?8-_A?%t$Z4>>Vccpm6_bv zK>2T>uJj9_*ls?Bh?0|p$t+IApGqwIN;(WM zOeL0hwdIy*pHwPAazq_%DvdFzbRRyCKkTq)pLj$`b|?}riN>*K)Kh0S09w$E+L9i&UGl*vgccgy$^`)YDMh!pMzw$ zT{9h;knAigVWpqS@1yiwMmgQDhakVV&4j&tJ6tvscyJ?OX~3W^6D#qp%8pZNumBd% z9YW;U_?rr#K3@uq^NicnMfg^J3yIMKH}6(w2S|U~zcFztm&0j|f!48%xzl**d<5UY zTk+R&xltshxwoq<7H{|mnBE@I1zo{{r@N6js=-4hni}z?4<0kP6;4r@_k@Se@FS(WN&AJm5K&yBP%3Wry|N8VLYu)7?DDgU@0^m zu19jN?-{9vn9J0f>9H7j%0~!z99@|(9kT4IJQglM@EMd=$#3b$(h{ark=E)FR&iyk zp>Z)R&RKJ_N-lgGGJPdf#c$??Z{6VDR}v_B;yo|^{)d`uLAFW43%E_PswNc2wI<1@ zc*#JZAkW!H|KY#DQTs#pgc1)s76fqyfNJLK;|xF$rIuz+KAvVQ$d|AHKi9zV%^32;mVN)~&=5ZE-rcuQZqL5qziXA=w>q=p1s0XzC zC=3TAjKU;i$weX8Y73$eKqLTKBnoI|n=!#=mh@~OL;iQdcn^&kHNf+$1Ym2@MIGyGm(@xk{;-#qMw>N2ZzAjg1d9RqinJ?X6@&W4t7qnnXe#lumL`dKt1avpEN)8BH@N zJ&NKBy7;45mjt7)V%Y-?dp@JzfF|_nS)ilH>L$ih-K<%SGy@Bx=&%(1JMrtF9O~`& z8ch^6nwxrhyP4jj^jOf7q@>gsayE7W3qRFX3ZH&r$R+ID`JxuL!U0MXRSSCHGsFxF z4}doMISD`L+r_wW7X3k~4o82S=N9(+On(z42b|UDZ-|^oFc`mXKiC#l)8I?bdK`rY znt;`8@cTzGyxJ2j08IBX- z*yfkOTszk~jDjWC!1OyQ{)P&moNp0??2K_4>GlK1-rArxXD83kfyj3U8J}jFl34#-*N9%rxka%4TX{G@O1=;l! zV}fGC$`-i)3~-$UlfeZqvNir`gcs}}9AO9vJ@ew<}pZ>Rt226-jTeZ)1V7DKRw;pRIu?U&Q0WOvBWi~do^XW{omSX01V>3CST!7;nv zATGWi>r%h_Q*(5-Uql9nxRdT0J8xY_WHYf|z=`!6;EMQgTF1nvfD`Lt%oK29T@b2O zA+;%({!}0f#hyAIeubra^)8tIHF^WH;4w2ns8Zc1mhP(C5}Bv< zR|8c~z8L?pxt183h?tpOl(93j*TZU`1jj?JhJQZ&%*;MQu$h^Cjz-MPt|uye1dKfzk0c1V+z$H8A?#p8{jX`&(elVwIqwic_l!10$%_hGxeb9-2LGd}#K)JwtQG z`)+8?dPjuj4sT{?2GLV<7RNg`XYss6ISVF2bC!(vtDGhKzxj|YX2kH&tSg|oQg^Ua z-*;r&fe``tfA>q8OJEM=))+HZFd^q$qkoJ> zT+W%u%D?S1tad}ba;o%x=t2M{f)RlOz#ls2iCdx(c*sWJ z2srX&7w6=GBe3J&FjR)u`9$Iv&`BN=38WeH`ULV9AV5m+$>nddP$Inn0a9Y_<#GDg zVT=k`$^%N)Ptx1vIl9e>dTz>GU2cVM-M# z%VnY$mrNEjyuf3&Vy8bQvWv5BA!RfdqNkmu(r~Anx0!DF2kSJcc7F`o4 z>h#|~4p%g0oC#S3^5n8w6}u0p7!gX)w`lMRpga%lGik$iP3&fa6Q`5#U>YBm6lh5*ddt7T zo9yump;o$Y%1em@XX-^&;c@$-7oI%Vr{;uGJTH)N*i8@nV?tyKFu_3?1!u-hO;?T8l#tXFa)rEcrMx$p@7cdlT-Z$fy zb*zMf0l9iw2HlN649A#ue4{y?NEY_|UU-t#K$7cym8AzhcH4au7dGe0I*}P_J4#7P zX_)ELl+xCsnx4w{#1pDe!{j`j9Q9-@s?i*`I{;r2t6)%}?f_(#;7GuxF6JC5`MuRr z3^t9*3tlyyOAKKh#ic>nFAv2;>l!IaRo!2KzK<0{V+{-$e*muRi3g35wNiaW+Vqix z71TS$Y_vO{u%5BC00&fy?m}TsgPzCQOBb_h(DN8kmEH!o*pA3{_0mYt1>#t*#WiL} ze*O8-!}Lrj#R$zJBT3gJt0Fb{O*nmh2%L7(rfDKR2T%DD=L#PR@SQq&I5K3!O$~Vn zjt|k#ZCY5?w=w+;U$=>W=Zm3AHCHcT9>$atUfor&9n-8N+2RaE*Gsx?Ee?tEWGU&d zv^XRLYqvzB#UZg_#{#8i0!WN>s)oa77wp5~<#hAy{t3Rrb_tN~(n&h*X&n#1x8)s_ zg`Jiej$-OPbtRFu0FH0>m-yPpo&eBvT)q}a%2bJrRnvkzH=AnGvS_*mCB{Gn3Mv}b zD1|&cQNzMw$(AnuPLc&saWAx;HSU?yU6+Hgp3j2SmG}C;9W*QFr3-oZ= z4nS|!t;4y9;tK#qx{J1{Qs~RDEdDmb+1V$S6z8!3jO5<*edxS11d<&O=O4$$!};F% z2Lk}-CG$^&p=BffHd~%huqgM6c(Qd>FJMY;U1;Hj;F}`2AZZxl5zK1X7(|90UVuv> z^^V<))LSox>x0%C2qT?!Wy@*+H&`GQ6_KlNy{;F~tI$a`ss}Vjb3p~0LcSiYS?sj@ z8>nZmGzwTw0%P>P!zoJMfTvn!;eTSjGkAq_51mJ?6`G6Is*9nuKKwFyhph)ETi~L{ z>N+5=Rj&Zh(gUWaT2kcSsn#Xp%ES7`3_~hMUEQj>vE{vB>HSklr|q*|bP~fVKyBDN z2qO#icWP~*f1^Cqu&?o%%4{)Kwo~7`GfG~; zeNu3x482pc3^o4ljQG3L_`_BV4#~{eiSnf1dUZK;dc)ynNM@!j7EwFck*1kLV0s29 z?(6ttzs1`l#X&nQLzu8U4vWV@E|_0|8CB4vf8a1pv}pl%_Ukq^%XEqCx(jgB&Jiqk-pCOkS)N=#E0Cqa=jRt^YsnlnNvLaM#GjF zOg#Z4647|#q!572o(9}Hy51Z|FqxEaziEfioh+Mfu+~gSPbNLMO=YZgV4Egki7mMq zIWgrfdQ!bh=AH2l*-c-BC}ekpJSRAX1q|sL0I;lm>I(Bc_I7*=XW(zMO$W&}^J0QL z+ak}$w}_*mQ6vv#hG6Yyu@1!-kA&IDFn4syxq?0~NOH(9iy7vqt4#4|!*qxp4Ku?q zqeu+XBUZ04iyi{vya+XCCxp%TJ_`qW*kry_-ZtN9m=VBJvfTP9hXtFqgH0oB1;|ro z#*)i70uT(HmYHGZGP@X^PQlQn-IG*#3p{hK0ZZ69%q)rLtEy6f_>`IEu-YHF)l;h` z=5od-NW5!~%pa`mLC!H_u*?~5PKEypawbn;c@oT$CG^}vP>qQS>(hy1GE+Q~nZ#p) zSp*q~J`*-G(JS`&<+9y3V=6b7?IO%vkYnZoq390%TB#nq`WFUGbov)`^Ei*F#puVb z0*bFM6F>i*i2na%$F5)HdAH8>;4`_=s?LWHneKWVAd{hSQ+OMD6?$?x;s@JAlY<#6 z+{bHku(eH-=uf6it`uL!e6ygC8ao+u@tt}gzPNbvSd^El=urZ%&dW8#ecs}}j&Jda zy>KL>0_iM^bRoWY((mJMGsC+O*>urFc7{P~nhWZgr8W%krHTM3#G0^T3ASX0b?TL6 znurQwYbcusEsl}RD5d|WvKd6CBp--uM#Dl4E$#{U7GD7t zqd0+VZU&qL?_?Yxn>j$9%J9~TYqa*0Jr4{)dh1PRa5Zg6O<=s!tANnQECP??Nt!{x}!{aveGg)8N8bE zWM~<#r+N-3&g=MdqX@d|eSiu|kU6h(yGa*ETJTUBSm!WIK6XAhW^O`J=MG`;!*b2SjHKr z)+{6bulbpO&ChUR>VMHCDASP}WXw9UJwLnzg(}XmL%9TH4^+3njK2gQTtJ)KiWSc1 z;9i9T>jB4D{k}KIU^Mjr%Z7^8Km>#Yj3KvA z$l~vFfb;HjDK{}K2<~+8#}Ja7YKlPF2@~Gwf{6$^YFWx;Err|(W6hv#mi>lHWn*?3 zY~oAMll3LXfkh5s$2osbD3-y-xeS&LW~qd25)dI~SFDHekPgVzHm$kZLWGU!6Q(=s zN+Dr(Q&R9nrAe($#+xoXrXH@L+{JJ_hHYdRFaFAp|~VFnjuEX;nI&l^QA(drk_EAt{@@ z40b$icy-s|)mr@)Z{fsr1)^JSk7M|ptJAeIKjd4R}Rn6We{&I3fmEVpLJl#M;+qGB*Os$?o? z><+5toXy^1!NG{toO0+gHZ$db)jWv=Xv&nsec*)iK{5zWDA)SPl*7Pi^kbI<$w^Um zDRsq4a>1$;%?DM@$*c{4zz!}roArjVh37a)B8!xQbDTnmuw#-^5~0TQYH9|&%V;D7 z&IQPlH;P>ZYwt2*CWd(BE+Zl#Z}>;d!`4&0&JUgGKurXm!;FzUkQ(ndpM|w(c%A8v zlzao>1$QGkUpmZSAw40J6rGd5dI(O{ZpG5EBQT-lEOc_1x4>U-Oh33q$vM`c{~R{i z6Kv&Qgna!GUc`mPe~2TQlESrd$)vRE!i%`fM>Zc2@9`eY;G83}VG)zV3rbN-?@Zuq zA>MRnPIh2C6+2-CiU7wCWgAkAb4_-YsEaAOicQhw-vE(skAv0WIOpAQO!ZBp%CWsxRZ?y6V-8>6=6>)be@f!}6Ej*Z#;cP!EGmL*GMtkA2{F4rK?LWb>Y@<2KS6}(y9MVV>i1+Y zyjZvhBTz}AsDc+Et-a93@v0=am!P4l?im1%-3DN3^1VQ10gu@|7X7FoYYm>6*E`|L zp??347gA%(uCLGJGgLwU4&cNKL~8$FQP5Xa1y4Cof#0&t?1$n(Z?sxR&|intV}stBo!dloGHip_mj~Nhmj4s zL9xYtd`O0aNWLDNp%xBu0iu9?JtRYsHoOU8jkg4;U3zF{Tm#RSn3xZ2ZrBXqt`>;m zhL{x%Tj71O1%jN?BQlLS_Jd3tsxdH}Gte?hT>*6~MQcH)i>Y$`lssmUd%9RuG&KT( zUQS`Biz#f~SK0x-2)%5!ZFOYH)2s)ehu=jT3BErA1ak6aTs&R;IINBJP1uKEJxI(7 zii@(q84q~lMqmVvBA|T}`_u7#`n;vLIq;;QSNTqtQ z8)?r40u3Td_W#SmG>0Ihc8B<1WD-p8odidrf|V9_esI$zEt=SV)bKd|Vl?t@F#e%h zIA*lC3fCX0^?)~y9o)-Nl6H~W2k^#J#4(q&i_~B`eu~5o$ciJAc2h5)B<&-;fi`JX z#b+t0=y#v&+Y^;=Ftz{3*W{CP{t^Z6=L?+fHw|7 zg5^0HA!c_0yzv!e-@s8akK~U<*-rp(yuG(0g~)ZQQ6D12`MrUG5S8#h(ehs|{O<++ zTP**FlK)}kKZ-;C9bz#8!x(`)S42Qu8G~VrLEsw1KwKGxVT?kaE27{GV;rJT#=#lR zNaR5g2?reGk$VvKx3L%nK&p5gijmHvL=_fy^Cs1nsQYXHkUY~)#Wz|duw-X1Q5rnY zK?}t2DaQTO;DvcAaxv7SpsKHmF#qLLgtq2`Aj6e?RpczIA{SCc?u8_yIDv}%o+?t8 z?d51yWCy?-HzA0?XCPo|;1<9e@96DF6t6fr>B&!x)3WHHd+z2%|8JQOI)@6=58rP@^J@L>@GvBIhBgH}zGKn*fHY zbg#j;CF=eF0926&@r6Q!4GM}GG z3bF&WNL1{m^Gu_-PZcJOVqP53yKS``VHcqdjb*_mCDu_)wk*#ToK5~X}1_Kg&eXPG|gM${057msg3LQcB_ z*3lRA|6kM#DEa@C-oPOL_ib<@dJazw zjoIDTadzT9MFSNI3AZuKA;Bug8Z%2g)-1!T=EU$MB$}_PIWf%i!$eLSeefB>@;BIj zG=}}|;I88K{bA9e0)h6u$Lri*-CK{a~5a zq2o&jv;7RjIESQUP&U{T*}fVw(Q@;rS17%!v1T)T^$G;Rs6q^!aobW2V@8c(Ru8sl zizV1#?8NVY%zso{q@2wYwaV?T+~LEeTG-u+f8F(Suw2eHWYPey%8*BPf0%>N1!fi+ zzw5Qe#}fD$gnHscDz~oAa4pdwtiHIw=j8>}8K%w1HJAegaLVJ4f=zk4mj;;h;jbCG3nvK`aXcrBjNR|!?%3POf4Eu2hqWjBd!a63X zfPV37fv5?u3W(};C{q;X`Y@N%3nGhpfi-el)*Sd*_waLP`-3zd{460Lm1@{exL4#*un_0 zg%M)bEW{QXV(nY|@qeuG&uhNz$e?PUf%G)7@yE!##SYVK=XNFd&|T}Oy|%55;TNmC zEff!|6xHYiVz?PPssAHZy5k_^y+!~~PPVn0Z_7^jj=l$f8K}H-h#o{Wc)}vRV3FRC z@8Zrs(|A9!%%#TsfE;Q9faF|E?r2}EH#kk~L ziI8glQsNzuh{XGVO57ufwxPs(J}d){kKC4cJPWL`<_r#Dx~!>B!ZQdy?KgiDt%-(K z`<}uUl|?3gf;29k^8_a2WMZsD7UDar>7?3s8$g?gk8xBM8BO8=7USdiJRgOEn3VEO zwGSlV0cAR%7~B6D$^BCz8EZ~L89==M53_vxKa|jfL zjX5%*#vIT+)?7we&CQz!qbZD%gInTf(1@ntbbR@YGh5=voD6%gC4MI+@Wiu;5m%Ei z>WjVb>R1OuOR9M{SVWEM)ooy6w|~!Hh0`+$W$PpG=wy*$oL@qnB6e;HJCY zf!95DMZox~911ARsTt{eJ=yY;VWM(bYAs+^UpViZF<( zTigUGa*za=iQ2=pY6aZqIR`TTA3q6xv2`%9U!zvL5-sB-I8?qr(GcPI{3B2j1A)LU z3xwzJD9gA#RW3FGz9rAHUA9^<%~c(8luhoecei62TCI3- zCh|Q@d5;UwDMUGX1<;3K4^B;{IUb&xOng4M>w4X1Ej64S+YdEBp9!eJOltK04F19+ zkb~gmBwb=aYf`P>H%#IbD3T3%Og{my@|SmXF$2lRG2#4ssa9VZ2Dr+i{y14s3GXjyuRkJUq=XEU;w6=oLF=;4!rk1fT37n}2K3O+s_gF$Ha>5)SyX#Q_;@ZQ_<|LZC7eK*E z(cx3uEdIeDtM3_xvzVqF1aSG9NlG0F1CDH(HXYx}O_2X)0NqIqK$`X*toU~R4d1GT z@SNEh>4Vc;0i3x4c6__hw^UV7Qq=)Rp3n*4TxgRx&cL_48xGwtAegBq`fHr${FDq}&PP(5-N0q()@ zK7>FCO#ZD@fZJg=PyIWV`Gv59&+Uqd=jr>IlJ(oH<6Ps|qd6(!>jxMKlOh)3NFeA3 zN8vyCsOt-s zX0^#po8ARPo7}XC6pJPU&DXDxzoO~J%p4UjGU@WFWjr(6Myv;py6YKaEMfl@o3XZ- zjP(jLR^^+JO%LR!H*?e9W zSe3-^a0WGi(M2R`4FHwj0iT5Gp1qLd%jO?JUGyzbbP>R6*v(U!0Y4qY-_(du*C}ok zt4N_7_=Ugdef;ePd*h6oOp-2yr~QcFE?z#L9gFON2y?b|oeftq;~?PW^N)s0g~uIQ z#~ag{`SH_W5*=P>y-2C@>aNJWrc~($Cw)qlWdIITs%(VYfTfB;(b8|J(guhAN|jZX ztx$C%rHaVQ*#+s^wAG?D1E#x<9c;_c#U8g254Ho-da=i2#h_odt&?P1huVU+jugy1 z$%+NDza12&;4Dezztf38;JyWL&w;X9~`83|)C~i4ez`p{&2e z>^sRTK<{AsLZGk0d^dQ~9ZoX-=OMbh(Doo3K>GpESfT9%F&UoS%+D8nfB?^-RCqkG*d$}H;9Oe#sRfe zKvYyza2`-mQE6#soS;!uR8(+|qT-BG#EA1moQi(qGEOr9MW~B9bU9i5&|SZ;HB{J2%aG z=i;SVt+{rB4_NDwa)HfCfe; zoC^hD=sai`G@x^nl=ZwDkdQ#@`4A20{2+>`shl4lBl)2g6X;nbn+-!VgEV-8ckq5< zEJkC?`~-O75~-CFpk0LtSd1i~&5oImIf|AG7OLk%1s}jAy;#msjodBYi@H_+DGb9t zMADFDS#={u~7pl-L%OkE2Xk0YB*6BJZZ#lkVF|o>#D5oHc{3-5kGSCLzMj8G>Q@1ySvcbknL=3v5M*`(R^rB<;gYkoUMvbC?IeetUZn2z?aid6?6BO^Ek@ zkK(ld-=jF~|KB=_gO@)ver)jcl}YB3q1@LT=g@K!m9OIVd!9>2JuIjyZol73Lwfr? z`jqN6AXtm5uYkvGzbBS`kG*856EeoZtQm!nNspJ|v)7$fKa8%NoaH>w>i~?noT;t( z!!sa_qRK-P{~#eYb@8a~uVu`JH1IayZ?~7@7QFhTxEZSQS9yF+wom*Ch@%oO@BqNO zAnQ2omH<#_0U+0dKj1ftx;%(DlcsZ=6fsU$26ttIt~Ela;;TSd@3#z5YiUrV-b1A5 z`)xtQZwJU#A>T7z_tp5>%rqo}n8*)Z@Llj`-a5l?5Ad9DPu@k*Vrcq+CT@&tNz@B? z5KZO%75G1MWBiw=*R#+Bw&W5pH-GYY3g$i=^#VfGod{V=b?GZ3wdPb`#^Yly5ABx# zwig}R1<}rxg0FMf{T9_k4DVwW=Ju+ z2Y$fip<_IPkJ}@4iT0sI)0_z$VQDHje^n@7@(&=I&kbXUW{vt<=B% z2nP7<_#r?`-#hI&e9p^qy&hU(DD~k&`7sJfvjrJs=ox<8vrSwTKZ{7I&+${i-uaP&>UM`%ddn}D4ZNgoig-4R_cvYjWCEu)%EF?T7J$kKU96M|+ z>DI5Lmt5+oC3~#8?m7T>LtA{ha1kI&>&@OnW_74jZat=?2c#vv9@AB?$H*1MpMcg} zQT#W0aRcC&jE<+9UXQ6uK|s46)1A^bG~fs8F(p09?xC9|5p6x{0Z$$6a1LyePu)1s z|44c8bZ6!?heK5zzijiYjF}O_I65T4N1p&hz1UL3e~8&0^kxf^;Cq5>A#f_-H<)j} zauj?A56r6g8v1U)Pip67Vj!YhZI!9aiGqKd$I39B%!Zkawj7KpPuLA2iG{7 z)~Zp)^ZQDh6=(jGRek*?G-^E^(O!UynxczQw*BHo2lKccKpH{Q63ngO!#ec~w;GJM z{r(ZzHmwrmqBiO+i3Uf*&}28MZG>`^MN;VlX5i;;(`0i%4&s`4Af| zFTBz^t&ZLvkXQq5qXyAOGj!)U3B4~LTD|T(rLma%Fs=o-89~PmfgMEY)i7|C{s6yG z`b+%A=^yZ$N*{7L`oeT>Ll~Zl!TQT{nF;0@I@(kS1^HUniY@0t1G zfbiBfZYa}=5=zgZCi-Ul%1|?Bj(X-_2d0XG;qZ)Z^ZzE4_V)Ki2?8??L&2I|#MSbx}9rMcrz4p=$RBOFmuhz|Tg|y8wIQzgXy4$ z*>WGog!Sd}Cx{HHeg^*YF)eqOcEGOXrgRM#ks_;1ldb-Q*XFdkcq}+c+I`d%2ysGd zxRO2!?T0o>Swp)HEK>Fp!Eqd)3!YJ$1_gj075;w}3NA9{c@}&!W=xX32V7}AIgWW= zLkLPY$C~;LT;=@&A^j#OL*2m!$M_u)Qk2TZp4lGAmj(FcNYzC2&p9VsKv#K+gb7Dg zpoJd<4F0O30uA{b4Y_m9*(0E4il4n7AvAtrPb{t4o^$SmjBz~UukLV?J4fSR4H_7K zj>gYfVM?7b#-F3{p9Un1KS$%g7Je9ij>a#38EC!;7V%2_)ckQc>%UV^;dLNIR#o5t zne*7dV8D*w%qk%+Q|Cx>xRB5w{zRH5JsQriOYXJ)SnX7;-dI)OOU*5!^8#1!Mg55? zl9G=Z9hp%_t)4jQB$Dn}#9XgH@z=dlj}g`Kp(_3T_DC}7LTgQbFUI;m4MXnyEz&u5 zTnT2!p!=>fklbHR%BtwQ6w|9G9@o=0;~?WLXodTwnR5L$CkY}lh(98(CRTuD`hUQVg z_v8r72A1GY|9~&(UDA9mysBpbihS6_WRI5qVQaj#whaclVId~r{IJvxsagi!348w=%#kH*+LF^ZC&ED=TZ z?!%lWQC_P}bsJ;2Y0Y$qO3iqY8}~89)Jk8YHOs5vcBscEh^nf)Q8f<(-;nMYXmnan zo$d{GHm0Z3>C943UYk1Ir0ePQUqCmE=n!UgI-Y@FV#42#m@+!;feN?YeviI|@%P)XhT{uje%b9nz$Q!DbzsE~arm@3Qr&L3UEA9XBoMV8 zJaZMPNgMA=7F@K($9N6h?AJWB~9D zzr+AE2t1*XXW%oy_wxf@0|I%Tx(fdr&WFzZKwVIare4E>`V`eJ2$X_aH1#{+d$L2e zfv%tyP2CA-)$jm(GA~r7o^*4?*2o@iJMe1|@ z9E8m;>q)`t-Wz~vk+`+3+iH`{7)fR)wBa&>e70(Rl)g*m*(#fij3J+`x|rqqcf59} zJG7NhThVh>y--{DdZ`*Ff+7eUQ9yHWiQ0oPhja`akRIJo;~`PTDvL9c4S%9$Od5j3 z!IuWPaoIy=aIUb43;sT3#vXhDTEAl0WA3ig%W?&huNcy$-k_WH9A7b{S1P5t+GFn7 zU*tHzsEk>U*$_1P_LzHivm6IXozX!MzC%<8uPpWgh{{MgzOwLcR`fRIfe^&97eHi& zM$6YN7C&%|PT9QLlrzdv*9~xZwaM|-2IswnJH?_l@M_c5Uh|PT6ql+YGgeDZ!1{~H zy{oSN6^tbnMJzqC(YEadOb{=xe&a($$RsHEgzDbiy%9h&G!Vf zJei@`ZG6_z*itzyKLGr*!|a|gHwtHFoWj44IvVA9M4L9krKUWvA(LFgl+^>ku2dn6<4Yl(Uw@KV`oimJ_*lE9=Gn{q;*hB8x z-Z?{V`6*zf?qsU6TT?|7GhK1FZ;vAY=Q%d^1LM)fE)0ZL@f5IsM9f^EYHh34S~1mH zo7LJJYDj4(#MDR(2h-{+PiF<#{2RwuK{()sND2=@1X~YFU=aRQ>J4VVJgGmSvzy z-E9(lmI<~_MSeEzjfsK{Gn~SMg!(g#8)ihp--2k3i7WwDO=eJC&az2w8;$nQgW$*R2?^IGGExFS;tM9 zKT-|NsXA`byokbJ(k13RRPI6O>7o1ZW%Cs{tOSE`l$%S-Ugi@QgA6QK|u5`KgC7M=P<<{OmP)Mkz(v6`LHC}gGs7wb&?2P zG}WZ}xJ?tm*!M9NR{QMT;D=`{9PY~82sSwvIyyjf1ffJl2M)3-I*2Ojxzcj#fDqb!^UmE5@0sPrQTm+7$xutAwe8#Efk!y@zXowUqaQ z;2XGfF-@vaO12**b3!I*L^LgahaH$Dvja0>BgP$=NneQ19hkZf0n3WW*M0_I2WGFO zX!SPR05jTSr7~4S>`hE>QuB%Px8VbKytp7ik%j zlOWHM1ReQR$;1Ny%UAD$E}5OPeU3u;H2zA|-&ghHeV_|~LCH7u<8`3dYe1D4f_)~E zm?)&gU>_sR1nt7C{L9W+xA!&o)q&WaeGK+qy&Jy>qA1U=Tmbk4K$X;O0H$u^ElLlQ z2C)3>+l&D2&59v*x9LDVMc(gD=xu^X(2s)=-2$pXB<0gz_ql^di~g)>sQ!8*>OeiB z>4eD;k`KKG%+V(|>sPt{y7M0Vnk$dM5LAD?Ibq=VgP^IT6UGB~`%j?*SZuGOs`43l z+c;r#8~j?)O2WTjjplPNhPi3=x1CT15~Gj$6QVX^-Wo45?peD%Z0ae1v0T8u4W$3? zE?{2?c9|6$bA>Nkk8-C45Iy#yHJW^S}GY`KFjQU&L7B0Rf?Q!)7X)KGSAlOY&7Cnh^v~8(L)ZdYA=3Z zz|j}BDuM_F!WPmVM|7cb9Pwx^mHVt}{!8+AG{T|qu&_x6AqCXT>~jE}IKKfP*)qGG zvMf3m`Y6UglCAQA%IfDg#ZAROLwnVa;zdr+k|<9L$Z5q)zfZu3kE|aitLeo8nbie8 zZwYaTu@<2cvmYo+Q7D2+%r)TI(=5hYbxh2DAlaHf!RX;K)I?rmoNay!x-AgRCw89& zP0VLO^ZYDm7Bnmi_Fi8iGk8z-I%AV)U z;%i8V%8D<6{Umc~CUKHo0u@d{N07f=4OVw)Gs*KqAPkT*#}H3G&3ODEAm-8b;v~;u zo5_@%m=S-1=z29@cKIb{mtUT{{J42i3UWMl6JbfemW^le5GlY?j+^xw(WgN*bt=c* zcsprGC1xL=RFgqQA>7CJ0;A%jDEIN*P9v zDwJy%--#HAZ)ge!!i>Cgr~3qK*Vc^Ph) zZ+JF=F9MI;db2P7>h8G5$ zBYvSXAScgTn<3W}W?-YJo7xUha5Qf=hx|#jakM+ks9TH1E}K&?B5xeg@Vm7Ui&rPT zMQU`!ZyGkN;3;=J-1daP78`gE1eK>=Ez=b9@h@tffrNj~#2cYKVR%Wi)?QzuJ)QQ_ zIXcVzB7SEPA}*Vw^Q;jKGYnVBb8693Ce!^)rn{-_MV?8TOOsXHKAMS!8$S1$xI$>M zh0uJTmb?WSLEcOu^w$z$6s&n^xM89p4hx}wxe(?Xf+aMy*88CYUSfr&+mYI#~|k!WG||LtCu~m!5rpV6FMECaZNO$H_ydBfq*{Wn2>1*`85!| zVSO*-f6qaPtM1n+aV($uDC&#ANjnhVr`m)}1n`cIHuZOm_BD#XWCABJ+Q(51*5%p? zXhiXULWB7~5dVAbgr8x@EmP;(onA;PaZP9l1xXyRQ=GT zChCERv$g~t8eckR2Y7ncc~tdGde68oshWUq{2TaK77VP~XAr|O2=kt9qiB?=MQdhj zs~j3#t!?|$+1iveqG5%XeUSfswyw}at!+;gHkZ5;UHmcLSfdfqKTv$#7Ha0-8RqLp zw`%6?rs8Ns!#B%y@k@nGExN>1ob@g96`IXLQ~yR2JhRsN4?x0C1rD8L?4o}t0N9uvoo)yfrZmhb&Pz!px1NoL7(+fgehQ-;u#PBNzvYYQA7aYp zI{=lE%DCrNlShtdWweAm4nd#DJSMq_DyWR<<i=kEnSj)yr2;o2%kbkrpm zF-e30N&iY>9WF!kmC1$tlI#kR7 zo}W<}x^qNiz6r1n6?csC2-8IrZ~eCK3Rq`1CW1mU#00ZlC4B=uoL zc?W2VM4Np*peDVz`%$DbjO@r(tHkUP&eK|-hrMx-h?hq8;DdI<4`%NM>v!DToq)Ch zDp5JUD2u@+Y6u5ie9Wf}hjwA1lKw;DFC0r zugpPb(ixe^IZWhKgS)|^K*<+f<``n$53jZb)l7!=#>?Rs2ld@)vmcEreN&n>OeO=; zH>a7+e!TH2eT(?DBGb2~MUj#vJJ&G$c-md1H49tVy09fH_{mnY0zg&+nwWTko{cX~ z1Db>m9Tx2o&+tFfK58P2u(HjJteUG)QakW93C}>P)+(YWkVa*d<<(U$B@F)nMN@v^ zs$t^!FXfeT_>#(?s=ag&cREC&zEHUs)=z569x>%L8QUWk9d*?+8qFu6dpqf65mya9 z8J;99g{4ROQdr`}R0-SjBC^V7Acr&B7ISLgJIJ&KBGt^%x?TBbeiL!-Mjys!y(iG! z7x=zX_=ufq^HG590$W$T;x|~Y_{sj)?uZ9_;fmi7dSTZ6466}(#jkl|z;?xNAPYcC zk=0VzLmGp7a0sl8a4I<=ZCc*3Cg07>p{2y$5&)okS${TM>98z$> z3Oyix^~FCj*ZADVn4EYOKRXD+a&&Qfa8+S!uc(>Y2G zxP8uy(b$pubZ#sRvaV$;cHMIT%-M+~dM3wA0I_V2TuNmSUB3kLc92HTF zP&~*0zT5MI#!$tg_e5nq|kil5!p3*s7Hs@t{ zdOQKngZvbucsK-`#ILsZ+#G{4KQMJOz8200D90-)v)pH(@-1k{+!YBa@ z@4Pws6YwnoZ3?0JU#}_Y!*5V=+c3ZH8nbNTU7n z4KRY12Z6_}P4D2sCd9ccc^Yt!FF_Cxvo!$mzk&COtB|{1vkX_3TX${r`!q1%ZW91S zsDV0QI%nk?K7zg3z%&9`t8ujaUo^vp;rLx`=YLf0 zFPdc15Grcj7iE6?e=7IMrrc?9eSS1?pM@x0QkQ%hmV5FDSmIfCxtC0+DfcE@?vq*W zHqey&r~h5Kw-{!fl>6nFbU)FMOoT+X++)sfY<^HKU zn@IDN>w9sfiLx1jqSi-Ierg-k-S?{Mi_&W7?>fC##j0F;G)j)^y{edg6oh!F<*2oQ zD7))um+NSHKZuD|ZKLU(6q#FaZA){Mi1m@3d{wb83XiXE8FRU*gz|FJEH5{up<)9? zmYXjq3MjL@-1IKS!h2`N?9K=XxI2T*2ne#=ogw}nsVsMA&QlN<3|uo0Vnh^#dVG-JjP6*6{BZM#()K+98R{S3DH+f$kq{8U5@{fPna_1*u|`2UkAD(3Yeg?d;jX=GIA7u{6uNTmHJTy~mHdP5X3 zwV6=u*=Zu6UQx9lR(1-Pi&vO0)-mwoy>RjGyCFk+!Dv>(rAHU`g4BdnHRp){-6QPyrqL*nqmmD*`WUJD z3`U!S;9HIqL+d*fT*?N_1a^kg2Fw%=)R0brmPx6~Dfk#k!CJCDTUXd>i%9($F1=lW z?*>tASEERXkVQRfLLIsj0ig)5=tOlM-}fZ|eVK*i(Z`4cOu_6DISZQ0T%gs5UPoWq z^Q+eo9;N&Chc5a_gT$&k5wXSrgVw$&>d#fZ85&X<1|fp9$;^4}Q78|t1WIyjsnh6} z56-kPCAe zvk9tfy7xlLX|O3a`rHI1W{hir-UR*t6ueA4>b3Zc*~L_QoDmBogZ0LmA$nq||D8Zx zfmn&^&xt_Y{HmeUIiJCL$IOt0Xv9iMC)7jqtvj8BHj|f~DID_35yD6&_H?r7&up@T z_3YA+QLx6Qq(6yt_tu|8;w`CRdtAjF%C;e7(B0)8IZ zxLL*RRa|5-yOcGM;fKf5CWLWiV-)Q^bt%~QK75!;^@y;u3(+{<2S6Sw`xk`5AZ23h zX?oFE#c~6oFG|!pss&$Vhb2a1^SJPWETXaGP}K%5Ek^1Q@TZy@v4OCDF1*#80a`K+ zxWQ33Y#W@h6W|)WHtdD}==v@<(dq$UM{Cf?2{#j;dpl zY1O4G>*8VJSM|JXrYo)f0*z2vh9XM0$ADl>>2@*rxkKN?rjgMs<8)5OIMPPqB|(>J zcWnaQ_3U(mw%4w-?mvMgcKy@3|4uKi*I$eOe7ftp(B14&-wzc#tqz$~F8(FZZ0&4a z{7a2Ssu4Nn47|QJ!z^Z`F9Vt<;xeqCzkxAfF(boy)Q|8Egu0qx6cV9+X4{{}o5p!H zI06#ylSQI7a_(S2Iigh79wx&#qa(0zH0iMjrsjlLW$j@yT*&`6hTWafxeGpAd`Fq# z`UM7KA|{)W7myysd<)s%Xj{w$j*di3o3e|UyMm%7eYcC$u|M|1fag7cl?h0Wa?B*M zh1Wdu31$(u6P`xLd3WHCiyHQ-lgbn_+i3W=ojxa77E`0 z)w$qXT+})l_`CMOslF#8CR~k!#By-`$>=H+J9ez^Ez3erM$~i7Dpc)SOc=>3QuLIR zUrlK|D)UI#rVL-A6~d^LMf6DsiC&0$*G)%gUm&_5F1O5<-h_bd2uh`3EWXk-+R<|wT8{wr4 zWM8H^4|YGw;9GQ>gCO(tYkp`khf89BdqcdtUiUkhwMrj5$Y!c)A}5OJ4?; zYmOtwqT#OtQ5~9Na%td}U-%ilf$ui(v$l9IN!?!rgW)js{HtjGtsD)UGXqALjMJfx z3Gl*%e13}`d$*X24g4le`1v~nQ!q(nF>@OLMnle$&(IuS`O+{f4olr-i_NE8DSM2^ z*o=8kzatsxAQ`Y%>4H%`b(hkMlG{O#`2=9E(co#%Z}yhKo@+=YgXj&|{`cWG!xGaq zs9tQ<>`BeuB}iXRyhHTSku=A>*gV)%o4w!o4>8avO8X-*mfgxcWB&Jt{}khngfSYp z;De@8^xk{3bw_CAvS)lRX&R-!yjOaF-dxICR~|@?R+aP(q=#Q3^z}A(KL9l+dOeqP z=**j_X_5OP{6f% zktE4TZBKZqL_=8G0~q)?xO@YKJhfeqE{Nf?Y&`7lDR^RixbI;2D)&TOdEcLBTOz)B7bZWOZHC}gL>b+7|R zmHiw)xv%L}>pn493(@T2X+e+BsyRX%0X&Wja)gF`1T54V^5SEU4|0wWCnG)Ot%S(k z=4$1Qc^tubX25wPb1v9<#|N3bJwDit5&YwWeHdtu4@xLly$GaScr5cbF}EA%{Iu{b z@$q)(jq`$uoEIF+*m7KOvJs2jSL*5YaUd``0^S0L-hP-lEtu>GJbLo^CK}s+jL1u7 z=sl1k>!((adCfJZ=H}9()@X?u-4?WoDp8}91{$r~$wW67rmAycnrZd(JrOD)ebj8^ zrEpegj%%Dbx-HSN={YqQwGEq|)4Vc-o1Qb=^vq8}v1j3?X9jQpwj5j!*#SeEgSUoY z>2HCS!7FKVB*G(~+DMj~eBT00)Or*S6aN)1+7g3Mm0yN2ge;tFITM0cZHAvu4gCnx zPwZRR2e|qtbT$VOw@dG;2_BC6Oh44GECUk09%YkEG?9c+YXnVQ1Q&gK4y3~{OHcZK z%UcDV_$W}bGu)wM6#W2_hX#<6HXfdvsL=-^9AWM2Kn@2D-Yt0A;VOxP7{7xjWezns z>I_Nb#RY6r=0HT6x}+Kx# zr;v)5f?;X85Z0+$PH{aTBH`Xn&Tze;0UYWAfC&_*9hG9RkMZ&YB`DSPX#A_+kVTTi zPDzw^DXC>0=_;w!vW{q}RQEol!qcET@do`~*r4Aj&82WXj|4$f7NF-wkS%h`P(4|? z8fF2h20f?Il`6Jk^rA_3yIs=4yaYnh>dnc}l1SV~T?_Nu_iSAgXBp#c2DNV6ICCJA zv?)Il-wr9I|bohpKFhCgC54CLtzGIT>lX}viv+SHKAUK)0mC_1@9 z<@fNH?~-$0Eeq&*aGrJq&K!wlSK|jW7>+6Mn>68vBeAR*iOKW^ClGW*e4Sg$D9