diff --git a/projects/APPLaunch/main/ui/model/setup_page_model.cpp b/projects/APPLaunch/main/ui/model/setup_page_model.cpp index 11af6f0f..1863b5a1 100644 --- a/projects/APPLaunch/main/ui/model/setup_page_model.cpp +++ b/projects/APPLaunch/main/ui/model/setup_page_model.cpp @@ -66,10 +66,13 @@ bool SetupPageModel::move_main(int direction, int item_count) return true; } -void SetupPageModel::enter_sub(int item_count, int center_row) +void SetupPageModel::enter_sub(int item_count, int center_row, int preferred_index) { view = SetupViewState::SUB; - sub_selected_index = clamp_index(std::min(center_row, item_count - 1), item_count); + if (preferred_index >= 0) + sub_selected_index = clamp_index(preferred_index, item_count); + else + sub_selected_index = clamp_index(std::min(center_row, item_count - 1), item_count); } bool SetupPageModel::move_sub(int direction, int item_count) diff --git a/projects/APPLaunch/main/ui/model/setup_page_model.hpp b/projects/APPLaunch/main/ui/model/setup_page_model.hpp index ecd6e100..ca067284 100644 --- a/projects/APPLaunch/main/ui/model/setup_page_model.hpp +++ b/projects/APPLaunch/main/ui/model/setup_page_model.hpp @@ -84,7 +84,8 @@ class SetupPageModel std::string value_title; bool move_main(int direction, int item_count); - void enter_sub(int item_count, int center_row = DEFAULT_CENTER_ROW); + void enter_sub(int item_count, int center_row = DEFAULT_CENTER_ROW, + int preferred_index = -1); bool move_sub(int direction, int item_count); void select_sub(int index, int item_count); void enter_value(std::string title, std::vector options, int selected_index); diff --git a/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp b/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp index a7dc6454..e306a07c 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp @@ -448,6 +448,7 @@ void BluetoothUiSession::handle_list_key(UISetupPage &page, uint32_t key) action_operation_.abort(action_token_); action_busy_ = false; SetupPageAccess access(page); + refresh_status(page); access.set_view(SetupViewState::SUB); access.build_sub_view(); break; diff --git a/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp b/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp index 35558d13..d6b27dd0 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp +++ b/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp @@ -21,6 +21,7 @@ struct MenuItem std::vector sub_items; std::function on_enter; std::function custom_key_handler; + int default_sub_selection = -1; }; } // namespace setting diff --git a/projects/APPLaunch/main/ui/page_app/setting/system.cpp b/projects/APPLaunch/main/ui/page_app/setting/system.cpp index b879425d..93c7784e 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/system.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/system.cpp @@ -249,6 +249,7 @@ void Update::append(UISetupPage &page, std::vector &menu) {"Version: --", false, false, nullptr}, {"Build: --", false, false, nullptr}, }; + item.default_sub_selection = 0; item.on_enter = [page_ptr]() { Update::refresh_version_info(*page_ptr); }; menu.push_back(item); } @@ -283,7 +284,6 @@ void UISetupPage::stop_update_timer(bool cancel_job) if (cancel_job && !update_job_id_.empty()) cp0_signal_osinfo_api({"UpdateJobCancel", update_job_id_}, nullptr); update_job_id_.clear(); - update_item_index_ = -1; if (had_job) launcher_toast().hide(); } @@ -307,7 +307,6 @@ void UISetupPage::start_update_job(const char *command, int item_index) launcher_toast().show("Launcher update unavailable"); return; } - update_item_index_ = item_index; update_timer_ = lv_timer_create(update_timer_cb, 500, this); if (!update_timer_) { stop_update_timer(); diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp index abda5d85..406d7e94 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp @@ -128,7 +128,6 @@ class UISetupPage : public AppPage lv_timer_t *update_timer_ = nullptr; lv_timer_t *volume_preview_timer_ = nullptr; std::string update_job_id_; - int update_item_index_ = -1; static constexpr int SCREEN_W = 320; static constexpr int SCREEN_H = 150; diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp index 7485d0a1..b092ae59 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp @@ -204,7 +204,7 @@ void UISetupPage::handle_main_key(uint32_t key) if (item.on_enter) item.on_enter(); if (!item.sub_items.empty()) { int sub_count = static_cast(item.sub_items.size()); - model_.enter_sub(sub_count, ROW_CENTER); + model_.enter_sub(sub_count, ROW_CENTER, item.default_sub_selection); build_sub_view(); } break; diff --git a/projects/APPLaunch/tests/test_setup_page_model.cpp b/projects/APPLaunch/tests/test_setup_page_model.cpp index ce644380..d53f1661 100644 --- a/projects/APPLaunch/tests/test_setup_page_model.cpp +++ b/projects/APPLaunch/tests/test_setup_page_model.cpp @@ -108,6 +108,10 @@ int main() assert(model.sub_selected_index == 0); assert(model.move_sub(-1, 2)); assert(model.sub_selected_index == 1); + model.enter_sub(2, SetupPageModel::DEFAULT_CENTER_ROW, 0); + assert(model.sub_selected_index == 0); + model.enter_sub(2, SetupPageModel::DEFAULT_CENTER_ROW, 99); + assert(model.sub_selected_index == 1); model.select_sub(0, 2); assert(model.sub_selected_index == 0); model.select_sub(99, 2); diff --git a/projects/APPLaunch/tests/test_updater_packaging.py b/projects/APPLaunch/tests/test_updater_packaging.py index 7e368e0d..e3d19c93 100755 --- a/projects/APPLaunch/tests/test_updater_packaging.py +++ b/projects/APPLaunch/tests/test_updater_packaging.py @@ -29,7 +29,12 @@ def run_update(fail_install: bool = False, unhealthy_process: bool = False, audit_required: bool = False, interrupted_candidate: bool = False, - compatible: bool = True) -> tuple[subprocess.CompletedProcess[str], str, str]: + compatible: bool = True, + candidate_version: str | None = None, + installed_version: str | None = None, + rollback_version: str | None = None, + after_version: str | None = None, + compare_exit: int = 0) -> tuple[subprocess.CompletedProcess[str], str, str]: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) fake_bin = root / "bin" @@ -60,6 +65,8 @@ def run_update(fail_install: bool = False, "1\n" if compatible else "0\n", encoding="ascii" ) if provide_rollback: + cache.mkdir() + (cache / "installed.deb").write_bytes(b"trusted rollback package") old_package = old_release / "applaunch_1.0_arm64.deb" old_package.write_bytes(b"trusted previous package") old_digest = hashlib.sha256(old_package.read_bytes()).hexdigest() @@ -73,17 +80,20 @@ def run_update(fail_install: bool = False, fake_bin / "dpkg-deb", 'case "$3" in Package) echo applaunch;; Architecture) echo arm64;; ' 'X-CardputerZero-Update-ABI) echo "${TEST_UPDATE_ABI:-1}";; ' - 'Version) case "$2" in *installed.deb|*rollback.deb) echo 1.0;; *) echo 2.0;; esac;; esac\n', + 'Version) case "$2" in *installed.deb|*rollback.deb) ' + 'echo "${TEST_ROLLBACK_VERSION:-1.0}";; *) ' + 'echo "${TEST_CANDIDATE_VERSION:-2.0}";; esac;; esac\n', ) executable( fake_bin / "dpkg-query", 'case "$2" in *Status*) echo installed;; *) ' - 'if [ -f "$TEST_MARKER" ]; then echo 2.0; else echo 1.0; fi;; esac\n', + 'if [ -f "$TEST_MARKER" ]; then echo "${TEST_AFTER_VERSION:-2.0}"; ' + 'else echo "${TEST_INSTALLED_VERSION:-1.0}"; fi;; esac\n', ) executable( fake_bin / "dpkg", 'echo "dpkg $*" >>"$TEST_LOG"\n' - 'case "$1" in --compare-versions) exit 0;; --audit) ' + 'case "$1" in --compare-versions) exit "${TEST_COMPARE_EXIT:-0}";; --audit) ' '[ ! -f "$TEST_AUDIT_MARKER" ] || echo "package needs configuration";; --configure) ' '[ "${TEST_FAIL_CONFIGURE:-0}" != 1 ] || exit 44; ' '[ "${TEST_KEEP_AUDIT_DIRTY:-0}" = 1 ] || rm -f "$TEST_AUDIT_MARKER";; -i) ' @@ -114,6 +124,11 @@ def run_update(fail_install: bool = False, "TEST_FAIL_CONFIGURE": "1" if fail_configure else "0", "TEST_KEEP_AUDIT_DIRTY": "1" if keep_audit_dirty else "0", "TEST_AUDIT_MARKER": str(root / "audit.marker"), + "TEST_CANDIDATE_VERSION": candidate_version or "", + "TEST_INSTALLED_VERSION": installed_version or "", + "TEST_ROLLBACK_VERSION": rollback_version or "", + "TEST_AFTER_VERSION": after_version or "", + "TEST_COMPARE_EXIT": str(compare_exit), "APPLAUNCH_UPDATER_REEXEC": "1", "APPLAUNCH_UPDATE_EXECUTABLE": str(app_executable), "APPLAUNCH_UPDATE_PROC_ROOT": str(proc), @@ -185,6 +200,30 @@ def run_update(fail_install: bool = False, assert missing_rollback.returncode != 0 assert status == "failed:rollback-unavailable" +local_to_cloud, status, commands = run_update( + candidate_version="2.0", + installed_version="3.0+local.test1", + rollback_version="3.0+local.test1", + after_version="2.0", + compare_exit=1, +) +assert local_to_cloud.returncode == 0, ( + local_to_cloud.stderr, local_to_cloud.stdout, status, commands +) +assert status == "succeeded:2.0" +assert "dpkg -i" in commands + +official_downgrade, status, commands = run_update( + candidate_version="2.0", + installed_version="3.0", + rollback_version="3.0", + after_version="2.0", + compare_exit=1, +) +assert official_downgrade.returncode != 0 +assert status == "failed:version-not-newer" +assert "dpkg -i" not in commands + incompatible, status, commands = run_update(compatible=False) assert incompatible.returncode != 0 assert status == "failed:incompatible" diff --git a/projects/AppStore b/projects/AppStore index 155f0e31..65f1225d 160000 --- a/projects/AppStore +++ b/projects/AppStore @@ -1 +1 @@ -Subproject commit 155f0e313a9960ab4d31249c90193ece01baf230 +Subproject commit 65f1225dbfe4db560c73930060eac715a5c2da9d diff --git a/projects/CameraApp/main_CameraApp b/projects/CameraApp/main_CameraApp index 785ac2a3..0e080b92 160000 --- a/projects/CameraApp/main_CameraApp +++ b/projects/CameraApp/main_CameraApp @@ -1 +1 @@ -Subproject commit 785ac2a33adafde93a4af4d5ef89677dc28d1fb7 +Subproject commit 0e080b92a0af56f688a6b9ee29d04a180debc9f3 diff --git a/projects/FactoryTest/main_FactoryTest b/projects/FactoryTest/main_FactoryTest index 14ced93f..f3277963 160000 --- a/projects/FactoryTest/main_FactoryTest +++ b/projects/FactoryTest/main_FactoryTest @@ -1 +1 @@ -Subproject commit 14ced93fa76d146986934e5859d00a6ff72f8b30 +Subproject commit f327796399c017ead5cd812f037c014349d94d90 diff --git a/projects/ZClaw/main/ui/zclaw_app.cpp b/projects/ZClaw/main/ui/zclaw_app.cpp index ab97e8a0..537a7dee 100644 --- a/projects/ZClaw/main/ui/zclaw_app.cpp +++ b/projects/ZClaw/main/ui/zclaw_app.cpp @@ -160,6 +160,7 @@ class ZClawApp : public AppPageRoot zclaw::KeyRouteContext context; context.startup = startup_workflow_.state(); context.input_open = input_dialog_.is_open(); + context.input_mode = input_dialog_.mode(); context.approval_pending = approvals_.pending(); context.setup_retry_pending = settings_workflow_.setup_retry_pending(); diff --git a/projects/ZClaw/main/ui/zclaw_input_model.cpp b/projects/ZClaw/main/ui/zclaw_input_model.cpp index af916073..bfa3fae9 100644 --- a/projects/ZClaw/main/ui/zclaw_input_model.cpp +++ b/projects/ZClaw/main/ui/zclaw_input_model.cpp @@ -6,7 +6,8 @@ namespace zclaw { bool input_is_single_line(InputMode mode) { - return mode != InputMode::Chat; + return mode != InputMode::Chat && mode != InputMode::SetupUriEdit && + mode != InputMode::ProviderUriEdit; } InputSubmission input_submission(InputMode mode, std::string value) @@ -19,9 +20,11 @@ InputSubmission input_submission(InputMode mode, std::string value) : InputSubmissionAction::SendChat; break; case InputMode::SetupEdit: + case InputMode::SetupUriEdit: submission.action = InputSubmissionAction::ApplySetupEdit; break; case InputMode::ProviderEdit: + case InputMode::ProviderUriEdit: submission.action = InputSubmissionAction::ApplyProviderEdit; break; case InputMode::PairingCode: diff --git a/projects/ZClaw/main/ui/zclaw_input_model.h b/projects/ZClaw/main/ui/zclaw_input_model.h index a6f23491..f891facc 100644 --- a/projects/ZClaw/main/ui/zclaw_input_model.h +++ b/projects/ZClaw/main/ui/zclaw_input_model.h @@ -7,7 +7,9 @@ namespace zclaw { enum class InputMode { Chat, SetupEdit, + SetupUriEdit, ProviderEdit, + ProviderUriEdit, PairingCode, }; diff --git a/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp b/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp index 6d04fdd4..d1f5cfb0 100644 --- a/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp +++ b/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp @@ -49,6 +49,7 @@ KeyEvent adapt_key_event(std::uint32_t key_code, int key_state, event.phase = adapt_key_phase(key_state); event.key = adapt_key(key_code); event.shift = (modifiers & KBD_MOD_SHIFT) != 0; + event.fn = (modifiers & KBD_MOD_FN) != 0; if (utf8) event.text = utf8; return event; diff --git a/projects/ZClaw/main/ui/zclaw_key_router.cpp b/projects/ZClaw/main/ui/zclaw_key_router.cpp index b913567b..6738a6f9 100644 --- a/projects/ZClaw/main/ui/zclaw_key_router.cpp +++ b/projects/ZClaw/main/ui/zclaw_key_router.cpp @@ -62,6 +62,10 @@ KeyAction route_key(const KeyRouteContext &context, const KeyEvent &event) if (context.input_open) { if (event.key == Key::Escape) return {KeyActionType::InputClose, {}}; + if (event.key == Key::Tab && + (context.input_mode == InputMode::SetupUriEdit || + context.input_mode == InputMode::ProviderUriEdit)) + return {KeyActionType::InputInsertNewline, {}}; if (event.key == Key::Tab) return {KeyActionType::InputToggleSecretVisibility, {}}; if (event.key == Key::Enter && !event.shift) diff --git a/projects/ZClaw/main/ui/zclaw_key_router.h b/projects/ZClaw/main/ui/zclaw_key_router.h index 8bf2a525..7842183f 100644 --- a/projects/ZClaw/main/ui/zclaw_key_router.h +++ b/projects/ZClaw/main/ui/zclaw_key_router.h @@ -1,5 +1,6 @@ #pragma once +#include "zclaw_input_model.h" #include "zclaw_settings_navigation_model.h" #include "zclaw_startup_model.h" @@ -39,11 +40,13 @@ struct KeyEvent { Key key = Key::Other; bool shift = false; std::string text; + bool fn = false; }; struct KeyRouteContext { StartupState startup = StartupState::CheckingNetwork; bool input_open = false; + InputMode input_mode = InputMode::Chat; bool approval_pending = false; bool setup_retry_pending = false; bool setup_in_flight = false; diff --git a/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp b/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp index 0f0ebe7d..46a92162 100644 --- a/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp +++ b/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp @@ -7,7 +7,6 @@ #include "zclaw_provider_manager.h" #include "zclaw_settings_coordinator.h" #include "zclaw_settings_panel.h" -#include "zclaw_secret_input_model.h" #include @@ -73,12 +72,13 @@ void ProviderWorkflow::edit_selected_field() const std::string *value = provider_field_value( &provider, settings_.state().provider_edit_field()); if (value) { - const bool secret = - settings_.state().provider_edit_field() == ProviderEditField::ApiKey; + const bool uri = + settings_.state().provider_edit_field() == ProviderEditField::Uri; input_.open_text(&fonts_, provider_field_name(settings_.state().provider_edit_field()), - secret ? secret_input_initial_text() : *value, - InputMode::ProviderEdit, secret); + *value, + uri ? InputMode::ProviderUriEdit : InputMode::ProviderEdit, + false); } } @@ -93,11 +93,8 @@ void ProviderWorkflow::apply_edit(const std::string &value) ProviderConfig provider = providers_.providers()[provider_index]; std::string *field = provider_field_value( &provider, settings_.state().provider_edit_field()); - if (field) { - *field = settings_.state().provider_edit_field() == ProviderEditField::ApiKey - ? apply_secret_input(*field, value) - : value; - } + if (field) + *field = value; settings_.state().set_provider_edit_field(ProviderEditField::None); std::string error; if (!providers_.replace(static_cast(provider_index), provider, diff --git a/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp b/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp index e8b82ee1..de1e39b8 100644 --- a/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp +++ b/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp @@ -7,7 +7,6 @@ #include "zclaw_provider_catalog.h" #include "zclaw_provider_manager.h" #include "zclaw_settings_coordinator.h" -#include "zclaw_secret_input_model.h" #include "zclaw_ui_config_manager.h" #include @@ -86,12 +85,13 @@ void SetupWorkflow::edit_selected_field() const std::string *value = setup_field_value(&provider, settings_.state().setup_edit_field()); if (value) { - const bool secret = - settings_.state().setup_edit_field() == SetupEditField::ApiKey; + const bool uri = + settings_.state().setup_edit_field() == SetupEditField::Uri; input_.open_text(&fonts_, setup_field_name(settings_.state().setup_edit_field()), - secret ? secret_input_initial_text() : *value, - InputMode::SetupEdit, secret); + *value, + uri ? InputMode::SetupUriEdit : InputMode::SetupEdit, + false); } } @@ -100,11 +100,8 @@ void SetupWorkflow::apply_edit(const std::string &value) ProviderConfig provider = providers_.setup_provider(); std::string *field = setup_field_value(&provider, settings_.state().setup_edit_field()); - if (field) { - *field = settings_.state().setup_edit_field() == SetupEditField::ApiKey - ? apply_secret_input(*field, value) - : value; - } + if (field) + *field = value; settings_.state().set_setup_edit_field(SetupEditField::None); std::string error; if (!providers_.update_setup_provider(provider, &error)) diff --git a/projects/ZClaw/tests/zclaw_input_routing_test.cpp b/projects/ZClaw/tests/zclaw_input_routing_test.cpp index 67dcad7c..f017f9c6 100644 --- a/projects/ZClaw/tests/zclaw_input_routing_test.cpp +++ b/projects/ZClaw/tests/zclaw_input_routing_test.cpp @@ -15,7 +15,9 @@ int main() using zclaw::InputSubmissionAction; assert(!zclaw::input_is_single_line(InputMode::Chat)); assert(zclaw::input_is_single_line(InputMode::SetupEdit)); + assert(!zclaw::input_is_single_line(InputMode::SetupUriEdit)); assert(zclaw::input_is_single_line(InputMode::ProviderEdit)); + assert(!zclaw::input_is_single_line(InputMode::ProviderUriEdit)); assert(zclaw::input_is_single_line(InputMode::PairingCode)); zclaw::InputSubmission submission = zclaw::input_submission(InputMode::Chat, "hello"); @@ -29,8 +31,12 @@ int main() InputSubmissionAction::None); assert(zclaw::input_submission(InputMode::SetupEdit, "").action == InputSubmissionAction::ApplySetupEdit); + assert(zclaw::input_submission(InputMode::SetupUriEdit, "").action == + InputSubmissionAction::ApplySetupEdit); assert(zclaw::input_submission(InputMode::ProviderEdit, "").action == InputSubmissionAction::ApplyProviderEdit); + assert(zclaw::input_submission(InputMode::ProviderUriEdit, "").action == + InputSubmissionAction::ApplyProviderEdit); using zclaw::Key; using zclaw::KeyActionType; @@ -51,6 +57,8 @@ int main() assert(!adapted.shift && adapted.text.empty()); adapted = zclaw::adapt_key_event(KEY_ESC, KBD_KEY_RELEASED, 0, ""); assert(adapted.key == Key::Escape && adapted.phase == KeyPhase::Released); + adapted = zclaw::adapt_key_event(KEY_UP, KBD_KEY_PRESSED, KBD_MOD_FN, nullptr); + assert(adapted.key == Key::Up && adapted.fn); adapted = zclaw::adapt_key_event(KEY_RESERVED, 99, KBD_MOD_CTRL, "x"); assert(adapted.key == Key::Other && adapted.phase == KeyPhase::Unknown); assert(!adapted.shift && adapted.text == "x"); @@ -100,6 +108,11 @@ int main() KeyActionType::InputSubmit); assert(routed(context, KeyPhase::Released, Key::Tab).type == KeyActionType::InputToggleSecretVisibility); + context.input_mode = InputMode::ProviderUriEdit; + assert(routed(context, KeyPhase::Released, Key::Tab).type == + KeyActionType::InputInsertNewline); + assert(routed(context, KeyPhase::Released, Key::Escape).type == + KeyActionType::InputClose); assert(routed(context, KeyPhase::Released, Key::Enter, true).type == KeyActionType::None); assert(routed(context, KeyPhase::Released, Key::Down).type == diff --git a/scripts/debian_packager.py b/scripts/debian_packager.py index d7cb6dc9..94c16889 100755 --- a/scripts/debian_packager.py +++ b/scripts/debian_packager.py @@ -554,7 +554,16 @@ def _updater_script_text() -> str: ;; esac fi -dpkg --compare-versions "$candidate" gt "$installed" || fail version-not-newer + +# Local verification builds use +local./+git. suffixes and can be newer than the +# current cloud release. Allow them to return to the official channel; regular +# official packages still cannot downgrade. +if ! dpkg --compare-versions "$candidate" ge "$installed"; then + case "$installed" in + *+local.*|*+git.*) ;; + *) fail version-not-newer ;; + esac +fi # Retain the last trusted package so a later upgrade can roll back after a # failed install or service health check.