From fe3a79ab1d2d79e56f8cb93c0ad87fac20425a3c Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 06:25:43 +0800 Subject: [PATCH 1/9] feat(qt): wire a real history scrollbar to the source reading anchor Add a vertical Qt scrollbar to the main screen. Its value is only the current display page top; navigation goes through HistoryViewport's rank/identity anchor, including new output while reading, bottom-follow, eviction clamping, resize and Find reveal. The scrollbar extent enters the existing Qt->core->ConPTY grid width path when visible; no second resize/store model is added. Extend the formal history regression with real mouse drag/page and scrollbar key events, exact source top/page identity, new-output anchor stability, narrow/restore, Find synchronization and two-session isolation. Fixed-layout legacy tests explicitly disable the scrollbar reservation in their own process so their pre-existing 80x24 fixtures remain unchanged; the scrollbar-enabled product path is covered by the history test. --- src/qt/find_test.cpp | 25 +++-- src/qt/hidden_retention_test.cpp | 1 + src/qt/history_navigation_test.cpp | 136 +++++++++++++++++++++++++++- src/qt/resize_smoke.cpp | 1 + src/qt/selection_migration_test.cpp | 1 + src/qt/terminal_modes_test.cpp | 1 + src/qt/terminal_widget.hpp | 59 +++++++++++- 7 files changed, 210 insertions(+), 14 deletions(-) diff --git a/src/qt/find_test.cpp b/src/qt/find_test.cpp index 38a82fcc..cd274d28 100644 --- a/src/qt/find_test.cpp +++ b/src/qt/find_test.cpp @@ -91,6 +91,7 @@ int main(int argc, char** argv) { std::cerr << "find test requires its owned Job supervisor\n"; return 2; } QApplication app(argc, argv); + termforge::qt::TerminalWidget::setHistoryScrollbarEnabledForTest(false); QApplication::setQuitOnLastWindowClosed(false); try { QElapsedTimer lifecycle; lifecycle.start(); @@ -293,14 +294,16 @@ int main(int argc, char** argv) { await([&] { return third.screen().retained_size() >= 78; }, third, lifecycle, "many-hit child output missing"); third_window.activateWindow(); third_terminal.setFocus(); QTest::qWait(30); - third_terminal.set_find_timer_interval_for_test(25); + third_terminal.set_find_timer_interval_for_test(1000); QTest::keyClick(&third_terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); auto* third_query = third_window.findChild(QStringLiteral("findQuery")); require(third_query && third_query->hasFocus(), "third Find query did not focus"); type_ascii(third_query, QStringLiteral("ZZ")); - QTest::qWait(35); require(third_terminal.find_search_status_for_test() == termforge::SearchStatus::running, - "long scan did not remain running after one bounded stage"); + "long scan did not expose an initial Searching stage"); + third_terminal.step_find_once_for_test(); // timing coordination only; same product step + require(third_terminal.find_search_status_for_test() == termforge::SearchStatus::running, + "long scan completed before the bounded first stage"); require(third_terminal.find_match_count_for_test() > 0, "long scan exposed no partial matches"); require(third_terminal.find_status_for_test().startsWith(QStringLiteral("Searching")), @@ -315,10 +318,11 @@ int main(int argc, char** argv) { require(third_terminal.find_current_index_for_test() != partial_before || third_terminal.find_status_for_test() == QStringLiteral("No further scanned match"), "running-scan Next neither advanced nor reported the scanned-prefix boundary"); - await([&] { - const auto state = third_terminal.find_search_status_for_test(); - return state == termforge::SearchStatus::complete || state == termforge::SearchStatus::capped; - }, third, lifecycle, "long scan did not complete"); + for (int guard = 0; guard < 20 && + (third_terminal.find_search_status_for_test() == termforge::SearchStatus::running); ++guard) + third_terminal.step_find_once_for_test(); + require(third_terminal.find_search_status_for_test() == termforge::SearchStatus::complete, + "long scan did not complete after bounded steps"); require(third_terminal.find_match_count_for_test() == 52, "long scan hit count differs"); require(third_terminal.find_status_for_test().contains(QStringLiteral("52 matches")), "long scan completion status differs"); @@ -326,8 +330,11 @@ int main(int argc, char** argv) { // Repeated short literals exceed 1024 in retained rows; the existing cap // is asserted through the real query entry, including finite wrap. type_ascii(third_query, QStringLiteral("AB")); - await([&] { return third_terminal.find_search_status_for_test() == termforge::SearchStatus::capped; }, - third, lifecycle, "1024-result cap was not reached through real child output"); + for (int guard = 0; guard < 20 && + third_terminal.find_search_status_for_test() == termforge::SearchStatus::running; ++guard) + third_terminal.step_find_once_for_test(); + require(third_terminal.find_search_status_for_test() == termforge::SearchStatus::capped, + "1024-result cap was not reached through real child output"); require(third_terminal.find_match_count_for_test() == 1024, "capped result count is not 1024"); require(third_terminal.find_status_for_test().contains(QStringLiteral("capped")), "capped status lacks the truncation notice"); diff --git a/src/qt/hidden_retention_test.cpp b/src/qt/hidden_retention_test.cpp index 064ff562..173af121 100644 --- a/src/qt/hidden_retention_test.cpp +++ b/src/qt/hidden_retention_test.cpp @@ -252,6 +252,7 @@ int main(int argc, char** argv) { return 2; } QApplication app(argc, argv); + termforge::qt::TerminalWidget::setHistoryScrollbarEnabledForTest(false); QApplication::setQuitOnLastWindowClosed(false); QByteArray child_log_path; try { diff --git a/src/qt/history_navigation_test.cpp b/src/qt/history_navigation_test.cpp index e100eebc..66df2f24 100644 --- a/src/qt/history_navigation_test.cpp +++ b/src/qt/history_navigation_test.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -91,13 +93,25 @@ void resize_public(TerminalWidget& terminal, SessionModel& model, int columns, i QElapsedTimer& lifecycle) { const QFontMetricsF metrics(terminal.font()); const int cell_width = qCeil(metrics.horizontalAdvance(QLatin1Char('M'))); const int cell_height = qCeil(metrics.height()) + 2; - terminal.resize(16 + columns * cell_width, 24 + (rows + 1) * cell_height); + terminal.resize(16 + terminal.history_scrollbar_extent_for_test() + columns * cell_width, 24 + (rows + 1) * cell_height); await([&] { const auto state = model.resize_state(); return state.requested_columns == columns && state.requested_rows == rows && !state.pending && state.transport_columns == columns && state.transport_rows == rows; }, model, lifecycle, "public Qt history resize did not finish"); } +QPoint scrollbar_slider_center(QScrollBar* bar) { + QStyleOptionSlider option; + option.initFrom(bar); + option.orientation = bar->orientation(); + return bar->style()->subControlRect(QStyle::CC_ScrollBar, &option, QStyle::SC_ScrollBarSlider, bar).center(); +} +void drag_scrollbar(QScrollBar* bar, int y) { + const QPoint start = scrollbar_slider_center(bar); + QTest::mousePress(bar, Qt::LeftButton, Qt::NoModifier, start); + QTest::mouseMove(bar, QPoint(start.x(), y)); + QTest::mouseRelease(bar, Qt::LeftButton, Qt::NoModifier, QPoint(start.x(), y)); +} bool start_and_open(SessionModel& model, TerminalWidget& terminal, QWidget& window, const wchar_t* executable, const QString& workspace, QElapsedTimer& lifecycle) { window.resize(1200, 900); @@ -164,8 +178,6 @@ int main(int argc, char** argv) { first_window.activateWindow(); first_terminal.setFocus(); QTest::qWait(20); - require(first_terminal.hasFocus(), "first history terminal did not regain focus"); - capture(first_terminal, QStringLiteral("history-bottom")); // Isolated native-wheel contract cases. Production queries // SPI_GETWHEELSCROLLLINES per wheel event; the diagnostic override @@ -338,6 +350,118 @@ int main(int argc, char** argv) { require(!page_contains(first_terminal, "TF_H_180"), "new output replaced the reading page"); capture(first_terminal, QStringLiteral("history-new-output")); + // Public vertical scrollbar: it reserves grid width through the existing + // Qt->core->ConPTY resize path; its value is a display top, never a + // durable source identity. Mouse drag/page/step and keys are exercised. + auto* bar = first_terminal.history_scrollbar_for_test(); + require(bar && bar->isVisible() && bar->maximum() > 0, "history scrollbar did not appear"); + const QFontMetricsF metrics(first_terminal.font()); + const int cell_width = (std::max)(1, qCeil(metrics.horizontalAdvance(QLatin1Char('M')))); + const int expected_columns = std::clamp( + (first_terminal.width() - 16 - first_terminal.history_scrollbar_extent_for_test()) / cell_width, 1, 500); + require(first.screen().width() == expected_columns, + "scrollbar extent did not enter the Qt/core grid width"); + require(first_terminal.history_state().maximum_top == static_cast(bar->maximum()), + "scrollbar range does not reflect HistoryViewport maximum_top"); + + // Keyboard top: exact source row is the oldest retained identity. + bar->setFocus(); + QTest::keyClick(bar, Qt::Key_Home); + await([&] { return first_terminal.history_state().top == 0; }, first, lifecycle, + "scrollbar Home did not reach the source top"); + require(!first_terminal.history_following() && bar->value() == 0, + "scrollbar Home state/value diverged"); + require(first_terminal.history_anchor() == first.screen().retained_line(0).id, + "scrollbar value was treated as a source identity instead of top"); + + // Actual mouse drag to the middle: value and source top agree; the page + // starts at the source row identified by the anchor. + drag_scrollbar(bar, bar->height() / 2); + await([&] { return !first_terminal.history_following() && bar->value() > 0; }, first, lifecycle, + "scrollbar mouse drag did not enter history"); + auto state = first_terminal.history_state(); + require(static_cast(bar->value()) == state.top, + "scrollbar value and HistoryViewport top diverged after drag"); + require(first_terminal.history_anchor() == first.screen().retained_line(state.top).id, + "scrollbar drag anchor does not match the displayed source top"); + const auto page = first_terminal.painted_page(); + capture(first_terminal, QStringLiteral("history-scrollbar")); + require(page && !page->mapping.sources().empty() && + page->mapping.sources().front().source.begin.line_id == + first.screen().retained_line(state.top).id, + "painted page does not start at the scrollbar-selected source row"); + + // Actual page-groove click advances by the page step without treating + // the new value as a durable identity. + const int before_page = bar->value(); + QStyleOptionSlider page_option; + page_option.initFrom(bar); + page_option.orientation = bar->orientation(); + const QRect slider = bar->style()->subControlRect( + QStyle::CC_ScrollBar, &page_option, QStyle::SC_ScrollBarSlider, bar); + QTest::mouseClick(bar, Qt::LeftButton, Qt::NoModifier, + QPoint(bar->width() / 2, (std::min)(bar->height() - 2, slider.bottom() + 5))); + await([&] { return bar->value() != before_page; }, first, lifecycle, + "scrollbar page-groove click did not move the value"); + state = first_terminal.history_state(); + require(static_cast(bar->value()) == state.top, + "page-groove click value/top diverged"); + + // New child output while reading keeps the same source anchor/top and + // updates the scrollbar range without following. + const uint64_t reading_anchor = first_terminal.history_anchor(); + await([&] { return has_text(first.screen(), "TF_H_180"); }, first, lifecycle, + "spontaneous output during scrollbar reading did not arrive"); + const auto reading_state = first_terminal.history_state(); + require(!first_terminal.history_following() && + first_terminal.history_anchor() == reading_anchor, + "new output moved the scrollbar reading anchor"); + require(static_cast(bar->value()) == reading_state.top, + "scrollbar value did not follow the reading top after new output"); + + // Actual scrollbar End key (same public focus target) returns to the + // bottom; the mouse page/drag paths above already exercised the pointer. + QTest::keyClick(bar, Qt::Key_End); + await([&] { return first_terminal.history_following(); }, first, lifecycle, + "scrollbar End did not resume following"); + require(bar->value() == bar->maximum(), "scrollbar bottom value is not maximum"); + + // Narrow/restore: the scrollbar-selected source anchor survives and the + // bar value tracks the new display top. + drag_scrollbar(bar, bar->height() / 2); + await([&] { return !first_terminal.history_following(); }, first, lifecycle, + "scrollbar drag before resize did not enter reading"); + const uint64_t resize_anchor = first_terminal.history_anchor(); + resize_public(first_terminal, first, 60, 20, lifecycle); + require(first_terminal.history_anchor() == resize_anchor, + "scrollbar reading anchor changed after narrow resize"); + require(static_cast(bar->value()) == first_terminal.history_state().top, + "scrollbar value/top diverged after narrow resize"); + resize_public(first_terminal, first, 80, 24, lifecycle); + require(first_terminal.history_anchor() == resize_anchor, + "scrollbar reading anchor changed after restored resize"); + require(static_cast(bar->value()) == first_terminal.history_state().top, + "scrollbar value/top diverged after restored resize"); + + // Find reveal must synchronize the scrollbar to the revealed source page. + first_terminal.open_find_for_test(); + first_terminal.set_find_query_for_test(QStringLiteral("TF_H_140")); + await([&] { return first_terminal.find_current_range_for_test().has_value(); }, first, lifecycle, + "Find did not reveal a history match"); + require(!first_terminal.history_following() && + static_cast(bar->value()) == first_terminal.history_state().top, + "Find reveal did not synchronize the scrollbar"); + first_terminal.close_find_for_test(); + QTest::keyClick(&first_terminal, Qt::Key_End, Qt::ControlModifier | Qt::ShiftModifier); + await([&] { return first_terminal.history_following(); }, first, lifecycle, + "Ctrl+Shift+End after Find did not restore following"); + require(second_terminal.history_following() && second_terminal.history_scrollbar_for_test()->value() == + second_terminal.history_scrollbar_for_test()->maximum(), + "second session scrollbar was affected by first session reading"); + require(first_terminal.hasFocus(), "first history terminal did not regain focus"); + capture(first_terminal, QStringLiteral("history-bottom")); + + // Ctrl+Shift+End returns to the live bottom; Ctrl+Shift+Home is top. QTest::keyClick(&first_terminal, Qt::Key_End, Qt::ControlModifier | Qt::ShiftModifier); await([&] { return first_terminal.history_following(); }, first, lifecycle, @@ -401,7 +525,11 @@ int main(int argc, char** argv) { std::cout << "{\"schema\":\"termforge-qt-history-navigation-v1\",\"status\":\"passed\"," "\"sessions\":2,\"wheel\":true,\"wheel_native\":true,\"wheel_zero\":true,\"wheel_page\":true,\"wheel_page_height\":true,\"wheel_query_failure\":true,\"wheel_setting_update\":true,\"anchor_resize\":true,\"output_during_read\":true," "\"top_bottom\":true,\"eviction\":true,\"ed3\":true,\"ris\":true," - "\"ordinary_input_bottom\":true,\"offscreen_only\":true,\"presented_done\":null}\n"; + "\"ordinary_input_bottom\":true,\"scrollbar_reserved_grid\":true," + "\"scrollbar_mouse_drag\":true,\"scrollbar_page_step\":true," + "\"scrollbar_anchor_sync\":true,\"scrollbar_resize\":true," + "\"scrollbar_find_sync\":true,\"scrollbar_sessions\":true," + "\"offscreen_only\":true,\"presented_done\":null}\n"; return 0; } catch (const std::exception& error) { std::cerr << "Qt history navigation: " << error.what() << '\n'; diff --git a/src/qt/resize_smoke.cpp b/src/qt/resize_smoke.cpp index 7a21bc9c..5114953d 100644 --- a/src/qt/resize_smoke.cpp +++ b/src/qt/resize_smoke.cpp @@ -394,6 +394,7 @@ int main(int argc, char** argv) { count != sizeof(gate) || std::string_view(gate, count) != "GO\n" || !IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) return 2; QApplication app(argc, argv); + termforge::qt::TerminalWidget::setHistoryScrollbarEnabledForTest(false); QApplication::setQuitOnLastWindowClosed(false); try { QTemporaryDir workspace; diff --git a/src/qt/selection_migration_test.cpp b/src/qt/selection_migration_test.cpp index 52f047e1..db6112ee 100644 --- a/src/qt/selection_migration_test.cpp +++ b/src/qt/selection_migration_test.cpp @@ -163,6 +163,7 @@ int main(int argc, char** argv) { std::cerr << "selection test requires its owned Job supervisor\n"; return 2; } QApplication app(argc, argv); + termforge::qt::TerminalWidget::setHistoryScrollbarEnabledForTest(false); QApplication::setQuitOnLastWindowClosed(false); try { QElapsedTimer lifecycle; lifecycle.start(); diff --git a/src/qt/terminal_modes_test.cpp b/src/qt/terminal_modes_test.cpp index 88daecf9..5ca78dc6 100644 --- a/src/qt/terminal_modes_test.cpp +++ b/src/qt/terminal_modes_test.cpp @@ -362,6 +362,7 @@ int main(int argc, char** argv) { count != sizeof(gate) || std::string_view(gate, count) != "GO\n" || !IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) return 2; QApplication app(argc, argv); + termforge::qt::TerminalWidget::setHistoryScrollbarEnabledForTest(false); QApplication::setQuitOnLastWindowClosed(false); try { QElapsedTimer lifecycle; lifecycle.start(); diff --git a/src/qt/terminal_widget.hpp b/src/qt/terminal_widget.hpp index 8a1a6d5a..4c0a6258 100644 --- a/src/qt/terminal_widget.hpp +++ b/src/qt/terminal_widget.hpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +59,11 @@ class TerminalWidget final : public QWidget { // Every widget owns its page and selection; row identities are never shared // with another session. Hits resolve the last painted page, not a newer row. HistoryViewport viewport_; + QScrollBar* history_scrollbar_ = nullptr; + int scrollbar_extent_ = 0; + bool updating_scrollbar_ = false; + bool scrollbar_reserved_ = false; + static inline bool scrollbar_enabled_for_test_ = true; std::optional painted_page_; // First painted display row inside the page and the page-relative display // caret. The protocol cursor itself is never moved by this widget. @@ -99,14 +107,40 @@ class TerminalWidget final : public QWidget { void synchronize_grid() { painted_page_.reset(); selection_anchor_.reset(); - const int columns = std::clamp((width() - margin * 2) / cell_width_, 1, 500); + const int available = width() - margin * 2 - (scrollbar_reserved_ ? scrollbar_extent_ : 0); + const int columns = std::clamp(available / cell_width_, 1, 500); const int rows = std::clamp((height() - margin * 3 - cell_height_) / cell_height_, 1, 200); model_.resize(columns, rows); } + void update_scrollbar() { + if (!history_scrollbar_) return; + if (!scrollbar_enabled_for_test_) { + history_scrollbar_->hide(); + if (scrollbar_reserved_) { scrollbar_reserved_ = false; synchronize_grid(); } + return; + } + const auto& screen = model_.screen(); + const auto state = viewport_.reconcile(screen); + updating_scrollbar_ = true; + history_scrollbar_->setRange(0, static_cast((std::min)(state.maximum_top, size_t{INT_MAX}))); + history_scrollbar_->setPageStep((std::max)(1, screen.height())); + history_scrollbar_->setSingleStep(1); + history_scrollbar_->setValue(static_cast((std::min)(state.top, size_t{INT_MAX}))); + history_scrollbar_->setGeometry(width() - margin - scrollbar_extent_, margin, + scrollbar_extent_, (std::max)(1, height() - margin * 2)); + const bool should_reserve = state.maximum_top > 0; + history_scrollbar_->setVisible(should_reserve); + updating_scrollbar_ = false; + if (should_reserve != scrollbar_reserved_) { + scrollbar_reserved_ = should_reserve; + synchronize_grid(); + } + } void resizeEvent(QResizeEvent* event) override { QWidget::resizeEvent(event); if (find_panel_) find_panel_->setGeometry(margin, margin, (std::max)(1, width() - margin * 2), 62); synchronize_grid(); + update_scrollbar(); } void showEvent(QShowEvent* event) override { QWidget::showEvent(event); @@ -285,6 +319,7 @@ class TerminalWidget final : public QWidget { const int64_t step = wheel_step_rows(); if (step > 0) { viewport_.scroll_by(model_.screen(), -static_cast(detents) * step); + update_scrollbar(); update(); } } @@ -292,10 +327,12 @@ class TerminalWidget final : public QWidget { } void scroll_history_top() { viewport_.scroll_to(model_.screen(), 0); + update_scrollbar(); update(); } void scroll_history_bottom() { viewport_.bottom(model_.screen()); + update_scrollbar(); update(); } static uint32_t navigation_virtual_key(int key) { @@ -586,6 +623,7 @@ class TerminalWidget final : public QWidget { if (const auto nav = find_search_.navigate(document, SearchDirection::next, find_anchor(), true)) { find_current_ = nav->match; find_current_index_ = nav->index; viewport_.reveal(model_.screen(), nav->match.range.begin.line_id); + update_scrollbar(); } } update(); @@ -605,6 +643,7 @@ class TerminalWidget final : public QWidget { find_current_ = nav->match; find_current_index_ = nav->index; find_last_wrapped_ = nav->wrapped; viewport_.reveal(model_.screen(), nav->match.range.begin.line_id); + update_scrollbar(); set_find_status(QStringLiteral("%1 / %2%3").arg(nav->index + 1) .arg(find_search_.matches().size()).arg(nav->wrapped ? QStringLiteral(" (wrapped)") : QString())); update(); @@ -784,6 +823,19 @@ class TerminalWidget final : public QWidget { resize(sizeHint()); grid_ready_ = true; clipboard_ = std::make_unique(*this); + history_scrollbar_ = new QScrollBar(Qt::Vertical, this); + history_scrollbar_->setObjectName(QStringLiteral("historyScrollbar")); + scrollbar_extent_ = style()->pixelMetric(QStyle::PM_ScrollBarExtent); + if (scrollbar_extent_ <= 0) scrollbar_extent_ = 14; + history_scrollbar_->hide(); + connect(history_scrollbar_, &QScrollBar::valueChanged, this, [this](int value) { + if (updating_scrollbar_) return; + const auto& screen = model_.screen(); + const auto state = viewport_.reconcile(screen); + if (value >= static_cast(state.maximum_top)) viewport_.bottom(screen); + else viewport_.scroll_to(screen, static_cast((std::max)(0, value))); + update(); + }); build_find_panel(); model_.on_changed = [this] { refresh_session(); }; } @@ -803,6 +855,7 @@ class TerminalWidget final : public QWidget { // Keep the display page and caret current before Qt asks the input // method for geometry, not only on the next paint. if (grid_ready_) refresh_painted_page(model_.screen()); + update_scrollbar(); update(); } // Diagnostic observation of the owned display chain; tests only read it and @@ -816,6 +869,9 @@ class TerminalWidget final : public QWidget { wheel_setting_override_ = setting; } ViewportState history_state() { return viewport_.reconcile(model_.screen()); } + static void setHistoryScrollbarEnabledForTest(bool enabled) { scrollbar_enabled_for_test_ = enabled; } + QScrollBar* history_scrollbar_for_test() { return history_scrollbar_; } + int history_scrollbar_extent_for_test() const { return scrollbar_extent_; } bool history_following() const { return viewport_.following(); } uint64_t history_anchor() const { return viewport_.anchor(); } // Internal selection extraction for the Qt regression and future copy UI. @@ -835,6 +891,7 @@ class TerminalWidget final : public QWidget { bool find_last_wrapped_for_test() const { return find_last_wrapped_; } void set_find_timer_interval_for_test(int milliseconds) { find_interval_ms_ = (std::max)(0, milliseconds); } SearchStatus find_search_status_for_test() const { return find_search_.status(); } + void step_find_once_for_test() { step_find(); } std::optional find_current_range_for_test() const { return find_current_ ? std::optional{find_current_->range} : std::nullopt; } From 90e72860ef4114686ff53f6ad4d799602b54ec45 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 06:26:31 +0800 Subject: [PATCH 2/9] docs(qt): record the history scrollbar and source-anchor slice --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 +- .../2026-11-07-qt-history-scrollbar/README.md | 25 ++++++++ .../screens/history-bottom.png | Bin 0 -> 22534 bytes .../screens/history-narrowed.png | Bin 0 -> 23437 bytes .../screens/history-new-output.png | Bin 0 -> 25527 bytes .../screens/history-scrollbar.png | Bin 0 -> 30355 bytes .../screens/history-scrolled.png | Bin 0 -> 24133 bytes .../validation.json | 59 ++++++++++++++++++ src/qt/README.md | 2 + 10 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-bottom.png create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-narrowed.png create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-new-output.png create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-scrollbar.png create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-scrolled.png create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index efe2e4e1..0b3b86ec 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -23,6 +23,7 @@ | Qt 正式路径有限历史浏览与源阅读锚点(#29 切片) | [真实 Qt 事件、受控 child 与截图](../experiments/shell/results/2026-11-05-qt-history-anchor/README.md):滚轮离开底部、历史页命中同一源行、窄/高 resize 与阅读中新输出保持锚点、`Ctrl+Shift+Home/End`、普通 Qt 输入回底、淘汰/ED3/RIS 失效钳制、两会话隔离;Qt 正式路径 8/8 | 单屏 offscreen、固定 80×24/60×24、短 ASCII 与有界历史;滚动条、PageUp/PageDown 历史映射、粘贴回底、阅读期间选择/复制/Find UI、物理呈现/Unicode/性能未覆盖;行为来自 R1 产品合同而非新 WindTerm oracle;#27/#29 不关闭 | | Qt 正式路径恢复 Windows 原生滚轮设置(#29 收尾) | [原生合同、注入回归与边界](../experiments/shell/results/2026-11-05-qt-wheel-native/README.md):每个真实滚轮事件读取 `SPI_GETWHEELSCROLLLINES`,0 禁用、正整数行数、`WHEEL_PAGESCROLL` 页步长=可见高度−1 至少1,查询失败回退3行;注入覆盖原生/0/3/页标记/半 detent/页高 24→20/查询失败/设置更新,真实 Qt+受控 child 历史与锚点回归 8/8 | 仅 Windows 11 x64 offscreen Qt 6.10.3;不修改宿主设置;页步长为 R1 产品合同非 WindTerm oracle;查询失败分支由注入验证而非真实策略变更;滚动条/翻页键/粘贴/呈现/DPI/Unicode/性能未覆盖;#317 待决、#27/#29 不关闭 | | Qt 历史/重排页选择源位置迁移(#29 切片) | [真实 Qt 鼠标/历史浏览、受控 child 与截图](../experiments/shell/results/2026-11-06-qt-selection-migration/README.md):反向跨软续行、部分列+硬行边界、60×20 重排/恢复、浏览页变化、无关输出、真实淘汰/ED3/RIS 失效、双会话隔离;字面文本与高亮列数独立核对,Qt 正式路径 9/9 | 单屏 offscreen、固定短 ASCII/单一软续行;宽字/结合字选择列、系统剪贴板/复制 UI、粘贴、选词选行、自动滚动、Find、物理呈现/DPI/其他尺寸/性能未覆盖;内部提取≠系统剪贴板;旧失败/A/B/C/#27/#29 状态不变 | +| Qt 历史滚动条与源阅读锚点(#29 切片) | [实现、真实鼠标/键盘与截图](../experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md):真实 QScrollBar 拖动/页区/Home/End,值仅显示页 top,经 HistoryViewport 源锚点定位;覆盖新输出锚点、回底、缩窄恢复、Find 同步、双会话;Qt 正式路径 10/10 | 离屏 Qt;不覆盖物理呈现/DPI、TUI/备用屏、自动滚动/选词选行/持久化/性能;滚动条+child 尺寸独立 ACK 仍为边界;不代表 #29/#30/R2 完成 | | Qt 有界系统复制入口(#29/#30 切片) | [实现、fake 入口回归与 hosted 阻塞](../experiments/shell/results/2026-11-06-qt-system-copy/README.md):Ctrl+Shift+C/实际 Copy 菜单接入 CF_UNICODETEXT,空/失效/容量/失败分列;本地 fake writer 入口测试通过,Qt 正式路径 9/9;hosted-only CF 入口通过(QPA/offscreen 分层) | **系统 CF 已在一次性 GitHub-hosted Windows run `42160062253` 分层验证**:offscreen 注入 owner 与 Windows QPA 默认 writer/真实控件 HWND 均独立读回键盘/实际菜单/空 sentinel/占用失败/释放后成功/淘汰;首次 offscreen winId 非 HWND 的 `OpenClipboard:1400` 只修正测试 owner,不改产品;宽字/结合字复制列、占用重试、粘贴、Find、物理呈现/性能未覆盖;不把 offscreen 缓存当 CF;旧 B/CD/A/B/C/#27/#29/#30 状态不变 | | Qt 有界字面量 Find(#29 切片) | [实现、本地确定性 child 与截图](../experiments/shell/results/2026-11-06-qt-find/README.md):Ctrl+Shift+F/查询/Previous/Next/Close,复用 TextSearch 的 4096 字节/1024 结果/字面量/ASCII case folding;源位置经 ViewMapping 在正常主屏与历史页高亮导航;本地 Qt 正式路径 10/10(qt_find 1/1) | 固定 ASCII 样例、离屏 Qt;中文/NFD 只声明字面标量匹配,不宣称字素/glyph 等价;不含选词/选行、滚动条/翻页键历史、TUI/备用屏 Find、物理呈现/DPI/性能阈值;不代表 #29/#30/R2 完成 | | 正式 Qt / 固定 ConPTY 固定 Unicode 样本贯通呈现与独立接收(#30 切片) | [链路、样本与实测](../experiments/shell/results/2026-11-04-qt-unicode-presentation/README.md):新增 `qt_unicode_render`,5 条固定样本经公开 Qt 事件→真实 child `ReadConsoleW` 精确接收→同字节 CR→CRLF 回显→核心解析→`TerminalWidget` 离屏抓图;receiver/echo SHA-256 与源字节逐字节一致,CJK 实测 fallback `Alibaba PuHuiTi`、emoji `Segoe UI Emoji`(advance 21>span 18 但无裁剪参考墨迹 3..17 未越格),结合标 `marks=1`,第二条空会话 0 字节且无样本文字;Qt 正式路径 6/6 | U+200D/U+FE0F 仍被 recovery parser 替换为 U+FFFD(只记录,不批准新聚类/宽度策略);offscreen + 宿主 fallback 依赖、`presented_done=null`,不覆盖物理呈现/原生 IME/真实 DPI/完整字素或 #30/#155 验收 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 96ebad97..589dbd13 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -23,7 +23,7 @@ | 新路线迁移入口 | 当前状态与边界 | 目标差距 | 路标 | |---|---|---|---| -| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差。Qt 已接入有限历史浏览:滚轮与 `Ctrl+Shift+Home/End` 通过同一 `HistoryViewport` 源锚点浏览保留历史,窄/高 resize、新输出、淘汰/ED3/RIS 下的锚点规则由真实 Qt 事件和受控 child 回归核对([固定结果](../experiments/shell/results/2026-11-05-qt-history-anchor/README.md));Windows 滚轮直接读取 `SPI_GETWHEELSCROLLLINES`,恢复 0 禁用、正整数行数与 `WHEEL_PAGESCROLL` 页步长,查询失败按 R1 回退 3 行([原生滚轮结果](../experiments/shell/results/2026-11-05-qt-wheel-native/README.md));历史/软续接/显示重排页的选区源位置迁移已端到端覆盖(正反/部分列/跨软硬行、重排/浏览/无关输出、淘汰/ED3/RIS 失效、双会话隔离;内部提取非系统剪贴板,见[固定结果](../experiments/shell/results/2026-11-06-qt-selection-migration/README.md));系统复制入口已接入 Ctrl+Shift+C/实际 Copy 菜单的有界 CF_UNICODETEXT 写入,并在 hosted Windows CI 以独立 CF 读回验证键盘/实际菜单/空选区/占用失败与释放后成功/淘汰 sentinel(见[系统复制结果](../experiments/shell/results/2026-11-06-qt-system-copy/README.md));正式 Qt Find 已接 Ctrl+Shift+F/查询/上一项/下一项/关闭,复用 TextSearch 的 4096 字节/1024 结果/字面量/ASCII case folding,并通过源位置+ViewMapping 在正常主屏与历史页高亮导航(本地确定性 child 10/10,见[Find 结果](../experiments/shell/results/2026-11-06-qt-find/README.md);不含选词/选行/物理呈现/性能阈值);滚动条/翻页键历史与粘贴回底未迁移 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据(已有固定**写后端点**样本:一次动作 91×20→61×14 后执行 CUP 7;2+DQ,观测 scroll 5、DSR 7;4、62 字符行;无 pre-write 原版正文,裁剪归因未知;见[双轴结果](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md))、完整 reflow、Qt 滚动条/翻页键历史与自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | +| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差。Qt 已接入有限历史浏览:滚轮与 `Ctrl+Shift+Home/End` 通过同一 `HistoryViewport` 源锚点浏览保留历史,窄/高 resize、新输出、淘汰/ED3/RIS 下的锚点规则由真实 Qt 事件和受控 child 回归核对([固定结果](../experiments/shell/results/2026-11-05-qt-history-anchor/README.md));Windows 滚轮直接读取 `SPI_GETWHEELSCROLLLINES`,恢复 0 禁用、正整数行数与 `WHEEL_PAGESCROLL` 页步长,查询失败按 R1 回退 3 行([原生滚轮结果](../experiments/shell/results/2026-11-05-qt-wheel-native/README.md));历史/软续接/显示重排页的选区源位置迁移已端到端覆盖(正反/部分列/跨软硬行、重排/浏览/无关输出、淘汰/ED3/RIS 失效、双会话隔离;内部提取非系统剪贴板,见[固定结果](../experiments/shell/results/2026-11-06-qt-selection-migration/README.md));系统复制入口已接入 Ctrl+Shift+C/实际 Copy 菜单的有界 CF_UNICODETEXT 写入,并在 hosted Windows CI 以独立 CF 读回验证键盘/实际菜单/空选区/占用失败与释放后成功/淘汰 sentinel(见[系统复制结果](../experiments/shell/results/2026-11-06-qt-system-copy/README.md));正式 Qt Find 已接 Ctrl+Shift+F/查询/上一项/下一项/关闭,复用 TextSearch 的 4096 字节/1024 结果/字面量/ASCII case folding,并通过源位置+ViewMapping 在正常主屏与历史页高亮导航(本地确定性 child 10/10,见[Find 结果](../experiments/shell/results/2026-11-06-qt-find/README.md);不含选词/选行/物理呈现/性能阈值);历史滚动条已接真实 QScrollBar/源阅读锚点(见[滚动条结果](../experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md)),翻页键历史与粘贴回底未迁移 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据(已有固定**写后端点**样本:一次动作 91×20→61×14 后执行 CUP 7;2+DQ,观测 scroll 5、DSR 7;4、62 字符行;无 pre-write 原版正文,裁剪归因未知;见[双轴结果](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md))、完整 reflow、Qt 滚动条/翻页键历史与自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | 自主 C++ 核心已具备双缓冲与限定 `?1049`、DECAWM、DECOM、DECCKM/DECTCEM、每屏 DECSTBM 边距、字符/行插删、ED3/RIS、DSR 6n 和部分 SGR 状态。依据分别见 [1049](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[DECAWM](../experiments/core/results/2026-10-29-cpp-decawm/README.md)及[支持表](../experiments/core/README.md)。[原版每屏 origin 对照](../research/ui/results/2026-11-02-mode-state-reference/README.md)确认备用屏进入时关闭 origin、主屏返回恢复自身状态及固定样本的 margin 下界;核心已改为[每屏保存 origin](../experiments/core/results/2026-11-02-origin-frame/README.md)。区域 LF、IL/DL 后的精确行序,以及[区域自动换行续接、RIS/ED3 历史结构发布](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)已有修正。完整网格/样式、resize 和运输交叉仍由 #27/#29 验收,不能据此声明完整 TUI 通过。 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md b/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md new file mode 100644 index 00000000..2e35d807 --- /dev/null +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md @@ -0,0 +1,25 @@ +# 正式 Qt:历史滚动条与源阅读锚点 + +本切片把既有 Win32 滚动条历史浏览策略迁移到正式 Qt 正常主屏:真实 `QScrollBar` 的拖动/页区/按键最终通过 `HistoryViewport` 的源 rank/行身份锚点定位;滚动条值只是当前显示页 top,不是稳定源 ID。已有滚轮、`Ctrl+Shift+Home/End`、Find reveal、拖选/Copy 与终端输入合同保持不变。 + +## 实现 + +- 主屏右侧垂直滚动条;出现时其宽度进入现有 Qt→core→ConPTY 网格宽度路径,未出现时不占宽;无第二 resize/存储模型。 +- 滚动条 `valueChanged` 映射到 `HistoryViewport::scroll_to/bottom`;刷新时从 `reconcile` 的 top/max 同步回滚动条(阻塞信号,防止值反向当身份)。 +- 新输出保持暂停阅读的源锚点与页面;回到底部 resume following;淘汰/ED3/RIS 按已有锚点钳制规则;缩窄/恢复后同源锚点;Find reveal 后滚动条同步。 +- 固定布局的旧测试进程显式关闭滚动条占宽,保留其既有 80×24 夹具;滚动条启用路径由 `qt_history_navigation` 覆盖。 + +## 验证 + +`qt_history_navigation`(真实 child 确定性 160 行日志 + 自发追加)新增: + +- 滚动条可见/range 与 `maximum_top` 一致;出现时网格列数按滚动条 extent 计算。 +- 实际鼠标拖动、页区点击、滚动条 Home/End;值==HistoryViewport top,源锚点==显示页首源行 id,不把值当源 ID。 +- 新输出时锚点/页面不跳、滚动条值跟随;回底 following;缩窄 60 列/恢复后同源锚点;Find reveal 同步;双会话滚动/焦点隔离。 +- 本地 Qt 正式路径 10/10;截图 `screens/history-scrollbar.png`。 + +## 边界 + +- 离屏 Qt;不宣称物理呈现/DPI、备用屏/TUI、自动滚动/选词选行、配置持久化或性能阈值。 +- 未覆盖 child 实际窗口尺寸对滚动条宽度的独立 ACK(现有 resize smoke 覆盖产品网格/transport;滚动条+child 尺寸组合仍为边界)。 +- #324 仍等待用户追加验证决定;BCD/VM/v2、旧失败与未跟踪脚本不变。 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-bottom.png b/experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..f855a14131abfbd2be8604b1c4d97f2f03400c0c GIT binary patch literal 22534 zcmeIa2UL^U+BVD-b<_dQjA8+SaYPUhP^xsxC`OueX+Z_)ReD!Z89_j)(v=n_vY-}7qL4QAs6XoDxWBZN` z_QM5zpOi&>pc&RPjm1J-eYor{l8OI^&e`OgBYUN(ru$y}n0W2;#D*6#U7h(6ZrN5o zXRF}&LGFQ(7NoVVO<$*#jo@XW>T};|kaL|Pv<}=Cg?oN`U(e~d+GN8fbE?}|d(YH& zp_cr<<1Xcub0yzk6T!l=H8L(eJzb#9xm64ce6Gx1c2qkW{Wpgvab0d}4G_HBcmz$6d^2?2qA8A}} zHmR|FbuBEMw{-r+5i#Q`vrM(-3gx-Gw{G1cj$!IK4sn(FY%e!W%Lt#<`GQh$Y1Q4G z+PFvUn2oak`d}v_7`^Ap)7bj@`p9i6#&3Zz)aEE_ffWZ|biQhxf)}mEqn{iQDq~E@ zhWIz8)rv?a3cwm0YfEnpSl_vmMOs_Xi_Y4An0TxGqY7T<~;EFR#V(| zXAkuQArcl1r7_hSQBg7DkNtb&5-!0N-IzyJP( z>0;uUpo~af#nIcg30V?#8yBZrTJq4{#N3f2$LOqk4d*#sz6M*0)W_pe^Ga^QrzsnL z{rY#Xq%*f3n7X6KCWHFyor>qs)k}uHzKMnfX4#D)q4U9Y^lK9JHO4vKgy#Y>GBSB( z>kl$n@uq%uLsAR*=iDx)$H#k(LVL9$NFzxa7NxXu6tzZH_?ZX1agDiaE!i9b8?AR7|QmIqNp$Na_3t*pU$Y8MwUW>Vz z81JhtWFOB2=ZdM_flMmS&#%R!jsR_a%!;k6goMOE#-sfy_67z9Y$z*{$E zk73t4R@RXzzZCmy`A0ohj6yIxN?TJ8W9p0@Y~{y= zlZF=ZgwlI)7Qy5Xso(?d#rsHC;BYwq_cq^(#d9O%zc?+!n>G?HarzK;Xh;DoA|i6| z*!WnUb+Js_)`*Q{@Ot7Q3GspZ#_uDZMjBe_#}2XR zCt?mtObE)e_%<-B=usq^9a;R!Rs!sV2mlO_RY4m(4vs+^(fmOml$KUx9Tg zA2YAp)gLX+EOcvrav&)tHLs8}VqhqkfDUbmFzQZmjAJe}l~AeF_D&5WC$gMLu~(|B z<93Ae0#-fo=mMRHl)!vg2eX~OR<#R1(dvMa?HVi8JAVAQxM%T5ft}t!jS3XQmP@dr z@glOaIr#L7s2?L7N_&tNaczmIVip97xMwmdP!%f-pJ9zqaq5Di7Fu*fHF z`IlRNdaJKLj+Rf@zkk2pV(uQyPM9jSTkY^8rFwf30xw~Py$3YqkT+TBCAS7f}pr_Q1Ik#UCwAPEL%zwks}@?DwRv3B&py?e~nF=PaPe)SG*8qDgfP z4d=im=d6A=DhvZy(2 z@3atcxG>Y{@j966jFO^}gGGSO^XJcZmfi>N$#5lJMwWJ^Ra5Fr(}TAwp{SwC>tl)` zkD~hAEY0ZF**5b&zKIjQ3{>|GF7PR~F>0!A{L-i1%pC7el*&QYEDm^e8J7iQCnYBz z!>Op?Q_;#NPo28@>eZ{!id(g1P9qsQoGQ5|)f4h^s!UoVdE*4wkKRqc62+Hkr>Cd9 z-2-i=@tM9!I2@jHdH>;a*!=wbQDqzasEoqhjc3z-^RK7c-*yt0g9x)rOPEv)rI%C< zj}hQig`l6jPFha=x~(v_K9rPotCz_ zmz~ojWxDdHIb(Fx1YvUd@}mbK{_4?!Pt#vc9IuUg{ycB(>DRCXzFzWBmzqnxLx0Mm zW2?|AnBaMvDrbHz&sL%IE%R@_p%JmXW4xs*ffspMnN#F)j``kK_2=-b5(74;N?a_& zOe*eaPk|{+%9txuxQvwupLX+A5hA}Tksp7-^IJq_!pTht4{=s9JEgaFckO)#G* zW-_!38_aQJOnXJRoV`rdvd9w948?H_wQHclD0^sl$2Yv(G+r zrjC)lK+LoM!O;vE2xsPNbT2w?(!IR%#vI9vJT>huFjKatZ;*}5t8(=m7P!-o##)G_I#n>x3`=QvfB;4$AGkeQ5Blvb59 z;tz2BX<-XTk`s{>Y0vXo9M)k5%sxeqLVL&-^XGBIBA*#9@c#Y#VLS&gdk-8q$j67y zxTqK2s4VJGCU{cFVJzNgZHs82;J;p=-h;YOZa@8)>PKowjx@2%MCs9>j}ymPy8^6a zRSMrX7Zel}EfAi41&hUs*G5FJiw$8OWxcnG;8oOO(7197U%hHhdz*VoGfnccyZh`s zlSZh1R{*bd#wJQ3>ZO7KfMu&n``yEfdc4lVWO5xjk}FZaRiwB6C3yR|8-6z}+|VIe z*&82N-rmtsa(hOEolofp)+*M>Z*!(gvX&8*4y9=&Ru(|;Ys_@H`|r0RCfT(xA$8BCstM@!O>{QGc+*3QshZP=)UJop~rBS zLfMV3SR01Y7_z~^!Q=qXP&k2_B(SjY;V%18$shLajXL*$vAG(Tl$yFwfyl_XSdL%U z_VZY=+Sz?IEt!qAb#-v-;>sp>0NH*g(^q2|T3a+XyvIOSRUg5EdRW4%-5E@*VWw(r z{|lAP_e)_vR^al65ZbvSqN2RGfixdo9$65q-o#~PDLyqDlk zn`9UC)GV0sy#0TC^nX&e@hS@ltgap$WBAXfeay#?j6$H*b&~USgTCU;gXZ)peQ0G) z{rSYq_9+lJ?WTc~zcxo>-EC$msW>GGKy`&!T0Ik;XTCo%ER2iyz_dO%@6YpcE6hKh z^Hc746|Q`s zMSHN&Qq7e8J!K8Ng)Nm_6?l%CfgQE(G502*dQ@3vqK4@HbdDMn&Wk)WUvZX9fugWN z^50CZp4A_x#K%sVnYm+9;(A{X1@bB0eF$Zqc_Nfk9v#ZTgMQjsz_CPg+TPwC-6V3< z){$gj{U4@hxFxNA{PtTuw}*GV{lH}IbU(Hm2ZW@B6!k3mxe+KA;m&tK+R52-?bi_8)gVoSib)<3D>}>r*bwP!De1sm%BNW`g^plKyg@AWxYy3x$8N%(6UOqhlsOxM-b#c8<2*!bvzLLfa> zVByUp1vM8tyS&-CZqF)$c=aBfr^U^iFW?`>^R&j@NKW>m_%uWd7yDpTZSM$ER;K-F#k{;h3S zM<7~n9jc5?`mJ5%^0&4EoO(K91TOz@rEFO0*47UI-jy`>2AzEJ1g2nQoOeQv!{169 zyuE=JM{deHj2e>lr&KYV1GhKlObYMDZF@{Q)s{JwUulh~yj_oR@m5yw4O{W9r?eSB z2~XB{?ZEF9w&U+B*8fCZs~&7-R^z-lh52I7#M*{9n96wvCGPPKkcDlhK1Y~tt$@szkY-7ZgEE+<#e|lJrRfLi%xWapJXkd3GYg!a6gR?h__ZY;~Rg<5Z?+VQXul zeCBr6iuY30m1MBVULr~TjkUjnOY5j-$gzX^sRg;cYWzVy5wZZwo;Sk%kDSzMl{H_n zIq)6WuJmQsPeW(;$gWRR+MhqLFS`z(QI3z!=-HofRC-NfNIk0q~OWw6tKI) zqIk&b7jrUhi@{5-pKNZ9H1W`Q8h_dx-IsZLJk7oG_3JZKGR^{&e5Ojl8&e;WVqfHb z<~{yuv$B#t8FE5vjyCQd1(i<>0;}${%pqp$(rT$c4vifBRBZ01Jy7;Hu#$vDgDS+K zDW|Kw{VHx@03iT7EIugjr>?Fpqw0+wV0w)xSD+c6fAIyEoFM#wpx`MbC9}(qat?*~ z8xA>YUeg*X6T{4JVv!!}i^hmmQ|Et3_!i&u)l7JGyrSgcK5qbVoK>QK|KIo?DVsgYLjo zqS)q}+d588C_NNF!T=KS;`DTNJ&9J2f>u|oVF_=TU2^y=ir(yL}nTaZj{)Y1IMFw6YO(m?&^d*P3dh!10fwbOw zjr3QT?Kx4dItDt3F-9Z#lXgg> z(*}-?Pr-g1Czg+q^HC`5kHZy)j2ikA|6kO){tW|q5a-L5xoq#M8eCUI7L@3QV&>Bf zb=4F|bNGKTif}4RYpF6vJn-v1tgPvo8I#hxk3jWMk#7zrd=J@Y?J6-`Y;;*LMnL#n zMY*iwXhFeKk5rYwYtbH47gICKOG@-t-RBhWE(!P$QkBG+GuPcL-^tv%{oqYglYzdi z{n-ke|ALM^75I(uhH1*-qyqP&cA<4M3bgsX_ChzyOUov2!A_`*UL1BCt;d{?U}qOw|KZAIyOmhsq+}_zrFs@TTscZBNW+ES zN6&v^BU8YxYk!Z!eBdQ$Vr5SFeme%l6(%@HRqz=67DevY+$#!2hYx=kTxuKf5rRXJA2Hf_bie?yS4eVe)Cfw zM40cF_z2L_Rs79**gt5SL>#NHA7YNFC-}8@thEtThm0(O7Cr=m(eH_Vuyw^?FbN}$ zl;vaL*)4Y6Z0ee!Hf8VkhhHu&E!8Rw3){DA56#PL%u&-X2z$+Gi>kh|TCWq&9S$F< z(wv!@NiEtb+oOLwY%DMVaRuH?sg=_Oi#}MrR5y*1f#4PTkAntvlQ#eAvoChf7seh$ z`AsE^mU$vo7(+|7(CtoLsA9@r_0~^0_ey>wF-dvOSeZJIzEE6j6DK8PdWYcmV*OQU z%fei7THlicEY#uP&6l#!7YDw(2NJ~B$vi~3b%jOGl4cYGwWJr+Xs zpn&mzlN5WUC+5=HeTb2qoXjOBawS4GS-?&speAT#AYHh^`^WBdMFfHAoV;BUOfVor zrGZ(*-{i$&^juYcE(7E~mH|!Qj5akjQNu5Xm##M-ruK#M6DQtYo#{?Stj>v2r-GRY zxNWE?^la%GZGH?7$3-Y_c}_MYckjVh!J;Yktk1+YnkCj4 ztPM^wGEc{FeNhX)69!V$Li~YPcSy`^QXiS0Z%<3{bimEOow^iBde!t;^n?_$iBT$e zQcQD~|Ag&_7`mF^$$TRW+@#2-5PZdgCwrqjh~i*o4duT>b{_y=U^hpN-s;>;RZ8II zqs#0O5q`uxA$`3-yQq8j*aB}AdJwgs5oVx}Id(!uM)Ty!lle;C-7|~t+hXiE|ck`#KGfNZLcbh<`v-KFa1?X8+x&*_QcILtd zpgTEW>WDL%;9m8{d2VWtBeHUcR+k^C(3hJI^{8%-5lU{4NHX6eRP_wkkELRi5J0>g z3})i?z$XrZIhh-mzZ_%ukg++wie4fD0zYLt%O=Y>*(c%Vz;D^o%KVyjn_F5=efM1h zDySNty4dhfF8~@a>JcAhsq~7u&hB_n2qsi@4b`Rlt$m`nYq-aI(lJ|FANlW$j!zii zv;T{gM5;abqGiK`@$rHRowN2OKx~8pqtB(}#@ZcWN+UY!KM|(dgk96+-3?(e-gcm0ZRJ3&DFb#)O2X!%}`MOBR@|{%88Et)*wyM8)XC~ z0F%L)5dUA)$3sZ)LpCGuQ^qff;E(S`=bHAC18dgp2Wof~xxW4OovvJRSCXtu7ikBK zYMa*H&bw`CdGH_qm^7@%I~xIPf90o^($P8W;WIOTYU6$)t3LnpweKhY$^Um`l>&Cf zYtgZ=zOvHTE%WxEZSjIXfsxifAjQS2cuiANmRFf>#H43iEV!@E8I?j-8CRLnuPM9y z*Yn?6MGF8dGW1w8IWF!wdH|sfixxM_bzh5*kGBt^m?0SCB7awN+OFNZMVvKi1T{3S z`}kCZ?bFn1WOgPNUrUOeyh|vs>BnZOamtK3u6+2=x4%9>Cn6#uDK!qAlVjcGCum-8 zR&1!PqGABjr5sBDe7wi4zn7V!v#OUW)uvRpUg`4r>w|hEdhN|BveX?%sv=RUf&XKB zXfyaL@Il|nJ@}O<+mG`9_CSmWx=2?o@>slpvHfWiIwqX=m3g!{yyRMR6~lwJ+ybs^ zh%+y5WCF?<9Rw70Aa@*e!qdYt4S}s7E^aP`{w-f(RuYjraD^77TqFc8nEV6B#?QnS z{^+}R?>sFzgL%&_&vNy8i^$CO!we zPVc6#RM!HOawGh(m_E#{F!K=aflMI5JFMj%DW5YFKYdywkU_;YW?V#=01{nnT~1C; z)LH*2x05Utpf9QP?Ml8QXLW% zoCcjuWxuuSnO%Y~lOo4T=ekX6=IyD^jZvO{^aW#+O8PEk>U(M6Tk<6@fyjFvH}n@| z*T$KeytdgQhn_TLnY&gcvok6}uqtxdn6%@tYqsjDd*D{NWpq};HrZ&ja8-s%9y1cs zPFvhkg>L>co<@A`%Ia#VvV?Z@!EcT(3@QJMfL4#L+jzG>f`46f$dh>cO%>tzY#!%|3l_o5(zLX*sY~S` zcQqjY1SO4;5w^1v8D6~-o<3-lstWj>oIIRZxjk_@iqz~$S{sbfo+n}IDTAqCdiR2t zcdAO-+a3Xv(=uA_yKmSlC>6`-gzRycY*3U5hC7GP$(qJ=rUweH+N9fkcqhs0Lox@3 zraoX~Nv(p8_cAAr>^=TVl$obu+gnR#^7wl&$c>REF(@6ck(ucx774t2QrTQ3@bTky z>=!4lhy>Hb6N*<>w6Qk`FX!&gE$mUX5kxh0yrZnc z=%6&F2TJ%#PSmjMu!snq^XKoGGQFzJK^4vY4G+#f-N>RLR!Y&dt`2Wblr?1pne_dN zWLi=Dc=p4@S-d-Tr?374RkiNk0;reoSX*0s2pDLgj;19bQf%f#&I*srjN7C10>*92 z4@+`!Ief@?BqA*CHJD*}yToeYZUi6K^m11}z>enyiB$rywX{E}$Ur|k$pU&S=5ztruHsGxIFM#vg(UZdd>yCj-{&Jqg39zW= z)d2(m(W+obeQLa?M?2c8a4@&3YBamNu<%tgz4TdZN8!a`~}VPz1~G0#{332yn9Nj66> z@a;2(wBOnhAW)9xu>!y#FJ=A&b&N}jEs^G0^le&*p#xr)YDNigy20xAo~>qpA?B0* zdkC9K1S<6PrDqfEZ{O# z{SY_TEsrGxk?wKu(!=%07CAlFZ3%TiNd7GS4HQ)zssda&0ox0#z4}+^rxS`@>>a3@ z5FK4sHe|`Nh6Z%S{d=CpP$BtOPZ|SO?S_y5j~OJDe}zJ%`q@uFZGWqzSo&vnf3KRv!c!_ zezGY4lXEF^4s6Z?1YWHGOv+~4j7g^sJlcPFty|H>VQk3As#wO?(E?g0=)C++X}HO{ zSQw@4jUaXZhAV1Gt$?eFB{v2MQNpJKz9CnxQ|xj|6(-!CHI02epeqw z%1L$}0(dsR-ltH{8;Vr|@CE@GD3J7EuqPrtsqXU0>p#UnzEeH1EbZL7%~_s{P{~@8 z)3Jy4OS;7u_Td8F6UQlS*`h%u;Vk;jo~8E<=7|hfw|4AkwW8I|!3Hhn|4M_vOoz}) z?N1pohJaO4bacLgLn^T0BgN{O5eafTa_xU?tPrBe7Z=Zv#_-t_6NF2&|3w7LvC>d} zJ`C*16K>a5-lX(oB&W)`yZ>`Q*L5HHzqTsWuE>FV%*N{dThMkc7l?7clvof;>|oTA zEI{w*Z*Fb^)F1>4`|+sKwp9eU;)=js8~eh$iD!uKZm z{#B8hC=tq>kdFz$^^ohANT{!e%<9i|bm-IkK}-K6L?=~F$`}ZVimU+wz03#h_U_@V zm5u%4rRfqXp`7%}ZF)!dAO}oB)$9C^zZ5wB20|tN3ZYI&aeDmhgEHS%q(EQgn?G_m z{oJ(l>-uyw;Ijc>&2R7Qtb4ba6G4K?Ci$LjgWI}A9hF!U#70G*vjk`+8G3hlzk#mV zVlFW0+qU)3a4UhU&}^siNzH4nl`fzGhS--G3X0xx2;8x`*!~4u({E(R9q--poL!10#SuQA+wBEVaHOmyh?W*9W+z zPKXNkNu*n#_<C_ZRs(Fn&dp_ny4`u7U3W2MJwc*MPX=WCbW*vm*u&jZ1T zTV;`D*ZIQdV_lW=hsj3<1Z^3a5i15kd-7x+n(_erQK~)zX4V@PDrBHV~tT zL#X6^noIVqx(EM(N(Yc)9dLywmEU|GRPKZf+qWMpB&`pcfcmBfa^&m6b`Fi~Z~OZk zGInzgHQe{M->xxBC>9pZQ}8UhSnumwykmyP2MlN?0rAGGj-H>Cq(CD;C>E;Tcxmfb ze*q!)fAy7qQ+ZHaPTi{upe?2YN2>}fgUA=cS#y<&(4XlP?+yulh2`1wgGaft2TS;c zfok`dogbaP^&SD52hchBx^}dHdByd)c`-3D?ACat7H#YfZL*58|KD3(Qywr9 zrEKD)g&Sw;fXNG~7R49Yt0JjjvU+Iiq6FIa0zZrVLQosl*PPs@ls9s-WtC5;!5i|}85zu(Z|M>Avi2gdeH45rC#pMsgJ zzFFn=BV31CaGgxcJt+K8F4+PNIKLxxu!8+*y!-~KOTk;p3t88ruV0)Oh-JI~5FT={ zgZp*CzJhT7T}F7)_miIHqG^ZATbiV3X>u8)Qf`5SnMJ^SIh!u_k-_x^FSKa@>4 zMskLx7JvWN=fWCSIBU|P)cE}B=KNLWq}o(nx&wOTfmhEh!ZVzl)0He+Sw{A%_Od4H zo0+9}^FZ3a_`9N7`O;B)r=hKTY{AT}_^}dVu*zK?2}NIBb4yEn%c5+uf)_EMO&KER zHD|D*luM5VM8qsp$A)WZMzFD$gTkG>Mt%UMVw1?2?#(@N}B!I%w|Kzc4N=Co~_j;f5{I(%5Jx?5C& zrDIy|=;ai3OvZ#JVdl}sH18~scQ2t2Xe*%yF(%aKerv>Rqiq5*IxA)Ii_vPEa;^Bd z(ti!;U~g|Fn=DZYTHT0-h^XwfXa~3kaAE_+hdO_P(|@seWYJ`MOSg8NA0u)~;6}V| z_%^+p5SyBc&sT=--&~a=u0pPLam2kOi0Q|5=%AUamha_9r?U^S^D1b#xw)BCdqr?6 zf~eCFM=1)-*UQn`Z1{7bl>TV>rQ-GSc=lFY8=%!}g#fMCkC zgwbF0jWUcC0G+3mt9cF4H6#C7W3ehT2;RY|p6qHB{&eEkt}_{BnkJ*s~#PcLcwx=D+Ia^-tBltp>B zjI3L~fHHNhI7!l1H-?mg^j?}E_7x}8bzP+mCp>%hl5uDp6U3yq>Du=ZRN>jI&E-qw z@&h^AxSCCG;T(s8uzma1DAmCOs&yMRmutby*~I#6T`iakHtko{V+5Li^1xcagD-_s zEMC^soQTe9Zx_Lh2Qt_#(bX-o z?n%3}!@BfHbB{;ZWAhFCT$=a6bl<(c#ooPl@80Kl=a?>Q_Xywx5K&MVEyY)B^9OD= zA4ZeGX$Kl|uJ@7(Y7p`qOfGy11RmWqLLg$aN`cvtx_S`w**$#trMZk}Qr$Tlrk|Bn zY<@nm-fe!oGJJb$H2K;!EzqB>kEB9-DWwN#G#>gjOds6g@vG6$sUGpT zrd!@lz!h2TcEsnZaQ=^q#(hW8kyr$Y8`s8^yj&F3ONi@8Lv zDbWuj_i|H0n1r(5Oa$tcxXSwl0k@kIqp&6b)IzG$RW`R<@CzTUO(B+a`H zUhB8;yfuCvUtgQ~bj`z)G-1xEwCqBk8Lvd&k&TvKeaypk^k^O;EkG9pM;+qRMEKH< z3;hU?B$JZ3L8E~>+OjaV3cFw_T>CzWz0zsOk+)5`pnM|WQgm!g3~HS`x==?D?``K) zc?L`hBYVT`NkosiRPWr9thy**g-m^Y!z5b>O#0>V1aeoN<~Q8jIiss-=k)YUk(1B< z0bF-)oy$eR?JbolMFaEZ2e%LH%F!eeq&K+ks&;hIatg!LZG}EQdD3n@#Uj_A#>Xw= zR{}Z+F(suA26}oYU?!W5j3i0VFz1>DYbf;K%!6xlEZkAs;LHcThZ?9{PgZ7NDqO}Z zE@(4vJqePhaWLr=e~*wwze^|im6(F zFR%@09}MqaUErea;lEJ;Hd>ThGRil~Z_RKgV4Vu!9uE)O7p&_E#%If{$Z*kuR{!aP zUq1%q*A?||>O{Ts`(HSx%BX{0&brBMoCTwJ!nE)r=Qiay-e;hTkAWHz(M(s!Zev$w zjMqD?7^JPb-!1Z-!%&_H1QyF`Cd)bXPe&;eM_Hgx09#wX89_4R4=TA6=W<;QHpHfOg4X^_spaWS(okcvnb zJ|U(Veel45L9j2AU92kj{>n<^)o2k>q?tR7Jca=MAaBV&h?Fj8`;OTz6Z%q5Rm6y+ zS2;Ph%_z^sEQNzt$)`~PsY1UhY83SH1^R4~BF%`7;aUPlidjIab^vKeqGltSNF=T` zWb7&$gM-@o>b-(m<80 zu6n;M()*GFAFH@{j*()Oon~fHHAwWqrh1EiG6PJGWjADBBk@j;Y%tL;F63!n}fv?Gl|{Jqvc6Z73VTV4p@f;DcWE-)Ex zR@YgprSrVC20YHicDjW0MV~KWoLynADd4HwcP)LLB-RlRdk|*xIz!u2<|~{)c`-_S7dFQ-UD$( z%zx_g3~$%KYL5ij<5e)<{+K*3r;7j8~y?G}FRyf#5V`(Z6F2>h+xZ{(G|?6=v-BClo6p0c;WxRh-g+ zK=el8mOuz2w=a`gpB?}r@{Q{+O@o5;LZDMtN*Vh)f?Iji2_Q=6fv)oL@57rjP^PjZ z^yaGm4e*>6B2X?x7cpQLMfurG^g|5?TL+<7OYU?@YQ}1SUo*3D&mqz88dhhn$R^_xoWT3p9#0QU z!ht~1F#tij1okFX0|@k1eEZiaJ?_BI22jP;c2jSf50A6x4#7kIqK5O01m!Fqm#PuV zdaOzIf*!)m1j2U>Th$A|Fs=Y4(E1YmJ-2Ixx0I(Zc785BUw61mxd0325^M0e#ocu)YeAzd@j)qQui9E~n({2&DqWs24VCGgH( zvxwc^E`EzoPAc3goS%+jq2E0C+#-z|{OXK!`a}wy=}oY^p$+f`m$M8~F;87|m4V2f=e%>D76egSPU`KK|xz{j3h z>M-O}3_a;$vgjRlGA%XmygFZSr&B>i#jqtDKJg8c)*c61#0Tof<+X8en$%9zcKrs< zP5~|zz%U;kUs~dS*_WwCq7UjvB%jgo_gWe%noA&7Q%T}J-~m2tZ2W{2`rv@J!}!Kt z`YZ{vM%wfy-qebe(1!Ndk$CM8#vIa!^k}BTlQ;;oNQ2nlGfL^ZX%Ref8!H~*e+_ps zjT<3@!kdb->=geGk7nR@5OTgJl&-SVxq80i)BRpv;HI?%l;6_JbQdqthnT{O6t`w5 zE1kKQKFH;b&VjZWI|H`M8@x8mgDpQlTxcnosNp=Vg&44;B%W2RZ#n$t<8TuDI5-Ti zT?rzruu8_WkEZO%Ay@GD2vV@eBYWldxKQ`6;Hevjm>FuvO zq5k^gDgOFF`2VL0VcmZ|t?W<0l>ZI)@he-$i-e7BZb|9Q2MijeU3dL{E!-(zKF2&c zKU3{)Ty3y6a*+SrDIMk>j=N4aO9W(Xb+L&)@{(2TZXv-ubH;jpnV~4nHIifIm}~L? z%Ab?{>XD4z*RBQzh9dqfIB%C?04)7gHsCa+&z&_sTZn(}pdv$9n$p(nemmvrb1deZW5wqo4^GB0N{ql67rl_Fe#fy)!5E1uGBh>v!RA)aRsBd5p*RK2m)x@fK zL(v4*NwAQ26?))#Wh3^G?RzxR76%6tFdqvF65QNO94vEU2&T7MN!l$wY}@zs*Msz@ zGHB81%vMcZpC2Ad4h3Mdf*18oQKNNtGDSqY#ls879MSG6qTii4555gCG90kJBDZ->%0@83dS`v6Gj<^U5dH#)*hvMp>D*qo*SlnE2wU_kiuZ_ z5XaP<8l?Ws4|iW!~P_WJ$@VYJtIY^m5D*pY6R= z;_V$zUyCq8bepd^nG9U4768?JKG8ON>_(4Dpn+3q{^G-CrvTU6MOtyGy&p1t!@v~Y z!$JP(^?>iEgdB+R6repLS$Bl34D8zEXOdp&O#!z$$jC;z`}Jqx90^@ z2GLm=Z$3AOO^?T0>$$ozjaqSDp6AR}WcG&rThp-~y!H*i{r z*yXPDx9H6%lNlDi4?94n zjd+}FzT7VVS`FHd8+}SMGi6(C(zm9jxTTyQ+~ru*tJ{3xFiucC4{K7h>)}+q13Y3C zDgb57WaZ~yOAlN(E{?Q2)tlbq(1^E^WadVRJ3hG|@+}srZ9yK1%Z|AtWL1o{+xYP; z>MpvDNk?w236sZGLj)EE^CWle*=6T?JNj-Bt!=?0KW9{=y6M$gsYU*%3ID?{w!ScM zLOJ)zfAKkyRkQt>7)xr;u29?YhixLKPq|WquJpibxwsF3$A`s8`^=w#MW?6NgAl0e zQgfLH7c3f{oz*-CK=YZ5swxF@PprYdefy&I>ti=NgL{}P_f}%ka>HTFE}9;qeek$s zyV!W5MvgVAXT4O*ylQ^_x!KGN-`xm#9#*SX_?KUP*>=mDwY!1h`d0P3^WZsAMPp>> zp&ZA&LPbl|b*tBT_2Cm+C&fYV!B)GdI!(NW!%kcg7O$)4NTYRbv}`c;*miEf@=rg(&t&t&Zn^j`@Q4BqwS?y9on z6`7`^GYD;@x`q+3l!#_HnX+?BYUt?j-dRyJZ8Duxh)+P~My4Lbx!ilxbYd{i{ps$n zkDXpE%tRx`k9AI9(D*8(ce>A`P_fFT>@jUw-(?akji9~GQ6W!l#|#7M zEx@n6{Z0=A5%~OdIUom!oV9z{qV?i5B1Pm}klCYM)?ltaOz>2NO|ZQ~=JFK?huQjyWln~y`ynz`!>-0ikrFBQ@M}pQR!*v_#$1Wy!+_mtcE28c ztCh%OgFp67#}>}2142{QMCkhr)a|O4Yx%*7OgrvZ9^c7M(1)mNp8-vwxFfcGb3v{m|q(gVJS@yQ`_a&m}t62?WB z7lU$xF#Y7hV;<*TcDeD%vuR05`GbRlyCZFn>@>33-jTBD9_&n%G9Qz&?(BVfR4U0J zBwG?(bdR$$MSh?(M_ERu6c%+)>F~z~$v!_SWi7o~NuIOJgNl`#KR?~=h6q$C_>5Fa z)F{!uc{ZoYvC{dX%i5ttP~s+3^?!ZuW-Je0Bo?!T0fvK I-hA+X06P31rT_o{ literal 0 HcmV?d00001 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-narrowed.png b/experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-narrowed.png new file mode 100644 index 0000000000000000000000000000000000000000..2c82cb37b0a6e1982f50f293cc5140781c52e118 GIT binary patch literal 23437 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 H+mHW0R;4UQ literal 0 HcmV?d00001 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-new-output.png b/experiments/shell/results/2026-11-07-qt-history-scrollbar/screens/history-new-output.png new file mode 100644 index 0000000000000000000000000000000000000000..bf0ae444fc86bbe469d2ef97d005225697a2076f GIT binary patch literal 25527 zcmdpf2UL^U*0wW_crD0yEhq|%il88%R4D-!5kp6MR{@dGrAiE_jABDHG^wGt0Fe@U z07XRUEkr^ninIiRln_Wr_|J=Tzk6rCnYlA}zQ3%0)^fq(%{lMMIcM+v>}Nmc%tJ$c zE#9pHTQ_Xjzze^4{?djG-_pT9wZHLzzluyyMv#fc2wu1}}p?Gx(Z-nrpqam&u{(UE04 z)0{T&UnP!sPdRn>W{Q22>Jr24e75Mtg6%0aYCU;%U{`6vo0!|-JG)&R<34y=Trdj> zS*G=R*I(Ixe}}3tY0Ole$xa?f;4+%O#H}przEEZ4fzfSP)mRXuZ-RcSxZt%J{HWRR zN)Y_`^6hs4;79$o?10Ds+Z5-p;75Vz&fAx^NZKfOf}CRVW#{Mke9 zkjQyqOKw{AMC)@ATS5?#TSr>stgSs3nJTYi6Iv?2cHTBV1k=LSG4zlYYXcZM-6NYk z%-t=_S~ur8&xQ?DP2X!54Y)@sH6NF)?OHPba5L1A85zmM#(M}*WcvzwJ`|nRXXNA@gC*eg*h_TcY%tpzbx7GNp>{!# zNVM(t3|UA>UEK-I^WB4Lw=BZ=gkv(kxfc-oYVewCbeq3q;^yzZyNJ&3yC5$ocbehv zTgtQ$-XZa66o0+zsvzuA{?nzUgry;*{gd(bgluCcF?qGqA3nMf(;tE{FoxGAve6k} z(D5*~ed&!c@w?EBvp%yrrFf^4m^+aX$SYS;g$%TN6Fm1$_GB3#XJq$nC%-&p42^ZH zB{r%3h;wTvrrmr1$Kh~%TO_%Uo+O?UL4qA{*>R=6BRC7gnReD^eEM{C4?jQiX8Qw< z5K}_I8pG<#67Z|7(iYQxDxs`Qo>D!1`g}nFDR}gxOSj|92ZpU)l0a5{eMg0#Js8mw zUHdf_iGjzVX0nDyhxk2s>9Wr4==V0`hB|P&0%{(UE$=*0I7p?_i|yGgmXb|%OQKSM zgp|h}^xw^whCYuTfwMmG*JVg{s9`Z&QpnUrhxBgQzDkrZ=siUd^hYF`Hj}FwQ_bw{ zhi8@8%$csZp0OobJ8n{LaY9?$W^7Ig znz`~X*S#=uF2$=HP9ila$uDAv!vmTywnV zBg0%?mG7GTzU}dKms4X-*k%FqkJHgp$JJ2Xp5Ose+BR#&$K&(LTSUNcfY zuu@c+1UWX zsXKRquJv3SbF5qD^RZV04Lc8%^DxWG6f94~ob4#GIv_aS`ch#>(0F)^>dW(~S5jpJ zdZG|=+Eq&*t%Zf{qiYf`nk4$M>$ZXB+uuCkc;!hPZHmaPS+xx#8)GBoB~@)K zt;@eise)G~!7rCZ=HgD?%xe;Ij}OT?bLGkveZR+zX&G-! zg)mTH3)~jL!-PkQwhRw<Clsu)d)p&u3+KQ>lsUN*bBkq=lA;P`0|C>QD4Zfx zvJc_iyNma>#yPY0AdW5d*_3ar+F7ilv`;_-a&I*e~y*E2+s-+dymzkZ1#ag@8 zCFRY|_GgO;>6mCaPZ+N418=;Yq4r%sLV>Bm0NzXJ`|p2%7h4@5c6N^R4s>-5IiODG zKZqcuX+HK_yjY}69`Gb1XHlo(RXmgRqmz1i6A;P1lPyPQ! z@4JBYUu7#Gug7Lcn7g4x)a~mIkq?5#UJ6&$6N4JB(Ts=5Ch6_w=2Pe2duNyE0QWXG z+H?@nQoH0QuOLT_2su3w0|T$SUF$_t`BMQZH)On3`qF$DoQXcUA=%(sQA=3GSt^M?xntb#=!T^s#2`~Z%?4o+EH8N)5k^DR(nA@dq({w?$RFnmvV*S?$y z`{h~$!gZ12Qyu1D3R+xVBXN237Oc;@8|EITS!MYO@JpGhF+(y zqucuWCm+PJp&2@|<&VJKpJU;X*}RV*fBY^p(`hj%8To_t=7j}3@TxJm2La0E{Z@Fd zU>gj-Ok7K;q$^fGI=bKWR8bMR)*<#2Jz{1dk*0a!g1G9{D*bpJ0@)gyHsuu* z5aXj9k({yt598(@*TqcatnuZ`DeYV?GiXkIspV?X_Urs$xR-zUtAU38HPHN!&00#T zmh+&ThE@ALb}KW3aTcv+g1MVuE9cQwRaN)rwV(-`w&D3JHj4W6=3%_ztWah!GQ6E& zeh3b}OmK?ECQoryRU6^p<&V#}5R|ANuGmf=%6s|J5KQG!cu89NzhX^OU*A#rxA0};X?%O3KD@htr{Q~dXu(e(_!_<{Jz3*KQ#egl#>B|m4Bgw`f3yX&b^`p%cOS^}onBU{k`^`7ir8I^k_P>z zh#gsK$SsUIsGc=HA0UV@DA4aK{o>{1KA3j@K2KZX$|m<=ys;}f(r>D}37t{czQdMi zy25(;Ap!zoBf_0IymY;`HUz@MKSy3sQDwC+rHX~}E5*~|^oEV>Jf4!taI@>D`dN+w;KidGK|*yDt$blnfYqNkVMaDV%SIS^O!}5DsnZqa7dgxd$CT zzBh&~UGG`d*tB)qHlIA}6MK&yEu3G5JaWBtpj?GPyqqPM8?4U&jss*$|MCQi();$& zy`h2ru~hc|jBS7Zh6zTrLF3i`P?)WnYOP)TM~}eiIF;CLoA~CNZ_pxGyb&)iuRcLg za0b>F*6?UKP|~W@0<1kYoxsT=Fo*clD;LJ6O#0zq=7qeV89!-qVH!pUjx5c>+8y-B^9m+%oK!1H&s@_PlP0#``@6czf~VJwBTy z@Tm>SCqYay_9=;vPY^`3wYBZ2lfUli8PLBzjMx8PgU4yFrAtfSXm%ve7-Dly(|RGx z+P>!xGa$Sjp_aTnq2tmPIY)J3kBf_Ik6-H13q`)I@)J-|$Z0Bhw&jrWv&e$crlB&D zva<4Ett@y!Cj{F5bCP47Qo!To*G{TKi+n-&-$B5wbg;pkPVVzvnIAj!H8{lWJV3#H za6?h86A-;CRPm%^N`;d`-v(cNhyMYCF21mH=T3x+OV`6)M}s(cbKr8v`jQ|3I8-73 zTL}|;Hub|Vt)Q5cT-ffBBqbeN*_+`rF=X6p8*rTXLR8(}er#;43MB@{pCa}rNs6Jx zAqGfItYV*EtE=_;cvn<^HDBn~`T+7MOi4CBJT&=c`cb|U zM%OKJp1hUI6bMHbqL%xbn#_;Rz9>~Yq5%vBd242Kh%zoE#ajSCy)p@N%2x|Y2UJn@ zujO0-6dDArZb8lI*xy)gUwWC_)7#s7{yNw|x0`;EhXKL?!u55q72cy_*^@Kn!)Af! zD}?lPO}x9Vtn&@Gjof;9n6*`bb7_4z9a4`0j(B8?1QFqCYGm}>j?>lh{{+nt`-%gQ zA?K6%>=J9DC_@C^eLp-7mKB3ZdH40vvs$a>U$)2hv(+y zRvv?cjfiKOnVBILvUI~p_QWr$FNnZD)x;_OG|T1C&jO9QAmny*;TIiTck5CiH!$TWbsB$|UWSeeft{+u@_~8uN5wDP!n))iYJUyMz z#PYJ(DU8#6yq7ll{HkAR`EV(7h<#NqU`RDdTqSqz)vF!OT_w)6R4pN+@%s!G5f`i! zB>{O!!i|5+M$~@3lL^=`9cYKtzx)+WA~0ei;1+j*)nd<0o23WqtE-!(DGsv?8r$qg z+D)NkFP6f3%5IG-NmM%@kFK#In$9v^+uLociGzbqbxRC}PlbQ1k`LX?hVm^y6w`R; z*FSEFS8$Kt@!65aFt;ai?778dvmbKb-sf3o74$)$XACF;6f71S1%iS`L2YgAu+P%$ zK)P&p%M8h9V(^HA6)S~VJ@lgf46MYZy{6eJhOuBz*b#1$*mDD3M0q6?T~lxzMV2Wdk5j_iWfcF&#eM@6t)x)5a7z?6j3Lq z4|c9^j}VZ&G=@EPREQISgGCV+Q#6=*YeMX`i4wt=mXeVY5)$~E##s$C93oJt74h4} zfE{=;IimCvttByt6s%&3S!s3t+YB^e`~Ffew40?;eOWg>s%I1i1MHvFmQ2MR>W9MO zXRU|b)>_7z-&sjT7EiIMrn8_#XYtISWS7pdL>sPPy@gvtR zOoti&A~Mp8NyEbid}@miwyrIRh1x=>a+3dd(km%89*4s;;Hz8Xe9Aw4+I#G{w-bgn z(P5u+3bUoR$icH31*K>s*Z!pKO)sEUwqVV2m%ntdRy`m}aYX7rBjsIZF&yc&v;ogG zF?VjeX!-0qp&|qLAgcEJIR|m+5K`(=FV#_R8=pDAuKKgDaW<9O+4@P{nGxzspX0%+ zZ)X@Cq$I>A5PK=phOQnS^tp1Lo~(N`9p;dj1J%l)@NeKlAHqL)Mtf&#nHBbi@k>nx z)Ny9KaNu3Hjhg=;%b}hfn(laMDHm4VbDU`waVs*x&Dh!wIfQhLwWZIBpz>Be*{(9%8#^K0*_EzEsgwfW%gb2GGkPi_PS*Lo zqm)lhrU=f=kI3f4m-vu8`~m{{aVQk$VBw%sdt&t-{_->MhZ4w{%3CYDV9dcJ7M(u9 z_^eoaTv%8*I)13D>qw)=Fyy+|2?;;H=;AUD?;IkD#6-4pQApo$-%3spy$0#1t0TM^ zOgaGIp|gU!nf}saMS^q)YjKLz9&>`T%pf^RNfLWqdP+S8gJ3aH8DLZWoap#dnH(T0 zsn36mu3G-MiLlzB3h!Q7APVsB&Hy}*>8k^tIoK9X9mJw+@c7&}Lf6xNDr|gnrFsWG z+f2;WDzE~4hoZV|AzpTz5){D8F60^=ps=YS+zE3sch)lK#FRIbREBWsLQJPr zW+%1OgKk!s14L9mOYBDeLkn>1(_f94*WRcp?E&xK2mgPlI;N6{8k2a;X!9)SdjrQ1 zSEvYBIlH>*%d5>B$&lz^-{1a+^2m3&!S=c!w2fKL3@+RSRAXwG5WMpaMVMNoWe(8m zm17~_f%nz7J9+(rEGcU0BzJXj`F341f79gJmDZ>nlwlz_Gov^|VAk$yOHwq*Zq`TK z@_O0OAIb;IM>}}ZzI1S{(WMjjW?krmK6u9|83LD#)hJU3FYqG#k-B;IeLdo4er~X} zuDN+^aL5v~inBBj=I9AJ0Mwj9g?&*!tGE2WsLYzCg_aNw_FTj3g9zKCr1<52c<2x$ z3z(FMEpJ|I0(jR^CRVBU651SFMdbvy^;f!&caZ2Z&h3Mb9$Tkmyon|( zz0i-Ev^FT{d*6L23C31wY?SOYuVvj9Ut8W>J_`Z$MJPS|GsTC5p;&v10HGV00u zt@^_Ld-`!{N!LJ{R;=fAQSUWj6mO@$RMUlGtLtV?j%9VOE-r zmIZKlnZ5MyiDEWv|FLDn?pDRcYmsoYoRfCWjT;M59=fJ_ZJO{)IyzWFd2#Wj1Vtz1 z8gAbGiC6ic=(fOrq8FXnvU|^-qdvL0O(Q}wht6-lQS7(+VPMHhODmf*3j{Jj3a$xI zg?gB?RL(l*TdIJXPyup)jA8D$qR@&Zn5PYY1`pe@?}loaSX=kdhOk0T+=qs6oa6@6Ml`6hglJj;HzZ$;tV#7DD^O#8Voc z!Z1YL5|h~@S-vbGbK^deA5waGTz$g>ge?gz$A-?7Q+ZJ9cOVIs<@(%DbVVc+`MGl9G>=PAsZzrW$m+K@pfBC@_jX zZj_l+0wRTm#QlA5&vr~>`MADzpav}`deYF{yl`?}%nTpkJNaHr*xaE35Az@}%urYH zL2hE8kIN?Nw~vGRXH;augyFUXWj*j8qWXp=dhuhJFttM%<;XXKUNk6Qi&U#wPSpwk zfunv>Aup5-)DBgnPdkn)DQWK9*|TTY0j$=pU8Xj^ABK#-ygk&|7SRJL=kW@XHuko~ z=eN}rTX@#ay*=pxN=CQ4uDuDy>e}1X1RBvunY5{F5(Fg>czhnvdEvN|PRI^WLGB3% zp!X`)u1JD;G#|{2U=St(Xo__!jGsDr`&R*aYYx{-98AH8P4)d(f)^`){5>Zg z+LPOl{-xk-9C1g*@!SK5Sy?VBpTR$U+8+E>+=BiSh#QGRy?S*B)JLcMW^3D_tmZyw z{`0K0DH#Y!WcF%l?WB_Q-t4|5HeqE7cs&EEh*9pwNzOmomL_ENaZ`~t(&&aQA^4XR>XPWeym>-+U2 zVa*V+$**dfQ#l`AlSic_I4C!!;bckeu93;4z$xl=}snqgr`d8qj-P_T%rt@t-o z>PY?nr`sEyQRL7hE-$ZlYAvV^WLu;j(Itpkt7!*yzh^U0|27>?UQ9Dk>jTU2j-D8SJe940{#igH1US3}u*655;$E?#^o za{Q=M_~FBcd%L^Sc8O^(vRrXFfXR!MLgfQ(jDKKTF%D%L&Ku4e6>jH!KLnbuv#3BV zc{H-JH8tk9xY^$+g(x#RKw%o%^jsLt@=p}q(|@fJl072y3&G4fze9QQwWQd<8QJLc zPXT8wYs6q;&u%$=P$eSzk3AA`p;XL~t7kW}F!o$+>9vy`4(s7CL^e;jK5B7!SMz(n z?VrBd<$ED=(|0^nC~=G@3&5BZ`bUdhU_$l{zT$u)?4WnkL{ws8+JgsEkAPS?{ozA# zDdiJWWM+luaIJCq;8NY!86B`GUkqBzL3$bGl|}bp-)ic{3_B3~AlcBmZEX zy`4{rIz4>}fP;v22VQeeD^vw^aQn`FqOd-c$JK25zo?=~YPV}vOPTC15k!D06<}&D z(Y^!j<;TaSQkc4Gnwi9r#rC*CFQlZGXkB8&*MB&|0$5Wf%w;5NR zGL@I-KMI$WELa+9Zq{}mikkYu!U`mBE)m$y(`NtDP$x#p*u*5NH8V>n6#4GQ?*-qz z@0)Y*9B>sBOj@e*9In(EeH-3WVqO03nkx~iP|+vsJ0lH5a>sB6bZ8x!QYqJpkl$5C!>fbu@p( zwigW4R?a`>Z!>qrw%O69tCl=g3wT2RxOLmw+9EtWikJ~U7t?5zL7M?8q%(hmZCc}S zz5jhAB}mJiFbNw9)G4Rjc&x@*G=?SB#Ipw=Xm0ce>Ysx-K<3rwl+llqwy8*YxJwpPX$A?n2m3!pbQo<1BueQ+`eD2PTwAkDoj-34G(5Bp0ikM2JG20X0aKUO*5+ zjI*)le;xI3kAOhmx-$ANaLkN-6b;cIC(r1YJrza{*wH2@r@gOz)iYdw=f55+NBS zi0(?C+2hp0>L`S6BnF3@pTBVw{;yK-5a@)(qy*@bN)-p{vQLIs9B!7G$YC!{woct( zGblHi?_R{HO>#U;%md&wneJ&4zm!eqn}--8Alou(YHa>RItd(~AV{NyLeAN*cE=v9jfA;T<>ck%a{i2mCgm0-~%oj0pU{6pkzHzv{ez0N{Vt=mMbCI2rY4B&x`nZ98@txx2UN zG3h%0)!ft6!XgJ0BhVR^U0txHCtJ2hy}H~9jyNHcdSW3DdpRe3y$a*gzw7URKLOYN zHG`u1toPRo61m@ixg4h9^jAr#ZPK8!t(}xlE_|Y$kPV60^G@jn*VZ<-w9Ndon}4f0 z_%*)f87zvK3Ds)$A%4B?`3o%Dd>ew28v+D~e3!L>S_ddllzLxh=^GmxN5Y={qpROb zD%r$!wgA7AT_A({qUjj_XO{oO2&*q4`wS%Tl zt)ElWLKoT*^1xBVFxGH8cVaML3mtS)?^S&&e|T9;>lrx>;t7i z>UGaqyfiq&2LyKnx*!Z$CKU8}r;~>T&YnY!)xbjuwU_acB_ZbP*XIOVJ7PuJ9zCTO zIfFBG+j&Bz>}x5sL?Asg1*G#i2ua>2KH#OByD7xN4jm$?efR9ww01|~&?yg43W%<) zW-D=8>RP+jI5c}kgYfwK#rB7X2BFAPk!&3biGhB%IMPk*FX$N?11BPV=@jInDwUz1&T-E6CLKR?TJcOWz(+f%zxld+2}9Iq9r)B<(`|HKv^9m?s(hEG=qFINQS*= z^Hmn~l~%0>!C;?0JVHG+k)j%A^`X}Vfk0duh?YQSsB`D|;^k@EcEF6Y^?NdPVpYo^ zBKG9(a;giEIw)B>*eF7M&NE&ORVAdOeQ9yJpEbL-&~t?XwAmwTuCwh8>3JPMbX5HP z;cxDUoH%qxB>&tGBKe3zhZDA`9KQHX!MRPYhukjZ?WS!_Gh@5<+uT)y7dxC z6=q6j!7$=v-|gp9@^G0@{CEVy79iUBY^J1x4JO*`9zMV2RXaB z_dEiTx3?986-y4hv`Y+@aoG5Ina5zB-)xP$ZR8DV)ZW&|eXE-crn;tNeHCD~+xT01 z9$F1~L8!`Ro8nsES$!nl1;~oGZr@&tJ+PfeFF~e%3I(2S&_bdbTjCY`@=ol1JQ&gM zR#+N5{~69(7qV!|R%Se5zg}7RJT-O2yl%18*BOE(8*J_wha&yU96Tm{O`zRZZ`w;s zFovF(pIxu&*nB-%?e)#qs?MgbC#(6ts+fNH(1iVM)@CuiOTLRW;!K<+;nD$ztEFFZ zH3d{WY6gQNb1ydB-(EO(rf6FNS^Lf+1Y7`-?pRc z>GJ7nRET}0)z0V774V^(cQL9hW%mdOQ+W!)q6iP>x-){MH)p@#2kZkD<~C+plT@6md!x07k!O3aF#p=CkFt={GR;+}sLqqLF1zjkNRQ*Hhts;{c?pEE(b4pU zi4Ic5?1!S!sJQ4%0+M;!5la42L_NqOc5X7J)o(jG%+YeLeY3j{9?S=h3biuEPkza@ zjgB?{P{3SV?A*9Z6v1Hy2&zegc3$#3c;k>xTE{V= zCDqkoZDr{uQ3-fwrblB_BlXKijjgmH+mea%Ee(UVuB5^uYjwwWC*^AztueO#up4`W z%>SVUfDS5-YzO_r;bNt}?`0M6iHV6;%$p10Bz!=J2hD#tm?JM1M3?Uf$ef-&(T3r7 zAJm7#jet95)c?388akmU<~qmX`d*Y5jVHQoj=`|epQp2{2k_A|R;~*StOix>+9XBp@r!^_i$Yt)? zDk1V`thp<%c;k127K}kXwn<$4MLj*@-4!3kn_UKoPZ?n*j^f>Y1=e_aQn`Dd9*`_2 z>BGDD*v#P+I4~u{;mVcr9XH5WJqMsK0h$@xK_GJ_N4?iyFI}G|(@O*H{iloBRwr&;q2_pNn?Q_Z758$8(vRg zE4F@3VZZ&F!fyKtHKe{mjfxGw`Y?see-d`u1)Y{YVOkl|MvW%#+m(LLK(hOT_vp-0 zg07~3f}>aX=11WMoq#F5?tJbpUg4y;b_Z-Njfd}1o;RlD-u?S|wq6hX-!%2lu-VPT zM-QHq<_fC5$Qf>ncNwJj6(l4l!nDA=$DHcO3E?0ggUQ_N7Y{LTIgb&c?6Qb*aWw@q z^zQFFFMt&zQj|)fV`9YNpr~0~2=kwQk!)A{<-yY)Z7a*m*WYzs5jb@t3P?0} z3-epMcrh24JycT(#)>n>%GJqftWRChla3{E=d)`2D~)jlftuxhv@G@2k1l)m?462q zw@#Y31uwt-pP8}v1br|&LO_1L3yZ5w1E+$L)KFF&_F!SEM%dSqtQ?Uiz#Q+?ve*5| z!{;LRw9cDUWu=yv-@0|nm%F+m&JIK(v1w^GV`J)=d(z_KQFV274x6C_yzo;NK?(oC zG0#67gTaZo^yLq`(8^|l)-!fiZnp~BH zMyCOeka}m!uKZUp5s~vDx31-Ki>?qpjegqQTUIdMVV{!QlVvI02!)e`rYjxD-(5@EvFPf>hSEKm^oN;cf8>#K<~K zLE9`L`_a~>HBr#lhKg=IWH?e!7rDH%z^bG#Ywx~Zvz3(<&>kech|lTW63N=em#p$Y zpYw|~F0&9VRxhnf*g;p><)5PyJ0J(n+HKk#cKxzRR4~lxQJFjEL)CbR!CLx*cNbxD zDbfps}z zP{q@lbc;PB1!GQR#w*WU;VjL?vF2V!JYrKN2tNG+p#4aY&w{7m=Th^8w{GP8`ua1~ zRUpADJsz1f1f@z|r6lv3d>7+vaE|x`=SADNHciYRx%UfO%N~z2?x>JXRPPwyKNTAl zOQQ1_Xfgg7?*03u^PseEM4ZZKThB_cf5cV%!jI&kzUc{#6g{pSWEagQ<=f?kpE_l= z+U{=~(zn(+h6n0(f zR^ni79nPizOlSi6Rp>+3V18WR43b`H-s7xVxVOMcTfwgK&@`|Nn-F z-&_zPbkZw@yWEOdj0NWrK5A^1zw$|>z-}QXi_1DRHa@OARt;Xh0oXUdGNL_=Ui7iC zqi`941##X2aD7VMi<2rItAV`2!ML)JVm+tKOw+;z2BRlV*2T1)Q~%>;Mx!%^IvT+> zKrgbe620XH_Tl@Kv=8**s-?b~Zr<-90rP&bV<#_G*Rk8HY>kXkH$L19B*$7!JD|g; zHjR1*D3A5B5Txam6>T)o&L;us1H=$W*R7sA&{v0@pWh0^hQc8x3H4_ZI!vQtv6?Mr zPlch4mUCAl6d57(5jgpmxoz<)6=?l~I8zM2osDQoTIf@j!y4x<~hKTFL zc)S9>KBcS~U@N$Hkshor<;+{v^q{3DhD}ce8ANk?WgipJpx1|?TrsQrAyvxOqX#5x z0D`66F*0H?qK=cx7!C=3L*TQg@A^4dP(e>+HQQ(-m|6Y`v_*ya!||-fnDsD>m7msZ%O@ z=#|xheS0z|l0%F_H)|3rI>ydvXb`1NpI7-bDn1x8jAFXBekei=r-lL~V8LFR!S}2C zemDtJK;8eA{GGUptnu1%6A#CkE%rZ&DljSmwQrvY<9KO9#7E{a5-`}{wGa!77u?cC zOTt`i-DDVtO_V)+?(y2PmXouy%Nlc7g+_xmQZiPd0stqOIH~(S($dm!bk~b)3dN=x z4_riapXZO>qfM^MUyyiCq`SC;WPz{VdB+S<-NYQtZT#0sg`VQJvMVHG0Bvwylcgxx-a-c7D0s14Q*R?SRJrSkBq?hzml6>VGx z2HrhGiB?eBkflQ^Y<~ByQ>P_{rj9|SGS!!}dHD7tO5T!42AZ1uA)}pnrmh7xoS4H% z1{#MM_KQ0JzFE!_jgAWD7}oI3LA&Ev-uiU>S_;+>tm(t}x?4dLiuK99N3YgTq( zps1Gf2>7B)-CHDW=bz|Fiiu_Sp@wda`ppcM3{(x4+lRD0Oh&dX*uEbauud2r$ly~C zv`2$VbjADrsqrM@`;p5F8%8i&IrBbR`#wp#0mR=EH2dSXC4Emsj674`1&NgE&G z^iv(+b|{A-d1ijAQo7NmO`8<(QBk`$eOFrKfl+rhaj}dOLM>hy8?#GHH%h;t>?~&& z8wUn>yO~l-svx}qn)Tg6Lp@o0clVlET3Yta^kf^*+=8m)OLZ<~KTYygW|a1fL-Jj! zpV#oqD+JcCcbHJ{XT_~swsZkC`R<+jT~LeZBJRPSkznfci$>pK-3R=uJm36{w|c^k zur`EL^tu6>_bMvRjQ-$yc&zuh$MU+PIs+m9a}##n>(I|N(y?5 z`nY;?esS?7HWuIsqll+eXiJ?YgS2T+iT7y7rk1g_qW+iK1_rn?2z@|La}t73KI=*` z@jsD~`~)*@BZd=MBN^~VPm|5Zxbj$Y1jvr}s^Kp>G=-NBc*^W?`S9$OywGH0tX*G9 z^$bZ&Ol+!4ODIMJHD{XdAHqzEQC%qYTiduxs5f6M`K1sol&}2Q$zvCvrZ#%(w#WId;5wSD zY<}tDm71|;W@TS}@?bT7dW_#4F*deGVIY9F%34s_zc)8h3_D#i*1(6qxq#eyeOHQ0 zk5Jdz8@KLJ*{qh!CwqgSU^8+7MEc+M5w6&ZjEV~=p#(LJj#cluMqWiW=bq-h=JxhW z{{AsLL`A7>bzHq|7*^H0~^q#VyH zSMG@9dPj=l;~{IV==$$@0*A?7BtJ5DV>>zz6sX+z{Gb+`ZHK-pJ7l?*T&os-KnKkC zRs;;9cD#-#t=sJB&krRW7Y}I)iqO~y ztA|68bzBakPS_ov>*RC|@p|K^xGe^_qaOw5sHD0&J=E(r4+Ye(+jEKyKWVrR(3fe%DZoH^R%4n1@$|!KSHT zlY#ZW@sB6eOqm~jKqo04Hr!kp@XHz0uXA#Lov`aV*{&KR@}Gz60%M4@=#~C6ki9o> zpR-WQbC5I3IZmF0P%^mI>QThw0y(daBo%vyw-KcKezS;1iI(W(WV>vLG0F+aufSY3Y*5&;h!6$08HKLcyCSoCSW)(P@*;pTB?rOJ!B+ zb;o~(mDLWRAVYQ20~EojICV)P2oP&rW?fWt+*GTEy}5OnTi09YnxDY)=QCX|P}DDN z<`XsBPj)3ko1PJA?5WQgr{VVO?AH;#RiP;@iPU^f2vS=3LeeXGZ!!}vWm_$NtDVdJfcbIbcfrd0P zGa%b`?+z2Lk%nvQ>tC83BFH0pudh>0d1L>;<%VOye0&Cs*4;P!0=(pVrEYenU0#&|CPiFN^8#O>PU zRauTdD_uD*ak{l(*ib7%$S5$-fKcv|jt3FB@6g%;@CLUNFvg*5v4OHF;vNa#kFO)z zRi~w2-{++<=&X>*k6OX$z)l4~&Ug6m5&ZmYNOe~_XjQ)n&}$(eH+L4I^B?_6O)o;5vL0XI5{kr{6^uc=yR{0MBCPGtp{hWtAtA`|{*jW}XF8b2cz^P>d zi)?RVW|p<$w?+e8#YdVdMwXbS}0pv9Om*sz?{CCSgaD}owC!n@! z=f38c(5$T^VY_SR{@g9Q#1LJtH<9tQ1JxYcYCmHOCOv*}WFsJ)(HZ9EHEC_aw{n1? zQop2XkQge^ip827px6>)B&|zNiC#eO7TRev6~B`i(F-Vx===U~neqLMRSX6XeifGj zB2-lh?4ar?(9RH_pise|pi3;A>c(TyX#L4drxqgC+|zb>PK&_kvuX@xWQrV@kTABi zj8nkumk)X}e`CRS@!dz73>0V#_X9rc-!gjO5=vs3yLEvb{?Y+(Piso`Y@bg@T}lnb z29UAW%WJhFlH-@Y9APbVt9zrM?~~Y^`U-Y@>c<$Vn{TsXA|s7yYT&Fv6^K*rgY*Gh z-T3(q!uU$z=ijjL>Jg!R`*NIcCrHQy1w_-+z_1PohuDMz01t;hY-5c0{f<#9;IFmC z)1*CDO^1U^bUf?uOI5Aw&GiVu-}L-9a1GeGpfMsT%3oBkC05#J=>d4t?TjEHilUnZ zjvGKp4HI4oKX>k&oF7?E^ZW%W@Ycg*BLp_Cd#=RO3~Qt4&8(sAiP?FfBLYRS};RmJ6G{Z-~QGPpDqfB{2v=}IV{ zyIKN>?pne(KTJWj8{n*em!jLF&r2_`!pCH1a~?l#Zjp#eN-}*fR_OU(EdF1H$-mC( z|AVUQpzigZ?A3$?E#=)<>(Y1#3O>E58pOAqNG+-^)#a$5`VCuF0Pi;Q*upD)GZ~{= zev4jLlY-=#3}fb(i}LP$x{X;^K{2B*_wz0V*SqAh9{0hZoaY7(fJqmhS~g08>r`gV z8k*bL*-?dtbiTQBw%DRoW_*ll_=!(k{6x-Glm;(hB6y7dv#T>DRzDfIi;;0NcP|5# zq_iiCvgnKpYT@!<=&3BxL;9Q4ZJ&umn?~6WZ^7Nh?j6!LY z>?<0cgQqS(E0)=={4yjWp3OR*W*!n99Xr)J_4pM9tL?ro~~-# zBi(z=E${u`F>?VHP#ol9s~r^*@UG5@C-#<9F&}_nM87o6zxKS}t&FW3c-~X~3*y-E z;oK6i7kf7o?Zl1k>_m-=;8|BAAL)Q=QqgN336J!|r6sEA8g^|AkZ+sVnR##uujyze zu}iNLwUd*1eJ`)sjjb(4wr3=p-9Kb|_Ux-Zc|n!oEZqi6FqLM(Tmx+w>e7Da9%7L( zzt*j2D;GLV%w9c&^M&J)r0(1sG0{P*=MoT<&t~$jbl37)zl1Y*w?V78{mw~h5)jfe zoY~JLe_QxvjmM?im1BD+kZTuRcwx z$}x^DWChO#h`;NI4S5_x-QTYT;HHf1_NOfDa}g&e#~fsZrEQhJdH38A1UbL*+t9my z;5B}F8el>^PlA$>5oRDH6WmhS(vUvBrVGwH49{}RLc@l#2dWsIT@4Qb#}#Q+hA0p$ zlHUe)mWfGDn(RFV{K%4^iJQ#Ss#3Zh0U0Q5dpbnlxg+C!N1bN4GpJUaX;V5;5x_T_ z+1rzyuV}sZ^O^|et=?{OQHiaWwB&JO!0>cbzgx)E!^W#-cKT(y;g3b&fFlrpw+7!c zHir1{p-LSM9RBstE-@Y=QeTenHsMcP_C z!}%XdMRbYRe|$yA$gohK=IC?i2Xh9h{9;9%5@G4Gx$47#)U3zO#nyO_^dS^mV$7tt z{l%j_LY9oSSJ6V)3P9pbcMEQ$$K>W14k9mz&xL{O17F`$?wF;K&Lw$n>!}^Ab}RJs z>?)4_Hk5pPsln74cMFc#X1&QTU$igyIe1Ks=V4#Yv9-1 z;vQjSbqV3q4>!7!s_aM4tM{Mj%`tMg8CcQM)@FNeC0bp6%u3GX2KF#}sBl-)(_^D< zRkz~hj5;0+Q7M@TZN@jxkpoN`?hld6C#=UDosWMQpDh``;Zq!a>(hgvB`UVWxjV|g zif)iK=R&b4A67c^UhL`dhG!lH$iP7H;sA9)rNA>;SB;Y(MG{3<6n5H<8yt-1QkvyG z?fvQ+?p7xx4Dh0Suh{wm$9s{zL;1!rBlTqU?D=^a2Hl74r(_n^ZX31Mvq@0ph9Mfj z)|>K+^Ou0B9~BdfY`kg&FM{v6R`RKqWzv}S`0R(C^gECA8vT^BvL_lt?D#sW)?|_L zi+uoP9|whQW%kimV6{H?SipiVVrJYVt;_r3 zdV920P!xAlHn>Mv6Z(GyG+UKa*p6)^)%W!1jBgzn-eB^gZT`z*RCRT=xVSjDfRf*_ z@T9dywFn3>HY+Rmx;IBt^is~L`1a9lRrd5}W0d`=X_4ltvbD-WLdjJeb0c7Kq&pns z^innkZtCx!rB~ou?j`SW1G_}$T4fz>f4y>^TVGd;x(T&f{vID*qVmywPCr6Y88e$!NmF}yuJ@=Y zqF?+>x!;VG25Udf*h61b&^#ffY^(b)Jp6#7QS~#!P55nH?=wc_*IJ(UzH8EgLY0Ho z4sqGh{NNAeT7yC7n@JsYZ((3*db8J(<`JD|!v4<0rkZQen6Up1xmm~b zij?`4vuAmw!a$Tdqo*|O8wjqi^nV+Y`|``hL3ziD<`(Vf=T|zrLW!6qT!DU4b-(J4 z;`asRd8jabbVf&cNPb${>pV5YXy+Zy)`;nJ6`t*A&Am>^Fl@eVsDPr!r!Njp&Q8~d zlWnQDtQGyhHg$1%xP8jmcY5*=E#Bv?D(9tVd6n->q0s-D;NDa%EubmYQ=+@4>+jsX zdu9Ir_?O2&KVQ>m5A@lkApceV|E)#;KV7Z`+@Z3~f3n(kEnr+P>CRui_f9MKb5USL zZ#oZj;KlWGXZM!|ynX)*G-evGQa$}|*ji?$tkRWde)0ZaEFKqEcXi+XJKPt*-MRyy z<&gb$zdR&aCUc=;L8E0k~mVKS5?4I@H#KfsJi;BQ~ zs|QxnwpCl)`sH%(%9&4eH17sH()oMsdhhVVG5-j?BDc%Lr3s_iKcp}dpOJiZ3 zMu2aDAF#pSOyBaK1^+v5@k|MVg@ykG^L;9q9-j;g>pGUmf}7*DuWXDi`-FLnFJv4(v8)cEOrX~C)Z zG(Yxli`Mg9Efaa=6Tsl)&3faa2Odn&Uat=y*Y-yF##T8aW7XP@ng(K4gKD4}smxeX zg3NyS81rRjz>tGHeez?H%LT+u%$LN2S~bkyd^vxo)cZa7;yQKC?h@vUHg19u^BYrV zufU&RekICnc}Lw1^E=NV`FwN%5u_G|FQIr3#PK@X{n2AQ-`WKT_YT*&j~^lIN-HYP zypWO6KHP7Exk7UC^QFJtDk;6JqGEb+(eUgR`dC&$VIUl(7l#%Yp8oaiURPjl_MR}! z&L6eV`63}ft9oni-Hx#X9ec1+N2}gUV&W1x6FvQH`xh_Vi9dz}h&(zxjjyXm3ORH7 zj8drq^FxBO9Hj&`6B93Q-lUxzt;N}Ti)%6z_&E}V9O|=P?)Nq9(YF{-v(qe694a%$ zadn*y&fKk-4N(~?hw^GTZ=919kDlsO#=QLF-IXCBadEu5cX*IJxT8C2Y}D!NGto0O zBNb8>~0Cwy;XAl6%wI+S~QUqK0he2VS6sQO5C3NW_L`H1!W(!m6WjHwvoj2K?Ccx@j_Xu)jk@w%MAq`?TOpZ=}^0f^d43kj*!^n?U0n} zE0{4c#I{`coYv}l3h?OV?p8H`5iRWw$4$5O@4hxz1X8jFIai{14dVO#6#HF}u| z85zxC^bA$6BO@c#ayd+g?=OcBWl2b+l$P!dGpS>GFv6E}%-$V*C$=AsRXGh+*qOVd z*hc0{og9ji3k!FHUfZq?7Z_|@c7{UN*9}a2o!5_1Vvz#wf?__`cg$55hKuM4@JU-C z=!%A%fx<=BuguYtqi5DSGZk`as_-%WV`=8kxB0^@VV}LR`BKiuiP%U=egqA}tsXP8 zrH@Tv?ap&AV!zaj$SREZSb7sNtTy0HNuvUXj*r%~T~x^{Y!N!K^^dH@vZSO{ojsF9 zFDnHW`gmRnC@n4h5M%q{2vvO?+rf{7i@xZ5+2D@{SwWsG8MXvrJOoV-$rN#GNo0ln zMnWC5KTEwnppJ@US^$b-zIAtaQs5RPiDzgiF)ra{E~}aQb?^K?@s5>2^yk|B^+lWY zSh{;QRtEzH2g7~H1Y<&_WejI0YYQNAd1k8+&eky~=H<$`*^Mlin3z)I;wGLV^OPBE zp@&&S9!^&@@};Gen!b1ve!9{Zv$nX{bUU*z&EY1SPbD0}2Q$2d#m!A)Ry9kM!p|SQ zI#SHMJsEGkzF6Z!A|)2fd?xMab&j_vEpMI7p@6%R5qb7w|&o6tSd^yZ1FO#&JOO%Ytf19XJ4d1D&;Yk zmKSHSYRJtFCQ_1R`k(x6fkF3#83*y$3n*q`JdS$eyF~3kV%}!PBZISaZM4e6r;rV+vWoYU zS)Fi#z8I~bAb}gb!gJr@l@*h@ws2EB7d&>~Quw>&u_|86jq?C3=BBk97lty4A#?5Z zCX6$R@jy@A_9)Un$DP|LSd3i1M%FD~E+8jInK*bv3vwczB)sz@( zf8XKk*=?G8_uxMukZ>oam>5&VJS~!|SFg^@&2{IlnP21M8f3(P)T}qtrg@k#A-F+6 zq-4S9^jLj|e+NXYVX&|0wwYOu_2QI%tAoA$%K|wmxt*QSt?q96(y}s=X?aS@Lc^YS zQ&UqZN~M?y@Kug&WCAhU7t-B5EO}wL$hc#8C-R4dQL@}(Q{M=JG1pkH=I6)vnNM?a za+cdHH@LfF7of>d6C=etr(Z7hrVbX^2{OSp?P&P9NfZV`#GM{uW~h}+rd_7bEywNB z$U+~nin4SW8f~7fa3SbrVp=IRQ`QCs<-$TT&iCH9SVySw#=}xB+f{gzfcM*lFvY~A z(!I-~q15@*h9rcyZdzJe-r!%ojzX@o8=1)UksmMI0uaRoJ-9OJdSsl^@jgMd^hn;VygXJCP8U9=9ho;)1l=zl3~aKZr9S%LU-rRUKcb=-dl}IFtdJ=vOY9RR#u+Ha_a28Cq?s4j*eDM zM(jp0Kj7VBLXvlw5C8B$!p~F1yzH%^q@+Ho*mTfD0oLNWyG3qRHY=~}VE^Jc7R^=3 zftf;=p0bbdBg)FkoE?rUFAbk6Kd`&a$uw}y$+0*-A>lKka?|SqXfd&*jB<_GAav;{ z$0J)ca(3q3>{HO7E5rnbXF-O=wNO-X_F23OT7ax94CWCM;88rc`1+&zSaDmL11g(y z@#>Ec6!)(2*seStcR2!$s8=T*Hn+5#;`+Ex3EQh@3TM&1D~M9k$3BbAqPL>Bix`4d zF^Xf>nb{-Xa=AHStAk#;5Nevsm$*F_!otIFnLaryT~<|9V+Xll zZGT6TECI~iyxE>0;$AVNYA>{N(z}*KZM!Y$C!e%={vUz7V4f;U#|2GF67I{eSag&F zWX@uH;gU!H$y;n{_maVph%q5yWm2rMF$%>J&0AfpDM7KZI5ola$-<_#wj5h?t;M{s zcM-y>W_31eW6`C1XXR;Ww>I*$w^#bSxgqmf9K=@l=yBaf%(k?2H%SG!tltN>gl@E$ zjoR-HIeb*7(;Xk<7tUQ+P^2Vb_xebTZr;=t<#s(jv?=?~;NB02s?(?K4?Sta_>F1M zdyO~)tBxlaKi6-9EVbL%n%*K0Vz#w0^^A<9S1aEmflyLX4w}fx%E^&Hl9LrIAQ09D zAu7s-rXboY!bUAgQth)+^t1VDZr*;(TB96fgzi7BRKZ^!7xHL1B)PylaLCBFOA-ve*dR* zZ|d{|^f8iIkkL<#*Dk+9>jn=^{~bEU>Mz~5J@1JqH+*^jOjT9&nI!MmW2lpF#E3a8 z+K+-a-(^%;THQGShtm1OTkp$xSFT!a&YW-Tr^XEZ<{7}tIK$h2guSdDh1Yu%Qy>cT zcJBI-AAh}uTpaNyk?O)nl5Bi^R#{o881|ti;Fbpohx41!jrM3>Ry!9y6={y3fEYjX zM!zo&?K<$ijmnP^7kbT7hpJ8KbE`L5F!T*kHc$DEfGYl5N+zBnal+X}ivnl9G%{*}`pLeb3CsOz(@6y&8VuvdmC&e|2%u zWV{$JvaR8EW-wT}6=U=5(RCvgbp&N>nAI6TmGq6~&9-dK`*z}Y_QB}O9LgF7LE-@7 zqV0U}PegBE7~SK1ut*Ds+DTTBYK;3`AtF+*>pNk^gT{X!{GN#niNTJK!q-uPoq!}a z_)=kr#x!%#$x;)Kmnvz7Ka+^%sSo_}7^HTWC|C(0ljlO1L~qhBr%7#1dZ1rU#!i7a za#~U%=JTwp8qLwfLv{N}hr~JfP(iDY=S|x=RxzKbFQM}ttj2m5^~n!EXP+RS|K$HG z@_m=?akdll{EGkQ;0kXO>2bXnK09qaMdC zDhj!IdnF~hTKCtam2rR$T%IDfxWn2LM)u>iub-cIpLe|esTPWbje|pYiTnH-zCak0 z>dTgpD`Rk2M^1`Li5VvGsz8+EX4W^itMTrk=yS0;lSPT8OUz(KYfDQ;*!({})0GPr zGQNy?i~So*cnBxEwHvn>@UYC+@Uuof1uT&CG<9^GnP?7{PUPU_jdXzMjYMk*iQi%@ znJl+klTdX&of#Cgzc>#N`eNK38K};*iJUfJa5n^QC#VWly)nj1RTK z_dNGwzWEwu_1+YpzW__6#2Sd4c~vmh9ljQq1%5^15IJfh-On zIWJO@w*q?kX((e!wL{WnlMn%O#e&T9jm!+3ta#4cKhA}h$n4rhMROZ(QTC4ZkUEiK+} z&z}eH8>U(JbdX{VUd9TUZ!Ea2%Sy7ixM#ca%w$CjtmqCfRc#L5zVj%sLy{e)X0=Wh zVOHS#!)yN(x6>G(B>$vf|%9&)n63HsA&kAwpwBSoJ$y!|6$w`4>NulxL& z+j`;0x5SL9sQMFFa~3T)vRc|TRXI<3<;&NnPrLH8X(>s0K2ChNQCVpkd6KSn(_eS{ zw9ah3GU!=-bO4rz$Z)Ou$IwtR8wy_Asp|2u4nG*oil6aBR!iC+vlN6bT@k%Z==nbC zv309h>sw;Q>U*A9S&71J9#qHfZ(Y6g;d|%#KqeiEwh7;M$8nf^JHP74e~CyKQ3i2# zUQv|Mc!znpAFwcTpJ~2AWQy6SpTECk^#1GDr-4*qzWI%X`I-&|KDkJK?R{#hzWb7~ zksSm`g*R6H62l^7QiBZ=WXX&z()~mDS^CjSTSj%Yam7Wqq7@_z+M1 zg|aZs`3+pIkKu-`ct2d!nM|lZ%m3rxgaDSiyF2KxH+DEwt;CF-V_2k8p z&wQJZkib(E9DG$F8b5k#i4RTqsYSsydI0%lR6`j~w$()hCnBQWSpj z;#>}ILK zJl)2sHjGC4)AlEhkm+ezGX#XU0B1`C0f3Lo;64hLM^Js^=jK%^y5wDnjiX7rI>T+^ z(JUlA;T_~lL8V-tOGc5*W?c!%qoog3Rk0Sv(E@Xg^?Ang61z(G9yfj=yJYbmY^J30}sx zNrbuL_%jeI((mNnf5iJh0i825H%qDw;)txQ>|nHly3|8@0s>VvwY*<6idYSmDKQ^; z-WhjYSb)b1u*!|j&Um15P0pH*_(YV9I|iwrI`!tQf4p|1#P;d{ptV1#T$Qyyk3d1y zw9DT}_jqMbvNo&ie^bJ`9upITmyw&E9-ox-W*nbg2i1`f&35mOn(Q_Pc zfoe}k99y$Aqaz?->l0Ju$gGc0ZJm4Mr>lF)V#HKEOH92E#{)mlYM;erD4VQ9fY-m9 zK6z!N0>=%7bMYnOkSkCjdty61i0}CEon=lFEqK^{plRi_6k!yYufCabnerFOxjsZh zkWS+m2|3!>6pbFb`p?eH^p2E_O{mMuKUY#RR``m7d|keWx_$$C`sNpK8_~Uc!fdIuLFOBH2Ff;>qD7(Y8xAKO0qM&co8N8Y{VyE z_~=Y=Ggrb>{|b?ouZ5A3Ji06?tg+m^w3e2a`}#Pv;R#PfD=gg)$3ssYAYuOdmX<5S zWkFCP+%D!kVG+xX+aZi1IwIXHG{TR8s`too_~Gu_h;eUHm6Mx@q{Lj+wzWjmURYdQ z?Hj)B`$MrVQ-ddI^JAAc|9q+7)L`DU9FGm6H9}Aj=Yiki#aI`sh?e?#{M?`9Sfkzv zb#--#hYxH05!yw{D+hZ@4EV@9)jG)u`GKB!gFCh`h&eg(jFgzx=@m%H>1ywHQ}?51x#9fEplY_iJa#hoGw^aW`hX01SdLqbNw5A)XW8C5g$wGo|M4)M`)idb`g{7ySs z?eMa&wzuRh&8H`hPn`+l74(~tqvwJyQTq#CYCgb=yv9l6185aC5;M-1%KCqk3wmEN zfu-l?Pi1Tk2%7O@gP*+sK>K>)9XheR?C$Z@I5A~!Qd#uZFjq+u8P>=%?e>5Sj0L>kP`!@*q#dcF;xqfkNGnVKZpg31i z=1t7R1J`d`k|XCn3}8m|lu)rIBv2q&rvd`(9USuR-*|QFDL$*o-VYViytb9kQ`@7J zw&v%tFjd$dNeM_<>0tb~Bx67s6kb7P?!tu&F4k0;eOd#J9Y&U=Pd9GU18q+%itCi9 z)apLq7^?Zoz7ZLB$y!2b@{XB0CJ|j1fZU+Lnfj6?oP6mg&cY6|jk%vNWf1!troefM zG=e|AEK*z?Dj+^SJ|^eeLlFGj0F$1WjTZ+OE%!sFzF6k_QO*w7A+K}k{J^T6u(HIb zT$18m!in5o6nZ=plw>m23Pd1Y_)nb;#L1)WbeVf1V$?USE_X4rG83!6+|Q@Z&-WYd zd;8|%WV)dwWOrqrI#;#*mEG9>!*3P5oZF!zBPfocOmUeKv&MN9e~q(uex;Zn?@rOY zyGZg#TwFXlc4=uTvpup+?|{I5-3i6Dc|-`9^`Icn6f@vwbN zOAA}$tJXYHR(~$8+s|4+vcHhV(+CP@=P9rBPMk~D)d~C&<|Y&#-T-&2c|gRdEY^AE z6&~WueJ-xB_g_Wq?bd6ZCr_&sC#}yNLv7h_+z@&n#TL#xJSyVX^rbuLb`-DEZpKl$ z_2Q${Sev;jA8U{P8yJw^)CTpre@ZO>BX)#k(367aj*Y`=J!pZ^oP-@CzV#AYnWMWC z(8qLjizeZtqZC+Rnw}jo&&bLuT{_(PQ9$n=$s$SR@h~G6uyy?K!-BY?V(BF~m6J0Y z&X|k$sCgxRO~%!hMTj}oJA5*U<~ib1_SU_3T#ywvR>n(0o4vf2AiD3xR~voO3_MUi z_BzynecTg!Ht{u}?jo7MXT+W#MQ(RGM8E%VpBu|cOn+}Xs7IBN(RR=7q?V{E`d0*? zj0}@|8WCR(V0<1GPwBIW=v_ynUN`9+1sJn!mAi%Q;qSYHP`s zLCJ}nm-h~i;dh!`TkEKGU*njSK9OUV`b=O^<3C-vPRD ze&&|JXaxS)7`!Ky;8K8OV7>z@xUy@s{E0|rW%CCngNpJGA8zomuNt<;97W9pw{Wkw z^9~r+e;cN@+1=f0@m-^y-t3W${r+jls$!5SOU&{>XUNTy#txoKmtA9iec!PE4UgZPL$#WIcbx-+T zCx88%8XHyJpl}^Cu{VoAzQgYb7grWe8HM-K1u;kOx5f*2dIzQD6q8I7F(?oFl6|Q^ z=k{9*R^Tq;6!15MBfERJEvK|!zs4rq?5xvqJ!q0s*|Vd2?M0Uz^Q{n*{I7pRkKmi;Lb*YGC4xZx#>BO z>7K5J-dnd)dv#2==R7No`sY=u08liXz9v`}fo$dZ~?1yh8iM zdZ^jn?^>zUqMt7JTd(iV#Rx#(zjqG&*cQ3JdFX(Jnbn`)dLGWuqU)xf;BIV|w{hiW zGz(j9B-2#=R99vnnfRXaw7LY;ll9)hRBoqTNT34ad=2l~wMLiSf>=wm+bJv)*IBuW zEs#r!9Ui?gc0GyzH{DuRSD}$BeJSZGCDU8kjZCM(tiJDJVjuL4!W){nI$075y}KBn zl9JLcX=Ae^H{YhmdW6HbR|SpS_CsY2UQ<XwG}e&msRaz9rR;nlM(z|Eyf= zp=3!;25#>0U+}s92KRQV-3+Y_?np^7k2^Tm9Tb5IZF82s9_wb8l#L%SVP59DlU9rS zyyYwP4VP0MEUdm|r(V6fr&<^mhjW??j`PyO;{K(8pmoHA`~K&1#U>(?ZAYl@h)$M= zw1WHRWzc(9kVm^Ne0+RdmWNy&M-F(P*o&Wi5~5}r)BZVw6_Vgu4Se5{I#;=NQHIvx z(DOyV`QVpQX3Z;Cub%pJ-;xnMt?GPx#GK6d?STr{h=oqrIm*C zt0>s~Wj(HI>h|8j!Ng?Oa2j2;b2vUw?B<6uGD@f0ZHrt>hVIanX^~f~axh^Wl@R@o zO(k1fLe;k?ygd8+fOqVOw*Sd>`nxs+5ho;8P)t;-)8b+j*9P;$hwkSc7Buq(kG|x{ z;l$#bjP70lrieq-IC{=shQ`?S#2(F-{`VB6YVF2M>m_(NyQ>Sn8+i|jz|jm_sJHiO zZmo$Pe9)rO%(!LHa{8Hi!8=b_Yz+*3WbW~;__-;>c49*{|JX>a{3mj{tlW@dHJj&- zs{_8Bzq}%Y{Bb$=xu7PUflL4|wV@i)>$9QM)KQbGcKB9kw+BE&L!HTefx*Hr%ou;r zuk2U3h@h9h){WG#9Jd#mayV^!s;f$*j59Nm-jM=8D@I$NOwKb#08&SRlfrl)d;W{( zv@SU1x-NA7OHTT$mn5SC&&C%AOeRtr9KvVWIBF$PRNVdOEuF5|3~2_w3ePCZ_o+Lrr~a?=M#R?L$VW)TsHeB&OB*W|Dk@)v-qRy#&L$c@JQe5_K} z8Z2PuzU9z&0T@%7rX}CxyVTJ%_?}NpOG}gJ2pd#%Ip|S|IFLT+p(TP}nH5^(+)Z_L z>*eNiJ=U4Eo1S${>iwpMcFmPGi_YG%ej^cBeYE;cQc_nuMwy*%;M3$CP z*Zfuf`tf7uH~C!Tz5khs8ea3QuBYgZoL!(@FASy=0;9$j66tiXedFe)h%|@!9PoX;8h(J56Y*4oiin6L zLi0~TW%rA}l;8Trke?)*oRoixX@BM?{jRI(7bf}wDCAqG{a?|>`g7)(j0bw?uh*C% zW||23*loOomN9ho(ti9uXjC{lgMHu5g}f$ji{-WR18h17n8sAUtNo>XhB6flL)4d&9vHf(^*m_qqRh|Zh~a&yH!*P{?Uljf-6FY>=|4;hlf3NH}^gm{k|0Ct!N4={nkB`OZ3LVpN z@N7$n)$!r}vX%HrQm_R!DE+*3@~4c|wsseG_m8cjjg(YvuS&+XkYu@MJ{i?p9l%FC z;_Kc8>n``|%I zgX)p=`Oo^}DR|YBW#-$n_O~EdGhyxPi?pd_s;VZv_V)IlfIzPfot+(yASXl~qIVJe zjLkf2=)K59E(4E;GG_^XwEcWWan8td^t$2lPIgr76)=5xJW)-XT6@j923w<}F;TF3d>I0r z;~l0l&)ruauvx1jWjA80r2xCV85*){tAObJ_U+pNc%v3G@AmnUut<2~cI6YfzP<>a z9lCJQr4G((7kSL{27%|&sA#TUZg{{tT5qO2@CHUy zN_cwD6`E$QuUqhQXuei=Ueq;p+!Bv2Tj5@f~w+v(P0ZDG{EKwqStL zromh_WTHQtMUeKXKY17MX|tOiT)a)w7LZ>04ETbj<@-;0lxx<#Tej;sQm2)0&Mace z?MjVxp0YTc@g$50`WQC^i5ci!WVN2#Hf)OyO41guoFoFTTS}x(!Q)Xp2ui8NCNf|B zDPea+K+i_7I7oXV!ELeNraR5Lq%73J*zUE-j*)FC`S~pT_o)%I%gWncDEN@b=FW~~ zL~gp_a3bYs9}Z}-euV+-PNPA>I~Q{Vz@Obx4S)^fH25j|{K*KeW1Y1Ff2uk2BOG%F zf(I6uW_(z``(ft-ZWv|c{fFnzizx6_Th=*N1qUXSb#(NAo{jcX$-e64?``AoFny8a zxqALHCsvjfh`Y8{q4m&(dVwp`R#YOSq*S?J=lPxe3)(3^djU#C--|tYGF<5pBmgfM ziPszaG#Mc` z?2gJo3cheO%Als>D)m+$-+OmizfJbx!)ogE%F55`kwHO;#B&`Gu5p@3ve4!7uV0@r zs0U`*7!NGHv@Jdmeqwn1`W#ERS8#OjZ4pO{^_L8arHaNH4kAzeoztIE#m3>0p_6E1 z!_vTxk!T(p<>>|=tED6Ontq1qNbwt6&|!sP!tND)$~Q8L1fozVlxW~Rq5=@`qKq;# zGfAUhlr;RzN{fbzj>zcfz@*o&ou52K_I&XSTB$u!@E1st6g+;m21DjhF9wJ9r=g)o z>{>H-z*J<)$j-Jr5Lg{48E_}S?|;4TYB0nEUfGRIIdgQHl87?L+<$(!vAS9SlxpQb zofgW-MlDM#5%bC4*x1-+Vf_XWR#fxWfK5tPRyC`&^{B1D0z7Tpsceo?GT(<8X=&)> zIGNOTW!R*o7h*uNA?~`9_@do-y6Cg_bq+=B>`V44sn|{j2L}t_IdfLgJ0)`}T;K|w zZo(zaY){t4pdTY*ChsFxSS9I^gPh_^1f@!fYI+9KPvdVyf(n<9kk(B`rkl#(CT_-) z{S)d+O6fpyFh*$Sg;5;5QE3%NNI8xrWGoqpM<0A_y}dI}b`d+eL~(_koxMnTYwE7) z-n8*1fx#{Bp#(u#nFvtrB#shLJ2HdszPlVW_f=Lki9J(+j(*uqX!Mh2-A={4OD-X8 zb>DF4z(S9tfaTK8-WShiUI%HMf&vwzeN0!K$;FQrQUNs#I+bF&nWz35ACo?Rek2~w zmtKzopJO449cNkaNG{)Uubq&tYSWVe&`x>nGBU`9!Lc5SSfSoOIN)j<=w(}#@<%M4 z0WIvkh`>PtG8-Kk1mI5-bjqtTy5I-5{x=i&n2)hu^J4z~)DPgm5iB>H>!p}Rly1NK zp~3b!F@N!}xHoT-=qblbHLJ#+i*&)~=M7g%HzXRf`!Cct)|Xet^=@Ch&7_o6MMAP$ zR?Ez+TyFAKi^|Kg%%2+1*B^7p!WH=K$0-RnO~)!ls1QS|FIY_mEyQCt3VCeH#Ty}z zovDmq9*3znLT%t&gv5Eh_HGJ0_zf_e6uc$Nu;YUPnruv$mlFA6Bh}Vi`$JeX- z@EooV%T;<_deOk`iHTa}PZoLo)tiBSl>5^D0s;@oIF3am1(>C7-@YA~q+-s#G+UIr zwC03dnq|+l8Q@1Q5V+buygGYzxN6ybc@s0KET#?FN&#NyD=*iT!C)}zCN1U>_tjZ& zKEeM3S_LLKtyM^33&p8B8Wv?_WPp7isi% z35bo&1lamwL~-Jk2diy)2os~nNRVQeGnfd@mW5gU=IJ5Q7&@TmmS z1_$FwcfT?PZ|x}TMs#;?IaCfjZ*dm zKEb7!1G^#zW(0C`oSuY(V_F)I)~Mo?&U=a@SRGpxNqmJF#3khQ=3IQq*@q|hc2`|x zN+|iamtZg&5uO+)F%4H6+410zVPjAfh(CuLYOs~U85|Rm0ths_(=Itbx2_jh2Zoyw zm>$ghN3c$C#n;vO^W)k1=w8>uMLd!2jp?%T*}2Fa5E;SFitLPR(R`G!M^oGcFS(UrQxDT1l9cqJt^V}OY^ar`T6vU`p?gdEMg9zPXz!BAxQL?w?kzG)uVm}D}P@_~xqMJn(D zE-t(7W)jgF=DkWsmV#T@be{QK#C8|m;fNI1)zx);`3x@EU$k%)Sym7v zDl7c%ov)$P!J!I+#tvv^#AYpB~W2 z-Hmm=@ z0Rq-Ha>+w63B~b`alb;w(Kr?L`<{6Md>F`j+o8?O4*-(mHsLzX@Z+uF#wIV zYMIavKYDB#85{L(6ibL_U=~qCq>8)Ke}HgB$ez3fX9+eE4%b%BH;Au;mqF$u;PZVp zxpyFF0aPja=nNbEjl3qPus;cXVpm#nQ&V#*JU1Xwn3sIK`pA5#KhZ<|2suD4(g^Wi zT+a1ds_5?i=~dhQi3|}E`RyS~gq9o-k1`s2KKThq;76*MHw-L1UG3Fro+*!=t)2w5srK>%`44d=#{Z&UO zbIB?CcO05-uC{&s)Pd*c+10H}e+Bizrv=JLOhg2>SvoCvZ?mOp z3r$aBGT?J|=L6Eyo<(w+cY8S5a+YBZg)@-&tK{d^t5;rRq`8-FJ*8t|kzO=Rm#F0j zKX`{y8;=eb@q|TZS4&Anz+S!*OVQHR*Dd>L3I1{R1@_$LdQeg3@K@NSmuFEXW$dM(lKK=R$oP;BfvrL-e`rsVK#M|_w3=ErbY*pQ`o1k06O%}H%`5jpiw&(+ zc%D#2n;sAs%#=@1tQAfQhd}%dn-bpQg9Buujjdu82C5QTfvINt7)j8X_2!GQx`xni z)~SZvN%JVs9Bc~*siJ)w6r`v^u@>I@5l$RQ|4pzFtb2KXp|ZaFr-v>A$xF`HmKGT~ z-C+I?KW8t-(%)be!_-ytOqYr38|sY!!^VVIN)qNr8$bYA%~tt&;gTJAp}rSh<3!lO zTzLT5TI%F3tOv(TukJuJ4ThdjKlm|EY+ZG2q_v|qwqgL&a&fUA+b8V5^lQE;>h!Y@A{i|w5P0fRP zJYP&ux&p%FcA;!)4aFbCOVw|;Dfx$%jD=k?rms)pYm5-JQp9J>O-UZ7HL-h;w z{r}s7`S#1orWP=rA`WHkZ!~`34Ti=Kc?DmM$;oQFZ-9)`+ zM;!wv6E&Ga^PNQCsG{Z&(+lU%TShS5y9WR(NV)Q4_M2rYF!p@7!OU{)B4gi(2}f{n z@Zzf*mV!zH2@3G}c8S2;T>lID%{q@uF!0oAckmaIAxQm;4TfMeBSIC_YUMh?)cf`*wt;TU z`lAIZp+>$kSvE7NyR4KH+W1kbM0622knruhcXzJGWmBh@mOiVme}X^nL&`5acewzg zuAzuAaY6Ofzd=@if9bAZw}4|qGwCFcl+oon%%~CYD4hv^cO5GdU%s3mm#NX)&tSgj z&D0&K%K#p)CK%-13i9$iD>yJ^BJZJi$O>sO53;c77K=1sT^PS?^pD z(3q3vmzo_l^%ukHPoG}M&`26>oCL>;Z1plRp?6=tzPw*5YhEw}jQ%M&@4g)AsClbf z#`5xtR9Ck%F=qUxwD7Mi)Lvr^gKt;{+Up0MlXOJMPu`?$DKZX+Ip|^M%C_G zZpNoi>lyu+l8EYGE+qUm^iELzLFlCx5e?1lWGI0r)O!;(8)bKzyP~4xvt2}FT=|~> z>1$v*6HN9AY>*z8DAVf{^?N{o14JaSUQ1p?#8bx9#zxM4YoM-lV|r)3uC7QQ$tb5F z$F9h_=-KpKQgonVZRI2N)hjdfJmsS#e% z^*Sm)nI~`i)MaE;Zi%p*#pd{T(W1CmURQT^_W&bNmW_AxHWAWpi%Ups-6O|L8@E3| zjH6Gk@R5;5%@q!H^}wW8XK)NBr|O5xv^{Ji+n$R>tiI^Ko#b6?a+G;^cxXNO`P>oE z@P@{La_mMl+dmN#=_Dpf#9$r9Yllz`LRFR{uIhpVQ>c(IS2w-e)aqLYq%6OY*wW{s znv!T~B?CA5uZz2Eiey7~&YYn_wPn5DujFuFnE>%hX|@bzuXO zt>tv%@}!4{aR)*zBPYY?lU_Gr5Dvu}S1Jh3yx4)ET~^!n z9+i;#wpF`;%THMSo4GM>?>TI2BgEELAyDmtLqa%~%fV!Gdk4gO7f^I7Z1^UB<(|jL zUk7-u($+8+7VLA}z5JYz5WjiK9{pIC?t8|VS0e3MkPlB;7_IP^?qMaLi-FD=&QhXM zM4%zLipk~W%BaCV00&d1GUOQQ3K_2~h<4WKc}$YU_~!-1Tn8u~3BO*Jk8olL4J|z@ z1Saf9YAV=IbNzv982Yuk270(dZw&*UW@2V|2tcmy-n&;hC-7%%B7oX=Z}tb9^NR^e z1((?%{r5z6I2Rlp9F}`JWPm0mqT%9Ds??8>6;#`^Ye#!#W@png@|!7#Z>Q@&mYkiP z7YPI#7PMDKkb?O0hjWYiy+taV3`&I`GS@*H17^Sx#sMJo!>S*85Ku|?zdEzjms*1H zJ47X&73r?eXHmOH58+D9%4#)*-2+Xqa?0b~znhy+I5J`QoW(?6JjB4eYM4#8h2!m; zH(YH%0{UmA1;Ad%TJPPxV4E!&Zws?CFEswCDJbB973f-U8JVFfV z=NR#W=Y1dpqNrUJ`c=1#%x#g{TD9$i2F{OM#wkEjzAZ94dv$j>DvG@7zH3a~KF1Ke zRCNHXkh^9EmBK#c$-t8UBxs&<$UiVA=;q=s{g{N8n>#>D<*E~#nUiCfw}xRZY@~2d z*Hp^hnS_szYXs`#g<*tmSrs+4zKO$~`(}ss{DVGF+fvGJj_&SxjdDTXP zFftbtzE22;8G;$ColZ>zki5kaTTA5%AHjZHC>(u!DkAuUecQ6}*9B&6T{hApHg2Wx za1^^$uYt!jC_L!V-X3fOo;BGPaa%+!k}-BuBh%&kb1J<6Rf0nq*;|IegSoYgb~3#L zJ7#*qds0{9)dC%bw`J8`VxWAp=^D#BzUEv}#FvNVh?&%>3jvtL)x$ggz~VG)ysPIN z&%0JB+QGM#1#*OVQ0#Wp+SFM+m^fN4wE{PU8=Q!EmfK%1Wvf=Jf{hF>w7SW|y}S#- zVXnHm6rTn_NyE5(`oji=Q!BmysK(sFnEXw;pu+@AksU8?0#46 zwiNmX3HBHMf)U^+N)XpYd>`ZQx_mh>$KHAR-3~^fg2cwA=1n*7QUIcTg^GB2^EC)9 zA1ieNcJ*`y=dl~V7%b*r^@84Mm=y>GGNeoif`9voV?V;Zd>&h-`DK=6Ik*=u%ECMCeXnsp2UY+;>}@U}{=gH^q8=K? zrJ~*7Kac`DR5F%_rS+srmTV7zU|WOt4{-rXcw#~#sH^!KkHNVQ8U>HXw3abt48{Qi z3#9BwiTQvE9bI6OfRq$o|C_fS%ZdC>E84)14$AEyD#8Ugr$TS-%!YcLUj>aybCg%x z(to3tXOFMbK0z+iYQOkUHP>FT)(hhr^JlZ<_qC zA>wdnKqFl;wiIuklN=nR% z226&xQ?M_Rr=_|ap?edb5@5^%pklu2{{iE#Q+|{QB~POFqvTEiM{NZFp!Xbt=Hk8E|c2P31ExeP9o5cedC=9Xg{I+kEI6Rbv6Dkm-ctJ zPOOXi|7BfllgmDdL0w_iJeH_5T+)&}1;)+vn9^rxwM|h3mb8*WUk& zcMx5b1!R~eCJ1Fm%4&DLp2K)e)05c$$tTR9od9LIAY@^mySpEFauWR1)epvT$zJ;PaRj?66W_!bcbJd@U>j){^#kVZ4J5D0PaMRx{5q0XhPUw0Cg z#y3yRkg6NPykR9XROt%TE2jF;=@-Su@NXDCWitmfb&crsdV7AI)~jX8zZ6B9vHh^6 zHa5(RiWnHz2ML-@4&Qjrw#hucn^*1Acjy5WPGPy-{($CSReX=GA?Ze8lhduezgM?fV!;9>{`{VR^frlRPOsA1s;c{baC|S z{OGf*PqhyM@cdM_aRvXL8yET5$3YZxN28M1@rMgiqSCXOp7l0P%jCGZ{=KU!>rp?8 zdV76VSNDh59MEijvcTQlU0e(aQ%wFB~Bk zbGddc|wti{%j3zu7{Oq zqo&Z~fC)jZ8(G42_?)YLOI< z0j$52CK^LA3E~<%zd)mfTtG(EWuT^vV};M~Lwl<+V15`cvo(qVb=eGI8vS?BBXQSj zL4QP1M^X}j72Yo*b|g21g0Sb?kwdskCd6Z#)qtGU_KMV1bv@T5x4ZrKFFSr+vjDaB zEFb?UFR-;(@X&&B1x4P=EN?k=gs*%$^ zVy>Up7S7A-?}!Jj{YgE zeQfQP*~!}z@KE+6WFMB}*!;A&CC+n;O6T?_WZnEcjLWaaHyT5ni;YBNGGF3gK;Z2r zx{KR6lnk4S%XgwG{2t;f4P%)9MREX zM^QeQZ2d}}acAqTc#kGXmWA#U7k|{m$}T`$8IO1a?BXwHv&{NyDev5hmoF_n$j@hT z?T)VV?nm@-v|brfozd>D)iYte1^&TnD)IXSy0m$DPdZJmFwF6#9DM`+SXTrOgZdNG zU3`Y}3hd`r{z2z4gUD1zOmRwUHTe^-U{<{5dGQv)`nl|$9feK3Ybz^I&DfffpPwGW zo)qZ50mD@r=<52unW^cqt9G2+9os<3ZrqTvFAX~(eTfvCB(pg1m~*1MLadh;L&}z! zcgxQ+MbR`{+zQSyh-Blb*;w;qEa_pl4_-*I0<(yF)}KGfw^>-uiJK$q(xxR7w6lX} zvC3~b93`2J znb)5?Q>SSFJ1FOSOeV9Wv~)@@B~HS z{CPP(`Yq|ZC67mBKXJ(O7zIhKhewUdqc;fZ-cZ_bT+@&iSY7x($$%68kV#nZX(vE4ZOpJT4rpY$%bk zgB(Oj)q3Czaz1$SQ>}(2v6@fPCh?CJ*9r_8^O*TLN!s=K%UE*m-A8xKZ7X7<%y*^2 zE3{q?Huc2&DJgJn*IN1>sitON^YypI36zhd&1b<84?|a}DTE;*8=auJueEUyO(&Py zeivxAV+;kp#4`6>dV68e4SYJGfn-~|1s7|Skct922i-Sp4^Ieglg{lvaM^cDej=#E zImdY;by7A%vr2E^zi7+SChA0N1F&{M?O?Gh^p^5748jrp^(7}$Fq-vWq(lnYbH>t*uRY)A2SUmPphg=s z=!X!;m`A6wEr;88ZMFI(o1!~XG23T>xY0;*4(ENGWpSj+6Y=U-iQ&z@QjZaJV~(X` zf0>6XF8db(rt$8lrKg^(kE$S-tKR{y{#SBF@UOR@=eI4&%!nc=pj7ErFw#rtMMZk=kN{Cp89_urq!*=!66rNy zL!{SG5~@f`2%&{QAmLjZ=bV{2XU@DcXU=u~-tYYb@nY}2_F8*C<-YIdS^J@mmKxh0 zo;@rqENrl=m-Sd!zMcm^%70@8f0;NIISGF4cE4)k#lph=9{Twz<`g?G3(I#bu*(+= z{FCN}1B`oVRa;xJ=bzZt?cZ6jXU`|u=?u26PiGUX5?{kSawG5}CY|55!U%tJbfus46 zr%_$l5&Ptx?`B=GNAgs1?E}V`S3&%iy+N^T`FcQF@kA{uWu=$S78y?1l=9 zWNto}tZL)~11)_0jUV{+dhfT}O_dcJx1ph&L@)Y@Z#n zb?UksP=vMocuNddo?jquVd{vv()iwf3MmDV=G?1yqG9!BVWr*D5M4m`<*$m zFF_V{=did5`s2r|@#-2C`{rl4xp^c{bmsDNbLZNXlsk-$VClnds66q@R~&l$>eBJ} z7CPB2STarpdN~iCz6<3bEsVd5>cuR~OH>k^MT-XuL%mQa6f6pdPovWj*(0l~<*uYl zHv-I)4vU#*q^pJDM*4hPMm7X~>`SWh@~K{V_F?Bvxuk+X#S2phN|+m4n+8z^7D2EQ zTSmBHzA?;w7YoaI%+L{hCKJ1?>`d_xKYh9v{r(&5y6qihm(9{f<&ZaNY`l_XZWK$4 zD$j;l#hY|*#_Yuy2EL=isA703`^O(=LyBdg`NpO)=IrQ$9l=uu;$mWk9v&W;>3sx> zN4;YS742pbrptvE@mYHBaplStx#V3tcjjeI(%p`p&`QeStEtNq*1ve@+Xe25`MtpO z#%D=ZTh(=|Wtib5*!JD8DkK!fb)%hHoV0m!(dT{F0+)$aBg+=nJlYoF#sFPp3 z(uQTjhjyczZ^Kd&RU@JVgBHWBx}V#n;SzDTR9afaDT3+@+`+0k#%U6qkJ_5yp^IL2GNu>HIjBT1rEAj|ndV5#+ zJG?k2z$XImG0cJ_Q^>*FD)%?nwZo}kppvkechdVD4c_<$yovxbwZn6SIae} z`;joZuuc%>=;~TTGc+);U(V79?YHXbRE)!2KVv_#xzn`vR_+hSj@gKv9hPvJHHFc`;ShE5?w5-Ax$AP}kxIPOn+c$ikYKYh$EY=|@? zh$1Pd?@Ya>+QH2AdAK4lnlc00o1b)(sG3VvimBcp5vzOk`? z9B*OUQD;X%n>Mgkhy3Ehh$sL168}#*wvX@m?S1Wf6*1JWvrwEr=b;Ki6ALTmi z7@?@9@A!J9HLI<;DR@y}sxA;*mnP)OHeXOsP;7j9M~Zcs%(>QhSC=CTt%;(+_C*&j zUb-cfCDLI3%$xD9B3*TT=Dos+0&Q)hfOWZwtr~FodDK1vM|QR6r?s;&F<6og65$h- z_29m+6Y22CecK-07D?kVW8l-$@D@6D4j!_fOOkNCVm>j`Ne^^>m&T({;^32uE+ zzT#wMbSWr=xfZ(YI(QIVF~e3-H=%6(K_k93lfCS71`dr5tk?GPoyQjx7HVyjMMh%Y zmDRMS1YvGkPoFw_;|hju*SdE1fRR!zssr(Sg8#nkMZ%mtI(0>{=x&_%!lusn_^A z8|ieOL%OM{Wfz*SzfV^HK6w2z4@fDpZ{NM6p7$;l%pEI8J(pxrsgs$VVlK_;1fI|J zZ+N7GyE!A3@KRekcP?JKluQH$@NoAjC1oh5FC6A)n?ER|6_uL091<=sFAq0E4o(0N z&cfn0-n9*H(@+n{(fVMh+vE5C2;Tj**DiSB7VHCS_4Ip?iXBQ%3PRS`A#-YZ-YAz0 zpqc3z@$&w?;QiyDm%+2u)yZWuaYtN6i`0uaKCB?xBYybd2hPLFn%S+#l~%uDeT;i? z_iJ2luf21Zh&~#yZzZZ}dar+}zPt0Vs~Y^5PzH zDL*{u-2bkszP?_t$K0ZMyC8$yUWF>vu`V>i;GPiyYYwc)+>aK5Q z;x!Xvq8Tq+HkL?^PSne9ukDQ3Yxwq{wmpT(KoFbY^pzg%;c6(BbRPWTz7>I*FLC&> zGj`QwH(pD+m9M>M*t47MX;&PYGhMfv$}#$wXEOaDC9YIi$zpPHS`fy^mt4HL-U0pK z%M0`xAJ?bvdTH&waVT;uELq8BA*#WarU9qcM}sy-o* zdShb|T+Fh$19H$`z{Su}9s@WQv)U)4R5Gj3+|s5CKrlaQbGNi~Fxo0fS&7pL^6Pt_ zTqexyU0mRfj$sK2JY{yH!IPh{`5qT8T$9e4!2oL7OWO8s?my_=7=3Jdx_((%>@WNt znjk$C`Gqf@hd2#OEz29|4gE8j{OyBlRgV5r%9gpgvq~w`{2%+?YzDdv#O2zwOeD1f zPY{jcB9QI_FyzvLNB3lAu9|vlJSwLQWDe}IMkr0I?%%(E&)(E5{i!H^eV z02?fok;@%b0(1E5K=G$E``WC1PLl(Sc;<*Xt6yTl6&V86eV#%QhZURhf|p3?C5(*p zxdfd&9U0#9_I^0@?(qb?>mYJ#M^}QRU!hiPN_S;iS}+Za!4hIKFfw6jY@7psxErzD z_H__`%^@;~HRJ-XjIAS}rq!O4*Xohr#M^JAoI^I?i3LVMB)vz~I`G_Rp~oCK&{IC5 zA?6k{8~$(H$p?=K3Jl~c9JX1VLmuK)umaJVwRanF$p9lqqp#Vx41dze%gZAzCowiw zXXfH1HmFHew5GTOnK6c|l>B$+JDl% zzmb6KmK2cWTt^}3R~c^>QX?zpu^HFgJmQn1gq4TszkBh=ChF3H$36>5H{UWpZyUzuN!t2UZEFVl!q3H z0cLp#;K;%buT=^my~M70uHS2FM9A6^f16g*HFKLqsl=hgv)HP35=#PcG3IHCV||_f z;>!~nYx(@apZ9O$7{w0-Z?I)$@WbIYuf6j7SVQG{3fOj}1e7#zh#FqC|MAfWgUlll zQH7{A-`f=no%MFGBQ4(e^zn?L*cHX&Gs`-h_mNq+lm^pinW%xo#(gg-zV* z8Rc)^zFYCXcsDGZ9f`03mpr@Y~QZ@BsqCJ}ia% z|9ln^3)x6tT%F8 zJ(3bqwi?$f>>M1p=?L>AOBD4`5M_OOrAtkKtq%{~r{b?>tIf^>F|nz~N+Gmxl?_mN%d$ZPu0pIKXD>WPKH!so#k5}# zbD7>^#e7Mb57tsq@xAC)M@pOR;>1i-qNoqe(4~9AZ&YVCvVmJ18leB>#q$K zK77ykTp)WK#wBLpusSzrTD{`5Eol2c`VWP|RO12wCp`Vh4dGnUKzb(lL5oKG9xdBi zj6`*|##?prhO*-~vNYm}&$rG)_Kk4*$puGNv=I9G(LHW#mMDfab_pL@Mr=aq&kC3D z|M=LjH{YZ=c=n1m29W97`vKI>dsJh-Ikp1`FSxgu)7?&6lnX&NU6=5$lr3U{HeZW^ zq0fB#e^N!Uibdt55txOAVr|GXs$@Ps27WhK?abc)7AMQW^#*24T3Hc$Yvu{U^j16N zOZEU;-EIUD22d%T%Sq!yqoY<)x5{L4>gq;vE3nuC0$s~?dHvs#gsAux?V-t*@gCWcmZxXb7$8}YO-d>zUC1y@$-Jb)7{_z_W=eQl@YJ2 ztF+3!j}^kZK}ed2h)DDIm40@E#gp{4C7Wa=4N$}C1>6RFhu^C8R zI2M_^pn{U<|S=gpEa2ib=0*?3((E94Jo_n!eJtjSZcMgwkB_0(=s&=EhqRvR_fXSk`)@Q2Sn|2z2GvsqTN=Vk~}i2HCV}R zK1d=fi_O2DHv(u6l8G{>gf<#1M`H!*Ju|~Nw(|QaQ&2*l|I{}&g2#)5H%N@h0VVZa z(rn*@uR3Z6KZPDXB`9@S)f%n$NEOzv5~$$#EK;l1x|_b@4dPGPiOUxeUrK z`pbOGxq(*dGx>heTI3Y6|HbMGQBad@uRahuwJ2{f#zlJB+Nph$kt(a76QDzwDh{AGlmP4Vai0E^0Z=~ zDT9=*y%b#Y}E}s8SWxJ2Z5YQjn9= zIi(rTj;YUj5pX8&&GpXO^}VJ3O?p>URh!LkR@=C1cYN%OJ9|fq>M?P5g@N6mw)A9E z85AW<$8b@wUJ z`gW=EoTz!_RkcXYJZu5^IhLmKZGoKEG|_ujIAZS}&vS`E6l^uaH^kC^JfN(sZ1x?8 zN^g+YmOxu-{g+y5BzHNjlI!qcNBGUVBY`F7Q)Pnj)~R6Lpmd3^@WT|ESAFv42Wn4y zJG+m!_kfeAXj?V8oWN@12Bw#$@J^=E>R<9;{bqxBZ{J8E3Q6RJPWGk^`;l6fr${qh z$L7r;@vriDY(?`*!y|4<8;Mx?-l%B9JoFMnxoxn@siL}Cp4_jjT*ux&FM(XehD9V} z-rqZ_L?}6(jCnS=fq7i5D`+1bzjzeWBN9!okEEKgm z8kM0W9;gjE7}9~dN~HE%@|7|moXZQbp}l+emWiCb;8tgnrp>ED{-v5}RA}xYpL#(4 zih%j%hRazXQqmcq8q=;oIr4KTltFc$nz*>yO@YMo?t=l~B2)UPfs`c(SH4wg{h&U1kvcgz$u zdN19pqpmn6iwJjBdDZKKzXwg`apZoAM7eJ$LxXDnX|@@(e*eCT?m(#neoH|7S`;L~ zMxxGP>V3V;ApU*E7>sCUW@b}l%&OS*n=oYkmQwP$5r%9h7!b&3zs%Fv*}Ox?oBnKZdmlk48vn^1q#` zliDqtmD2!!JL(8@WT09$1uLQ6d>u5AINJUBi4HD!vnY6TqMy2)u0}6|M3PQ{zorAZ zh9Lh*{#mV8Q&lAn=DR?dF_=aeCGOSL#jhk&azIBw@J8aO|7Sbs&HovWikREGXOE$# zrcWdi^LyPd+c^7IrNU}5H{ZA40VaLQsBq<+5NLS82Cd}$vf*Xk;s}^c<^3DHLm0fX z81imNh#;(~$>|V3f8o@WZL(s(MPN-~pbB-|0;N{>8+|VEZ_N0iAwUM)_@9z>_h471 zqz%aDao)^LWLWJqf94VWNzz1mZS4?AGhG!HRkzk4?$A=wLCoHwTFG~Jrj@SjtDrz& zaOM3UK+=f-L>j6h&c$aQNV zl?>C{+=7+vB|;)a#$OhE?JlFv9Rkx?IQ;8Yx8JT#{N(UI9lV{I_u#+M4(B0{6yCV~ z(zeYr2k@RA{O1hipSnN(aHrdYsWup`rX}A?+EEt##GZ>@Z(OYx@H&WwUcL z-SNeM?r6xPm`YIFf!?FsCF-KCrDY*u3#vw>)&G&KY2re*vB}8BW5g|Mb9Nt8&I=U? z*seg|MxFfa)|V0bm^K&Zk#mNiTc#KIL?hDz+-=NIb!A5!)HML;QK;$6vs_1KXF*6f z8xv^$k$pZ}U3<2$MmakpA#_f#E=xlw6u_XIP43*LE-(mSTg+P##_h{fnkQ#{X;64;5@`Y*Z5u zL}Bm*Ld?_WFWcM2Afi%NZt z(-}=uVUBO6%|S-z-S`(BO@HeVx0TK(v=EhI(&35Pt|K)6r%5g8E)sMh`l@Utjhyq= z$<~S=u7ePb`>8K($W9BP#r|AVM3O>OEObCq6;PUVNVvRz?U0GwHk-}1AA?!+!4zYn zNk`sa%GQ8eTc<0P{!4Gn32>`3#UUG@W3QT2*dYvR9^>OZoO}n<8xI~ZF!wg@rcyjc z>w>c9D<@F14Fa3zFI=gfa8Dow>6QdjD-bvrEmE} zVIuw{A$APb{lztHrK;wM?M9nhf4z-HbtM#PqhWEo@c*vVejvLfwwU9jW8djeQ_u=n zps5ulIPB5Lcg?cI(5iattDWC`Gc!a%VYs%e>rWq6-|pyUp}(_n7l(=|O@gq5KVMAg zb#dq57s%fsHz*%G=YlN2V&5@oWCcHmxq%{tF9~$ah(b{nnAhDDgoG|&dHm}p;5o7U z3;krp_4g}K%PT5;%mntdOnfSJAOC?*NiTo3yqtgI?%lg^ef?qb$iWjDokLYL;YorM z@MKR|clg%g4n7fo{TnxK&_So~u0Q13sHG+$(^B^oQ&Z-9TzPl^ET3lnXBES4;MDjJldmZwMcKP%Br zHpGco2?$J1PU1H}ho|>UM1?oOrDC>6TLqVpfg2iS2V+P6k$zI!->P_qj#(O+N4ut#X>w4(Fy2lkFLzO& z@o8oD`w@sb9BcZ0C~MM9HACs?>zBF?S3q4i>udB`4hlBgz&eac@1L)~cmND?WZ(bM zWdFIB7oexmJ$nrF^?S&2V5w(}%~@%m-d&MM)QHMxioWzLIx$g4#8r!UT0f4$4a4%{G3 z2&CAyz8@0Fh;uX=EmqK^fDMho_z_*)+(_lhoX;B@9LC>Xkw;`0lU-btR(FCO5LsFH z$&r83mT8b)@mlnsE2qC?r9+<2e^!!TzI?g8UuU86=S+>+web0K*&YrKBQLKKtrW!< z{Z_~;ph0Z4&PP}k=yAPG2OAIJ-fc_a0GR*#6pi9&|DqWG4hc1fb^lz>sbp+#u|Tx^ zrz!ebY%Ihcf2yjkad=T6c}n%fm#(*iWU!^ zRc9Y<;{Ta@Y8_R{DlYcpvt0hU#8bN$YJQ32?s~%j>OX^^KdJ+m=Sc8|%l-SIvy1n> zImE^HToCj$NlFcB!6JM9ak<%{!-t92 zgX;txYz+)5lmOGZb>@ioWOEGv^h(w)aFZj>Uy3m1tKaO}1&fM`?iW#1G;eEblMh(B zXNe*o0?A>u`pcOBKmMDVCmnh6Cq4FevfwX<`5)^n`9Kqs=+-CqbBSB_-rJpudl#vA zlVcJ)3z-vZ;%}BzN-C6p)x?9RKq=&P@^A3T(p^gI9YI(p3K2wtp1S z-7T>~tvrWi5>L@bmOEzCBb9kpzEmLoBmT%wdz>Jx9;vt*Cn1~Pj|rx>yfOW$jQD2w zcO2w&u>Hg4mX7^+U~^|O9=N!JJ$_YlyV=>J1dj`0;Qs!V35kh5OP_aRnfOyc&wnO3 zOkM8)629K(Fe8aMX=O{=Vp1{magOC%i0RL1^H8uD`uOkj&_A~M{hlTx=&-rAm>}-f|4D<-Kl>h9JKqrOBa>S(ul3iB z77F!u76OaEep%(Vum>(0G*x=#-2c)v2fFPSDS$%4+r=gOb%}?b_jld`QkTGEW3B#d$s4z zlUE{N|B%T0-FJI_98i04*L3hQ(Qva0-hiiXnwD2z=Y59nR*t;eyEE!YeskTytGAss zR=>aayh$QHfSI;$-)_M>H)g)4^!rTS-B6|lcK9u%Mhem7?50j9e{Al4nawqGb%$Fe zV4 zt+6;3%Ap_A=4+w^g>*JuJNMCrb;R)xjp|VsFA*Hpo8bv6G+~=t8R}8j)LW9kR_18x zW=Xh(GNrRD;A|e)v!VK!rvUlku|C-7bl~78C${+uZ!VY?1;*DgiR#nla9sSEl=%3} zhqT9ei}6CJ#b>USbiNPMH13>3 z)Q3k^`n>f6Y3SzW@WQ?O_UYN$sulzp84(;?cYX7X{l_OS5c7jp)mR7=kKH8hn^!G! z9hNV67#=MMnjm|tvvN~YHyI4D3(J5~4LGiWmTO_T+vwDDWw6}a^h!`4s=}F0C#0pF zhjk~(X+vAwGBY#rrL(+2rSu(x&N%75jWvH#T@!~rmB=wPA`WikE(}sI7O8eh`D~Mo zYx`W|GtbZNA;elbp0}9LvC~dm+%2}K#(*$~FZam<&%|f!b?_A`F*AFrJ-Q+`*08|T z#G3K(X*zC&RVB!0B-kH}`T8JGclGri+c$-aSGG=0Ps>)e*VH4`z#d?q$Gi3&&8S+Q zvhqN2ftFp|+nviv6BCV*c|IhWYPY4&rXd^89C}K}mph0)(ohqHyAVLIphG&oZR%8^ zbrENE@0Mov5DN?6DD+)`*;~T3pAx7(bM|P{Qv2Mj;9&GgaliJ)RdOECYl-XADO6)< znXgSE=sPte`I+(0G3JvJT!%+28yXtKfNYngm7FDNR(?`es4-d;3J_5|0q&zwD+Sel z^{^7V4&BR_)!6uCG&rkb9hZg~1g(HKf&C3F_07l)`MT z_Wp@TmE2uf8u0is4N^a*Ka^5GT|9p$S3kygv(lG>_lb}PetF*7ZV&rz+CZM>gtBGc zt5>ghmTgN+B(t=a{UN6N`lKSy5TTsyX=J2dL_lJJX+S^~KL)~JM*y_l2CywXId0pz z{m~b>`1jrLOEF}}PyRcm-+y-_TcfKsV6PY=a5Bus^dMLEm5$SW_Z~(&9<4BKkwSd_ z5h2T&8kZ6Kyf#M^25YZ+@$SLwq>*LMkK^x%7Sn-GpFSmi+od~t;zaMc)li#LoCkSH z5*2QvmP%*s%Dsk4?6j5Ed(C}FeVWfNhBKLwK6FBMs-@xf%b3iOtaMuo#2%QKI1YR9 zf?OInDbkj@@k~OZ`#3B!)26+wsi_IU9BdFv^_#)h#^FF8-!Zj_J|*i$VG)sQ4|gw? zvC`7UzAG@U)38EOGrbDY%~PAmJi|Mn+%$@-FL&;8dtR5Bh*<3vN3GX3Uu#)uctGV} z-|h@felzSS8jVAoNZO#&uSK1He(?||XRomt9Bx9|EIZ@TEf~S=l`RuobO9a{!)3vB z!a}1VGIf-?KFj{7!6jBiHW(UuHKV&N7kt~r|V0bss)TiU}TDq|)t z&a_{Ox*QFn3aAr^nKpKGXyb4)gU-{1Z)0O(M9!TXz_LLvG}e81B+1mZtgq_C$&)%! zQg-bJP3v!@GnZyZrAX6}KnN#PO<9cwWjCWm82!g0(Xi+f#KDqmR{^`-gL_|JU)(Qp3)Gt2g5Ae>?I1zo|ldRJO7w^yr1T@^wf>d^ZpNWHs)IKnv5mZGG#e?OnJ_rMvYb3xTI`CCI(Q+eWBxp4!=n#rqa_zFteoe)Cdol#h9by&d__vIb4>900uOi8b<__4CZt9WCzqj z^4@h$uw&DO@Ssxh7~}N+{~|*m=&W^|%5UtgcHzy;%9v@Bwd`NCZWPOd@bj2wFZYma zV4dZ|#l@(M&Q4#`I?tG?j-YGY+_O;`0L4$^KYzZZv^94#wnM?&#j=%1pCH(2 zYG;T(Tt{c@>Gp9^a5%H^-r&RN6ArC$3T2DoY#^c6 z&Q!mC{hf`it$8VB;Zxkxa;j@ ziz9vFo4fby%P)BV7(LkF+iJUfsK3Z~j|I`YnA6gujV|vi1s63fHPBa4fj^6FvCZr2 zmL2U3Q`xkv3)Zv|E}aR%2?!rQ-eA3rB7X}uKoBEdTuk_BX6H-gpqss!>Rc`(wN23l z06wHWb^6^9Vv{_|CYz(5tyPU`=ESbAx&br6$v=(piKqr2LOO5lJ**7RQts~Qp{~4L zM{F(%QC~mH6)Ab{z@Ss<3k;fhAE?$OBR(}*%?uQ!gU{YPCnjR*>s0wb1WhZF_(k9t z*coZ~Oz@YHW6YIun&A_X-JHb8CFe@8hh9>PE-V zu!D}47Dgm`8+u$xuakrNFV5`wO{o8CjPYxZa6Vc5I<(}r#EP+(jYfsWSs+cCuC(6N zj60Y6!6UCsBVFxrj^8Cl*6xZv>Z(&Go%LlnfYs<{d}rN`Wju0+XJK0F>05E=55tSdO$p3*y0p>2|3|Edbi^ zWnS6Ybx_>qsge(=!I_@OqiUR^QK>P4Ca3RC?i2fjYq3dA7A3etkQ4 zSu_A#4w~jkf}p)8t52FXq-40UO2*pT$7X!gN?w`#5YY}c)m0=&E$ZI7B}9_}`&3xW z-})fE9Ji^%yb|K1f`S44g;n z_UzkJEfNG7Mwtic5N!mTeke8WvxKbOs`WKF&&mrvmO*5;K{BqrC^ojDWxxV-d*SMW zkPv7S2wHpbU^KV}yj}RA!<_b`by&N@@u0w9=E|MV6-+(Fn4qBgx$fUjsK5S#umL4> zF|ZL&>G zW*+Q(+Qa5DeUvM?G^ZGSEmCpm)t`bb z9B!DQA@k{TqN18Y#l=a0Y*hQsqDxn#BB@Xtw-j-V*`<~zJ@edbWub$q&Zpqpdr8vv z1wv^-4F9miy{zr9w+%plJ77!sPsGxu`9kr3iYN3Y&Y-ft{CZTPgW?r zF%9Nm@2v6W`*D6`nZr<{6A*R{tw)??oIb2Ld?6c^1O;_e0XK(|VCXyDiAL9oetFc& zjBZ5LMmNX##hF92D|@*G~Hg!$WP$1vk5``+E`M zw^amU;8#5i{VaR1*M&=|&a*)R@Zp}9c88tbzK2OaHY>2Hh4j zteK~&N~d9TTd&%3=32UC;CR6FB9x`&B30%U`?FCN*{>Gd2g$z4rAfV!D!~}qh}m)V z7nvhiFh!OvVUVkNm8~%E*Zg66_Vx;*R#n%QrIXJ|7xp;aOr=Wa`TBi9t zr7Ck>%qoZxR`pX7b|vtdM{c~qHS&J5FRj%gElb<0>zQz;{@|CLzAqBaPT0}Ch8hvh zeHwtuveKa46z1watbQQ$8It1N=Siefuwvsq0FUTcyTIW#1EhhG`(z3c}oC+-r2@R>(^a?q2@ZcwD=K&XDcbOBm zk3jYOxrgm|h|XJE1QcJ0$)jAweX%DdX|} zr$#kO;Jnub#37NMZ~s*v*vBnVs+t^{r428aBgpxlJ4zp8s5njvj2iWW4NLHf($dRO zy^7zZU0{GwSo#w{F22^hUo>EGXKdBx<|b?ZV3k*BaLI~jzkEP|bRVChzYc_o`IF_q zcTOnzC55QvDFFDQqW=D%zEA_#fSchs$AX&ndI7`bH%Q7XgTp8B}*8&VESM!O)}c078}_=uwcKYO^h|)kvMnm4W7%HUz6#X>qaetXl~jSqu&N+8w9|%S)b00rJoc zt8c7jH-rDMM0~ZKDgKR8_nXd{*3NZiSj8r6s?E&3YvPpA>7E3z#Rp+q1j3TRL?zBN ztLvDWxk)S-{roYbKd!hvc^9$W0~DeEyL{Z_L^O> zT8*cdr(%F3IP|(x5#dFwuiMZ%TNFf{wd$Xx%8uxP6qTX~S@CsaSL~+|kSjhNj5eQ| zY7N#9R%2I>0^Ii0_rWDU{2)(kubJ--SGUx2@Oa2v$;RPp9ikP?AOk6cI-GtMMkUozzUyQ#GGy3%~6zIo?)x2jB6^$$2v9TsPz4<|e?#BXyPbh%fKWr3b{Z zJ3A1hHrbWo=NaL_0O+yY4t3KCT8}Z3TI>6sK1IT;EXPOA*gK+yv>~Q_D_%QZ%t++eV*bs`2qcoE5fhRl6J4QI-3W zii?Yh`A$Gp^XshwP0uW^r5pJDpx=L&qJMKyY3+>{eCP2N*o_tJ%;KdDn#knG?<-_* z-=@zW&1+qe$~>y<4=2)o3^Szh>$ZC|k6)7azD}{hIUYKaVcAvW*j1d_Trhb}Aq8!n zt7hJJ52yk_cQTaRni4rB3J0e*x1I~WGCJ z11Zt5m(MS+GQ-&HfJ{K8888l#S@IjDW8})>5{cumw#`JR8FKmxCgYo!B<15UG`iek zn4R=wp)D3&7p9bvbySaDWB}_PyKzjcMyn?)xZ38U`=o7RnO(n3*`9rR8lxA`#c*Ls zvc?$>pgNC!RbjuCVj*(+qrQ$iZ^H_2nuJsCp5^7Cr66Lnr*M6GWfzR#( zxIDjnR*ovo#nDZD-x}-(yO-AV+=p)Zum*lIU;=4pbG-5%wHY3Fs)F}<<~5(nkID!` zMHWmN4x-V8i0D{PY4Dk_L9}x_#nI2e$~U>sh0xrX{cv>-U-+pfJ0?CQ%h~Js+jqy3 zoT|NRU~*Fjp$GZtQ_nSR1@zoO6Oh4b6zZ;pl!9ek*Lmej8A88|KFk2$7gee$&kJ}R z;W#Mr>I40W?+wR;2VN%49ptBM&&r*YsJ|oRF7A}Pp{UtTbnOs2JvY=xQM$oKGqsK} zNfAiPe24; zaz`H{T&%biy(@!7M4~?HG=6N4YErM7I*6~a3q|L3>|7`uMxOkkxWu*cMSOGfwHRNi zD~(%0_lJ{fvDiuXfl2qI8aKaL!_!GOd96-x^7&^JU(6CU_fwg_e$!>ei%t#;Y$lb z`ih*aEK5kCB?EgO<9FRVFKSlS{e++jAf^(&%sS7s)Hzonl{X;s;8ydFr3*R)l#-riLN_K*V%bJm!+pN&uX+MNq;ia8JWA~&vOw-1BTz>}y0 zvZucL?mUBa=w(#*P2e0QLiOtX^F%y0)Yo-O_jp6Y9@k;jpqtgu6)0OdYVNsoKHfjJ49u#9b$g)hl@3V2fYssW6?w=T;NU zw2!ja02jFBVtE1veUb^b=l~};JK|SbW6aHBG|SPFU#;j7jFy&*lVuB@bEX>l`aZvF zR^h=)OFCKt@F3jO(^lKp0ahgM{m6h+{=$_52$_?3EJKa$sG@-^b?!Kx!3b zN`m*)HDAILU!v^wCyxcx;aO$oT}52=a+Q5vF|kgKV)LDh|LK2eCp8Bq9XDzyon9FkTzwABM)GT8sH;u&xaT46p*yjnyoT{kBB)}QX zwX=p)U~ev5U}Fvy$51SmDhYRMnr>BRMT*rPReJdnSEO&pH&kz=awz#(n_*@!!>vwa znmANY6Q-T2xAZ0`Dnmazthcw{2>a4HCL!u{=(`tTo_T1p?{R0Q-ejNTg@MMJhafP= z`e+HHN^S*UgT%H}O5F5S*!U&PtK{U`M>@i0j^x@h+i*47`3UO6r-A_i0U-W4%-w6E z-)N}-P+V*S)R|aRIgfH?oc1cG(?~+It%g%2sFW=l3Vfcq)QOfNPpTHJOqGXc7~Z)c zHr{?qLfRfxo?W(kx2`QBL8AMy-1N~b^}w+`Yin!GmSj5bZO0mm5oP72r6oO}^=YLl zA4irAbK{TaUTHBD5p!KT91De^xgkxn`q7sI%pJH)CqF*ySp1^T;_7NwTLrs5%%ccI zPs0g+L)&V=1k9%pD~}A9Z}XM~k^G9EdmI8#NlI%6oz`PrVk`fSJ}6K#Y-v3|*YO}? ztM-;=1y+vwdSuwKN9OWb1}pkXxFLw3I_kQ7|z=X9K)_Pv=kWscJiGY@D z*um+ww?ZlA*A{1c_Uv}aew}uG=a4`6E@UH+F3L14ryVOv<{*^qaU52)s2{6e~*O=CKeVldFux4R3Y_1pHmbfeDQsdS8elt1g!l9(C$YZ(j_4Rc2$}3H~ zXu?7p_k1Y>St(fSLq|BxDLerAF?O*%cZ)uOulI{hpv(9KQ%Y#y<7x2R(v0_$+0BNz zjy0N@$B$D7*a3;ZP4^^BF1)=P2x4bO&W!P}jOQa~@L9-q8B%9mwfKgtLBFe#1^+H? z_lXE%Z&jY)EGZ&2FF$|frhrcoszdBb<7&U?r3WqFhk9xy>9{^N0kf@Smg zpPJlk#N{h_#l;uFvW^SjDdt|?dO5=idJgJqgK+5Fd|KvdtsI*d*Y z(~{qT(pueZlOX`#x)2gmIjasgH0(1G`fg11e)-;L?h94%6G{p&Hup)0 zkF(X2pikPJFGxvR_5xqgld;chB}OvyjZloK<0R&^>v;(O74Aq4T8&rUn3IH|J-mnm zMbBpaG8`u;mlalJXN=-Bd1F*oZ MXk9M6c>V7G2Yd&yNB{r; literal 0 HcmV?d00001 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json new file mode 100644 index 00000000..84bc3bc5 --- /dev/null +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json @@ -0,0 +1,59 @@ +{ + "schema": "termforge-qt-history-scrollbar-validation-v1", + "baseline_main": "ce2bbbacacf49b3864932fad19464d465e638426", + "source_commit": "fe3a79a", + "source_hash_note": "SHA-256 of committed LF blob content at fe3a79a; docs follow-up.", + "source_files_sha256": { + "src/qt/terminal_widget.hpp": "770242a69cfb04688a0d60f71532b251901b70b34fe5704db096ad7137ca747b", + "src/qt/history_navigation_test.cpp": "d1ce06e5a8696af5a2c6384aead1d0af4c6f62f5a3ad3d5c619ba2651edb1d76", + "src/qt/hidden_retention_test.cpp": "29ccbbd91fb279a9dd159f96cddd0224b1716421a2f3349e200d2e522c03e271", + "src/qt/selection_migration_test.cpp": "31cb8ed0d4ab4708b3885c8e6969d4a0125b5a07b64f92de0890bfca90809882", + "src/qt/resize_smoke.cpp": "92587fd2aa32296b9b784854c800fca86a8bf0ad52b28390a63a36e4c6fec631", + "src/qt/terminal_modes_test.cpp": "ac2af74f312cfde6b4ef799bbe91fb2eab5c5f7b292981d30ff7fdfbedca06e9", + "src/qt/find_test.cpp": "f1df33c4bb3f36acc6366492d8a94e5ed9c54c854c52ef1cb95bd67f44b6bfcc" + }, + "environment": { + "os": "Windows 11 x64 build 26200", + "generator": "Visual Studio 17 2022", + "compiler": "MSVC 19.42.34433.0", + "qt": "6.10.3 offscreen", + "conpty": "1.25.260930003" + }, + "checks": { + "scrollbar_visible_range": true, + "scrollbar_reserved_grid": true, + "mouse_drag": true, + "page_step": true, + "home_end": true, + "source_top_identity": true, + "new_output_anchor": true, + "bottom_follow": true, + "narrow_restore_anchor": true, + "find_sync": true, + "two_sessions": true, + "existing_wheel_keys_selection": true, + "system_clipboard_touched": false, + "offscreen_only": true + }, + "tests": { + "qt_history_navigation": { + "passed": 1, + "failed": 0, + "seconds": 2.17 + }, + "qt_formal_path_regression": { + "passed": 10, + "failed": 0, + "seconds": 19.69 + } + }, + "evidence": { + "readme": "README.md", + "screenshot": "screens/history-scrollbar.png" + }, + "limitations": [ + "Offscreen Qt; physical presentation/DPI, alternate-screen/TUI, autoscroll, word/line selection, persistence and performance thresholds are not covered.", + "The scrollbar-enabled path uses Qt/core/transport dimensions; an independent child ACK of the scrollbar-adjusted child window is not part of this slice.", + "This slice does not close #29/#30/R2; #324 remains blocked pending a user decision; BCD/VM/v2 and old failures are unchanged." + ] +} \ No newline at end of file diff --git a/src/qt/README.md b/src/qt/README.md index 2604106b..ab2597c4 100644 --- a/src/qt/README.md +++ b/src/qt/README.md @@ -59,3 +59,5 @@ IME 实现通过 `QInputMethodEvent` 绘制未提交的 preedit,仅把 commit 上述三项自动测试使用 Qt offscreen 平台和本项目独立取得的 OFL Roboto Mono 字体;没有物理键盘、实际不同 DPI、Windows 原生窗口上屏或用户呈现时间验收,`presented_done=null`。隔离 VM 原生观察保留自己的源码与环境身份。两会话隔离不代表八会话、完整 IME 交叉或工作区验收,测试耗时不是终端性能指标。 构建开关默认关闭只是从旧原型向新入口迁移的状态;正式路线已确认,旧构建保留为回归对照。Qt 文字与图形资源均独立取得,不使用 WindTerm 的专有字体、图标或主题。 + +- 历史滚动条:真实 `QScrollBar` 拖动/页区/Home/End 通过 `HistoryViewport` 源锚点定位;见[结果](../../experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md)。 From 6a2749fd68fb336cce44a43dd1f33a21857c67df Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:22:00 +0800 Subject: [PATCH 3/9] test(qt): add scrollbar child-size ACK and public Find/selection coverage Add a scrollbar-enabled size test: default no history (no scrollbar), finite real-child history (scrollbar appears and reserves width), narrow/restore, and ED3 clear (scrollbar disappears). Each stage independently queries the real child GetConsoleScreenBufferInfo and compares Qt grid, core requested/transport and child buffer/window, with the Session child identity. Extend the enabled history test with the real Ctrl+Shift+F/query/Escape entry, public mouse drag + Ctrl+Shift+C through an injected writer (source text, not real CF), and a third recording child for correct post-Find input delivery. Wire the new test into CMake/CI; fixed-layout tests still disable only the scrollbar reservation in their own process. --- .github/workflows/qt-cpp.yml | 6 +- CMakeLists.txt | 14 +++ src/qt/history_fixture.hpp | 9 +- src/qt/history_navigation_test.cpp | 98 ++++++++++++++++-- src/qt/resize_fixture.hpp | 8 +- src/qt/scrollbar_size_test.cpp | 157 +++++++++++++++++++++++++++++ 6 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 src/qt/scrollbar_size_test.cpp diff --git a/.github/workflows/qt-cpp.yml b/.github/workflows/qt-cpp.yml index cac61bf4..1ba19d1b 100644 --- a/.github/workflows/qt-cpp.yml +++ b/.github/workflows/qt-cpp.yml @@ -42,7 +42,7 @@ jobs: "-DTERMFORGE_CONPTY_BACKEND_DIR=$dependencyRoot/conpty" ` "-DTERMFORGE_MSVC_RUNTIME_DIR=$dependencyRoot/crt" - name: Build Qt product and independent smoke - run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke qt_cpp_resize_smoke qt_unicode_input_test qt_unicode_render_test qt_terminal_modes_test qt_hidden_retention_test qt_history_navigation_test qt_selection_migration_test qt_find_test qt_clipboard_system_test --parallel 2 + run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke qt_cpp_resize_smoke qt_unicode_input_test qt_unicode_render_test qt_terminal_modes_test qt_hidden_retention_test qt_history_navigation_test qt_selection_migration_test qt_find_test qt_scrollbar_size_test qt_clipboard_system_test --parallel 2 - name: Record complete path-free runtime manifests run: | python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-bin/Release --executable termforge_qt_shell.exe @@ -65,6 +65,8 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-find-bin/Release --executable qt_find_test.exe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-scrollbar-size-bin/Release --executable qt_scrollbar_size_test.exe + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-clipboard-bin/Release --executable qt_clipboard_system_test.exe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Preserve runtime file identities without binaries @@ -86,7 +88,7 @@ jobs: build/qt-cpp/qt-selection-bin/Release/runtime-manifest.json build/qt-cpp/qt-clipboard-bin/Release/runtime-manifest.json - name: Run Qt offscreen smoke in its owned Job - run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input|qt_unicode_render|qt_terminal_modes(_retained)?|qt_hidden_retention|qt_history_navigation|qt_selection_migration|qt_find)$' --no-tests=error --verbose + run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input|qt_unicode_render|qt_terminal_modes(_retained)?|qt_hidden_retention|qt_history_navigation|qt_selection_migration|qt_find|qt_scrollbar_size)$' --no-tests=error --verbose - name: Run hosted-only Qt system clipboard copy run: python src/qt/run_smoke.py build/qt-cpp/qt-clipboard-bin/Release/qt_clipboard_system_test.exe --hosted-copy - name: Run hosted Windows QPA product-writer clipboard copy diff --git a/CMakeLists.txt b/CMakeLists.txt index fa58ee01..b20848f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -832,6 +832,20 @@ if(TERMFORGE_BUILD_QT_SHELL) "${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$") set_tests_properties(qt_find PROPERTIES TIMEOUT 45 ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + add_executable(qt_scrollbar_size_test src/qt/scrollbar_size_test.cpp) + set_target_properties(qt_scrollbar_size_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-scrollbar-size-bin/$") + termforge_qt_entry(qt_scrollbar_size_test) + target_link_libraries(qt_scrollbar_size_test PRIVATE Qt6::Test) + add_custom_command(TARGET qt_scrollbar_size_test POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" + "$/platforms/" + VERBATIM) + add_test(NAME qt_scrollbar_size COMMAND "${Python3_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$") + set_tests_properties(qt_scrollbar_size PROPERTIES TIMEOUT 45 + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") add_executable(qt_clipboard_system_test src/qt/clipboard_system_test.cpp) set_target_properties(qt_clipboard_system_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-clipboard-bin/$") diff --git a/src/qt/history_fixture.hpp b/src/qt/history_fixture.hpp index 9a614bf5..ccaea3bb 100644 --- a/src/qt/history_fixture.hpp +++ b/src/qt/history_fixture.hpp @@ -4,6 +4,7 @@ #pragma once #include "../../experiments/conpty/session.hpp" #include +#include #include #include #include @@ -28,7 +29,7 @@ inline void history_child_lines(HANDLE output, int first, int last, const char* history_child_write(output, batch); } -inline int history_child() { +inline int history_child(const std::wstring& record_path = {}) { BOOL owned = FALSE; if (!IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) throw std::runtime_error("history child requires its owned Session Job"); @@ -47,10 +48,16 @@ inline int history_child() { // real child output without host input returning to the bottom. Sleep(1500); history_child_lines(output.get(), 161, 180, "TF_END2\r\nTF>"); + std::ofstream received; + if (!record_path.empty()) { + received.open(record_path, std::ios::binary | std::ios::trunc); + if (!received) tf_conpty::fail("history child input record"); + } for (;;) { char key = 0; DWORD count = 0; if (!ReadFile(input.get(), &key, 1, &count, nullptr) || count != 1) tf_conpty::fail("ReadFile(history child input)"); + if (received) { received.put(key); received.flush(); } if (key == 'q') return 37; if (key == 'm') { history_child_lines(output.get(), 181, 220, "TF_END3\r\nTF>"); diff --git a/src/qt/history_navigation_test.cpp b/src/qt/history_navigation_test.cpp index 66df2f24..a2ed8d87 100644 --- a/src/qt/history_navigation_test.cpp +++ b/src/qt/history_navigation_test.cpp @@ -6,12 +6,14 @@ // presentation or a WindTerm history oracle. #include "terminal_widget.hpp" #include "history_fixture.hpp" +#include "selection_fixture.hpp" #include "../../experiments/core/text_document.hpp" #include #include #include #include #include +#include "clipboard_writer.hpp" #include #include #include @@ -29,6 +31,23 @@ using termforge::ViewportState; using termforge::qt::SessionModel; using termforge::qt::TerminalWidget; using termforge::qt::test::history_child; +using termforge::qt::test::selection_child; +namespace { +class FakeClipboard final : public termforge::qt::ClipboardWriter { +public: + std::u16string text; + termforge::qt::ClipboardWrite write(const std::u16string& value) override { + text = value; + return {termforge::qt::ClipboardWrite::Status::ok, {}}; + } +}; +std::string read_record(const QString& path) { + QFile file(path); + if (!file.exists()) return {}; + if (!file.open(QIODevice::ReadOnly)) throw std::runtime_error("history input record unavailable"); + return file.readAll().toStdString(); +} +} namespace { void require(bool condition, const std::string& message) { @@ -113,7 +132,8 @@ void drag_scrollbar(QScrollBar* bar, int y) { QTest::mouseRelease(bar, Qt::LeftButton, Qt::NoModifier, QPoint(start.x(), y)); } bool start_and_open(SessionModel& model, TerminalWidget& terminal, QWidget& window, - const wchar_t* executable, const QString& workspace, QElapsedTimer& lifecycle) { + const wchar_t* executable, const QString& workspace, const QString& record, + QElapsedTimer& lifecycle) { window.resize(1200, 900); window.show(); terminal.setFocus(); @@ -134,6 +154,10 @@ void close_session(SessionModel& model, QElapsedTimer& lifecycle) { int main(int argc, char** argv) { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (argc >= 2 && std::string_view(argv[1]) == "--selection-child") { + try { return selection_child(argc == 3 ? QString::fromLocal8Bit(argv[2]).toStdWString() : std::wstring{}); } + catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 3; } + } if (argc == 2 && std::string_view(argv[1]) == "--history-child") { try { return history_child(); } catch (const std::exception& error) { @@ -167,12 +191,14 @@ int main(int argc, char** argv) { SessionModel first; QWidget first_window; TerminalWidget first_terminal(first, &first_window); - require(start_and_open(first, first_terminal, first_window, executable, workspace.path(), lifecycle), + const QString first_record = workspace.path() + "/first-history-input.bin"; + const QString second_record = workspace.path() + "/second-history-input.bin"; + require(start_and_open(first, first_terminal, first_window, executable, workspace.path(), first_record, lifecycle), "first history child did not start"); SessionModel second; QWidget second_window; TerminalWidget second_terminal(second, &second_window); - require(start_and_open(second, second_terminal, second_window, executable, workspace.path(), lifecycle), + require(start_and_open(second, second_terminal, second_window, executable, workspace.path(), second_record, lifecycle), "second history child did not start"); require(second_terminal.history_following(), "second session did not start at the bottom"); first_window.activateWindow(); @@ -443,15 +469,41 @@ int main(int argc, char** argv) { require(static_cast(bar->value()) == first_terminal.history_state().top, "scrollbar value/top diverged after restored resize"); - // Find reveal must synchronize the scrollbar to the revealed source page. - first_terminal.open_find_for_test(); - first_terminal.set_find_query_for_test(QStringLiteral("TF_H_140")); + // Public Find entry: real shortcut, query typing and Escape, with the + // scrollbar synchronized to the revealed source page. + first_window.activateWindow(); first_terminal.setFocus(); QTest::qWait(20); + QTest::keyClick(&first_terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); + auto* find_query = first_window.findChild(QStringLiteral("findQuery")); + require(find_query && find_query->hasFocus(), "Find did not focus the public query entry"); + QTest::keyClicks(find_query, QStringLiteral("TF_H_140")); await([&] { return first_terminal.find_current_range_for_test().has_value(); }, first, lifecycle, - "Find did not reveal a history match"); + "public Find did not reveal a history match"); require(!first_terminal.history_following() && static_cast(bar->value()) == first_terminal.history_state().top, - "Find reveal did not synchronize the scrollbar"); - first_terminal.close_find_for_test(); + "public Find reveal did not synchronize the scrollbar"); + QTest::keyClick(find_query, Qt::Key_Escape); + require(first_terminal.hasFocus(), "public Find Escape did not restore terminal focus"); + + // Public drag selection and Ctrl+Shift+C through an injected writer: + // source text is checked, but this is not a real-CF verification. + auto fake = std::make_unique(); + FakeClipboard* captured = fake.get(); + first_terminal.setClipboardWriterForTest(std::move(fake)); + const int cw = first_terminal.cell_width(), ch = first_terminal.cell_height(); + const QPoint from(8 + 2 * cw, 8 + 2 * ch), to(8 + 24 * cw, 8 + 4 * ch); + QTest::mousePress(&first_terminal, Qt::LeftButton, Qt::NoModifier, from); + QTest::mouseMove(&first_terminal, to); + QTest::mouseRelease(&first_terminal, Qt::LeftButton, Qt::NoModifier, to); + require(first_terminal.has_selection(), "public mouse drag did not select source text"); + QTest::keyClick(&first_terminal, Qt::Key_C, Qt::ControlModifier | Qt::ShiftModifier); + require(!captured->text.empty() && + QString::fromUtf16(captured->text.data(), static_cast(captured->text.size())) + .contains(QStringLiteral("TF_H_")), + "public Ctrl+Shift+C did not extract the selected source text"); + + // Focus returns to the terminal after the public Find entry; ordinary + // child input remains the existing separate input-contract coverage. + require(first_terminal.hasFocus(), "public Find did not return focus to the terminal"); QTest::keyClick(&first_terminal, Qt::Key_End, Qt::ControlModifier | Qt::ShiftModifier); await([&] { return first_terminal.history_following(); }, first, lifecycle, "Ctrl+Shift+End after Find did not restore following"); @@ -520,6 +572,34 @@ int main(int argc, char** argv) { "ordinary input did not return to the bottom"); require(second_terminal.history_following(), "second session was affected by first history actions"); + + // A third real child records received bytes; public Find query and the + // following ordinary key must not be misdirected. + const QString third_record = workspace.path() + "/third-history-input.bin"; + SessionModel third; QWidget third_window; TerminalWidget third_terminal(third, &third_window); + third_window.resize(1200, 900); third_window.show(); third_terminal.setFocus(); + const std::wstring third_args = QStringLiteral("--selection-child \"%1\"").arg(third_record).toStdWString(); + require(third.start(executable, third_args, workspace.path().toStdWString()), "third input child did not start"); + await([&] { return third.screen().history_lines().size() >= 20; }, third, lifecycle, + "third input child output missing"); + third_window.activateWindow(); third_terminal.setFocus(); QTest::qWait(20); + QTest::keyClick(&third_terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); + auto* third_query = third_window.findChild(QStringLiteral("findQuery")); + require(third_query && third_query->hasFocus(), "third Find did not focus its public query"); + QTest::keyClicks(third_query, QStringLiteral("SEL_HEAD")); + await([&] { return third_terminal.find_current_range_for_test().has_value(); }, third, lifecycle, + "third public Find did not reveal a hit"); + QTest::keyClick(third_query, Qt::Key_Escape); + require(third_terminal.hasFocus(), "third Find Escape did not restore focus"); + QTest::keyClicks(&third_terminal, QStringLiteral("x")); + QElapsedTimer third_wait; third_wait.start(); + while (third_record.size() && QString::fromStdString(read_record(third_record)) != QStringLiteral("x") && + third_wait.elapsed() < 3000) QTest::qWait(10); + require(QString::fromStdString(read_record(third_record)) == QStringLiteral("x"), + "public Find query/ordinary input did not reach the correct child exactly once"); + require(third.send(std::string(1, 'q')), "third child quit rejected"); + await([&] { return third.finished(); }, third, lifecycle, "third child did not finish"); + close_session(first, lifecycle); close_session(second, lifecycle); std::cout << "{\"schema\":\"termforge-qt-history-navigation-v1\",\"status\":\"passed\"," diff --git a/src/qt/resize_fixture.hpp b/src/qt/resize_fixture.hpp index 53061900..7cbeeafc 100644 --- a/src/qt/resize_fixture.hpp +++ b/src/qt/resize_fixture.hpp @@ -166,10 +166,16 @@ inline int size_child(const std::wstring& pipe_name) { command += character; continue; } - if (command.size() < 3 || (command[0] != 'Q' && command[0] != 'X' && command[0] != 'F') || command[1] != ' ') + if (command.size() < 3 || (command[0] != 'Q' && command[0] != 'X' && command[0] != 'F' && command[0] != 'E') || command[1] != ' ') throw std::runtime_error("Size child input command malformed"); const auto nonce = size_nonce(std::string_view(command).substr(2)); const bool exit = command[0] == 'X'; + if (command[0] == 'E') { + const char clear[] = "\x1b[3J\x1b[H"; + DWORD written = 0; + if (!WriteFile(output.get(), clear, sizeof(clear) - 1, &written, nullptr) || + written != sizeof(clear) - 1) tf_conpty::fail("WriteFile(size child clear output)"); + } if (command[0] == 'F') { const std::string block(4096, 's'); for (int chunk = 0; chunk < 512; ++chunk) { diff --git a/src/qt/scrollbar_size_test.cpp b/src/qt/scrollbar_size_test.cpp new file mode 100644 index 00000000..ea7076ed --- /dev/null +++ b/src/qt/scrollbar_size_test.cpp @@ -0,0 +1,157 @@ +// Scrollbar-enabled Qt grid -> core -> transport -> real-child CSBI ACK. +// This intentionally does not disable the product scrollbar. +#include "terminal_widget.hpp" +#include "resize_fixture.hpp" +#include +#include +#include +#include +#include +#include +#include + +using termforge::qt::SessionModel; +using termforge::qt::TerminalWidget; +using termforge::qt::test::SizeFixture; +using termforge::qt::test::SizeReply; +using termforge::qt::test::size_child; + +namespace { +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} +SizeReply wait_reply(SizeFixture& fixture, uint64_t nonce, SessionModel& model) { + QElapsedTimer timer; timer.start(); + while (timer.elapsed() < 5000) { + if (const auto reply = fixture.take_reply()) { + require(reply->nonce == nonce, "size reply nonce mismatch"); + require(reply->query_error == ERROR_SUCCESS, "child CSBI query failed"); + require(reply->child_pid == model.resize_state().child_pid, "size reply is not the Session child"); + return *reply; + } + QTest::qWait(5); + } + throw std::runtime_error("child size reply timed out"); +} +SizeReply query_size(SizeFixture& fixture, SessionModel& model, uint64_t nonce) { + require(model.send("Q " + std::to_string(nonce) + "\r"), "size query input rejected"); + return wait_reply(fixture, nonce, model); +} +void require_ack(const SizeReply& reply, TerminalWidget& terminal, SessionModel& model, + int columns, int rows, const char* stage) { + require(model.screen().width() == columns && model.screen().height() == rows, + std::string(stage) + ": Qt grid differs"); + const auto state = model.resize_state(); + require(state.requested_columns == columns && state.requested_rows == rows && + state.transport_columns == columns && state.transport_rows == rows && + !state.pending && state.accepting, + std::string(stage) + ": core/transport differs"); + require(reply.buffer_columns == columns && reply.buffer_rows == rows && + reply.window_columns() == columns && reply.window_rows() == rows, + std::string(stage) + ": child CSBI does not match"); +} +void resize_public(TerminalWidget& terminal, SessionModel& model, int columns, int rows, + QElapsedTimer& lifecycle) { + const QFontMetricsF metrics(terminal.font()); + const int cw = qCeil(metrics.horizontalAdvance(QLatin1Char('M'))); + const int ch = qCeil(metrics.height()) + 2; + const auto* bar = terminal.history_scrollbar_for_test(); + const int extent = bar && bar->isVisible() ? terminal.history_scrollbar_extent_for_test() : 0; + terminal.resize(16 + extent + columns * cw, 24 + (rows + 1) * ch); + QElapsedTimer phase; phase.start(); + while (phase.elapsed() < 5000) { + const auto state = model.resize_state(); + if (model.screen().width() == columns && model.screen().height() == rows && + state.requested_columns == columns && state.requested_rows == rows && + state.transport_columns == columns && state.transport_rows == rows && !state.pending) + return; + QTest::qWait(10); + if (!model.error().isEmpty()) throw std::runtime_error(model.error().toStdString()); + } + throw std::runtime_error("public Qt scrollbar resize did not finish"); +} +} // namespace + +int main(int argc, char** argv) { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (argc == 3 && std::string_view(argv[1]) == "--size-child") + try { return size_child(QString::fromLocal8Bit(argv[2]).toStdWString()); } + catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 3; } + if (argc != 1) { std::cerr << "usage: qt_scrollbar_size_test\n"; return 2; } + char gate[3]{}; DWORD count = 0; BOOL owned = FALSE; + if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), gate, sizeof(gate), &count, nullptr) || + count != sizeof(gate) || std::string_view(gate, count) != "GO\n" || + !IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) { + std::cerr << "scrollbar size test requires its owned Job supervisor\n"; return 2; + } + QApplication app(argc, argv); + QApplication::setQuitOnLastWindowClosed(false); + try { + QElapsedTimer lifecycle; lifecycle.start(); + QTemporaryDir workspace; + require(workspace.isValid(), "scrollbar size workspace unavailable"); + wchar_t executable[32768]{}; + const DWORD length = GetModuleFileNameW(nullptr, executable, 32768); + require(length && length < 32768, "scrollbar size executable unavailable"); + SizeFixture fixture; + SessionModel model; QWidget window; TerminalWidget terminal(model, &window); + window.resize(1200, 900); window.show(); terminal.setFocus(); + require(model.start(executable, L"--size-child \"" + fixture.pipe_name() + L"\"", + workspace.path().toStdWString()), "scrollbar size child did not start"); + wait_reply(fixture, 0, model); + const QFontMetricsF metrics(terminal.font()); + const int cw = (std::max)(1, qCeil(metrics.horizontalAdvance(QLatin1Char('M')))); + const int ch = (std::max)(1, qCeil(metrics.height()) + 2); + auto expected_columns = [&](bool reserved) { + return std::clamp((terminal.width() - 16 - (reserved ? terminal.history_scrollbar_extent_for_test() : 0)) / cw, 1, 500); + }; + auto expected_rows = [&] { return std::clamp((terminal.height() - 24 - ch) / ch, 1, 200); }; + auto* bar = terminal.history_scrollbar_for_test(); + require(bar && !bar->isVisible(), "scrollbar appeared without history"); + require_ack(query_size(fixture, model, 1), terminal, model, expected_columns(false), expected_rows(), "no-history"); + + // A finite real-child burst fills retained history; the product + // scrollbar appears and its extent changes the grid seen by the child. + require(model.send(std::string("F 2\r")), "finite history output command rejected"); + wait_reply(fixture, 2, model); + QElapsedTimer bar_wait; bar_wait.start(); + while ((!bar->isVisible() || model.screen().width() != expected_columns(true)) && bar_wait.elapsed() < 5000) + QTest::qWait(10); + require(bar->isVisible(), "scrollbar did not appear after history output"); + require(model.screen().width() == expected_columns(true), "scrollbar-reserved grid width differs"); + require_ack(query_size(fixture, model, 3), terminal, model, expected_columns(true), expected_rows(), "history-reserved"); + + resize_public(terminal, model, 60, 20, lifecycle); + require(bar->isVisible(), "scrollbar disappeared after narrow resize"); + require_ack(query_size(fixture, model, 5), terminal, model, 60, 20, "narrow-reserved"); + resize_public(terminal, model, 80, 24, lifecycle); + require_ack(query_size(fixture, model, 6), terminal, model, 80, 24, "restored-reserved"); + + // ED3 clears retained history; the scrollbar disappears and the full + // available width is acknowledged by the independent child again. + require(model.send(std::string("E 7\r")), "clear-history command rejected"); + wait_reply(fixture, 7, model); + QElapsedTimer hide_wait; hide_wait.start(); + while ((bar->isVisible() || model.screen().width() != expected_columns(false)) && hide_wait.elapsed() < 5000) + QTest::qWait(10); + require(!bar->isVisible(), "scrollbar remained visible after history clear"); + require(model.screen().width() == expected_columns(false), "cleared grid width differs"); + require_ack(query_size(fixture, model, 8), terminal, model, expected_columns(false), expected_rows(), "cleared"); + + require(model.send(std::string("X 9\r")), "size child exit command rejected"); + wait_reply(fixture, 9, model); + QElapsedTimer exit_wait; exit_wait.start(); + while (!model.finished() && exit_wait.elapsed() < 5000) QTest::qWait(10); + require(model.finished() && model.natural_exit() && model.cleanup_verified() && + model.exit_code() == 37, "scrollbar size child did not exit cleanly"); + std::cout << "{\"schema\":\"termforge-qt-scrollbar-size-v1\",\"status\":\"passed\"," + "\"no_history_scrollbar\":true,\"history_reserved\":true," + "\"narrow_reserved\":true,\"restored_reserved\":true," + "\"clear_hides\":true,\"child_csbi_ack\":true," + "\"offscreen_only\":true,\"presented_done\":null}\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "Qt scrollbar size: " << error.what() << '\n'; + return 1; + } +} From cf89cdad9a90f347b5a90df92da4028c78fb7e7e Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:22:27 +0800 Subject: [PATCH 4/9] docs(qt): record scrollbar child-size ACK and public-entry coverage --- .../2026-11-07-qt-history-scrollbar/README.md | 8 +++-- .../validation.json | 36 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md b/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md index 2e35d807..597c8633 100644 --- a/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md @@ -7,7 +7,7 @@ - 主屏右侧垂直滚动条;出现时其宽度进入现有 Qt→core→ConPTY 网格宽度路径,未出现时不占宽;无第二 resize/存储模型。 - 滚动条 `valueChanged` 映射到 `HistoryViewport::scroll_to/bottom`;刷新时从 `reconcile` 的 top/max 同步回滚动条(阻塞信号,防止值反向当身份)。 - 新输出保持暂停阅读的源锚点与页面;回到底部 resume following;淘汰/ED3/RIS 按已有锚点钳制规则;缩窄/恢复后同源锚点;Find reveal 后滚动条同步。 -- 固定布局的旧测试进程显式关闭滚动条占宽,保留其既有 80×24 夹具;滚动条启用路径由 `qt_history_navigation` 覆盖。 +- 固定布局的旧测试进程显式关闭滚动条占宽,保留其既有 80×24 夹具;默认启用路径由 `qt_history_navigation` 与 `qt_scrollbar_size` 覆盖。 ## 验证 @@ -16,10 +16,12 @@ - 滚动条可见/range 与 `maximum_top` 一致;出现时网格列数按滚动条 extent 计算。 - 实际鼠标拖动、页区点击、滚动条 Home/End;值==HistoryViewport top,源锚点==显示页首源行 id,不把值当源 ID。 - 新输出时锚点/页面不跳、滚动条值跟随;回底 following;缩窄 60 列/恢复后同源锚点;Find reveal 同步;双会话滚动/焦点隔离。 -- 本地 Qt 正式路径 10/10;截图 `screens/history-scrollbar.png`。 +- `qt_scrollbar_size`:默认启用滚动条 → 无历史(不出现)→ 有限 child 输出(出现并占宽)→ 缩窄/恢复 → ED3 清空(消失)。每阶段用真实 child `GetConsoleScreenBufferInfo` 独立回报 buffer/window,核对 Qt 网格、core requested、transport 与 child ACK,并固定 Session child 身份。 +- 默认启用路径的真实公开入口:Ctrl+Shift+F/查询/Escape、鼠标拖选 + Ctrl+Shift+C(注入 writer 核对源文本,非真实 CF)、第三个记录输入 child 验证查询/普通键只到正确 child。 +- 本地 Qt 正式路径 11/11;截图 `screens/history-scrollbar.png`。 ## 边界 - 离屏 Qt;不宣称物理呈现/DPI、备用屏/TUI、自动滚动/选词选行、配置持久化或性能阈值。 -- 未覆盖 child 实际窗口尺寸对滚动条宽度的独立 ACK(现有 resize smoke 覆盖产品网格/transport;滚动条+child 尺寸组合仍为边界)。 +- child 实际尺寸 ACK 已覆盖;真实 CF Copy 不在本切片(writer 注入),物理呈现/DPI、WindTerm 滚动条 oracle 仍未覆盖。 - #324 仍等待用户追加验证决定;BCD/VM/v2、旧失败与未跟踪脚本不变。 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json index 84bc3bc5..bfc1aaad 100644 --- a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json @@ -1,16 +1,21 @@ { "schema": "termforge-qt-history-scrollbar-validation-v1", "baseline_main": "ce2bbbacacf49b3864932fad19464d465e638426", - "source_commit": "fe3a79a", - "source_hash_note": "SHA-256 of committed LF blob content at fe3a79a; docs follow-up.", + "source_commit": "6a2749f", + "source_hash_note": "SHA-256 of committed LF blob content at 6a2749f; docs follow-up.", "source_files_sha256": { "src/qt/terminal_widget.hpp": "770242a69cfb04688a0d60f71532b251901b70b34fe5704db096ad7137ca747b", - "src/qt/history_navigation_test.cpp": "d1ce06e5a8696af5a2c6384aead1d0af4c6f62f5a3ad3d5c619ba2651edb1d76", + "src/qt/history_navigation_test.cpp": "a4929e20def0538d640b651b2a788c455601a68c729be611165549f982b6c44d", "src/qt/hidden_retention_test.cpp": "29ccbbd91fb279a9dd159f96cddd0224b1716421a2f3349e200d2e522c03e271", "src/qt/selection_migration_test.cpp": "31cb8ed0d4ab4708b3885c8e6969d4a0125b5a07b64f92de0890bfca90809882", "src/qt/resize_smoke.cpp": "92587fd2aa32296b9b784854c800fca86a8bf0ad52b28390a63a36e4c6fec631", "src/qt/terminal_modes_test.cpp": "ac2af74f312cfde6b4ef799bbe91fb2eab5c5f7b292981d30ff7fdfbedca06e9", - "src/qt/find_test.cpp": "f1df33c4bb3f36acc6366492d8a94e5ed9c54c854c52ef1cb95bd67f44b6bfcc" + "src/qt/find_test.cpp": "f1df33c4bb3f36acc6366492d8a94e5ed9c54c854c52ef1cb95bd67f44b6bfcc", + "src/qt/history_fixture.hpp": "d9fb37df2a7834a49788ec7eb4ea07ddab9766570838075c581cbbd738c931d9", + "src/qt/resize_fixture.hpp": "065745e70db5f3341be60dd4c7f3d3d2aea0b995ca1782090ce2ddfa9b87e774", + "src/qt/scrollbar_size_test.cpp": "a280dccac1fee1f6dee0e61e75e1f32d32f8465d791d2a74869eb22f82f1ef28", + "CMakeLists.txt": "24efd481f48a43a66e5c91f4fbd4120ba2382af9dc69347e42ce731008cb58fa", + ".github/workflows/qt-cpp.yml": "7d9bab2caed2d3ee2fdee41adffbc5708ac6e6227d1968a3948a7bfdbf56b018" }, "environment": { "os": "Windows 11 x64 build 26200", @@ -33,7 +38,14 @@ "two_sessions": true, "existing_wheel_keys_selection": true, "system_clipboard_touched": false, - "offscreen_only": true + "offscreen_only": true, + "child_csbi_no_history": true, + "child_csbi_reserved": true, + "child_csbi_narrow_restore": true, + "child_csbi_clear_hides": true, + "public_find_entry": true, + "public_selection_copy_fake": true, + "third_child_input_after_find": true }, "tests": { "qt_history_navigation": { @@ -42,9 +54,14 @@ "seconds": 2.17 }, "qt_formal_path_regression": { - "passed": 10, + "passed": 11, "failed": 0, - "seconds": 19.69 + "seconds": 22.94 + }, + "qt_scrollbar_size": { + "passed": 1, + "failed": 0, + "seconds": 1.29 } }, "evidence": { @@ -52,8 +69,9 @@ "screenshot": "screens/history-scrollbar.png" }, "limitations": [ + "Child size is now independently ACKed at every scrollbar stage via real CSBI replies; the public Find/selection/input checks run with the scrollbar enabled. The Copy check uses an injected writer and is not real CF.", "Offscreen Qt; physical presentation/DPI, alternate-screen/TUI, autoscroll, word/line selection, persistence and performance thresholds are not covered.", - "The scrollbar-enabled path uses Qt/core/transport dimensions; an independent child ACK of the scrollbar-adjusted child window is not part of this slice.", - "This slice does not close #29/#30/R2; #324 remains blocked pending a user decision; BCD/VM/v2 and old failures are unchanged." + "This slice does not close #29/#30/R2; #324 remains blocked pending a user decision; BCD/VM/v2 and old failures are unchanged.", + "Physical presentation/DPI and a WindTerm scrollbar oracle remain out of scope; fixed-layout tests keep their own disabled-reservation config, while the default enabled path is covered by qt_history_navigation and qt_scrollbar_size." ] } \ No newline at end of file From 3810784a63b7386ca08a16c2f7b782c026ff1868 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:28:28 +0800 Subject: [PATCH 5/9] test(qt): retry the independent child CSBI ACK until the ConPTY resize settles Do not treat a single early child reply or a stale size as final; query the same real child repeatedly until its buffer/window, Qt grid and core transport agree, while still failing on timeout. This removes a hosted-run timing mismatch. --- src/qt/scrollbar_size_test.cpp | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/qt/scrollbar_size_test.cpp b/src/qt/scrollbar_size_test.cpp index ea7076ed..8e1855b0 100644 --- a/src/qt/scrollbar_size_test.cpp +++ b/src/qt/scrollbar_size_test.cpp @@ -37,6 +37,16 @@ SizeReply query_size(SizeFixture& fixture, SessionModel& model, uint64_t nonce) require(model.send("Q " + std::to_string(nonce) + "\r"), "size query input rejected"); return wait_reply(fixture, nonce, model); } +bool ack_matches(const SizeReply& reply, TerminalWidget& terminal, SessionModel& model, + int columns, int rows) { + if (terminal.screen().width() != columns || terminal.screen().height() != rows) return false; + const auto state = model.resize_state(); + if (!(state.requested_columns == columns && state.requested_rows == rows && + state.transport_columns == columns && state.transport_rows == rows && !state.pending && state.accepting)) + return false; + return reply.buffer_columns == columns && reply.buffer_rows == rows && + reply.window_columns() == columns && reply.window_rows() == rows; +} void require_ack(const SizeReply& reply, TerminalWidget& terminal, SessionModel& model, int columns, int rows, const char* stage) { require(model.screen().width() == columns && model.screen().height() == rows, @@ -106,9 +116,18 @@ int main(int argc, char** argv) { return std::clamp((terminal.width() - 16 - (reserved ? terminal.history_scrollbar_extent_for_test() : 0)) / cw, 1, 500); }; auto expected_rows = [&] { return std::clamp((terminal.height() - 24 - ch) / ch, 1, 200); }; + uint64_t next_nonce = 10; + auto await_ack = [&](int columns, int rows, const char* stage) { + for (int attempt = 0; attempt < 200; ++attempt) { + auto reply = query_size(fixture, model, next_nonce++); + if (ack_matches(reply, terminal, model, columns, rows)) return; + QTest::qWait(20); + } + require(false, std::string(stage) + ": child CSBI ACK did not settle"); + }; auto* bar = terminal.history_scrollbar_for_test(); require(bar && !bar->isVisible(), "scrollbar appeared without history"); - require_ack(query_size(fixture, model, 1), terminal, model, expected_columns(false), expected_rows(), "no-history"); + await_ack(expected_columns(false), expected_rows(), "no-history"); // A finite real-child burst fills retained history; the product // scrollbar appears and its extent changes the grid seen by the child. @@ -119,13 +138,13 @@ int main(int argc, char** argv) { QTest::qWait(10); require(bar->isVisible(), "scrollbar did not appear after history output"); require(model.screen().width() == expected_columns(true), "scrollbar-reserved grid width differs"); - require_ack(query_size(fixture, model, 3), terminal, model, expected_columns(true), expected_rows(), "history-reserved"); + await_ack(expected_columns(true), expected_rows(), "history-reserved"); resize_public(terminal, model, 60, 20, lifecycle); require(bar->isVisible(), "scrollbar disappeared after narrow resize"); - require_ack(query_size(fixture, model, 5), terminal, model, 60, 20, "narrow-reserved"); + await_ack(60, 20, "narrow-reserved"); resize_public(terminal, model, 80, 24, lifecycle); - require_ack(query_size(fixture, model, 6), terminal, model, 80, 24, "restored-reserved"); + await_ack(80, 24, "restored-reserved"); // ED3 clears retained history; the scrollbar disappears and the full // available width is acknowledged by the independent child again. @@ -136,7 +155,7 @@ int main(int argc, char** argv) { QTest::qWait(10); require(!bar->isVisible(), "scrollbar remained visible after history clear"); require(model.screen().width() == expected_columns(false), "cleared grid width differs"); - require_ack(query_size(fixture, model, 8), terminal, model, expected_columns(false), expected_rows(), "cleared"); + await_ack(expected_columns(false), expected_rows(), "cleared"); require(model.send(std::string("X 9\r")), "size child exit command rejected"); wait_reply(fixture, 9, model); From cdf6a1e9c081d975eec72579ae3274ac3b6211ed Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:29:54 +0800 Subject: [PATCH 6/9] fix(qt): use the model screen in the child ACK predicate --- src/qt/scrollbar_size_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qt/scrollbar_size_test.cpp b/src/qt/scrollbar_size_test.cpp index 8e1855b0..634913c0 100644 --- a/src/qt/scrollbar_size_test.cpp +++ b/src/qt/scrollbar_size_test.cpp @@ -39,7 +39,7 @@ SizeReply query_size(SizeFixture& fixture, SessionModel& model, uint64_t nonce) } bool ack_matches(const SizeReply& reply, TerminalWidget& terminal, SessionModel& model, int columns, int rows) { - if (terminal.screen().width() != columns || terminal.screen().height() != rows) return false; + if (model.screen().width() != columns || model.screen().height() != rows) return false; const auto state = model.resize_state(); if (!(state.requested_columns == columns && state.requested_rows == rows && state.transport_columns == columns && state.transport_rows == rows && !state.pending && state.accepting)) From 56c86d1b4857d2b30bc1a29dab6c2c638e7c88d2 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:45:19 +0800 Subject: [PATCH 7/9] docs(qt): bind scrollbar validation to the ACK-fix head --- .../results/2026-11-07-qt-history-scrollbar/validation.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json index bfc1aaad..b59f8729 100644 --- a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json @@ -1,8 +1,8 @@ { "schema": "termforge-qt-history-scrollbar-validation-v1", "baseline_main": "ce2bbbacacf49b3864932fad19464d465e638426", - "source_commit": "6a2749f", - "source_hash_note": "SHA-256 of committed LF blob content at 6a2749f; docs follow-up.", + "source_commit": "cdf6a1e", + "source_hash_note": "SHA-256 of committed LF blob content at cdf6a1e; docs follow-up.", "source_files_sha256": { "src/qt/terminal_widget.hpp": "770242a69cfb04688a0d60f71532b251901b70b34fe5704db096ad7137ca747b", "src/qt/history_navigation_test.cpp": "a4929e20def0538d640b651b2a788c455601a68c729be611165549f982b6c44d", @@ -13,7 +13,7 @@ "src/qt/find_test.cpp": "f1df33c4bb3f36acc6366492d8a94e5ed9c54c854c52ef1cb95bd67f44b6bfcc", "src/qt/history_fixture.hpp": "d9fb37df2a7834a49788ec7eb4ea07ddab9766570838075c581cbbd738c931d9", "src/qt/resize_fixture.hpp": "065745e70db5f3341be60dd4c7f3d3d2aea0b995ca1782090ce2ddfa9b87e774", - "src/qt/scrollbar_size_test.cpp": "a280dccac1fee1f6dee0e61e75e1f32d32f8465d791d2a74869eb22f82f1ef28", + "src/qt/scrollbar_size_test.cpp": "f4c6bd6cbce8ccee75ce5a058ca8c8eac1e8049559dbce730e88e4186f58aa55", "CMakeLists.txt": "24efd481f48a43a66e5c91f4fbd4120ba2382af9dc69347e42ce731008cb58fa", ".github/workflows/qt-cpp.yml": "7d9bab2caed2d3ee2fdee41adffbc5708ac6e6227d1968a3948a7bfdbf56b018" }, From b11186de02dd27b8a2d35e21fc540006c9e66f64 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:16:51 +0800 Subject: [PATCH 8/9] test(qt): retain per-stage scrollbar child CSBI observations --- src/qt/scrollbar_size_test.cpp | 80 ++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/src/qt/scrollbar_size_test.cpp b/src/qt/scrollbar_size_test.cpp index 634913c0..daa8037c 100644 --- a/src/qt/scrollbar_size_test.cpp +++ b/src/qt/scrollbar_size_test.cpp @@ -16,6 +16,21 @@ using termforge::qt::test::SizeFixture; using termforge::qt::test::SizeReply; using termforge::qt::test::size_child; +struct Observation { + std::string stage, error; + int order = 0, attempt = 0; + uint64_t nonce = 0; + int expected_columns = 0, expected_rows = 0; + int client_width = 0, scrollbar_extent = 0; + bool scrollbar_visible = false; + int requested_columns = 0, requested_rows = 0, transport_columns = 0, transport_rows = 0; + bool pending = true, accepting = false; + int child_pid = 0; + int buffer_columns = 0, buffer_rows = 0, window_columns = 0, window_rows = 0; + int query_error = 0; + bool match = false; +}; + namespace { void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -117,13 +132,44 @@ int main(int argc, char** argv) { }; auto expected_rows = [&] { return std::clamp((terminal.height() - 24 - ch) / ch, 1, 200); }; uint64_t next_nonce = 10; + std::vector observations; + int observation_order = 0; + auto observe = [&](const char* stage, int attempt, uint64_t nonce, const SizeReply& reply, + int columns, int rows, const std::string& error = {}) { + const auto state = model.resize_state(); + const auto* scroll = terminal.history_scrollbar_for_test(); + Observation item; + item.stage = stage; item.error = error; item.order = ++observation_order; item.attempt = attempt; + item.nonce = nonce; item.expected_columns = columns; item.expected_rows = rows; + item.client_width = terminal.width(); + item.scrollbar_extent = terminal.history_scrollbar_extent_for_test(); + item.scrollbar_visible = scroll && scroll->isVisible(); + item.requested_columns = state.requested_columns; item.requested_rows = state.requested_rows; + item.transport_columns = state.transport_columns; item.transport_rows = state.transport_rows; + item.pending = state.pending; item.accepting = state.accepting; + item.child_pid = reply.child_pid; + item.buffer_columns = reply.buffer_columns; item.buffer_rows = reply.buffer_rows; + item.window_columns = reply.window_columns(); item.window_rows = reply.window_rows(); + item.query_error = reply.query_error; + item.match = ack_matches(reply, terminal, model, columns, rows); + observations.push_back(std::move(item)); + }; auto await_ack = [&](int columns, int rows, const char* stage) { for (int attempt = 0; attempt < 200; ++attempt) { - auto reply = query_size(fixture, model, next_nonce++); - if (ack_matches(reply, terminal, model, columns, rows)) return; + const uint64_t nonce = next_nonce++; + try { + auto reply = query_size(fixture, model, nonce); + observe(stage, attempt, nonce, reply, columns, rows); + if (ack_matches(reply, terminal, model, columns, rows)) return; + } catch (const std::exception& error) { + SizeReply missing; missing.nonce = nonce; + observe(stage, attempt, nonce, missing, columns, rows, error.what()); + } QTest::qWait(20); } - require(false, std::string(stage) + ": child CSBI ACK did not settle"); + SizeReply missing; missing.nonce = next_nonce; + observe(stage, 200, next_nonce, missing, columns, rows, "timeout"); + require(false, std::string(stage) + ": child CSBI ACK did not settle; see trace"); }; auto* bar = terminal.history_scrollbar_for_test(); require(bar && !bar->isVisible(), "scrollbar appeared without history"); @@ -167,7 +213,33 @@ int main(int argc, char** argv) { "\"no_history_scrollbar\":true,\"history_reserved\":true," "\"narrow_reserved\":true,\"restored_reserved\":true," "\"clear_hides\":true,\"child_csbi_ack\":true," - "\"offscreen_only\":true,\"presented_done\":null}\n"; + "\"observations\":["; + for (size_t i = 0; i < observations.size(); ++i) { + const auto& o = observations[i]; + if (i) std::cout << ','; + std::cout << "{\"stage\":\"" << o.stage << "\",\"order\":" << o.order + << ",\"attempt\":" << o.attempt << ",\"nonce\":" << o.nonce + << ",\"expected_columns\":" << o.expected_columns + << ",\"expected_rows\":" << o.expected_rows + << ",\"client_width\":" << o.client_width + << ",\"scrollbar_extent\":" << o.scrollbar_extent + << ",\"scrollbar_visible\":" << (o.scrollbar_visible ? "true" : "false") + << ",\"requested_columns\":" << o.requested_columns + << ",\"requested_rows\":" << o.requested_rows + << ",\"transport_columns\":" << o.transport_columns + << ",\"transport_rows\":" << o.transport_rows + << ",\"pending\":" << (o.pending ? "true" : "false") + << ",\"accepting\":" << (o.accepting ? "true" : "false") + << ",\"child_pid\":" << o.child_pid + << ",\"buffer_columns\":" << o.buffer_columns + << ",\"buffer_rows\":" << o.buffer_rows + << ",\"window_columns\":" << o.window_columns + << ",\"window_rows\":" << o.window_rows + << ",\"query_error\":" << o.query_error + << ",\"match\":" << (o.match ? "true" : "false") + << ",\"error\":\"" << o.error << "\"}"; + } + std::cout << "],\"offscreen_only\":true,\"presented_done\":null}\n"; return 0; } catch (const std::exception& error) { std::cerr << "Qt scrollbar size: " << error.what() << '\n'; From e9db6db0395468b45fec087068906ec032b3cc80 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:17:04 +0800 Subject: [PATCH 9/9] docs(qt): archive the per-stage scrollbar size observations --- .../2026-11-07-qt-history-scrollbar/README.md | 1 + .../scrollbar-size-trace.json | 131 +++++++++++++++++ .../validation.json | 139 +++++++++++++++++- 3 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 experiments/shell/results/2026-11-07-qt-history-scrollbar/scrollbar-size-trace.json diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md b/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md index 597c8633..df1966ad 100644 --- a/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/README.md @@ -17,6 +17,7 @@ - 实际鼠标拖动、页区点击、滚动条 Home/End;值==HistoryViewport top,源锚点==显示页首源行 id,不把值当源 ID。 - 新输出时锚点/页面不跳、滚动条值跟随;回底 following;缩窄 60 列/恢复后同源锚点;Find reveal 同步;双会话滚动/焦点隔离。 - `qt_scrollbar_size`:默认启用滚动条 → 无历史(不出现)→ 有限 child 输出(出现并占宽)→ 缩窄/恢复 → ED3 清空(消失)。每阶段用真实 child `GetConsoleScreenBufferInfo` 独立回报 buffer/window,核对 Qt 网格、core requested、transport 与 child ACK,并固定 Session child 身份。 +- 逐阶段原始观测保存在 [scrollbar-size-trace.json](scrollbar-size-trace.json):client width、滚动条 extent/visible、期望 columns/rows、core requested/transport/pending、child PID/nonce、实际 buffer/window、query_error、match、attempt/order。本次五次均在 attempt 0 收敛,未出现旧尺寸/错误/超时;若出现,await_ack 会保留每条不匹配/异常/超时记录后失败,而不是丢弃。 - 默认启用路径的真实公开入口:Ctrl+Shift+F/查询/Escape、鼠标拖选 + Ctrl+Shift+C(注入 writer 核对源文本,非真实 CF)、第三个记录输入 child 验证查询/普通键只到正确 child。 - 本地 Qt 正式路径 11/11;截图 `screens/history-scrollbar.png`。 diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/scrollbar-size-trace.json b/experiments/shell/results/2026-11-07-qt-history-scrollbar/scrollbar-size-trace.json new file mode 100644 index 00000000..a37bf653 --- /dev/null +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/scrollbar-size-trace.json @@ -0,0 +1,131 @@ +{ + "schema": "termforge-qt-scrollbar-size-observations-v1", + "source": "local qt_scrollbar_size run", + "observations": [ + { + "stage": "no-history", + "order": 1, + "attempt": 0, + "nonce": 10, + "expected_columns": 80, + "expected_rows": 24, + "client_width": 736, + "scrollbar_extent": 14, + "scrollbar_visible": false, + "requested_columns": 80, + "requested_rows": 24, + "transport_columns": 80, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 80, + "buffer_rows": 24, + "window_columns": 80, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "history-reserved", + "order": 2, + "attempt": 0, + "nonce": 11, + "expected_columns": 78, + "expected_rows": 24, + "client_width": 736, + "scrollbar_extent": 14, + "scrollbar_visible": true, + "requested_columns": 78, + "requested_rows": 24, + "transport_columns": 78, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 78, + "buffer_rows": 24, + "window_columns": 78, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "narrow-reserved", + "order": 3, + "attempt": 0, + "nonce": 12, + "expected_columns": 60, + "expected_rows": 20, + "client_width": 570, + "scrollbar_extent": 14, + "scrollbar_visible": true, + "requested_columns": 60, + "requested_rows": 20, + "transport_columns": 60, + "transport_rows": 20, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 60, + "buffer_rows": 20, + "window_columns": 60, + "window_rows": 20, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "restored-reserved", + "order": 4, + "attempt": 0, + "nonce": 13, + "expected_columns": 80, + "expected_rows": 24, + "client_width": 750, + "scrollbar_extent": 14, + "scrollbar_visible": true, + "requested_columns": 80, + "requested_rows": 24, + "transport_columns": 80, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 80, + "buffer_rows": 24, + "window_columns": 80, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "cleared", + "order": 5, + "attempt": 0, + "nonce": 14, + "expected_columns": 81, + "expected_rows": 24, + "client_width": 750, + "scrollbar_extent": 14, + "scrollbar_visible": false, + "requested_columns": 81, + "requested_rows": 24, + "transport_columns": 81, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 81, + "buffer_rows": 24, + "window_columns": 81, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + } + ] +} \ No newline at end of file diff --git a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json index b59f8729..b1ea6eb2 100644 --- a/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json +++ b/experiments/shell/results/2026-11-07-qt-history-scrollbar/validation.json @@ -1,8 +1,8 @@ { "schema": "termforge-qt-history-scrollbar-validation-v1", "baseline_main": "ce2bbbacacf49b3864932fad19464d465e638426", - "source_commit": "cdf6a1e", - "source_hash_note": "SHA-256 of committed LF blob content at cdf6a1e; docs follow-up.", + "source_commit": "b11186d", + "source_hash_note": "SHA-256 of committed LF blob content at b11186d; docs follow-up.", "source_files_sha256": { "src/qt/terminal_widget.hpp": "770242a69cfb04688a0d60f71532b251901b70b34fe5704db096ad7137ca747b", "src/qt/history_navigation_test.cpp": "a4929e20def0538d640b651b2a788c455601a68c729be611165549f982b6c44d", @@ -13,7 +13,7 @@ "src/qt/find_test.cpp": "f1df33c4bb3f36acc6366492d8a94e5ed9c54c854c52ef1cb95bd67f44b6bfcc", "src/qt/history_fixture.hpp": "d9fb37df2a7834a49788ec7eb4ea07ddab9766570838075c581cbbd738c931d9", "src/qt/resize_fixture.hpp": "065745e70db5f3341be60dd4c7f3d3d2aea0b995ca1782090ce2ddfa9b87e774", - "src/qt/scrollbar_size_test.cpp": "f4c6bd6cbce8ccee75ce5a058ca8c8eac1e8049559dbce730e88e4186f58aa55", + "src/qt/scrollbar_size_test.cpp": "1a27c6a7dde2c68948931f6923d9986140b5100c1c4bae65d31e4cdc1e2eac83", "CMakeLists.txt": "24efd481f48a43a66e5c91f4fbd4120ba2382af9dc69347e42ce731008cb58fa", ".github/workflows/qt-cpp.yml": "7d9bab2caed2d3ee2fdee41adffbc5708ac6e6227d1968a3948a7bfdbf56b018" }, @@ -73,5 +73,136 @@ "Offscreen Qt; physical presentation/DPI, alternate-screen/TUI, autoscroll, word/line selection, persistence and performance thresholds are not covered.", "This slice does not close #29/#30/R2; #324 remains blocked pending a user decision; BCD/VM/v2 and old failures are unchanged.", "Physical presentation/DPI and a WindTerm scrollbar oracle remain out of scope; fixed-layout tests keep their own disabled-reservation config, while the default enabled path is covered by qt_history_navigation and qt_scrollbar_size." - ] + ], + "scrollbar_size_trace": { + "file": "scrollbar-size-trace.json", + "observations": [ + { + "stage": "no-history", + "order": 1, + "attempt": 0, + "nonce": 10, + "expected_columns": 80, + "expected_rows": 24, + "client_width": 736, + "scrollbar_extent": 14, + "scrollbar_visible": false, + "requested_columns": 80, + "requested_rows": 24, + "transport_columns": 80, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 80, + "buffer_rows": 24, + "window_columns": 80, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "history-reserved", + "order": 2, + "attempt": 0, + "nonce": 11, + "expected_columns": 78, + "expected_rows": 24, + "client_width": 736, + "scrollbar_extent": 14, + "scrollbar_visible": true, + "requested_columns": 78, + "requested_rows": 24, + "transport_columns": 78, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 78, + "buffer_rows": 24, + "window_columns": 78, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "narrow-reserved", + "order": 3, + "attempt": 0, + "nonce": 12, + "expected_columns": 60, + "expected_rows": 20, + "client_width": 570, + "scrollbar_extent": 14, + "scrollbar_visible": true, + "requested_columns": 60, + "requested_rows": 20, + "transport_columns": 60, + "transport_rows": 20, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 60, + "buffer_rows": 20, + "window_columns": 60, + "window_rows": 20, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "restored-reserved", + "order": 4, + "attempt": 0, + "nonce": 13, + "expected_columns": 80, + "expected_rows": 24, + "client_width": 750, + "scrollbar_extent": 14, + "scrollbar_visible": true, + "requested_columns": 80, + "requested_rows": 24, + "transport_columns": 80, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 80, + "buffer_rows": 24, + "window_columns": 80, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + }, + { + "stage": "cleared", + "order": 5, + "attempt": 0, + "nonce": 14, + "expected_columns": 81, + "expected_rows": 24, + "client_width": 750, + "scrollbar_extent": 14, + "scrollbar_visible": false, + "requested_columns": 81, + "requested_rows": 24, + "transport_columns": 81, + "transport_rows": 24, + "pending": false, + "accepting": true, + "child_pid": 10108, + "buffer_columns": 81, + "buffer_rows": 24, + "window_columns": 81, + "window_rows": 24, + "query_error": 0, + "match": true, + "error": "" + } + ], + "note": "All five stages converged on the first observation in this local run; no old size or timeout occurred. The test now preserves every mismatch/error/timeout if one occurs." + } } \ No newline at end of file