From 18023c4037d046860b3732b49dc4762a4ead59c7 Mon Sep 17 00:00:00 2001 From: C Date: Mon, 6 Jul 2026 04:05:57 -0700 Subject: [PATCH] feat: add tui (#758) This commit also contains an entire redesign of the GUI interface, which now contains a sidebar for all panels. Hopefully checkra1n approves :) --- Makefile | 6 +- images/logo.png | Bin 6718 -> 9329 bytes resources/DFUHelperDeviceInfo.json | 123 ++++---- src/event.cpp | 465 ++++++++++++++++++++--------- src/event.hpp | 38 ++- src/m8/run.c | 30 ++ src/m8/run.h | 7 + src/main.cpp | 14 +- src/sequence.cpp | 5 +- src/sequence.hpp | 6 +- src/state.hpp | 27 -- src/tui/.gitkeep | 0 src/tui/Tui.cpp | 408 +++++++++++++++++++++++++ src/tui/Tui.hpp | 71 +++++ src/tui/TuiBranding.cpp | 52 ++++ src/tui/TuiBranding.hpp | 10 + src/tui/TuiDfuArt.cpp | 139 +++++++++ src/tui/TuiDfuArt.hpp | 21 ++ src/tui/TuiDfuPanel.cpp | 359 ++++++++++++++++++++++ src/tui/TuiDfuPanel.hpp | 51 ++++ src/tui/TuiExploitPanel.cpp | 134 +++++++++ src/tui/TuiExploitPanel.hpp | 38 +++ src/tui/TuiMainPanel.cpp | 227 ++++++++++++++ src/tui/TuiMainPanel.hpp | 25 ++ src/tui/TuiPanel.hpp | 49 +++ src/tui/TuiRecoveryPanel.cpp | 139 +++++++++ src/tui/TuiRecoveryPanel.hpp | 39 +++ src/tui/TuiSettingsPanel.cpp | 190 ++++++++++++ src/tui/TuiSettingsPanel.hpp | 28 ++ src/tui/TuiText.hpp | 83 +++++ src/utils.c | 3 + src/wx/AppFrame.cpp | 36 +-- src/wx/AppFrame.hpp | 2 +- src/wx/DevicePanel.hpp | 2 +- src/wx/DfuPanel.cpp | 70 +++-- src/wx/DfuPanel.hpp | 2 +- src/wx/ExploitPanel.cpp | 67 ++--- src/wx/ExploitPanel.hpp | 2 +- src/wx/MainPanel.cpp | 223 ++++++++------ src/wx/MainPanel.hpp | 5 +- src/wx/RecoveryPanel.cpp | 52 ++-- src/wx/SettingsPanel.cpp | 134 +++++++-- 42 files changed, 2895 insertions(+), 487 deletions(-) delete mode 100644 src/state.hpp delete mode 100644 src/tui/.gitkeep create mode 100644 src/tui/Tui.cpp create mode 100644 src/tui/Tui.hpp create mode 100644 src/tui/TuiBranding.cpp create mode 100644 src/tui/TuiBranding.hpp create mode 100644 src/tui/TuiDfuArt.cpp create mode 100644 src/tui/TuiDfuArt.hpp create mode 100644 src/tui/TuiDfuPanel.cpp create mode 100644 src/tui/TuiDfuPanel.hpp create mode 100644 src/tui/TuiExploitPanel.cpp create mode 100644 src/tui/TuiExploitPanel.hpp create mode 100644 src/tui/TuiMainPanel.cpp create mode 100644 src/tui/TuiMainPanel.hpp create mode 100644 src/tui/TuiPanel.hpp create mode 100644 src/tui/TuiRecoveryPanel.cpp create mode 100644 src/tui/TuiRecoveryPanel.hpp create mode 100644 src/tui/TuiSettingsPanel.cpp create mode 100644 src/tui/TuiSettingsPanel.hpp create mode 100644 src/tui/TuiText.hpp diff --git a/Makefile b/Makefile index af0868e..5f03aab 100644 --- a/Makefile +++ b/Makefile @@ -23,20 +23,22 @@ payloads: name=$$(basename "$$file"); \ name=$${name%.*}; \ echo " XXD $$file"; \ + echo "#pragma once" > "src/gen/payloads/$$name.h"; \ xxd -i "$$file" \ | sed 's/unsigned char/static const unsigned char/g' \ | sed 's/unsigned int/static const size_t/g' \ - > "src/gen/payloads/$$name.h"; \ + >> "src/gen/payloads/$$name.h"; \ done @for file in images/*; do \ name=$$(basename "$$file"); \ name=$${name%.*}; \ echo " XXD $$file"; \ + echo "#pragma once" > "src/gen/images/$$name.h"; \ xxd -i "$$file" \ | sed 's/unsigned char/static const unsigned char/g' \ | sed 's/unsigned int/static const size_t/g' \ - > "src/gen/images/$$name.h"; \ + >> "src/gen/images/$$name.h"; \ done xxd -i -n "DFUHelperDeviceInfo" resources/DFUHelperDeviceInfo.json > src/gen/DFUHelperDeviceInfo.h diff --git a/images/logo.png b/images/logo.png index 29020e5cab19c9ac844950f30d0ac12929bd3268..281da53950d28afeea07bcd5b7ebbe9fc1820351 100644 GIT binary patch delta 8661 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 diff --git a/resources/DFUHelperDeviceInfo.json b/resources/DFUHelperDeviceInfo.json index e852978..0e9c334 100644 --- a/resources/DFUHelperDeviceInfo.json +++ b/resources/DFUHelperDeviceInfo.json @@ -82,17 +82,16 @@ "image_name":"ipodtouch", "image_width":116, "image_height":240, - "image_offset_x":100, "buttons":{ "top":{ "name":"- Top button", - "x":192, - "y":-4 + "x":237, + "y":0 }, "vol_down":{ "name":"Volume down -", - "x":8, - "y":62 + "x":46, + "y":56 } }, "sequence":"ipodtouch7" @@ -101,17 +100,16 @@ "image_name":"ipodtouch", "image_width":116, "image_height":240, - "image_offset_x":100, "buttons":{ "top":{ "name":"- Top button", - "x":196, - "y":-4 + "x":237, + "y":0 }, "home":{ "name":"Home button -", - "x":50, - "y":210 + "x":89, + "y":190 } }, "sequence":"iphone-old" @@ -120,17 +118,16 @@ "image_name":"iphonese", "image_width":119, "image_height":240, - "image_offset_x":100, "buttons":{ "top":{ "name":"- Top button", - "x":200, - "y":-4 + "x":237, + "y":0 }, "home":{ "name":"Home button -", - "x":56, - "y":210 + "x":90, + "y":195 } }, "sequence":"iphone-old" @@ -139,17 +136,16 @@ "image_name":"iphone6s", "image_width":123, "image_height":240, - "image_offset_x":100, "buttons":{ "side":{ "name":"- Side button", - "x":224, - "y":58 + "x":244, + "y":54 }, "home":{ "name":"Home button -", - "x":60, - "y":210 + "x":90, + "y":195 } }, "sequence":"iphone6" @@ -158,17 +154,16 @@ "image_name":"iphone7", "image_width":123, "image_height":240, - "image_offset_x":100, "buttons":{ "side":{ "name":"- Side button", - "x":224, - "y":58 + "x":244, + "y":54 }, "vol_down":{ "name":"Volume down -", - "x":14, - "y":78 + "x":44, + "y":72 } }, "sequence":"iphone7" @@ -177,17 +172,16 @@ "image_name":"iphone8", "image_width":120, "image_height":240, - "image_offset_x":100, "buttons":{ "side":{ "name":"- Side button", - "x":220, - "y":58 + "x":248, + "y":54 }, "vol_down":{ "name":"Volume down -", - "x":10, - "y":78 + "x":44, + "y":72 } }, "sequence":"iphone8" @@ -196,17 +190,16 @@ "image_name":"iphonex", "image_width":121, "image_height":240, - "image_offset_x":100, "buttons":{ "side":{ "name":"- Side button", - "x":220, - "y":68 + "x":246, + "y":62 }, "vol_down":{ "name":"Volume down -", - "x":10, - "y":75 + "x":44, + "y":70 } }, "sequence":"iphone8" @@ -217,14 +210,14 @@ "image_height":240, "buttons":{ "top":{ - "name":"Top button", - "x":149, - "y":5 + "name":"- Top button", + "x":266, + "y":0 }, "home":{ - "name":"Home button", - "x":89, - "y":225 + "name":"Home button -", + "x":94, + "y":200 } }, "sequence":"iphone-old" @@ -235,14 +228,14 @@ "image_height":240, "buttons":{ "top":{ - "name":"Top button", - "x":140, - "y":4 + "name":"- Top button", + "x":260, + "y":0 }, "home":{ - "name":"Home button", - "x":86, - "y":224 + "name":"Home button -", + "x":92, + "y":200 } }, "sequence":"iphone-old" @@ -251,17 +244,16 @@ "image_name":"siriremote", "image_width":79, "image_height":240, - "image_offset_x":120, "buttons":{ "menu":{ "name":"Menu -", - "x":84, - "y":84 + "x":110, + "y":74 }, "playpause":{ "name":"Play/Pause -", - "x":52, - "y":146 + "x":80, + "y":130 } }, "sequence":"appletv4" @@ -270,11 +262,10 @@ "image_name":"atv_std_brd", "image_width":300, "image_height":240, - "image_offset_x":0, "buttons":{ "dfu":{ "name":"-- DFU button", - "x":120, + "x":170, "y":48 } }, @@ -295,7 +286,7 @@ "top", "vol_down" ], - "description":"Press and hold the Top and Volume down buttons together", + "description":"HOLD Top and Volume down", "duration":4, "action":"reboot" }, @@ -303,7 +294,7 @@ "buttons":[ "vol_down" ], - "description":"Release the Top button BUT KEEP HOLDING the Volume down button", + "description":"Release Top and KEEP holding Volume down", "duration":10 } ], @@ -320,7 +311,7 @@ "top", "home" ], - "description":"Press and hold the Top\n and Home buttons\n together", + "description":"HOLD Top and Home button", "duration":4, "action":"reboot" }, @@ -328,7 +319,7 @@ "buttons":[ "home" ], - "description":"Release the Top button\n BUT KEEP HOLDING the\n Home button", + "description":"Release Top and KEEP holding Home button", "duration":10 } ], @@ -345,7 +336,7 @@ "side", "home" ], - "description":"Press and hold the Side\n and Home buttons\n together", + "description":"HOLD Side and Home button", "duration":4, "action":"reboot" }, @@ -353,7 +344,7 @@ "buttons":[ "home" ], - "description":"Release the Side button\n BUT KEEP HOLDING the\n Home button", + "description":"Release Side and KEEP holding Home button", "duration":10 } ], @@ -370,7 +361,7 @@ "side", "vol_down" ], - "description":"Press and hold the Side\n and Volume down\n buttons together", + "description":"HOLD Side and Volume down", "duration":4, "action":"reboot" }, @@ -378,7 +369,7 @@ "buttons":[ "vol_down" ], - "description":"Release the Side button\n BUT KEEP HOLDING the\n Volume down button", + "description":"Release Side and KEEP holding Volume down", "duration":10 } ], @@ -395,7 +386,7 @@ "side", "vol_down" ], - "description":"Press and hold the Side\n and Volume down\n buttons together", + "description":"HOLD Side and Volume down", "duration":4, "action":"reboot" }, @@ -403,7 +394,7 @@ "buttons":[ "vol_down" ], - "description":"Release the Side button\n BUT KEEP HOLDING the\n Volume down button", + "description":"Release Side and KEEP holding Volume down", "duration":10 } ], @@ -420,7 +411,7 @@ "menu", "playpause" ], - "description":"Press and hold Menu\n and Play/Pause together\n ", + "description":"Press and hold Menu and Play/Pause together", "duration":7 }, { @@ -443,7 +434,7 @@ "buttons":[ "dfu" ], - "description":"Press and hold the DFU\n button", + "description":"Press and hold the DFU button", "duration":10, "action":"reboot" } diff --git a/src/event.cpp b/src/event.cpp index 02c314e..aba3fd2 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,183 +1,362 @@ -#ifdef WITH_GUI +#if defined(WITH_GUI) || defined(WITH_TUI) #include "event.hpp" - -#include "wx/AppFrame.hpp" -#include "state.hpp" #include "sequence.hpp" #include "utils.h" -#include +#include +#include #include +#include +#include #include #include #include -static inline DeviceState make_fresh_state() -{ - return DeviceState{}; +namespace { + +struct ManagedDevice { + DeviceState state; + bool normalPresent = false; + bool recoveryPresent = false; +}; + +std::mutex g_mutex; +std::vector g_callbacks; +std::unordered_map g_devices; // keyed by ECID +std::unordered_map g_udidToEcid; +uint64_t g_activeEcid = 0; +std::once_flag g_startOnce; +irecv_device_event_context_t g_recoveryCtx = nullptr; + +void dispatch_callbacks(const std::vector& callbacks, const DeviceState& state) { + for (const auto& cb : callbacks) { + if (cb) + cb(state); + } } -void normal_device_event_cb( - const idevice_event_t* event, - void* user_data) -{ - #ifdef WITH_GUI - auto* frame = static_cast(user_data); - if (!frame || !event) - return; - #endif +DeviceState build_public_state_locked() { + DeviceState out; + out.connectedDeviceCount = static_cast(g_devices.size()); + out.multipleDevices = out.connectedDeviceCount > 1; - DeviceState state = make_fresh_state(); - - if (event->event == IDEVICE_DEVICE_REMOVE) - { - #ifdef WITH_GUI - send_device_state(frame, state); - #endif - return; + if (g_devices.size() != 1) { + g_activeEcid = 0; + return out; } - if (event->event != IDEVICE_DEVICE_ADD) - return; + auto it = g_devices.begin(); + if (g_activeEcid != 0) { + auto activeIt = g_devices.find(g_activeEcid); + if (activeIt != g_devices.end()) + it = activeIt; + } + g_activeEcid = it->first; + out = it->second.state; + out.connected = true; + out.connectedDeviceCount = 1; + out.multipleDevices = false; + return out; +} + +void publish_state() { + std::vector callbacks; + DeviceState state; + { + std::lock_guard lock(g_mutex); + callbacks = g_callbacks; + state = build_public_state_locked(); + } + dispatch_callbacks(callbacks, state); +} + +bool lockdown_get_string(lockdownd_client_t client, const char* key, std::string& out) { + plist_t value = nullptr; + if (lockdownd_get_value(client, nullptr, key, &value) != LOCKDOWN_E_SUCCESS || !value) + return false; + + char* raw = nullptr; + plist_get_string_val(value, &raw); + if (raw) { + out = raw; + free(raw); + } + + plist_free(value); + return !out.empty(); +} + +bool lockdown_get_uint(lockdownd_client_t client, const char* key, uint64_t& out) { + plist_t value = nullptr; + if (lockdownd_get_value(client, nullptr, key, &value) != LOCKDOWN_E_SUCCESS || !value) + return false; + + plist_get_uint_val(value, &out); + plist_free(value); + return out != 0; +} + +bool build_normal_state(const idevice_event_t* event, DeviceState& out, std::string& connectionType) { idevice_t device = nullptr; - if (idevice_new(&device, event->udid) != IDEVICE_E_SUCCESS) - return; - - state.connected = true; - state.mode = DeviceMode::Normal; - state.udid = event->udid ? event->udid : ""; + if (!event || idevice_new(&device, event->udid) != IDEVICE_E_SUCCESS || !device) + return false; lockdownd_client_t client = nullptr; - - if (lockdownd_client_new_with_handshake(device, &client, "palera1n") == LOCKDOWN_E_SUCCESS) - { - plist_t product = nullptr; - plist_t version = nullptr; - plist_t ecid = nullptr; - - char *p = nullptr, *v = nullptr; - uint64_t ecid_val = 0; - - lockdownd_get_value(client, nullptr, "ProductType", &product); - lockdownd_get_value(client, nullptr, "ProductVersion", &version); - lockdownd_get_value(client, nullptr, "UniqueChipID", &ecid); - - if (product) plist_get_string_val(product, &p); - if (version) plist_get_string_val(version, &v); - if (ecid) plist_get_uint_val(ecid, &ecid_val); - - state.productType = p ? p : ""; - state.productVersion = v ? v : ""; - state.ecid = ecid_val; - - free(p); - free(v); - - if (product) plist_free(product); - if (version) plist_free(version); - if (ecid) plist_free(ecid); - - lockdownd_client_free(client); + if (lockdownd_client_new_with_handshake(device, &client, "palera1n") != LOCKDOWN_E_SUCCESS) { + idevice_free(device); + return false; } + out.connected = true; + out.mode = DeviceMode::Normal; + out.udid = event->udid ? event->udid : ""; + + lockdown_get_string(client, "ProductType", out.productType); + lockdown_get_string(client, "ProductVersion", out.productVersion); + lockdown_get_string(client, "ConnectionType", connectionType); + lockdown_get_uint(client, "UniqueChipID", out.ecid); + + out.isSupported = SequenceIsSupported(out.productType); + + lockdownd_client_free(client); idevice_free(device); - - state.isSupported = SequenceIsSupported(state.productType); - - #ifdef WITH_GUI - send_device_state(frame, state); - #endif + return true; } -void recovery_device_event_cb( - const irecv_device_event_t* event, - void* user_data) -{ - #ifdef WITH_GUI - auto* frame = static_cast(user_data); - if (!frame || !event) - return; - #endif +bool build_recovery_state(const irecv_device_event_t* event, DeviceState& out) { + if (!event || !event->device_info || !event->device_info->ecid) + return false; - DeviceState state = make_fresh_state(); - - if (event->type == IRECV_DEVICE_REMOVE) - { - #ifdef WITH_GUI - send_device_state(frame, state); - #endif - return; - } - - if (event->type != IRECV_DEVICE_ADD) - return; - - state.connected = true; - - if (event->mode == IRECV_K_DFU_MODE) - { - state.mode = DeviceMode::DFU; - #ifdef WITH_GUI - send_device_state(frame, state); - #endif - return; - } - - if (!(event->mode == IRECV_K_RECOVERY_MODE_1 || - event->mode == IRECV_K_RECOVERY_MODE_2 || - event->mode == IRECV_K_RECOVERY_MODE_3 || - event->mode == IRECV_K_RECOVERY_MODE_4)) - { - return; - } - - state.mode = DeviceMode::Recovery; - - uint64_t ecid_val = 0; - if (event->device_info && event->device_info->ecid) - ecid_val = event->device_info->ecid; - state.ecid = ecid_val; + out.connected = true; + out.ecid = event->device_info->ecid; + out.mode = (event->mode == IRECV_K_DFU_MODE) ? DeviceMode::DFU : DeviceMode::Recovery; irecv_client_t client = nullptr; - int attempts = 0; - const int max_attempts = 10; - - while (attempts < max_attempts) - { - if (irecv_open_with_ecid(&client, state.ecid) == IRECV_E_SUCCESS) + const int maxAttempts = 10; + while (attempts < maxAttempts) { + if (irecv_open_with_ecid(&client, out.ecid) == IRECV_E_SUCCESS) break; - - attempts++; + ++attempts; std::this_thread::sleep_for(std::chrono::milliseconds(300)); } - if (client) - { - LOG_VERBOSE("Opened client after %d attempts", attempts); - + if (client) { irecv_device_t device = nullptr; - if (irecv_devices_get_device_by_client(client, &device) == IRECV_E_SUCCESS && device) - { - state.productType = device->product_type ? device->product_type : ""; - state.displayName = device->display_name ? device->display_name : ""; + if (irecv_devices_get_device_by_client(client, &device) == IRECV_E_SUCCESS && device) { + out.productType = device->product_type ? device->product_type : ""; + out.displayName = device->display_name ? device->display_name : ""; } - irecv_close(client); - } - else - { - LOG_ERROR("Failed to open irecv client after retries"); + } else { + LOG_VERBOSE("Failed to open irecv client after retries for ECID=%llu", static_cast(out.ecid)); } - state.isSupported = SequenceIsSupported(state.productType); - - #ifdef WITH_GUI - send_device_state(frame, state); - #endif + out.isSupported = SequenceIsSupported(out.productType); + return true; } -#endif // WITH_GUI +void handle_normal_add(const idevice_event_t* event) { + if (!event) + return; + + if (event->conn_type != CONNECTION_USBMUXD) { + LOG_VERBOSE("Ignoring non-USB normal mode device event"); + return; + } + + DeviceState discovered; + std::string connectionType; + if (!build_normal_state(event, discovered, connectionType)) + return; + + if (!connectionType.empty() && connectionType != "USB") { + LOG_VERBOSE("Ignoring over-the-air normal mode device (ConnectionType=%s)", connectionType.c_str()); + return; + } + + if (discovered.ecid == 0) { + LOG_VERBOSE("Ignoring normal mode device without ECID"); + return; + } + + { + std::lock_guard lock(g_mutex); + ManagedDevice& m = g_devices[discovered.ecid]; + m.state = discovered; + m.normalPresent = true; + m.recoveryPresent = false; + + if (!discovered.udid.empty()) + g_udidToEcid[discovered.udid] = discovered.ecid; + } + + publish_state(); +} + +void handle_normal_remove(const idevice_event_t* event) { + if (!event || !event->udid) + return; + + { + std::lock_guard lock(g_mutex); + auto mapIt = g_udidToEcid.find(event->udid); + if (mapIt == g_udidToEcid.end()) + return; + + const uint64_t ecid = mapIt->second; + g_udidToEcid.erase(mapIt); + + auto devIt = g_devices.find(ecid); + if (devIt == g_devices.end()) + return; + + devIt->second.normalPresent = false; + if (!devIt->second.normalPresent && !devIt->second.recoveryPresent) + g_devices.erase(devIt); + } + + publish_state(); +} + +void handle_recovery_add(const irecv_device_event_t* event) { + DeviceState discovered; + if (!build_recovery_state(event, discovered)) + return; + + { + std::lock_guard lock(g_mutex); + ManagedDevice& m = g_devices[discovered.ecid]; + m.state = discovered; + m.recoveryPresent = true; + m.normalPresent = false; + } + + publish_state(); +} + +void handle_recovery_remove(const irecv_device_event_t* event) { + bool changed = false; + { + std::lock_guard lock(g_mutex); + + uint64_t ecid = 0; + if (event && event->device_info) + ecid = event->device_info->ecid; + + if (ecid == 0) { + for (const auto& [key, value] : g_devices) { + if (!value.recoveryPresent) + continue; + if (ecid != 0) { + ecid = 0; + break; + } + ecid = key; + } + } + + if (ecid == 0) + return; + + auto devIt = g_devices.find(ecid); + if (devIt == g_devices.end()) + return; + + devIt->second.recoveryPresent = false; + if (!devIt->second.normalPresent && !devIt->second.recoveryPresent) + g_devices.erase(devIt); + + for (auto it = g_udidToEcid.begin(); it != g_udidToEcid.end();) { + if (it->second == ecid && g_devices.find(ecid) == g_devices.end()) + it = g_udidToEcid.erase(it); + else + ++it; + } + + changed = true; + } + + if (changed) + publish_state(); +} + +bool is_supported_recovery_mode(irecv_mode mode) { + return mode == IRECV_K_DFU_MODE || + mode == IRECV_K_RECOVERY_MODE_1 || + mode == IRECV_K_RECOVERY_MODE_2 || + mode == IRECV_K_RECOVERY_MODE_3 || + mode == IRECV_K_RECOVERY_MODE_4; +} + +} // namespace + +void register_device_state_callback(DeviceStateCallback cb) { + if (!cb) + return; + + DeviceState snapshot; + { + std::lock_guard lock(g_mutex); + g_callbacks.push_back(cb); + snapshot = build_public_state_locked(); + } + + cb(snapshot); +} + +void ensure_device_event_system_started() { + std::call_once(g_startOnce, []() { + if (idevice_event_subscribe(normal_device_event_cb, nullptr) != IDEVICE_E_SUCCESS) + LOG_ERROR("Failed to subscribe to normal mode device events"); + + if (irecv_device_event_subscribe(&g_recoveryCtx, recovery_device_event_cb, nullptr) != IRECV_E_SUCCESS) + LOG_ERROR("Failed to subscribe to recovery/DFU device events"); + + char** udids = nullptr; + int count = 0; + if (idevice_get_device_list(&udids, &count) == IDEVICE_E_SUCCESS && udids) { + for (int i = 0; i < count; ++i) { + idevice_event_t synthetic_event; + synthetic_event.event = IDEVICE_DEVICE_ADD; + synthetic_event.udid = udids[i]; + synthetic_event.conn_type = CONNECTION_USBMUXD; + handle_normal_add(&synthetic_event); + } + idevice_device_list_free(udids); + } + }); +} + +void normal_device_event_cb(const idevice_event_t* event, void* user_data) { + (void)user_data; + if (!event) + return; + + if (event->event == IDEVICE_DEVICE_ADD) { + handle_normal_add(event); + return; + } + + if (event->event == IDEVICE_DEVICE_REMOVE) + handle_normal_remove(event); +} + +void recovery_device_event_cb(const irecv_device_event_t* event, void* user_data) { + (void)user_data; + if (!event) + return; + + if (event->type == IRECV_DEVICE_ADD && is_supported_recovery_mode(event->mode)) { + handle_recovery_add(event); + return; + } + + if (event->type == IRECV_DEVICE_REMOVE) + handle_recovery_remove(event); +} + +#endif // WITH_GUI || WITH_TUI diff --git a/src/event.hpp b/src/event.hpp index 078c620..cd5c24c 100644 --- a/src/event.hpp +++ b/src/event.hpp @@ -1,21 +1,49 @@ -#ifdef WITH_GUI +#if defined(WITH_GUI) || defined(WITH_TUI) #ifndef EVENT_H #define EVENT_H -#include "state.hpp" +#include +#include +#include -#include +#ifdef WITH_GUI +# include +#endif #include #include #include -class MainFrame; +enum class DeviceMode +{ + None, + Normal, + Recovery, + DFU +}; + +struct DeviceState +{ + bool connected = false; + bool isSupported = false; + bool multipleDevices = false; + DeviceMode mode = DeviceMode::None; + std::string productVersion; + std::string productType; + std::string displayName; + std::string udid; + uint64_t ecid = 0; + uint32_t connectedDeviceCount = 0; +}; + +using DeviceStateCallback = std::function; +void register_device_state_callback(DeviceStateCallback cb); +void ensure_device_event_system_started(); void normal_device_event_cb(const idevice_event_t* event, void* user_data); void recovery_device_event_cb(const irecv_device_event_t* event, void* user_data); #endif // EVENT_H -#endif // WITH_GUI +#endif // WITH_GUI || WITH_TUI diff --git a/src/m8/run.c b/src/m8/run.c index 119e148..0ea870a 100644 --- a/src/m8/run.c +++ b/src/m8/run.c @@ -23,6 +23,36 @@ #define PID_PONGO (0x4141) #define PID_DFU (0x1227) +#if defined(WITH_GUI) || defined(WITH_TUI) + +const char* stage_to_string(int stage) +{ + switch (stage) { + case STAGE_SETUP: return "Checking if device is ready"; + case STAGE_SPRAY: return "Setting up the exploit (this is the heap spray)"; + case STAGE_PATCH: return "Right before trigger (this is the real bug setup)"; + case STAGE_YOLODFU: return "Booting PongoOS..."; + case STAGE_PONGO: return "Booting..."; + case STAGE_DONE: return "Done."; + default: return "Idle."; + } +} + +int stage_to_progress(int stage) +{ + switch (stage) { + case STAGE_SETUP: return 30; + case STAGE_SPRAY: return 40; + case STAGE_PATCH: return 50; + case STAGE_YOLODFU: return 60; + case STAGE_PONGO: return 70; + case STAGE_DONE: return 100; + default: return 0; + } +} + +#endif // WITH_GUI || WITH_TUI + static void *exploit_thread(void *arg) { LOG_VERBOSE("Starting thread for DFU devices"); diff --git a/src/m8/run.h b/src/m8/run.h index 0c53201..e291cd6 100644 --- a/src/m8/run.h +++ b/src/m8/run.h @@ -22,6 +22,13 @@ enum exploit_stage { STAGE_DONE = 6, }; +#if defined(WITH_GUI) || defined(WITH_TUI) + +const char* stage_to_string(int stage); +int stage_to_progress(int stage); + +#endif // WITH_GUI || WITH_TUI + typedef struct { atomic_int result; atomic_bool stop; diff --git a/src/main.cpp b/src/main.cpp index f943f4b..fd6e159 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,7 +11,9 @@ #endif #include "m8/run.h" - +#ifdef WITH_TUI +# include "tui/Tui.hpp" +#endif #include "utils.h" #include "globals.h" #include "paleinfo.h" @@ -31,9 +33,9 @@ void print_credits() { ":: Made by: asdfugil, kok3shidoll, claration, mineek\n" ":: nekohaxx, plooshi, staturnz\n" ":: ======== Thanks to =======>\n" - ":: Thanks to: llsc12, itsnebulalol, lrdsnow, kirb, ehilwyma\n" - ":: opa334, 0x7ff, sbingner, nikias (libimobiledevice)\n" - ":: dedbeddedbed, tihmstar\n" + ":: Thanks to: itsnebulalol, llsc12, lrdsnow, dedbeddedbed\n" + ":: kirb, ehilwyma, opa334, 0x7ff, alfiecg25, sneko, sbingner\n" + ":: nikias, tihmstar\n" ":: Checkra1n (Siguza, axi0mx, littlelailo et al.)\n" ":: Procursus (Hayden Seay, Cameron Katri, Keto et al.)\n" ":: ==========================>\n\n" @@ -329,8 +331,8 @@ int main(int argc, char* argv[], char* envp[]) { wxEntryCleanup(); return 0; } else if (palerain_flags & palerain_option_tui) { - LOG_WARN("TUI is not implemented yet"); - return 1; + ui_run(); + return 0; } else { shared_t state{}; return exploit(&state); diff --git a/src/sequence.cpp b/src/sequence.cpp index e49c6f9..43d803d 100644 --- a/src/sequence.cpp +++ b/src/sequence.cpp @@ -1,4 +1,4 @@ -#ifdef WITH_GUI +#if defined(WITH_GUI) || defined(WITH_TUI) #include #include "gen/DFUHelperDeviceInfo.h" @@ -28,7 +28,6 @@ DfuSequence ParseSequence(const std::string& deviceKey) seq.imageName = layout.value("image_name", ""); seq.imageWidth = layout.value("image_width", 0); seq.imageHeight = layout.value("image_height", 0); - seq.imageOffsetX = layout.value("image_offset_x", 0); if (layout.contains("buttons")) { @@ -85,4 +84,4 @@ bool SequenceIsSupported(const std::string& deviceKey) return !seq.steps.empty(); } -#endif // WITH_GUI +#endif // WITH_GUI || WITH_TUI diff --git a/src/sequence.hpp b/src/sequence.hpp index 2817635..4a88f8e 100644 --- a/src/sequence.hpp +++ b/src/sequence.hpp @@ -1,9 +1,10 @@ -#ifdef WITH_GUI +#if defined(WITH_GUI) || defined(WITH_TUI) #ifndef SEQUENCE_H #define SEQUENCE_H #include +#include struct DfuButton { @@ -26,7 +27,6 @@ struct DfuSequence std::string imageName; int imageWidth = 0; int imageHeight = 0; - int imageOffsetX = 0; std::vector buttons; std::vector steps; @@ -37,4 +37,4 @@ bool SequenceIsSupported(const std::string& deviceKey); #endif // SEQUENCE_H -#endif // WITH_GUI +#endif // WITH_GUI || WITH_TUI diff --git a/src/state.hpp b/src/state.hpp deleted file mode 100644 index 05fc11c..0000000 --- a/src/state.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef STATE_H -#define STATE_H - -#include -#include - -enum class DeviceMode -{ - None, - Normal, - Recovery, - DFU -}; - -struct DeviceState -{ - bool connected = false; - bool isSupported = false; - DeviceMode mode = DeviceMode::None; - std::string productVersion; - std::string productType; - std::string displayName; - std::string udid; - uint64_t ecid; -}; - -#endif // STATE_H diff --git a/src/tui/.gitkeep b/src/tui/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/tui/Tui.cpp b/src/tui/Tui.cpp new file mode 100644 index 0000000..0b33696 --- /dev/null +++ b/src/tui/Tui.cpp @@ -0,0 +1,408 @@ +#ifdef WITH_TUI + +#include "Tui.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "TuiPanel.hpp" +#include "TuiMainPanel.hpp" +#include "TuiSettingsPanel.hpp" +#include "TuiRecoveryPanel.hpp" +#include "TuiDfuPanel.hpp" +#include "TuiExploitPanel.hpp" +#include "TuiBranding.hpp" +#include "../globals.h" +#include "../utils.h" +#include "../paleinfo.h" +#include "../event.hpp" + +#define WIDTH 80 +#define HEIGHT 24 + +static constexpr int LEFT_PANEL_WIDTH = 65; +static constexpr int RIGHT_PANEL_WIDTH = WIDTH - LEFT_PANEL_WIDTH; +static constexpr int RIGHT_INNER_WIDTH = RIGHT_PANEL_WIDTH - 2; +static constexpr int MAIN_BUTTON_VERTICAL_GAP = 1; +static constexpr int SIDEBAR_BUTTON_WIDTH = 13; +static constexpr int SIDEBAR_WALL_GAP = 1; +static constexpr short COLOR_PAIR_SUBTITLE = 1; + +static std::string trim_button_label(std::string label) { + while (!label.empty() && label.front() == ' ') { + label.erase(label.begin()); + } + while (!label.empty() && label.back() == ' ') { + label.pop_back(); + } + + return label; +} + +static std::string format_sidebar_button_label(const char* raw_label, int button_width) { + if (button_width < 2) { + return ""; + } + + if (!raw_label) { + return std::string(static_cast(button_width), ' '); + } + + std::string label(raw_label); + if (label.size() >= 2 && label.front() == '[' && label.back() == ']') { + label = label.substr(1, label.size() - 2); + } + + label = trim_button_label(label); + + const int inner_width = button_width - 2; + if (inner_width <= 0) { + return "[]"; + } + + if (static_cast(label.size()) > inner_width) { + label.resize(static_cast(inner_width)); + } + + const int left_pad = std::max(0, (inner_width - static_cast(label.size())) / 2); + const int right_pad = std::max(0, inner_width - static_cast(label.size()) - left_pad); + + return "[" + std::string(static_cast(left_pad), ' ') + label + std::string(static_cast(right_pad), ' ') + "]"; +} + +static void draw_panel_border(int y, int x, int width, int height, const char* title = nullptr) { + if (title) { + char title_buf[64]; + std::snprintf(title_buf, sizeof(title_buf), "[ %s ]", title); + int title_len = static_cast(std::strlen(title_buf)); + int title_start_x = x + (width - title_len) / 2; + + mvhline(y, x, ACS_HLINE, title_start_x - x); + mvprintw(y, title_start_x, "%s", title_buf); + mvhline(y, title_start_x + title_len, ACS_HLINE, (x + width) - (title_start_x + title_len)); + } else { + mvhline(y, x, ACS_HLINE, width); + } + + mvhline(y + height - 1, x, ACS_HLINE, width); + mvvline(y, x, ACS_VLINE, height); + mvvline(y, x + width - 1, ACS_VLINE, height); + + mvaddch(y, x, ACS_ULCORNER); + mvaddch(y, x + width - 1, ACS_URCORNER); + mvaddch(y + height - 1, x, ACS_LLCORNER); + mvaddch(y + height - 1, x + width - 1, ACS_LRCORNER); +} + +static TuiFrame* g_frame = nullptr; + +TuiFrame::TuiFrame() { + InitPanels(); + g_frame = this; + InitDeviceEventListeners(); +} + +TuiFrame::~TuiFrame() { + if (g_frame == this) { + g_frame = nullptr; + } +} + +void TuiFrame::InitPanels() { + m_panels.resize(PANEL_COUNT); + m_panels[PANEL_MAIN] = std::make_unique(this); + m_panels[PANEL_OPTIONS] = std::make_unique(this); + m_panels[PANEL_START_1] = std::make_unique(this); + m_panels[PANEL_START_2] = std::make_unique(this); + m_panels[PANEL_FINAL] = std::make_unique(this); +} + +void TuiFrame::InitDeviceEventListeners() { + register_device_state_callback([](const DeviceState& state) { + update_tui_device_state(&state); + }); + ensure_device_event_system_started(); +} + +void TuiFrame::ActivatePanelIfNeeded(PanelState previous_panel) { + if (m_currentPanel == previous_panel) { + return; + } + + if (m_panels[m_currentPanel]) { + m_panels[m_currentPanel]->on_show(); + } +} + +void TuiFrame::DrawUi() { + int rows, cols; + getmaxyx(stdscr, rows, cols); + + erase(); + + if (rows < HEIGHT || cols < WIDTH) { + mvprintw(rows / 2, (cols - 34) / 2, "Terminal too small, need 80x24"); + refresh(); + return; + } + + int start_y = (rows - HEIGHT) / 2; + int start_x = (cols - WIDTH) / 2; + + + const int left_x = start_x; + const int right_x = start_x + LEFT_PANEL_WIDTH; + + draw_panel_border(start_y, left_x, LEFT_PANEL_WIDTH, HEIGHT, "palera1n - Version beta " PALERAIN_VERSION); + draw_panel_border(start_y, right_x, RIGHT_PANEL_WIDTH, HEIGHT); + + if (m_panels[m_currentPanel]) { + m_panels[m_currentPanel]->draw(start_y, left_x, m_selected); + } + + if (m_panels[m_currentPanel]) { + int btn_cnt = m_panels[m_currentPanel]->get_button_count(); + int total_items = m_panels[m_currentPanel]->get_total_items(); + const char **panel_buttons = m_panels[m_currentPanel]->get_buttons(); + + const int sidebar_x = right_x; + const int sidebar_inner_x = sidebar_x + 1; + const int sidebar_inner_width = RIGHT_INNER_WIDTH; + const int sidebar_top = start_y + 1; + const int sidebar_bottom = start_y + HEIGHT - 2; + const int content_top = sidebar_top + SIDEBAR_WALL_GAP; + const int content_bottom = sidebar_bottom; + const int content_x = sidebar_inner_x + SIDEBAR_WALL_GAP; + const int content_width = std::max(1, sidebar_inner_width - (SIDEBAR_WALL_GAP * 2)); + + draw_tui_sidebar_branding(sidebar_top, sidebar_inner_x, sidebar_inner_width); + + std::vector labels; + labels.reserve(static_cast(btn_cnt)); + const int button_width = std::clamp(SIDEBAR_BUTTON_WIDTH, 8, content_width); + + for (int i = 0; i < btn_cnt; ++i) { + labels.push_back(format_sidebar_button_label(panel_buttons[i], button_width)); + } + + const int buttons_block_height = std::max(1, btn_cnt + ((btn_cnt - 1) * MAIN_BUTTON_VERTICAL_GAP)); + const int first_y = std::max(content_top, content_bottom - buttons_block_height + 1); + const int button_x = content_x + std::max(0, (content_width - button_width) / 2); + + for (int i = 0; i < btn_cnt; ++i) { + if (i > 0) { + const int separator_y = first_y + (i * (MAIN_BUTTON_VERTICAL_GAP + 1)) - 1; + mvhline(separator_y, sidebar_inner_x, ACS_HLINE, sidebar_inner_width); + } + + int btn_idx = total_items - btn_cnt + i; + bool enabled = m_panels[m_currentPanel]->is_button_enabled(btn_idx); + const std::string& label = labels[static_cast(i)]; + + if (!enabled) { + attron(A_DIM); + mvprintw(first_y + (i * (MAIN_BUTTON_VERTICAL_GAP + 1)), button_x, "%s", label.c_str()); + attroff(A_DIM); + } else { + if (btn_idx == m_selected) attron(A_REVERSE); + mvprintw(first_y + (i * (MAIN_BUTTON_VERTICAL_GAP + 1)), button_x, "%s", label.c_str()); + if (btn_idx == m_selected) attroff(A_REVERSE); + } + } + } + + refresh(); +} + +void TuiFrame::HandleInput(int ch, int start_y, int start_x) { + if (ch == ERR || !m_panels[m_currentPanel]) return; + + int total_items = m_panels[m_currentPanel]->get_total_items(); + if (total_items == 0) return; + + PanelState previous_panel = m_currentPanel; + + switch(ch) { + case KEY_UP: + case KEY_LEFT: { + int prev = m_selected; + do { + prev = (prev - 1 + total_items) % total_items; + } while (!m_panels[m_currentPanel]->is_button_enabled(prev) && prev != m_selected); + m_selected = prev; + break; + } + + case KEY_DOWN: + case KEY_RIGHT: { + int next = m_selected; + do { + next = (next + 1) % total_items; + } while (!m_panels[m_currentPanel]->is_button_enabled(next) && next != m_selected); + m_selected = next; + break; + } + + case KEY_ENTER: + case 10: + case 13: + if (!m_panels[m_currentPanel]->is_button_enabled(m_selected)) return; + m_panels[m_currentPanel]->handle_enter(m_selected, start_y, start_x); + break; + } + + ActivatePanelIfNeeded(previous_panel); +} + +void TuiFrame::Run() { + m_running = true; + std::setlocale(LC_ALL, ""); + initscr(); + noecho(); + cbreak(); + curs_set(0); + keypad(stdscr, TRUE); + timeout(100); + + if (has_colors()) { + start_color(); + use_default_colors(); + init_pair(COLOR_PAIR_SUBTITLE, COLOR_YELLOW, -1); + } + + int ch; + + while (m_running) { + PanelState previous_panel = m_currentPanel; + + if (m_panels[m_currentPanel]) { + m_panels[m_currentPanel]->update(); + } + + ActivatePanelIfNeeded(previous_panel); + + DrawUi(); + ch = getch(); + + if (ch == 'q' || ch == 'Q') { + Stop(); + continue; + } + + int rows, cols; + getmaxyx(stdscr, rows, cols); + if (rows >= HEIGHT && cols >= WIDTH) { + int start_y = (rows - HEIGHT) / 2; + int start_x = (cols - WIDTH) / 2; + HandleInput(ch, start_y, start_x); + } + } + + endwin(); +} + +void TuiFrame::ShowMain(int selected) { + m_currentPanel = PANEL_MAIN; + m_selected = selected; +} + +void TuiFrame::ShowSettings(int selected) { + m_currentPanel = PANEL_OPTIONS; + m_selected = selected; +} + +void TuiFrame::ShowRecovery(int selected) { + m_currentPanel = PANEL_START_1; + m_selected = selected; +} + +void TuiFrame::ShowDfu(int selected) { + m_currentPanel = PANEL_START_2; + m_selected = selected; +} + +void TuiFrame::ShowExploit(int selected) { + m_currentPanel = PANEL_FINAL; + m_selected = selected; +} + +PanelState TuiFrame::GetCurrentPanel() const { + return m_currentPanel; +} + +int TuiFrame::GetSelected() const { + return m_selected; +} + +void TuiFrame::SetSelected(int selected) { + m_selected = selected; +} + +bool TuiFrame::IsRunning() const { + return m_running; +} + +void TuiFrame::Stop() { + m_running = false; +} + +DeviceState TuiFrame::GetDeviceState() const { + std::lock_guard lock(m_deviceMutex); + return m_device; +} + +void TuiFrame::UpdateDeviceState(const DeviceState& state) { + { + std::lock_guard lock(m_deviceMutex); + m_device = state; + } + + const PanelState previous_panel = m_currentPanel; + if (m_currentPanel != PANEL_MAIN && m_currentPanel != PANEL_OPTIONS && m_panels[m_currentPanel]) { + m_panels[m_currentPanel]->handle_device_update(state); + ActivatePanelIfNeeded(previous_panel); + } +} + +TuiMainPanel* TuiFrame::GetMainPanel() const { + return static_cast(m_panels[PANEL_MAIN].get()); +} + +TuiSettingsPanel* TuiFrame::GetSettingsPanel() const { + return static_cast(m_panels[PANEL_OPTIONS].get()); +} + +TuiRecoveryPanel* TuiFrame::GetRecoveryPanel() const { + return static_cast(m_panels[PANEL_START_1].get()); +} + +TuiDfuPanel* TuiFrame::GetDfuPanel() const { + return static_cast(m_panels[PANEL_START_2].get()); +} + +TuiExploitPanel* TuiFrame::GetExploitPanel() const { + return static_cast(m_panels[PANEL_FINAL].get()); +} + +void update_tui_device_state(const DeviceState* new_state) { + if (!g_frame || !new_state) { + return; + } + + g_frame->UpdateDeviceState(*new_state); +} + +void ui_run(void) { + TuiFrame frame; + frame.Run(); +} + +#endif // WITH_TUI diff --git a/src/tui/Tui.hpp b/src/tui/Tui.hpp new file mode 100644 index 0000000..acbbd93 --- /dev/null +++ b/src/tui/Tui.hpp @@ -0,0 +1,71 @@ +#ifdef WITH_TUI + +#ifndef TUI_HPP +#define TUI_HPP + +#include +#include +#include + +#include "../event.hpp" +#include "TuiPanel.hpp" + +class TuiPanel; +class TuiMainPanel; +class TuiSettingsPanel; +class TuiRecoveryPanel; +class TuiDfuPanel; +class TuiExploitPanel; + +class TuiFrame { +public: + TuiFrame(); + ~TuiFrame(); + + void Run(); + + void ShowMain(int selected = 1); + void ShowSettings(int selected = 5); + void ShowRecovery(int selected = 1); + void ShowDfu(int selected = 1); + void ShowExploit(int selected = 0); + + PanelState GetCurrentPanel() const; + int GetSelected() const; + void SetSelected(int selected); + + bool IsRunning() const; + void Stop(); + + DeviceState GetDeviceState() const; + void UpdateDeviceState(const DeviceState& state); + + TuiMainPanel* GetMainPanel() const; + TuiSettingsPanel* GetSettingsPanel() const; + TuiRecoveryPanel* GetRecoveryPanel() const; + TuiDfuPanel* GetDfuPanel() const; + TuiExploitPanel* GetExploitPanel() const; + +private: + void InitPanels(); + void InitDeviceEventListeners(); + void ActivatePanelIfNeeded(PanelState previous_panel); + void DrawUi(); + void HandleInput(int ch, int start_y, int start_x); + + PanelState m_currentPanel = PANEL_MAIN; + int m_selected = 1; + bool m_running = true; + + DeviceState m_device; + mutable std::mutex m_deviceMutex; + + std::vector> m_panels; +}; + +void ui_run(void); +void update_tui_device_state(const DeviceState* new_state); + +#endif // TUI_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiBranding.cpp b/src/tui/TuiBranding.cpp new file mode 100644 index 0000000..261020f --- /dev/null +++ b/src/tui/TuiBranding.cpp @@ -0,0 +1,52 @@ +#ifdef WITH_TUI + +#include "TuiBranding.hpp" + +#include +#include + +#include + +static constexpr int kBrandingLeftTrim = 3; + +void draw_tui_sidebar_branding(int y, int x, int width) { + static const char* art[] = { + " ++ ", + " #@@# ", + " +@@@@+ ", + " :@@@@@@: ", + " #@@@@@@# ", + " +@@@@@@@@+ ", + " :#*@@@@@@*#: ", + " %##palera1n% ", + " .####%@@%####. ", + " @############@ ", + "=@@*########*@@=", + "+@@@@%####%@@@@+", + ":@@@@%%**%%@@@@:", + " +@@%%*@@*%%@@+ ", + " ++%@@@@%++ ", + " :++: " + }; + static const int art_count = static_cast(sizeof(art) / sizeof(art[0])); + + int max_art_width = 0; + for (int i = 0; i < art_count; ++i) { + const int line_width = std::max(0, static_cast(std::strlen(art[i])) - kBrandingLeftTrim); + max_art_width = std::max(max_art_width, line_width); + } + + const int centered_x = x + std::max(0, (width - max_art_width) / 2); + const int art_x = std::max(x, centered_x - 7); + + attron(A_DIM); + for (int i = 0; i < art_count; ++i) { + const char* line = art[i]; + const int line_len = static_cast(std::strlen(line)); + const int trim = std::min(kBrandingLeftTrim, line_len); + mvprintw(y + i, art_x, "%.*s", width, line + trim); + } + attroff(A_DIM); +} + +#endif // WITH_TUI diff --git a/src/tui/TuiBranding.hpp b/src/tui/TuiBranding.hpp new file mode 100644 index 0000000..c7794e7 --- /dev/null +++ b/src/tui/TuiBranding.hpp @@ -0,0 +1,10 @@ +#ifdef WITH_TUI + +#ifndef TUI_BRANDING_HPP +#define TUI_BRANDING_HPP + +void draw_tui_sidebar_branding(int y, int x, int width); + +#endif // TUI_BRANDING_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiDfuArt.cpp b/src/tui/TuiDfuArt.cpp new file mode 100644 index 0000000..ae667f7 --- /dev/null +++ b/src/tui/TuiDfuArt.cpp @@ -0,0 +1,139 @@ +#ifdef WITH_TUI + +#include "TuiDfuArt.hpp" + +#include + +static const char* kIpadArt[] = { + " __ ", + " ┌───────────────────┐ ", + " │ o === │ ", + " │███████████████████│ ", + " |│███████████████████│ ", + " |│███████████████████│ ", + " │███████████████████│ ", + " │███████████████████│ ", + " │███████████████████│ ", + " │███████████████████│ ", + " │███████████████████│ ", + " │███████████████████│ ", + " │███████████████████│ ", + " │ ┌─┐ │ ", + " │ └─┘ │ ", + " └───────────────────┘ ", +}; + +static const char* kIphoneSeArt[] = { + " __ ", + " ┌─────────────┐ ", + " │ o === │ ", + " │█████████████│ ", + " |│█████████████│ ", + " |│█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │ ┌─┐ │ ", + " │ └─┘ │ ", + " └─────────────┘ ", +}; + +static const char* kIphone7Art[] = { + " ", + " ┌─────────────┐ ", + " │ o === │ ", + " │█████████████│ ", + " |│█████████████│| ", + " |│█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │ ┌─┐ │ ", + " │ └─┘ │ ", + " └─────────────┘ ", +}; + +static const char* kIphoneXArt[] = { + " ", + " ┌─────────────┐ ", + " │███ o === ███│ ", + " │█████████████│ ", + " |│█████████████││ ", + " |│█████████████││ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " │█████████████│ ", + " └─────────────┘ ", +}; + +static const char* kRemoteArt[] = { + " ", + " ┌─────────┐ ", + " │ │ ", + " │ │ ", + " │ │ ", + " │ ┌─┐ ┌─┐ │ ", + " │ └─┘ └─┘ │ ", + " │ ┌─┐ ┌─┐ │ ", + " │ └─┘ │+│ │ ", + " │ ┌─┐ │-│ │ ", + " │ └─┘ └─┘ │ ", + " │ │ ", + " │ │ ", + " │ │ ", + " └─────────┘ ", +}; + +static const char* kAtvStdBrdArt[] = { + " art not yet implemented ", +}; + +static const DfuAsciiPreview kPreviews[] = { + { "ipad", kIpadArt, 16, 20, -1 }, + { "ipadmini", kIpadArt, 16, 19, -1 }, + { "ipodtouch", kIphoneSeArt, 16, 22, -1 }, + { "ipodtouch7", kIpadArt, 16, 20, -1 }, + { "siriremote", kRemoteArt, 15, 22, -1 }, + { "atv_std_brd", kAtvStdBrdArt, 1, 20, -1 }, + { "iphonese", kIphoneSeArt, 16, 22, -1 }, + { "iphone6s", kIphone7Art, 16, 22, -1 }, + { "iphone7", kIphone7Art, 16, 22, -1 }, + { "iphone8", kIphone7Art, 16, 22, -1 }, + { "iphonex", kIphoneXArt, 16, 22, -1 }, +}; + +const DfuAsciiPreview* find_dfu_ascii_preview(const std::string& imageName) { + for (const auto& preview : kPreviews) { + if (imageName == preview.imageName) { + return &preview; + } + } + return nullptr; +} + +void draw_dfu_ascii_preview(const DfuAsciiPreview& preview, int sx, int sy) { + const int start_x = sx + preview.xOffset; + const int start_y = sy + 4 + preview.yOffset; + + attron(A_DIM); + for (int i = 0; i < preview.lineCount; ++i) { + mvprintw(start_y + i, start_x, "%s", preview.lines[i]); + } + attroff(A_DIM); +} + +#endif // WITH_TUI diff --git a/src/tui/TuiDfuArt.hpp b/src/tui/TuiDfuArt.hpp new file mode 100644 index 0000000..611aed5 --- /dev/null +++ b/src/tui/TuiDfuArt.hpp @@ -0,0 +1,21 @@ +#ifdef WITH_TUI + +#ifndef TUI_DFU_ART_HPP +#define TUI_DFU_ART_HPP + +#include + +struct DfuAsciiPreview { + const char* imageName; + const char* const* lines; + int lineCount; + int xOffset; + int yOffset; +}; + +const DfuAsciiPreview* find_dfu_ascii_preview(const std::string& imageName); +void draw_dfu_ascii_preview(const DfuAsciiPreview& preview, int sx, int sy); + +#endif // TUI_DFU_ART_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiDfuPanel.cpp b/src/tui/TuiDfuPanel.cpp new file mode 100644 index 0000000..1e4fdaf --- /dev/null +++ b/src/tui/TuiDfuPanel.cpp @@ -0,0 +1,359 @@ +#ifdef WITH_TUI + +#include "TuiDfuPanel.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "Tui.hpp" +#include "TuiDfuArt.hpp" +#include "TuiText.hpp" +#include "TuiRecoveryPanel.hpp" +#include "../sequence.hpp" +#include "../event.hpp" +#include "../globals.h" +#include "../paleinfo.h" + +static const char *buttons[] = { "Back", "Start" }; +static constexpr int kDfuPanelCoordinateWidth = 310; +static constexpr int kTuiDeviceCanvasWidth = 52; +static constexpr int kDfuButtonCoordinateMaxY = 200; +static constexpr int kDfuContentX = 2; +static constexpr int kDfuContentWidth = 60; +static constexpr int kDfuStepsMaxLines = 5; +static constexpr int kDfuStepsBottomY = 22; +static constexpr int kDfuButtonX = 2; +static constexpr int kDfuButtonY = 4; + +TuiDfuPanel::TuiDfuPanel(TuiFrame* frame) + : TuiPanel(frame) {} + +const char** TuiDfuPanel::get_buttons() const { + return buttons; +} + +void TuiDfuPanel::reset_sequence_state() { + m_index = 0; + m_stepRemaining = -1; + m_actionExecutedIndex = -1; + m_isEnteringDfu = false; + m_dfuSuccess = false; + m_waitingForDfuTransition = false; + m_dfuSuccessAt = std::chrono::steady_clock::time_point{}; + m_last_tick = std::chrono::steady_clock::time_point{}; +} + +void TuiDfuPanel::load_sequence_for_product(const std::string& product_type, bool reset_state) { + if (m_current_product_type != product_type) { + m_current_product_type = product_type; + m_sequence = ParseSequence(product_type); + } + + if (reset_state) { + reset_sequence_state(); + } +} + +void TuiDfuPanel::on_show() { + const DeviceState state = GetFrame()->GetDeviceState(); + load_sequence_for_product(state.productType, true); + GetFrame()->SetSelected(1); + + if (palerain_flags & palerain_option_quick) { + start_sequence(); + } +} + +void TuiDfuPanel::update() { + if (!m_waitingForDfuTransition || !m_dfuSuccess) { + return; + } + + const auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - m_dfuSuccessAt).count() < 3) { + return; + } + + m_waitingForDfuTransition = false; + GetFrame()->ShowExploit(0); +} + +bool TuiDfuPanel::is_button_enabled(int btn_idx) const { + (void)btn_idx; + return !m_isEnteringDfu && !m_dfuSuccess; +} + +void TuiDfuPanel::start_sequence() { + if (m_sequence.steps.empty()) { + return; + } + + m_isEnteringDfu = true; + m_dfuSuccess = false; + m_index = 0; + m_stepRemaining = -1; + m_actionExecutedIndex = -1; + m_last_tick = std::chrono::steady_clock::now(); +} + +void TuiDfuPanel::update_sequence_timer() { + if (!m_isEnteringDfu || m_sequence.steps.empty() || m_index >= m_sequence.steps.size()) { + return; + } + + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - m_last_tick).count() >= 1) { + m_last_tick = now; + + const auto& step = m_sequence.steps[m_index]; + if (m_stepRemaining < 0) { + m_stepRemaining = step.duration; + } + + if (m_stepRemaining > 0) { + --m_stepRemaining; + } + + if (m_stepRemaining == 0) { + ++m_index; + m_stepRemaining = -1; + } + + if (m_index < m_sequence.steps.size()) { + const auto& activeStep = m_sequence.steps[m_index]; + if (!activeStep.action.empty() && m_actionExecutedIndex != (int)m_index) { + m_actionExecutedIndex = m_index; + if (activeStep.action == "reboot") { + reboot(); + } + } + } + } +} + +int TuiDfuPanel::scale_coord(int value, int input_span, int output_span) const { + if (input_span <= 0 || output_span <= 1) { + return 0; + } + + return (value * (output_span - 1)) / input_span; +} + +void TuiDfuPanel::draw_sequence_buttons(int button_x, int button_y, int button_width, int button_height) const { + if (m_sequence.buttons.empty()) { + return; + } + + std::unordered_set active_buttons; + if (m_isEnteringDfu && m_index < m_sequence.steps.size()) { + const auto& buttons_for_step = m_sequence.steps[m_index].activeButtons; + active_buttons.insert(buttons_for_step.begin(), buttons_for_step.end()); + } + + for (const auto& btn : m_sequence.buttons) { + const int mapped_x = button_x + scale_coord(btn.x, kDfuPanelCoordinateWidth, button_width); + const int mapped_y = button_y + scale_coord(btn.y, kDfuButtonCoordinateMaxY, button_height); + + const int label_x = std::clamp(mapped_x, button_x, button_x + std::max(0, button_width - 1)); + + const bool is_active = active_buttons.find(btn.id) != active_buttons.end(); + if (is_active) { + attron(A_BOLD); + } else { + attron(A_DIM); + } + + mvprintw(mapped_y, label_x, "%s", btn.name.c_str()); + + if (is_active) { + attroff(A_BOLD); + } else { + attroff(A_DIM); + } + } +} + +void TuiDfuPanel::draw_wrapped_steps(int steps_x, int steps_width, int max_lines, int bottom_y) const { + if (steps_width <= 0 || max_lines <= 0 || m_sequence.steps.empty()) { + return; + } + + struct RenderedLine { + bool is_current; + bool is_completed; + std::string text; + }; + + std::vector rendered_lines; + rendered_lines.reserve(static_cast(max_lines)); + + for (size_t i = 0; i < m_sequence.steps.size(); ++i) { + const auto& step = m_sequence.steps[i]; + const int seconds_left = (m_isEnteringDfu && i == m_index && m_stepRemaining >= 0) + ? m_stepRemaining + : ((i < m_index) || m_dfuSuccess) ? 0 : step.duration; + + std::string text = std::to_string(i + 1) + ". " + step.description + " (" + std::to_string(seconds_left) + ")"; + std::vector wrapped; + tui_text::append_wrapped_lines(text, steps_width, wrapped); + + const bool is_current = m_isEnteringDfu && i == m_index; + const bool is_completed = (i < m_index) || m_dfuSuccess; + for (const auto& line : wrapped) { + if (static_cast(rendered_lines.size()) >= max_lines) { + break; + } + rendered_lines.push_back({ is_current, is_completed, line }); + } + + if (static_cast(rendered_lines.size()) >= max_lines) { + break; + } + } + + const int start_y = bottom_y - static_cast(rendered_lines.size()) + 1; + for (size_t i = 0; i < rendered_lines.size(); ++i) { + const auto& line = rendered_lines[i]; + + if (line.is_current) { + attron(A_BOLD); + } else if (line.is_completed) { + attron(A_DIM); + } + + mvprintw(start_y + static_cast(i), steps_x, "%.*s", steps_width, line.text.c_str()); + + if (line.is_current) { + attroff(A_BOLD); + } else if (line.is_completed) { + attroff(A_DIM); + } + } +} + +void TuiDfuPanel::draw(int sy, int sx, int selected) { + (void)selected; + update_sequence_timer(); + + const int content_x = sx + kDfuContentX; + + if (m_sequence.steps.empty()) { + mvprintw(sy + 2, content_x, "No DFU helper sequence is available for this device."); + } else if (m_dfuSuccess) { + mvprintw(sy + 2, content_x, "Device entered DFU mode successfully."); + } else { + tui_text::draw_wrapped_text( + sy + 2, + content_x, + kDfuContentWidth, + "Time to put the device into DFU mode.", + 2 + ); + } + + const int steps_x = content_x; + const int steps_width = kDfuContentWidth; + const int steps_max_lines = kDfuStepsMaxLines; + const int steps_bottom_y = sy + kDfuStepsBottomY; + + const int button_x = sx + kDfuButtonX; + const int button_y = sy + kDfuButtonY; + const int button_width = kTuiDeviceCanvasWidth - 1; + const int button_height = (steps_bottom_y - steps_max_lines + 1) - button_y; + + if (const DfuAsciiPreview* preview = find_dfu_ascii_preview(m_sequence.imageName)) { + draw_dfu_ascii_preview(*preview, sx, sy); + } + + draw_sequence_buttons(button_x, button_y, button_width, button_height); + draw_wrapped_steps(steps_x, steps_width, steps_max_lines, steps_bottom_y); +} + +void TuiDfuPanel::handle_enter(int selected, int sy, int sx) { + switch(selected) { + case 0: { // Back + reboot(); + m_isEnteringDfu = false; + m_dfuSuccess = false; + GetFrame()->ShowMain(0); + break; + } + case 1: { // Start + start_sequence(); + break; + } + } +} + +void TuiDfuPanel::handle_device_update(const DeviceState& state) { + if (state.connected && !state.productType.empty()) { + load_sequence_for_product(state.productType, false); + } + + if (!state.connected) { + if (!m_isEnteringDfu) { + reset_sequence_state(); + GetFrame()->ShowMain(1); + } + return; + } + + if (m_isEnteringDfu && state.mode == DeviceMode::DFU) { + m_isEnteringDfu = false; + m_dfuSuccess = true; + m_waitingForDfuTransition = true; + m_dfuSuccessAt = std::chrono::steady_clock::now(); + m_index = m_sequence.steps.size(); + m_stepRemaining = -1; + + GetFrame()->SetSelected(1); + return; + } + + if (m_isEnteringDfu && state.mode == DeviceMode::Normal) { + reset_sequence_state(); + if (GetFrame()->GetRecoveryPanel()) { + GetFrame()->GetRecoveryPanel()->SetStatusText("Hmm... It seems like the device didn't enter recovery mode."); + } + GetFrame()->ShowRecovery(0); + } + + if (m_isEnteringDfu && state.mode == DeviceMode::Recovery) { + reset_sequence_state(); + GetFrame()->ShowMain(0); + } + +} + +void TuiDfuPanel::reboot() { + std::thread([this]() { + const auto state = GetFrame()->GetDeviceState(); + + if (!state.connected || state.mode != DeviceMode::Recovery) + return; + + irecv_client_t client = nullptr; + int attempts = 0; + const int max_attempts = 8; + + while (attempts < max_attempts) { + if (irecv_open_with_ecid(&client, state.ecid) == IRECV_E_SUCCESS) + break; + attempts++; + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + + if (!client) return; + irecv_setenv(client, "auto-boot", "true"); + irecv_saveenv(client); + irecv_reboot(client); + irecv_close(client); + }).detach(); +} + +#endif // WITH_TUI diff --git a/src/tui/TuiDfuPanel.hpp b/src/tui/TuiDfuPanel.hpp new file mode 100644 index 0000000..df858e4 --- /dev/null +++ b/src/tui/TuiDfuPanel.hpp @@ -0,0 +1,51 @@ +#ifdef WITH_TUI + +#ifndef TUI_DFU_PANEL_HPP +#define TUI_DFU_PANEL_HPP + +#include "TuiPanel.hpp" +#include "../sequence.hpp" +#include +#include + +class TuiDfuPanel : public TuiPanel { +public: + explicit TuiDfuPanel(TuiFrame* frame); + ~TuiDfuPanel() override = default; + + void on_show() override; + void update() override; + void draw(int sy, int sx, int selected) override; + void handle_enter(int selected, int sy, int sx) override; + void handle_device_update(const DeviceState& state) override; + bool is_button_enabled(int btn_idx) const override; + + int get_total_items() const override { return 2; } + int get_button_count() const override { return 2; } + const char** get_buttons() const override; + +private: + void load_sequence_for_product(const std::string& product_type, bool reset_state); + void reset_sequence_state(); + void start_sequence(); + void reboot(); + void update_sequence_timer(); + int scale_coord(int value, int input_span, int output_span) const; + void draw_sequence_buttons(int button_x, int button_y, int button_width, int button_height) const; + void draw_wrapped_steps(int steps_x, int steps_width, int max_lines, int bottom_y) const; + + DfuSequence m_sequence; + std::string m_current_product_type; + size_t m_index = 0; + int m_stepRemaining = -1; + int m_actionExecutedIndex = -1; + bool m_isEnteringDfu = false; + bool m_dfuSuccess = false; + bool m_waitingForDfuTransition = false; + std::chrono::steady_clock::time_point m_dfuSuccessAt; + std::chrono::steady_clock::time_point m_last_tick; +}; + +#endif // TUI_DFU_PANEL_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiExploitPanel.cpp b/src/tui/TuiExploitPanel.cpp new file mode 100644 index 0000000..0d50d4b --- /dev/null +++ b/src/tui/TuiExploitPanel.cpp @@ -0,0 +1,134 @@ +#ifdef WITH_TUI + +#include "TuiExploitPanel.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "Tui.hpp" +#include "TuiText.hpp" +#include "../m8/run.h" + +static const char *buttons[] = { "Back" }; +static constexpr int kExploitContentX = 2; +static constexpr int kExploitContentWidth = 60; + +TuiExploitPanel::TuiExploitPanel(TuiFrame* frame) + : TuiPanel(frame) {} + +const char** TuiExploitPanel::get_buttons() const { + return buttons; +} + +void TuiExploitPanel::draw(int sy, int sx, int selected) { + (void)selected; + const int content_x = sx + kExploitContentX; + tui_text::draw_wrapped_text( + sy + 2, + content_x, + kExploitContentWidth, + "Installing jailbreak, this will take a moment. If the device asks for a passcode please enter it. Do not disconnect the device until finished.", + 3 + ); + + std::string status_text; + int progress = 0; + { + std::lock_guard lock(m_status_mutex); + status_text = m_status_text; + progress = m_progress; + } + + mvprintw(sy + 8, content_x, "%.*s", kExploitContentWidth, status_text.empty() ? stage_to_string(STAGE_RESET) : status_text.c_str()); + + const int bar_width = 50; + const int filled = (std::clamp(progress, 0, 100) * bar_width) / 100; + std::string bar(static_cast(bar_width), '-'); + for (int i = 0; i < filled; ++i) { + bar[static_cast(i)] = '#'; + } + + mvprintw(sy + 10, sx + 2, "[%s] %3d%%", bar.c_str(), std::clamp(progress, 0, 100)); +} + +void TuiExploitPanel::handle_enter(int selected, int sy, int sx) { + if (selected == 0) { + GetFrame()->ShowMain(1); + } +} + +void TuiExploitPanel::handle_device_update(const DeviceState& state) { + (void)state; +} + +void TuiExploitPanel::on_show() { + exploit(); +} + +bool TuiExploitPanel::is_button_enabled(int btn_idx) const { + (void)btn_idx; + return !m_is_exploiting; +} + +void TuiExploitPanel::set_status(const std::string& status, int progress) { + std::lock_guard lock(m_status_mutex); + m_status_text = status; + m_progress = std::clamp(progress, 0, 100); +} + +void TuiExploitPanel::exploit() { + if (m_is_exploiting) { + return; + } + + m_is_exploiting = true; + set_status(stage_to_string(STAGE_RESET), 0); + + std::thread([this]() { + const DeviceState deviceState = GetFrame()->GetDeviceState(); + if (!deviceState.connected || deviceState.mode != DeviceMode::DFU) { + set_status("Device is not in DFU mode.", 0); + m_is_exploiting = false; + return; + } + + auto sharedState = std::make_shared(); + atomic_init(&sharedState->stage, STAGE_RESET); + atomic_init(&sharedState->stop, false); + atomic_init(&sharedState->result, 0); + + auto isFinished = std::make_shared>(false); + + std::thread worker([sharedState, isFinished]() { + ::exploit(sharedState.get()); + isFinished->store(true); + }); + + for (;;) { + int stage = atomic_load(&sharedState->stage); + int progress = stage_to_progress(stage); + bool finished = isFinished->load(); + set_status(stage_to_string(stage), progress); + + if (stage == STAGE_DONE || finished) { + atomic_store(&sharedState->stop, true); + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + if (worker.joinable()) { + worker.join(); + } + + m_is_exploiting = false; + }).detach(); +} + +#endif // WITH_TUI diff --git a/src/tui/TuiExploitPanel.hpp b/src/tui/TuiExploitPanel.hpp new file mode 100644 index 0000000..c33d056 --- /dev/null +++ b/src/tui/TuiExploitPanel.hpp @@ -0,0 +1,38 @@ +#ifdef WITH_TUI + +#ifndef TUI_EXPLOIT_PANEL_HPP +#define TUI_EXPLOIT_PANEL_HPP + +#include "TuiPanel.hpp" + +#include +#include +#include + +class TuiExploitPanel : public TuiPanel { +public: + explicit TuiExploitPanel(TuiFrame* frame); + ~TuiExploitPanel() override = default; + + void draw(int sy, int sx, int selected) override; + void handle_enter(int selected, int sy, int sx) override; + void handle_device_update(const DeviceState& state) override; + bool is_button_enabled(int btn_idx) const override; + void on_show() override; + + int get_total_items() const override { return 1; } + int get_button_count() const override { return 1; } + const char** get_buttons() const override; +private: + void exploit(); + void set_status(const std::string& status, int progress); + + std::mutex m_status_mutex; + std::string m_status_text; + int m_progress = 0; + std::atomic m_is_exploiting = false; +}; + +#endif // TUI_EXPLOIT_PANEL_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiMainPanel.cpp b/src/tui/TuiMainPanel.cpp new file mode 100644 index 0000000..6db6ada --- /dev/null +++ b/src/tui/TuiMainPanel.cpp @@ -0,0 +1,227 @@ +#ifdef WITH_TUI + +#include "TuiMainPanel.hpp" + +#include + +#include + +#include "Tui.hpp" +#include "TuiText.hpp" +#include "../event.hpp" +#include "../globals.h" +#include "../paleinfo.h" + +static const char *buttons[] = { "Options", "Start", "Quit" }; +static constexpr int kMainContentX = 2; +static constexpr int kMainContentWidth = 60; +static constexpr int kMainSeparatorX = 1; +static constexpr int kMainSeparatorWidth = 63; + +static std::string get_device_title(const DeviceState& state) { + if (!state.connected) { + if (state.connectedDeviceCount > 1) { + return "Multiple devices detected"; + } + return "No device connected"; + } + + const std::string product = state.productType.empty() ? "Unknown" : state.productType; + if (state.mode == DeviceMode::DFU) { + return "DFU Mode device"; + } + return product; +} + +static std::string get_device_subtitle(const DeviceState& state) { + if (!state.connected) { + if (state.connectedDeviceCount > 1) { + return std::to_string(state.connectedDeviceCount) + + "USB devices connected. Disconnect extras and keep only one device attached."; + } + return "Please connect a device to get started. Ensure version range is 15.0+"; + } + + const std::string version = state.productVersion.empty() ? "Unknown" : state.productVersion; + const std::string ecid = state.ecid != 0 ? ("ECID: " + std::to_string(state.ecid)) : "ECID: Unknown"; + + switch (state.mode) { + case DeviceMode::Normal: + if (state.isSupported) { + return "Connected in normal mode * iOS " + version + "\n" + ecid; + } + return "Not supported * iOS " + version + "\n" + ecid; + case DeviceMode::Recovery: + if (state.isSupported) { + return "Connected in recovery mode\n" + ecid; + } + return "Not supported\n" + ecid; + case DeviceMode::DFU: + return "Sorry, jailbreaking in DFU mode is not supported."; + case DeviceMode::None: + return "Please connect a device to get started. Ensure version range is 15.0+"; + } + + return ""; +} + +TuiMainPanel::TuiMainPanel(TuiFrame* frame) + : TuiPanel(frame) {} + +const char** TuiMainPanel::get_buttons() const { + return buttons; +} + +void TuiMainPanel::draw(int sy, int sx, int selected) { + const DeviceState state = GetFrame()->GetDeviceState(); + const int content_x = sx + kMainContentX; + int y = sy + 2; + const int bottom_y = sy + 22; + const int content_bottom = bottom_y - 1; + + (void)selected; + + const std::string title = get_device_title(state); + const std::string subtitle = get_device_subtitle(state); + + attron(A_BOLD); + mvprintw(y, content_x, "%.*s", kMainContentWidth, title.c_str()); + attroff(A_BOLD); + ++y; + + if (has_colors()) { + attron(COLOR_PAIR(1)); + } + y += tui_text::draw_wrapped_text(y, content_x, kMainContentWidth, subtitle, 2); + if (has_colors()) { + attroff(COLOR_PAIR(1)); + } + ++y; + + if (y <= content_bottom) { + mvhline(y, sx + kMainSeparatorX, ACS_HLINE, kMainSeparatorWidth); + y += 2; + } + + const std::string made_by = + "Made by: asdfugil, claration, kok3shidoll, mineek, plooshi, staturnz"; + const std::string thanks_to = + "Thanks to: itsnebulalol, llsc12, lrdsnow, dedbeddedbed, kirb, ehilwyma, " + "opa334, 0x7ff, alfiecg24, sneko, sbingner, nikias, " + "tihmstar, Checkra1n (Siguza, axi0mx, littlelailo et al.), Procursus " + "(Hayden Seay, Cameron Katri, Keto et al.)"; + + if (y <= content_bottom) { + y += tui_text::draw_wrapped_text(y, content_x, kMainContentWidth, made_by, 2); + } + + if (y <= content_bottom) { + ++y; + } + + if (y <= content_bottom) { + y += tui_text::draw_wrapped_text(y, content_x, kMainContentWidth, thanks_to, 4); + } + + if (y <= content_bottom) { + ++y; + } + + if (y <= content_bottom) { + mvhline(y, sx + kMainSeparatorX, ACS_HLINE, kMainSeparatorWidth); + y += 1; + } + + if (y <= content_bottom) { + mvprintw(y, content_x, "Twitter: @palera1n"); + ++y; + } + if (y <= content_bottom) { + mvprintw(y, content_x, "Website: https://palera.in"); + ++y; + } + + if (y <= content_bottom) { + attron(A_BOLD); + y += tui_text::draw_wrapped_text( + y, + content_x, + kMainContentWidth, + "NOTE: Please ensure you've made a backup of your device before proceeding.", + 2 + ); + attroff(A_BOLD); + } + + if (y <= content_bottom) { + mvhline(y, sx + kMainSeparatorX, ACS_HLINE, kMainSeparatorWidth); + y += 2; + } + + const bool quick_mode_enabled = (palerain_flags & palerain_option_quick) != 0; + const std::string quick_mode_prefix = std::string("[") + (quick_mode_enabled ? "x" : " ") + "] "; + const std::string quick_mode_label = "Enable Quick Mode"; + const int quick_mode_total_width = static_cast(quick_mode_prefix.size() + quick_mode_label.size()); + const int quick_mode_x = content_x + std::max(0, kMainContentWidth - quick_mode_total_width); + + attron(A_DIM); + mvprintw(bottom_y, content_x, "%.*s", kMainContentWidth, "Made with <3 by C (claration)"); + attroff(A_DIM); + + mvprintw(bottom_y, quick_mode_x, "%s", quick_mode_prefix.c_str()); + + if (selected == 0) { + attron(A_REVERSE); + } + mvprintw(bottom_y, quick_mode_x + static_cast(quick_mode_prefix.size()), "%s", quick_mode_label.c_str()); + if (selected == 0) { + attroff(A_REVERSE); + } +} + +void TuiMainPanel::handle_enter(int selected, int sy, int sx) { + switch(selected) { + case 0: + palerain_flags ^= palerain_option_quick; + break; + + case 1: + GetFrame()->ShowSettings(5); + break; + + case 2: { + DeviceState state = GetFrame()->GetDeviceState(); + bool can_start = (state.connected && state.isSupported && state.mode != DeviceMode::DFU); + + if (can_start) { + if (state.mode == DeviceMode::Recovery) { + GetFrame()->ShowDfu(1); + } else { + GetFrame()->ShowRecovery(1); + } + } + break; + } + + case 3: + GetFrame()->Stop(); + break; + } +} + +void TuiMainPanel::handle_device_update(const DeviceState& state) {} + +bool TuiMainPanel::is_button_enabled(int btn_idx) const { + DeviceState state = GetFrame()->GetDeviceState(); + + switch(btn_idx) { + case 0: return true; + case 1: return true; + case 2: return (state.connected && state.isSupported && state.mode != DeviceMode::DFU); + case 3: return true; + } + + return false; +} + +#endif // WITH_TUI diff --git a/src/tui/TuiMainPanel.hpp b/src/tui/TuiMainPanel.hpp new file mode 100644 index 0000000..a63971b --- /dev/null +++ b/src/tui/TuiMainPanel.hpp @@ -0,0 +1,25 @@ +#ifdef WITH_TUI + +#ifndef TUI_MAIN_PANEL_HPP +#define TUI_MAIN_PANEL_HPP + +#include "TuiPanel.hpp" + +class TuiMainPanel : public TuiPanel { +public: + explicit TuiMainPanel(TuiFrame* frame); + ~TuiMainPanel() override = default; + + void draw(int sy, int sx, int selected) override; + void handle_enter(int selected, int sy, int sx) override; + void handle_device_update(const DeviceState& state) override; + bool is_button_enabled(int btn_idx) const override; + + int get_total_items() const override { return 4; } + int get_button_count() const override { return 3; } + const char** get_buttons() const override; +}; + +#endif // TUI_MAIN_PANEL_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiPanel.hpp b/src/tui/TuiPanel.hpp new file mode 100644 index 0000000..2dd791a --- /dev/null +++ b/src/tui/TuiPanel.hpp @@ -0,0 +1,49 @@ +#ifdef WITH_TUI + +#ifndef TUI_PANEL_HPP +#define TUI_PANEL_HPP + +#include + +#include "../event.hpp" + +class TuiFrame; + +enum PanelState { + PANEL_MAIN = 0, + PANEL_OPTIONS, + PANEL_START_1, + PANEL_START_2, + PANEL_FINAL, + PANEL_COUNT +}; + +class TuiPanel { +public: + explicit TuiPanel(TuiFrame* frame) + : m_frame(frame) {} + + virtual ~TuiPanel() = default; + + virtual void on_show() {} + virtual void update() {} + + virtual void draw(int sy, int sx, int selected) = 0; + virtual void handle_enter(int selected, int sy, int sx) = 0; + virtual void handle_device_update(const DeviceState& state) = 0; + virtual bool is_button_enabled(int btn_idx) const { return true; } + + virtual int get_total_items() const = 0; + virtual int get_button_count() const = 0; + virtual const char** get_buttons() const = 0; +protected: + TuiFrame* GetFrame() const { + return m_frame; + } +private: + TuiFrame* m_frame = nullptr; +}; + +#endif // TUI_PANEL_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiRecoveryPanel.cpp b/src/tui/TuiRecoveryPanel.cpp new file mode 100644 index 0000000..b966110 --- /dev/null +++ b/src/tui/TuiRecoveryPanel.cpp @@ -0,0 +1,139 @@ +#ifdef WITH_TUI + +#include "TuiRecoveryPanel.hpp" + +#include +#include +#include +#include + +#include "Tui.hpp" +#include "TuiText.hpp" +#include "../globals.h" +#include "../paleinfo.h" + +#include +#include + +static const char *buttons[] = { "Back", "Next" }; +static constexpr int kRecoveryContentX = 2; +static constexpr int kRecoveryContentWidth = 60; + +TuiRecoveryPanel::TuiRecoveryPanel(TuiFrame* frame) + : TuiPanel(frame), m_status_text(""), m_is_entering_recovery(false), m_buttons_disabled(false) {} + +void TuiRecoveryPanel::SetStatusText(const std::string& text) { + m_status_text = text; + m_preserve_status_on_show = true; +} + +const char** TuiRecoveryPanel::get_buttons() const { + return buttons; +} + +bool TuiRecoveryPanel::is_button_enabled(int btn_idx) const { + return !m_buttons_disabled; +} + +void TuiRecoveryPanel::on_show() { + if (!m_preserve_status_on_show) { + m_status_text = ""; + } + m_preserve_status_on_show = false; + m_is_entering_recovery = false; + m_buttons_disabled = false; + GetFrame()->SetSelected(1); + + if (palerain_flags & palerain_option_quick) { + m_status_text = "Entering recovery mode..."; + m_buttons_disabled = true; + enter_recovery_mode(); + } +} + +void TuiRecoveryPanel::draw(int sy, int sx, int selected) { + const int content_x = sx + kRecoveryContentX; + const std::string intro = "The device needs to be put into DFU mode to apply the jailbreak. This is a manual process and we will guide you through it."; + tui_text::draw_wrapped_text(sy + 2, content_x, kRecoveryContentWidth, intro, 6); + + if (!m_status_text.empty()) { + tui_text::draw_wrapped_text(sy + 9, content_x, kRecoveryContentWidth, m_status_text, 3); + } +} + +void TuiRecoveryPanel::handle_enter(int selected, int sy, int sx) { + if (m_buttons_disabled) return; + + switch(selected) { + case 0: + GetFrame()->ShowMain(1); + break; + case 1: + m_status_text = "Entering recovery mode..."; + m_buttons_disabled = true; + enter_recovery_mode(); + break; + } +} + +void TuiRecoveryPanel::handle_device_update(const DeviceState& state) { + if (m_is_entering_recovery && state.mode == DeviceMode::Recovery) { + m_status_text = "Device is now in recovery mode."; + std::this_thread::sleep_for(std::chrono::seconds(3)); + m_status_text = ""; + m_is_entering_recovery = false; + m_buttons_disabled = false; + GetFrame()->ShowDfu(1); + return; + } + + if (m_is_entering_recovery && state.connected && state.mode == DeviceMode::Normal) { + m_is_entering_recovery = false; + m_buttons_disabled = false; + m_status_text = "Hmm... It seems like the device didn't enter recovery mode. Please try again."; + return; + } + + if (!m_is_entering_recovery && !state.connected) { + m_buttons_disabled = false; + GetFrame()->ShowMain(1); + return; + } +} + +void TuiRecoveryPanel::enter_recovery_mode() { + m_is_entering_recovery = true; + m_buttons_disabled = true; + + std::thread([this]() { + DeviceState state = GetFrame()->GetDeviceState(); + if (!state.connected) { + m_is_entering_recovery = false; + m_buttons_disabled = false; + return; + } + + idevice_t device = nullptr; + lockdownd_client_t client = nullptr; + + if (idevice_new(&device, state.udid.c_str()) != IDEVICE_E_SUCCESS) { + m_is_entering_recovery = false; + m_buttons_disabled = false; + return; + } + + if (lockdownd_client_new_with_handshake(device, &client, "palera1n") != LOCKDOWN_E_SUCCESS) { + idevice_free(device); + m_is_entering_recovery = false; + m_buttons_disabled = false; + return; + } + + lockdownd_enter_recovery(client); + + lockdownd_client_free(client); + idevice_free(device); + }).detach(); +} + +#endif // WITH_TUI diff --git a/src/tui/TuiRecoveryPanel.hpp b/src/tui/TuiRecoveryPanel.hpp new file mode 100644 index 0000000..dc8c668 --- /dev/null +++ b/src/tui/TuiRecoveryPanel.hpp @@ -0,0 +1,39 @@ +#ifdef WITH_TUI + +#ifndef TUI_RECOVERY_PANEL_HPP +#define TUI_RECOVERY_PANEL_HPP + +#include "TuiPanel.hpp" + +#include +#include + +class TuiRecoveryPanel : public TuiPanel { +public: + explicit TuiRecoveryPanel(TuiFrame* frame); + ~TuiRecoveryPanel() override = default; + + void SetStatusText(const std::string& text); + + void on_show() override; + void draw(int sy, int sx, int selected) override; + void handle_enter(int selected, int sy, int sx) override; + void handle_device_update(const DeviceState& state) override; + bool is_button_enabled(int btn_idx) const override; + + int get_total_items() const override { return 2; } + int get_button_count() const override { return 2; } + const char** get_buttons() const override; + +private: + void enter_recovery_mode(); + + std::string m_status_text; + bool m_preserve_status_on_show = false; + std::atomic m_is_entering_recovery; + bool m_buttons_disabled; +}; + +#endif // TUI_RECOVERY_PANEL_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiSettingsPanel.cpp b/src/tui/TuiSettingsPanel.cpp new file mode 100644 index 0000000..ccc0433 --- /dev/null +++ b/src/tui/TuiSettingsPanel.cpp @@ -0,0 +1,190 @@ +#ifdef WITH_TUI + +#include "TuiSettingsPanel.hpp" + +#include +#include +#include + +#include "Tui.hpp" +#include "TuiText.hpp" + +#include "../globals.h" +#include "../paleinfo.h" + +static const char *buttons[] = { "Back" }; +static constexpr int kSettingsContentX = 2; +static constexpr int kSettingsContentWidth = 60; +static constexpr int kSettingsBootArgsWidth = 56; + +TuiSettingsPanel::TuiSettingsPanel(TuiFrame* frame) + : TuiPanel(frame) {} + +const char** TuiSettingsPanel::get_buttons() const { + return buttons; +} + +void TuiSettingsPanel::draw(int sy, int sx, int selected) { + const int content_x = sx + kSettingsContentX; + const bool is_rootful = (palerain_flags & palerain_option_rootful) != 0; + const int setup_selection = (palerain_flags & palerain_option_setup_rootful) ? 1 + : (palerain_flags & palerain_option_setup_partial_root) ? 2 : 0; + + tui_text::draw_wrapped_text( + sy + 2, + content_x, + kSettingsContentWidth, + "You may set the following options. If you don't know what they mean you'll probably have no reason to set them.", + 2 + ); + + int states[] = { + !!(palerain_flags & palerain_option_safemode), + !!(palerain_flags & palerain_option_verbose_boot), + !!(palerain_flags & palerain_option_force_revert), + !!(palerain_flags & palerain_option_flower_chain) + }; + + const char *labels[] = { + "Safe Mode", + "Verbose Boot", + "Restore System", + "Dark Blockchain" + }; + + for (int i = 0; i < 2; i++) { + mvprintw(sy + 6 + i, content_x, "[%c] ", states[i] ? 'x' : ' '); + if (selected == i) attron(A_REVERSE); + printw("%s", labels[i]); + if (selected == i) attroff(A_REVERSE); + } + + mvprintw(sy + 8, sx + 6, "Boot Arguments:"); + if (selected == 2) attron(A_REVERSE); + mvprintw(sy + 9, sx + 6, "%-56s", boot_args); + if (selected == 2) attroff(A_REVERSE); + + for (int i = 2; i < 4; i++) { + mvprintw(sy + 8 + i, content_x, "[%c] ", states[i] ? 'x' : ' '); + if (selected == i + 1) attron(A_REVERSE); + printw("%s", labels[i]); + if (selected == i + 1) attroff(A_REVERSE); + } + + mvprintw(sy + 13, content_x, "Jailbreak Type:"); + if (selected == 5) attron(A_REVERSE); + mvprintw(sy + 14, content_x + 2, "(%c) Rootless", is_rootful ? ' ' : 'x'); + if (selected == 5) attroff(A_REVERSE); + + if (selected == 6) attron(A_REVERSE); + mvprintw(sy + 15, content_x + 2, "(%c) Rootful", is_rootful ? 'x' : ' '); + if (selected == 6) attroff(A_REVERSE); + + mvprintw(sy + 17, content_x, "Rootful Options:"); + + for (int i = 0; i < 3; ++i) { + const int item_idx = 7 + i; + const int row = sy + 18 + i; + const bool active = is_rootful; + const bool is_selected = selected == item_idx; + const bool checked = setup_selection == i; + + if (!active) attron(A_DIM); + if (is_selected) attron(A_REVERSE); + + const char* option_label = i == 0 ? "Boot" : (i == 1 ? "Create FakeFS" : "Create BindFS"); + mvprintw(row, content_x + 2, "(%c) %s", checked ? 'x' : ' ', option_label); + + if (is_selected) attroff(A_REVERSE); + if (!active) attroff(A_DIM); + } +} + +void TuiSettingsPanel::edit_boot_args(int start_y, int start_x) { + curs_set(1); + timeout(-1); + + int field_y = start_y + 9; + int field_x = start_x + 6; + int max_len = kSettingsBootArgsWidth; + + char temp_buf[128]; + std::snprintf(temp_buf, sizeof(temp_buf), "%s", boot_args); + int len = static_cast(std::strlen(temp_buf)); + + while (1) { + mvprintw(field_y, field_x, "%-56s", ""); + mvprintw(field_y, field_x, "%s", temp_buf); + move(field_y, field_x + len); + refresh(); + + int input_ch = getch(); + + if (input_ch == KEY_ENTER || input_ch == 10 || input_ch == 13) { + break; + } else if (input_ch == KEY_BACKSPACE || input_ch == 127 || input_ch == '\b') { + if (len > 0) temp_buf[--len] = '\0'; + } else if (input_ch >= 32 && input_ch <= 126) { + if (len < max_len) { + temp_buf[len++] = static_cast(input_ch); + temp_buf[len] = '\0'; + } + } + } + + std::snprintf(boot_args, sizeof(boot_args), "%s", temp_buf); + curs_set(0); + timeout(100); +} + +void TuiSettingsPanel::handle_enter(int selected, int sy, int sx) { + switch(selected) { + case 0: palerain_flags ^= palerain_option_safemode; break; + case 1: palerain_flags ^= palerain_option_verbose_boot; break; + case 2: edit_boot_args(sy, sx); break; + case 3: palerain_flags ^= palerain_option_force_revert; break; + case 4: palerain_flags ^= palerain_option_flower_chain; break; + case 5: + palerain_flags &= ~(palerain_option_rootless | palerain_option_rootful); + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); + palerain_flags |= palerain_option_rootless; + break; + case 6: + palerain_flags &= ~(palerain_option_rootless | palerain_option_rootful); + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); + palerain_flags |= palerain_option_rootful; + break; + case 7: + if (palerain_flags & palerain_option_rootful) { + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); + } + break; + case 8: + if (palerain_flags & palerain_option_rootful) { + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); + palerain_flags |= palerain_option_setup_rootful; + } + break; + case 9: + if (palerain_flags & palerain_option_rootful) { + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); + palerain_flags |= palerain_option_setup_partial_root; + } + break; + case 10: GetFrame()->ShowMain(0); break; + } +} + +void TuiSettingsPanel::handle_device_update(const DeviceState& state) { + return; +} + +bool TuiSettingsPanel::is_button_enabled(int btn_idx) const { + if (btn_idx >= 7 && btn_idx <= 9) { + return (palerain_flags & palerain_option_rootful) != 0; + } + + return true; +} + +#endif // WITH_TUI diff --git a/src/tui/TuiSettingsPanel.hpp b/src/tui/TuiSettingsPanel.hpp new file mode 100644 index 0000000..c0e430b --- /dev/null +++ b/src/tui/TuiSettingsPanel.hpp @@ -0,0 +1,28 @@ +#ifdef WITH_TUI + +#ifndef TUI_SETTINGS_PANEL_HPP +#define TUI_SETTINGS_PANEL_HPP + +#include "TuiPanel.hpp" + +class TuiSettingsPanel : public TuiPanel { +public: + explicit TuiSettingsPanel(TuiFrame* frame); + ~TuiSettingsPanel() override = default; + + void draw(int sy, int sx, int selected) override; + void handle_enter(int selected, int sy, int sx) override; + void handle_device_update(const DeviceState& state) override; + bool is_button_enabled(int btn_idx) const override; + + int get_total_items() const override { return 11; } + int get_button_count() const override { return 1; } + const char** get_buttons() const override; + +private: + void edit_boot_args(int start_y, int start_x); +}; + +#endif // TUI_SETTINGS_PANEL_HPP + +#endif // WITH_TUI diff --git a/src/tui/TuiText.hpp b/src/tui/TuiText.hpp new file mode 100644 index 0000000..ded46ae --- /dev/null +++ b/src/tui/TuiText.hpp @@ -0,0 +1,83 @@ +#ifdef WITH_TUI + +#ifndef TUI_TEXT_HPP +#define TUI_TEXT_HPP + +#include + +#include +#include + +namespace tui_text { + +inline void append_wrapped_lines(const std::string& text, int width, std::vector& out) { + if (width <= 0) { + return; + } + + size_t start = 0; + while (start <= text.size()) { + size_t end = text.find('\n', start); + if (end == std::string::npos) { + end = text.size(); + } + + std::string paragraph = text.substr(start, end - start); + size_t cursor = 0; + + if (paragraph.empty()) { + out.push_back(""); + } + + while (cursor < paragraph.size()) { + size_t remaining = paragraph.size() - cursor; + if (remaining <= static_cast(width)) { + out.push_back(paragraph.substr(cursor)); + break; + } + + size_t cut = cursor + static_cast(width); + size_t split = paragraph.rfind(' ', cut); + if (split == std::string::npos || split <= cursor) { + split = cut; + } + + out.push_back(paragraph.substr(cursor, split - cursor)); + cursor = split; + while (cursor < paragraph.size() && paragraph[cursor] == ' ') { + ++cursor; + } + } + + if (end == text.size()) { + break; + } + start = end + 1; + } +} + +inline int draw_wrapped_text(int y, int x, int width, const std::string& text, int max_lines = 0) { + if (width <= 0 || text.empty()) { + return 0; + } + + std::vector lines; + append_wrapped_lines(text, width, lines); + + int rendered = 0; + for (const auto& line : lines) { + if (max_lines > 0 && rendered >= max_lines) { + break; + } + mvprintw(y + rendered, x, "%.*s", width, line.c_str()); + ++rendered; + } + + return rendered; +} + +} // namespace tui_text + +#endif // TUI_TEXT_HPP + +#endif // WITH_TUI diff --git a/src/utils.c b/src/utils.c index 6d035e6..c3c9b6f 100644 --- a/src/utils.c +++ b/src/utils.c @@ -48,6 +48,9 @@ static const char* get_timestamp(void) { } void log_write(log_level_t level, const char *fmt, ...) { + if (palerain_flags & palerain_option_tui) + return; + va_list args; va_start(args, fmt); diff --git a/src/wx/AppFrame.cpp b/src/wx/AppFrame.cpp index f374cd7..e14a773 100644 --- a/src/wx/AppFrame.cpp +++ b/src/wx/AppFrame.cpp @@ -6,17 +6,14 @@ #include "../utils.h" #include "../event.hpp" -#include - -#include -#include -#include - wxDEFINE_EVENT(EVT_DEVICE_STATE_UPDATE, wxCommandEvent); void send_device_state(MainFrame* frame, const DeviceState& state) { + if (!wxTheApp) return; + wxTheApp->CallAfter([frame, state]() { + if (!frame) return; auto* evt = new wxCommandEvent(EVT_DEVICE_STATE_UPDATE); evt->SetClientData(new DeviceState(state)); wxPostEvent(frame, *evt); @@ -28,7 +25,7 @@ MainFrame::MainFrame() wxID_ANY, "palera1n - Version beta " + wxString(PALERAIN_VERSION), wxDefaultPosition, - wxSize(480, 360), + wxSize(480, 348), wxDEFAULT_FRAME_STYLE & ~(wxMAXIMIZE_BOX | wxRESIZE_BORDER)) { auto* root = new wxBoxSizer(wxVERTICAL); @@ -61,27 +58,10 @@ MainFrame::MainFrame() ShowMain(); - std::thread([this]() - { - idevice_event_subscribe(normal_device_event_cb, this); - - while (true) - sleep_ms(1000); - }).detach(); - - std::thread([this]() - { - static irecv_device_event_context_t ctx = nullptr; - - irecv_device_event_subscribe( - &ctx, - recovery_device_event_cb, - this - ); - - while (true) - sleep_ms(1000); - }).detach(); + register_device_state_callback([this](const DeviceState& state) { + send_device_state(this, state); + }); + ensure_device_event_system_started(); } void MainFrame::ShowMain() diff --git a/src/wx/AppFrame.hpp b/src/wx/AppFrame.hpp index bb80348..0bf9207 100644 --- a/src/wx/AppFrame.hpp +++ b/src/wx/AppFrame.hpp @@ -4,7 +4,7 @@ #ifdef WITH_GUI #include -#include "../state.hpp" +#include "../event.hpp" #include "MainPanel.hpp" #include "SettingsPanel.hpp" diff --git a/src/wx/DevicePanel.hpp b/src/wx/DevicePanel.hpp index bae4bd4..ccabf7a 100644 --- a/src/wx/DevicePanel.hpp +++ b/src/wx/DevicePanel.hpp @@ -2,7 +2,7 @@ #define DEVICEPANEL_H #include -#include "../state.hpp" +#include "../event.hpp" class MainFrame; class DevicePanel : public wxPanel diff --git a/src/wx/DfuPanel.cpp b/src/wx/DfuPanel.cpp index be31e76..267e793 100644 --- a/src/wx/DfuPanel.cpp +++ b/src/wx/DfuPanel.cpp @@ -5,7 +5,7 @@ #include "AppFrame.hpp" #include "DevicePanel.hpp" -#include "../state.hpp" +#include "../event.hpp" #include "../sequence.hpp" #include "../utils.h" #include "../globals.h" @@ -17,6 +17,7 @@ #include #include #include +#include #include "../gen/images/atv_std_brd.h" #include "../gen/images/ipad.h" @@ -28,6 +29,7 @@ #include "../gen/images/iphonex.h" #include "../gen/images/ipodtouch.h" #include "../gen/images/siriremote.h" +#include "../gen/images/logo.h" #include @@ -35,12 +37,23 @@ DfuPanel::DfuPanel(MainFrame* frame, wxWindow* parent) : DevicePanel(frame, parent) { Bind(wxEVT_SHOW, &DfuPanel::OnShow, this); - auto* root = new wxBoxSizer(wxVERTICAL); - m_headerText = new wxStaticText(this, wxID_ANY, "Time to put the device into DFU mode. Locate the buttons as marked below\non your device and check the instructions on the right."); - root->Add(m_headerText, 0, wxALL, 10); + auto* root = new wxBoxSizer(wxHORIZONTAL); + auto* left = new wxBoxSizer(wxVERTICAL); + wxMemoryInputStream stream(images_logo_png, images_logo_png_len); + wxImage img(stream, wxBITMAP_TYPE_PNG); + wxBitmap bmp(img); + auto* logo = new wxStaticBitmap(this, wxID_ANY, bmp); + auto* logoText = new wxStaticText(this, wxID_ANY, "DFU"); + auto* right = new wxBoxSizer(wxVERTICAL); - auto* contentRow = new wxBoxSizer(wxHORIZONTAL); + m_headerText = new wxStaticText(this, wxID_ANY, + "Time to put the device into DFU mode.", + wxDefaultPosition, + wxDefaultSize, + wxST_WRAP + ); + left->Add(m_headerText, 0, wxTOP | wxLEFT | wxRIGHT, 10); m_devicePanel = new wxPanel(this); m_devicePanel->SetMinSize(wxSize(310, 260)); @@ -51,11 +64,18 @@ DfuPanel::DfuPanel(MainFrame* frame, wxWindow* parent) wxBitmap() ); - contentRow->Add(m_devicePanel, 1, wxTOP, 10); - m_stepsSizer = new wxBoxSizer(wxVERTICAL); - contentRow->Add(m_stepsSizer, 2, wxEXPAND | wxALL, 0); + auto* deviceSizer = new wxBoxSizer(wxVERTICAL); + deviceSizer->AddStretchSpacer(1); + deviceSizer->Add(m_deviceImage, 0, wxALIGN_CENTER); + deviceSizer->AddStretchSpacer(1); + m_devicePanel->SetSizer(deviceSizer); - root->Add(contentRow, 1, wxEXPAND); + left->Add(m_devicePanel, 1, wxEXPAND | wxTOP, 0); + + left->AddStretchSpacer(1); + + m_stepsSizer = new wxBoxSizer(wxVERTICAL); + left->Add(m_stepsSizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10); m_backButton = new wxButton(this, wxID_ANY, "Back"); m_startButton = new wxButton(this, wxID_ANY, "Start"); @@ -82,18 +102,21 @@ DfuPanel::DfuPanel(MainFrame* frame, wxWindow* parent) m_stagnentTimer.Bind(wxEVT_TIMER, [this](wxTimerEvent&) { - m_headerText->SetLabel("Time to put the device into DFU mode. Locate the buttons as marked below\non your device and check the instructions on the right."); + m_headerText->SetLabel("Time to put the device into DFU mode."); m_backButton->Enable(); m_startButton->Enable(); GetMainFrame()->ShowExploit(); }); - auto* bottomRow = new wxBoxSizer(wxHORIZONTAL); - bottomRow->AddStretchSpacer(); - bottomRow->Add(m_backButton, 0, wxRIGHT | wxLEFT | wxBOTTOM, 12); - bottomRow->Add(m_startButton, 0, wxRIGHT | wxLEFT | wxBOTTOM, 12); + right->Add(logo, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, 10); + right->Add(logoText, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 5); + right->AddStretchSpacer(); + right->Add(m_backButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + right->Add(m_startButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); - root->Add(bottomRow, 0, wxEXPAND); + root->Add(left, 1, wxEXPAND); + root->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0, wxEXPAND | wxTOP | wxBOTTOM, 10); + root->Add(right, 0, wxEXPAND); SetSizer(root); } @@ -143,6 +166,14 @@ void DfuPanel::SetDeviceState(const DeviceState& state) m_startButton->Enable(); GetMainFrame()->ShowMain(); } + + if (!state.connected) + { + m_isEnteringDfu = false; + m_backButton->Enable(); + m_startButton->Enable(); + GetMainFrame()->ShowMain(); + } } void DfuPanel::OnShow(wxShowEvent& event) @@ -254,7 +285,6 @@ void DfuPanel::LoadDevice(const std::string& productType) ); m_deviceImage->SetBitmap(wxBitmap(img)); - m_deviceImage->SetPosition(wxPoint(m_sequence.imageOffsetX, 0)); } } @@ -290,7 +320,7 @@ void DfuPanel::LoadDevice(const std::string& productType) lbl->SetForegroundColour(i == 0 ? kActive : kInactive); - m_stepsSizer->Add(lbl, 0, wxBOTTOM, 5); + m_stepsSizer->Add(lbl, 0, wxTOP, 5); m_stepLabels.push_back(lbl); } @@ -452,14 +482,10 @@ void DfuPanel::Reboot() std::this_thread::sleep_for(std::chrono::milliseconds(250)); } - if (!client) - { - return; - } + if (!client) return; irecv_setenv(client, "auto-boot", "true"); irecv_saveenv(client); irecv_reboot(client); - irecv_close(client); }).detach(); } diff --git a/src/wx/DfuPanel.hpp b/src/wx/DfuPanel.hpp index af62642..2d4726d 100644 --- a/src/wx/DfuPanel.hpp +++ b/src/wx/DfuPanel.hpp @@ -5,7 +5,7 @@ #include #include "DevicePanel.hpp" -#include "../state.hpp" +#include "../event.hpp" #include "../sequence.hpp" class DfuPanel; diff --git a/src/wx/ExploitPanel.cpp b/src/wx/ExploitPanel.cpp index 1e5880d..eddc7ee 100644 --- a/src/wx/ExploitPanel.cpp +++ b/src/wx/ExploitPanel.cpp @@ -3,6 +3,10 @@ #include "ExploitPanel.hpp" #include +#include +#include +#include +#include #include #ifdef __cplusplus @@ -17,42 +21,19 @@ using std::atomic_bool; #include "AppFrame.hpp" #include "../m8/run.h" #include "../utils.h" - -static const char* stage_to_string(int stage) -{ - switch (stage) { - case STAGE_SETUP: return "Checking if device is ready"; - case STAGE_SPRAY: return "Setting up the exploit (this is the heap spray)"; - case STAGE_PATCH: return "Right before trigger (this is the real bug setup)"; - case STAGE_YOLODFU: return "Booting PongoOS..."; - case STAGE_PONGO: return "Booting..."; - case STAGE_DONE: return "Done."; - default: return "Idle."; - } -} - -static int stage_to_progress(int stage) -{ - switch (stage) { - case STAGE_SETUP: return 30; - case STAGE_SPRAY: return 40; - case STAGE_PATCH: return 50; - case STAGE_YOLODFU: return 60; - case STAGE_PONGO: return 70; - case STAGE_DONE: return 100; - default: return 0; - } -} +#include "../gen/images/logo.h" ExploitPanel::ExploitPanel(MainFrame* frame, wxWindow* parent) : DevicePanel(frame, parent) { - auto* root = new wxBoxSizer(wxVERTICAL); - - auto* descriptionText = new wxStaticText(this, wxID_ANY, - "Installing jailbreak, this will take a moment. If the device asks for a passcode\n" - "please enter it. Do not disconnect the device until finished." - ); + auto* root = new wxBoxSizer(wxHORIZONTAL); + auto* left = new wxBoxSizer(wxVERTICAL); + wxMemoryInputStream stream(images_logo_png, images_logo_png_len); + wxImage img(stream, wxBITMAP_TYPE_PNG); + wxBitmap bmp(img); + auto* logo = new wxStaticBitmap(this, wxID_ANY, bmp); + auto* logoText = new wxStaticText(this, wxID_ANY, "Jailbreak"); + auto* right = new wxBoxSizer(wxVERTICAL); m_statusText = new wxStaticText(this, wxID_ANY, stage_to_string(STAGE_RESET)); @@ -68,16 +49,22 @@ ExploitPanel::ExploitPanel(MainFrame* frame, wxWindow* parent) frame->ShowMain(); }); - root->Add(descriptionText, 0, wxALL, 10); - root->Add(m_statusText, 0, wxALL, 10); - root->Add(m_progressBar, 0, wxALL | wxEXPAND, 20); + left->Add(new wxStaticText(this, wxID_ANY, "Installing jailbreak, this will take a moment. If the device asks for a passcode please enter it. Do not disconnect the device until finished.", + wxDefaultPosition, + wxDefaultSize, + wxST_WRAP + ), 0, wxALL, 10); + left->Add(m_statusText, 0, wxALL, 10); + left->Add(m_progressBar, 0, wxALL | wxEXPAND, 20); - root->AddStretchSpacer(); - auto* bottomRow = new wxBoxSizer(wxHORIZONTAL); - bottomRow->AddStretchSpacer(); - bottomRow->Add(m_backButton, 0, wxRIGHT | wxBOTTOM, 12); + right->Add(logo, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, 10); + right->Add(logoText, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 5); + right->AddStretchSpacer(); + right->Add(m_backButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); - root->Add(bottomRow, 0, wxEXPAND); + root->Add(left, 1, wxEXPAND); + root->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0, wxEXPAND | wxTOP | wxBOTTOM, 10); + root->Add(right, 0, wxEXPAND); SetSizer(root); } diff --git a/src/wx/ExploitPanel.hpp b/src/wx/ExploitPanel.hpp index 461a10a..ff6b3de 100644 --- a/src/wx/ExploitPanel.hpp +++ b/src/wx/ExploitPanel.hpp @@ -5,7 +5,7 @@ #include #include "DevicePanel.hpp" -#include "../state.hpp" +#include "../event.hpp" class ExploitPanel; class ExploitPanel : public DevicePanel diff --git a/src/wx/MainPanel.cpp b/src/wx/MainPanel.cpp index 788dbb6..c3d0b03 100644 --- a/src/wx/MainPanel.cpp +++ b/src/wx/MainPanel.cpp @@ -12,20 +12,22 @@ #include "AppFrame.hpp" #include "DevicePanel.hpp" -#include "../state.hpp" +#include "../event.hpp" #include "../globals.h" #include "../utils.h" #include "../paleinfo.h" #include "../gen/images/logo.h" -const char* deviceTextString = "Connect your iPhone, iPod touch, iPad, or AppleTV to begin."; +const char* deviceTextString = "No device connected"; +const char* deviceTextString2 = "Please connect a device to get started.\nEnsure version range is 15.0+"; MainPanel::MainPanel(MainFrame* frame, wxWindow* parent) : DevicePanel(frame, parent) { - auto* root = new wxBoxSizer(wxVERTICAL); - wxInitAllImageHandlers(); + auto* root = new wxBoxSizer(wxHORIZONTAL); + auto* left = new wxBoxSizer(wxVERTICAL); + auto* right = new wxBoxSizer(wxVERTICAL); wxMemoryInputStream stream(images_logo_png, images_logo_png_len); wxImage img(stream, wxBITMAP_TYPE_PNG); @@ -33,101 +35,70 @@ MainPanel::MainPanel(MainFrame* frame, wxWindow* parent) auto* logo = new wxStaticBitmap(this, wxID_ANY, bmp); - auto* titletext = new wxStaticText(this, wxID_ANY, "Welcome to palera1n!"); - wxFont titlefont = titletext->GetFont(); - titlefont.SetWeight(wxFONTWEIGHT_BOLD); - titletext->SetFont(titlefont); + m_deviceTitle = new wxStaticText(this, wxID_ANY, deviceTextString); + m_deviceSubtitle = new wxStaticText(this, wxID_ANY, deviceTextString2); - m_deviceText = new wxStaticText( + wxFont titleFont = m_deviceTitle->GetFont(); + titleFont.SetPointSize(titleFont.GetPointSize() + 6); + titleFont.SetWeight(wxFONTWEIGHT_SEMIBOLD); + m_deviceTitle->SetFont(titleFont); + + m_deviceSubtitle->SetForegroundColour(wxColour(160, 160, 160)); + + auto* credtext = new wxStaticText( this, wxID_ANY, - deviceTextString + "Made by: asdfugil, claration, kok3shidoll, mineek, plooshi, staturnz\n\n" + "Thanks to: itsnebulalol, llsc12, lrdsnow, dedbeddedbed, kirb, ehilwyma, " + "opa334, 0x7ff, alfiecg24, sneko, sbingner, nikias, tihmstar, " + "Checkra1n (Siguza, axi0mx, littlelailo et al.), Procursus (Hayden Seay, " + "Cameron Katri, Keto et al.)", + wxDefaultPosition, + wxDefaultSize, + wxST_WRAP ); - auto* left = new wxBoxSizer(wxVERTICAL); - left->Add(titletext, 0, wxTOP | wxLEFT, 10); - left->Add(m_deviceText, 0, wxTOP | wxLEFT, 10); + auto* twitterLabel = new wxStaticText(this, wxID_ANY, "Twitter:"); + auto* twitter = new wxStaticText(this, wxID_ANY, "@palera1n"); - auto* row = new wxBoxSizer(wxHORIZONTAL); - row->Add(left, 0, wxALIGN_TOP); - row->AddStretchSpacer(); - row->Add(logo, 0, wxTOP | wxRIGHT, 10); + auto* websiteLabel = new wxStaticText(this, wxID_ANY, "Website:"); + auto* website = new wxStaticText(this, wxID_ANY, "https://palera.in"); - root->Add(row, 0, wxEXPAND); + twitter->SetForegroundColour(wxColour(0, 120, 215)); + website->SetForegroundColour(wxColour(0, 120, 215)); - root->Add(new wxStaticLine(this), 0, wxEXPAND | wxALL, 10); + twitter->SetCursor(wxCursor(wxCURSOR_HAND)); + website->SetCursor(wxCursor(wxCURSOR_HAND)); - auto* credtext = new wxStaticText(this, wxID_ANY, - "Made by: asdfugil, kok3shidoll, C, mineek, nekohaxx, plooshi, staturnz\n\n" - "Thanks to: llsc12, itsnebulalol, lrdsnow, kirb, ehilwyma opa334, 0x7ff,\n" - "sbingner, nikias (libimobiledevice), Checkra1n (Siguza, axi0mx, littlelailo\n" - "et al.), Procursus (Hayden Seay, Cameron Katri, Keto et al.)" - ); - - auto* credrow = new wxBoxSizer(wxHORIZONTAL); - - auto* lovetext = new wxStaticText(this, wxID_ANY, - #ifdef __APPLE__ - "With 💖 from C (claration)" - #else - "With <3 from C (claration)" - #endif - ); - - auto* link1 = new wxStaticText(this, wxID_ANY, "@palera1n"); - auto* link2 = new wxStaticText(this, wxID_ANY, "https://palera.in"); - - link1->SetForegroundColour(wxColour(0, 120, 215)); - link2->SetForegroundColour(wxColour(0, 120, 215)); - - link1->SetCursor(wxCursor(wxCURSOR_HAND)); - link2->SetCursor(wxCursor(wxCURSOR_HAND)); - - link1->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { + twitter->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) + { wxLaunchDefaultBrowser("https://twitter.com/intent/follow?screen_name=palera1n"); }); - link2->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { + website->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) + { wxLaunchDefaultBrowser("https://palera.in"); }); - credrow->Add(lovetext, 0, wxALIGN_BOTTOM | wxLEFT, 0); - credrow->AddStretchSpacer(); + auto* twitterRow = new wxBoxSizer(wxHORIZONTAL); + twitterRow->Add(twitterLabel, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); + twitterRow->Add(twitter, 0, wxALIGN_CENTER_VERTICAL); - auto* linkSizer = new wxBoxSizer(wxVERTICAL); - linkSizer->Add(link1, 0, wxALIGN_RIGHT); - linkSizer->Add(link2, 0, wxALIGN_RIGHT); + auto* websiteRow = new wxBoxSizer(wxHORIZONTAL); + websiteRow->Add(websiteLabel, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, 5); + websiteRow->Add(website, 0, wxALIGN_CENTER_VERTICAL); - credrow->Add(linkSizer, 0, wxALIGN_CENTER_VERTICAL, 0); - root->Add(credtext, 0, wxLEFT | wxRIGHT, 10); - root->Add(credrow, 0, wxEXPAND | wxLEFT | wxRIGHT, 10); + auto* logoText = new wxStaticText(this, wxID_ANY, "palera1n"); - root->Add(new wxStaticLine(this), 0, wxEXPAND | wxALL, 10); - - auto* notetext = new wxStaticText(this, wxID_ANY, - "NOTE: Please ensure you have a backup of your device before applying the\n" - "jailbreak. While data loss is unlikely, we won't be responsible if something goes\n" - "wrong. Use at your own risk."); - - wxFont notefont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - #ifdef __APPLE__ - notefont.SetPointSize(12); - #else - notefont.SetPointSize(9); - #endif - notefont.SetStyle(wxFONTSTYLE_ITALIC); - notetext->SetFont(notefont); - - root->Add(notetext, 0, wxLEFT | wxRIGHT, 10); - - root->AddStretchSpacer(); - - auto* quickMode = new wxCheckBox(this, wxID_ANY, "Quick Mode"); + auto* quickMode = new wxCheckBox(this, wxID_ANY, "Enable Quick Mode"); + auto* donateButton = new wxButton(this, wxID_ANY, "Donate"); auto* optionsButton = new wxButton(this, wxID_ANY, "Options"); + m_startButton = new wxButton(this, wxID_ANY, "Start"); m_startButton->Disable(); quickMode->SetValue(palerain_flags & palerain_option_quick); + quickMode->Bind(wxEVT_CHECKBOX, [&](wxCommandEvent& e) { if (e.IsChecked()) @@ -144,22 +115,66 @@ MainPanel::MainPanel(MainFrame* frame, wxWindow* parent) m_startButton->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&) { if (frame->GetDeviceState().mode == DeviceMode::Normal) - { frame->ShowRecovery(); - } else if (frame->GetDeviceState().mode == DeviceMode::Recovery) - { frame->ShowDfu(); - } }); - auto* bottomRow = new wxBoxSizer(wxHORIZONTAL); - bottomRow->AddStretchSpacer(); - bottomRow->Add(quickMode, 0, wxRIGHT | wxTOP, 1); - bottomRow->Add(optionsButton, 0, wxRIGHT | wxLEFT | wxBOTTOM, 12); - bottomRow->Add(m_startButton, 0, wxRIGHT | wxLEFT | wxBOTTOM, 12); + left->Add(m_deviceTitle, 0, wxLEFT | wxRIGHT | wxTOP, 10); + left->Add(m_deviceSubtitle, 0, wxLEFT | wxRIGHT | wxBOTTOM, 10); + left->Add(new wxStaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + left->Add(credtext, 0, wxLEFT | wxRIGHT | wxBOTTOM, 10); - root->Add(bottomRow, 0, wxEXPAND); + auto* linksCol = new wxBoxSizer(wxVERTICAL); + linksCol->Add(twitterRow, 0); + linksCol->Add(websiteRow, 0); + + auto* warningNote = new wxStaticText(this, wxID_ANY, + "NOTE: Please ensure you've made a backup of your device before proceeding.\n", + wxDefaultPosition, + wxDefaultSize, + wxST_WRAP + ); + + wxFont noteFont = warningNote->GetFont(); + noteFont.SetStyle(wxFONTSTYLE_ITALIC); + warningNote->SetFont(noteFont); + warningNote->SetForegroundColour(wxColour(200, 80, 80)); + + auto* linksRow = new wxBoxSizer(wxHORIZONTAL); + linksRow->Add(linksCol, 0, wxLEFT | wxRIGHT | wxALIGN_TOP, 10); + linksRow->AddStretchSpacer(); + linksRow->Add(warningNote, 0, wxRIGHT | wxALIGN_TOP, 10); + + left->Add(linksRow, 0, wxEXPAND | wxBOTTOM, 10); + + left->AddStretchSpacer(); + + left->Add(new wxStaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + + #if __APPLE__ + auto* madeBy = new wxStaticText(this, wxID_ANY, "Made with 💖 by C (claration)"); + #else + auto* madeBy = new wxStaticText(this, wxID_ANY, "Made with <3 by C (claration)"); + #endif + + auto* quickModeRow = new wxBoxSizer(wxHORIZONTAL); + quickModeRow->Add(madeBy, 0, wxALIGN_CENTER_VERTICAL); + quickModeRow->AddStretchSpacer(); + quickModeRow->Add(quickMode, 0, wxALIGN_CENTER_VERTICAL); + + left->Add(quickModeRow, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + + right->Add(logo, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, 10); + right->Add(logoText, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 5); + right->AddStretchSpacer(); + right->Add(donateButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + right->Add(optionsButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + right->Add(m_startButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + + root->Add(left, 1, wxEXPAND); + root->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0, wxEXPAND | wxTOP | wxBOTTOM, 10); + root->Add(right, 0, wxEXPAND); SetSizer(root); } @@ -168,11 +183,25 @@ void MainPanel::SetDeviceState(const DeviceState& state) { DevicePanel::SetDeviceState(state); - if (m_deviceText) + if (m_deviceTitle && m_deviceSubtitle) { if (!state.connected) { - m_deviceText->SetLabel(deviceTextString); + if (state.connectedDeviceCount > 1) + { + m_deviceTitle->SetLabel("Multiple devices detected"); + m_deviceSubtitle->SetLabel( + wxString::Format( + "%u USB devices connected. Disconnect extras and keep only one device attached.", + state.connectedDeviceCount + ) + ); + } + else + { + m_deviceTitle->SetLabel(deviceTextString); + m_deviceSubtitle->SetLabel(deviceTextString2); + } m_startButton->Enable(false); } else @@ -187,28 +216,34 @@ void MainPanel::SetDeviceState(const DeviceState& state) { case DeviceMode::Normal: if (state.isSupported) { - m_deviceText->SetLabel("Connected " + productString + " (iOS " + versionString + ") in normal mode.\n" + ecidString); + m_deviceTitle->SetLabel(productString); + m_deviceSubtitle->SetLabel("Connected in normal mode • iOS " + versionString + "\n" + ecidString); m_startButton->Enable(); } else { - m_deviceText->SetLabel("Sorry, " + productString + " (iOS " + versionString + ") is not supported.\n" + ecidString); + m_deviceTitle->SetLabel(productString); + m_deviceSubtitle->SetLabel("Not supported • iOS " + versionString + "\n" + ecidString); m_startButton->Enable(false); } break; case DeviceMode::Recovery: if (state.isSupported) { - m_deviceText->SetLabel("Connected " + productString + " in recovery mode.\n" + ecidString); + m_deviceTitle->SetLabel(productString); + m_deviceSubtitle->SetLabel("Connected in recovery mode\n" + ecidString); m_startButton->Enable(); } else { - m_deviceText->SetLabel("Sorry, " + productString + " is not supported.\n" + ecidString); + m_deviceTitle->SetLabel(productString); + m_deviceSubtitle->SetLabel("Not supported\n" + ecidString); m_startButton->Enable(false); } break; case DeviceMode::DFU: - m_deviceText->SetLabel("Sorry, DFU mode is not supported."); + m_deviceTitle->SetLabel("DFU Mode device"); + m_deviceSubtitle->SetLabel("Sorry, jailbreaking in DFU mode is not supported.\n"); m_startButton->Enable(false); break; case DeviceMode::None: - m_deviceText->SetLabel(deviceTextString); + m_deviceTitle->SetLabel(deviceTextString); + m_deviceSubtitle->SetLabel(deviceTextString2); m_startButton->Enable(false); break; } diff --git a/src/wx/MainPanel.hpp b/src/wx/MainPanel.hpp index dd958ae..6f0dc56 100644 --- a/src/wx/MainPanel.hpp +++ b/src/wx/MainPanel.hpp @@ -5,7 +5,7 @@ #include #include "DevicePanel.hpp" -#include "../state.hpp" +#include "../event.hpp" class MainPanel; class MainPanel : public DevicePanel @@ -14,7 +14,8 @@ public: explicit MainPanel(MainFrame* frame, wxWindow* parent); void SetDeviceState(const DeviceState& state) override; private: - wxStaticText* m_deviceText = nullptr; + wxStaticText* m_deviceTitle = nullptr; + wxStaticText* m_deviceSubtitle = nullptr; wxButton* m_startButton = nullptr; }; diff --git a/src/wx/RecoveryPanel.cpp b/src/wx/RecoveryPanel.cpp index 72866ee..f6bc290 100644 --- a/src/wx/RecoveryPanel.cpp +++ b/src/wx/RecoveryPanel.cpp @@ -3,16 +3,21 @@ #include "RecoveryPanel.hpp" #include +#include +#include +#include +#include #include #include "AppFrame.hpp" #include "DevicePanel.hpp" -#include "../state.hpp" +#include "../event.hpp" #include "../utils.h" #include "../globals.h" #include "../paleinfo.h" +#include "../gen/images/logo.h" #include #include @@ -22,20 +27,19 @@ RecoveryPanel::RecoveryPanel(MainFrame* frame, wxWindow* parent) : DevicePanel(frame, parent) { Bind(wxEVT_SHOW, &RecoveryPanel::OnShow, this); - auto* root = new wxBoxSizer(wxVERTICAL); - - root->Add(new wxStaticText(this, wxID_ANY, - "The device needs to be put into DFU mode to apply the jailbreak. This is a\n" - "manual process and we will guide you through it.\n" - "In order to prevent filesystem corruption through hard reset, the device will\n" - "be put into recovery mode first. Click next when you are ready." - ), 0, wxALL, 10); + auto* root = new wxBoxSizer(wxHORIZONTAL); + auto* left = new wxBoxSizer(wxVERTICAL); + wxMemoryInputStream stream(images_logo_png, images_logo_png_len); + wxImage img(stream, wxBITMAP_TYPE_PNG); + wxBitmap bmp(img); + auto* logo = new wxStaticBitmap(this, wxID_ANY, bmp); + auto* logoText = new wxStaticText(this, wxID_ANY, "Recovery"); + auto* right = new wxBoxSizer(wxVERTICAL); m_statusText = new wxStaticText(this, wxID_ANY, ""); - root->Add(m_statusText, 0, wxALL, 10); - m_backButton = new wxButton(this, wxID_ANY, "Back"); m_nextButton = new wxButton(this, wxID_ANY, "Next"); + m_backButton->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&) { frame->ShowMain(); @@ -47,7 +51,6 @@ RecoveryPanel::RecoveryPanel(MainFrame* frame, wxWindow* parent) m_nextButton->Disable(); EnterRecoveryMode(); }); - m_timer.Bind(wxEVT_TIMER, [this](wxTimerEvent&) { m_backButton->Enable(); @@ -58,13 +61,24 @@ RecoveryPanel::RecoveryPanel(MainFrame* frame, wxWindow* parent) m_statusText->SetLabel(""); }); - auto* bottomRow = new wxBoxSizer(wxHORIZONTAL); - bottomRow->AddStretchSpacer(); - bottomRow->Add(m_backButton, 0, wxRIGHT | wxBOTTOM, 12); - bottomRow->Add(m_nextButton, 0, wxRIGHT | wxLEFT | wxBOTTOM, 12); + left->Add( + new wxStaticText(this, wxID_ANY, + "The device needs to be put into DFU mode to apply the jailbreak. This is a manual process and we will guide you through it.", + wxDefaultPosition, + wxDefaultSize, + wxST_WRAP + ), 0, wxALL, 10); + left->Add(m_statusText, 0, wxALL, 10); - root->AddStretchSpacer(); - root->Add(bottomRow, 0, wxEXPAND); + right->Add(logo, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, 10); + right->Add(logoText, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 5); + right->AddStretchSpacer(); + right->Add(m_backButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + right->Add(m_nextButton, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); + + root->Add(left, 1, wxEXPAND); + root->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0, wxEXPAND | wxTOP | wxBOTTOM, 10); + root->Add(right, 0, wxEXPAND); SetSizer(root); } @@ -85,7 +99,7 @@ void RecoveryPanel::SetDeviceState(const DeviceState& state) if (m_isEnteringRecovery && state.mode == DeviceMode::Normal) { m_isEnteringRecovery = false; - m_statusText->SetLabel("Hmm... It seems like the device didn't enter recovery mode. Please try again."); + m_statusText->SetLabel("Hmm... It seems like the device didn't enter recovery mode."); m_backButton->Enable(); m_nextButton->Enable(); } diff --git a/src/wx/SettingsPanel.cpp b/src/wx/SettingsPanel.cpp index 096ff11..44b6ddf 100644 --- a/src/wx/SettingsPanel.cpp +++ b/src/wx/SettingsPanel.cpp @@ -3,32 +3,90 @@ #include "SettingsPanel.hpp" #include +#include +#include +#include +#include #include "AppFrame.hpp" #include "DevicePanel.hpp" #include "../utils.h" #include "../globals.h" #include "../paleinfo.h" +#include "../gen/images/logo.h" SettingsPanel::SettingsPanel(MainFrame* frame, wxWindow* parent) : DevicePanel(frame, parent) { - auto* root = new wxBoxSizer(wxVERTICAL); + auto* root = new wxBoxSizer(wxHORIZONTAL); + auto* left = new wxBoxSizer(wxVERTICAL); + wxMemoryInputStream stream(images_logo_png, images_logo_png_len); + wxImage img(stream, wxBITMAP_TYPE_PNG); + wxBitmap bmp(img); + auto* logo = new wxStaticBitmap(this, wxID_ANY, bmp); + auto* logoText = new wxStaticText(this, wxID_ANY, "Options"); + auto* right = new wxBoxSizer(wxVERTICAL); - root->Add(new wxStaticText(this, wxID_ANY, - "You may set the following options. If you don't know what they mean you'll\n" - "probably have no reason to set them." + left->Add(new wxStaticText(this, wxID_ANY, + "You may set the following options. If you don't know what they mean you'll probably have no reason to set them.", + wxDefaultPosition, + wxDefaultSize, + wxST_WRAP ), 0, wxALL, 10); auto* optionSafemode = new wxCheckBox(this, wxID_ANY, "Safe Mode"); auto* optionVerbose = new wxCheckBox(this, wxID_ANY, "Verbose Boot"); auto* optionRevert = new wxCheckBox(this, wxID_ANY, "Restore System"); auto* optionDarkBlockchain = new wxCheckBox(this, wxID_ANY, "Dark Blockchain"); + auto* bootArgs = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize); + auto* rootType = new wxRadioBox( + this, + wxID_ANY, + "Jailbreak Type", + wxDefaultPosition, + wxDefaultSize, + 2, + (const wxString[]){ + "Rootless", + "Rootful" + }, + 1, + wxRA_SPECIFY_COLS + ); + + auto* setupMode = new wxRadioBox( + this, + wxID_ANY, + "Rootful Options", + wxDefaultPosition, + wxDefaultSize, + 3, + (const wxString[]){ + "Boot", + "Create FakeFS", + "Create BindFS" + }, + 1, + wxRA_SPECIFY_COLS + ); optionSafemode->SetValue(palerain_flags & palerain_option_safemode); optionVerbose->SetValue(palerain_flags & palerain_option_verbose_boot); optionRevert->SetValue(palerain_flags & palerain_option_force_revert); optionDarkBlockchain->SetValue(palerain_flags & palerain_option_flower_chain); + bootArgs->SetValue(wxString(boot_args, wxConvUTF8)); + if (palerain_flags & palerain_option_rootful) + rootType->SetSelection(1); + else + rootType->SetSelection(0); + + if (palerain_flags & palerain_option_setup_rootful) + setupMode->SetSelection(1); + else if (palerain_flags & palerain_option_setup_partial_root) + setupMode->SetSelection(2); + else + setupMode->SetSelection(0); + setupMode->Enable(rootType->GetSelection() == 1); optionSafemode->Bind(wxEVT_CHECKBOX, [&](wxCommandEvent& e) { @@ -37,7 +95,6 @@ SettingsPanel::SettingsPanel(MainFrame* frame, wxWindow* parent) else palerain_flags &= ~palerain_option_safemode; }); - optionVerbose->Bind(wxEVT_CHECKBOX, [&](wxCommandEvent& e) { if (e.IsChecked()) @@ -45,7 +102,6 @@ SettingsPanel::SettingsPanel(MainFrame* frame, wxWindow* parent) else palerain_flags &= ~palerain_option_verbose_boot; }); - optionRevert->Bind(wxEVT_CHECKBOX, [&](wxCommandEvent& e) { if (e.IsChecked()) @@ -53,7 +109,6 @@ SettingsPanel::SettingsPanel(MainFrame* frame, wxWindow* parent) else palerain_flags &= ~palerain_option_force_revert; }); - optionDarkBlockchain->Bind(wxEVT_CHECKBOX, [&](wxCommandEvent& e) { if (e.IsChecked()) @@ -61,13 +116,6 @@ SettingsPanel::SettingsPanel(MainFrame* frame, wxWindow* parent) else palerain_flags &= ~palerain_option_flower_chain; }); - - root->Add(optionSafemode, 0, wxLEFT | wxRIGHT, 10); - root->Add(optionVerbose, 0, wxLEFT | wxRIGHT | wxTOP, 10); - root->Add(new wxStaticText(this, wxID_ANY, "Boot Arguments:"), 0, wxALL, 10); - - auto* bootArgs = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize); - bootArgs->SetValue(wxString(boot_args, wxConvUTF8)); bootArgs->Bind(wxEVT_TEXT, [&](wxCommandEvent& e) { wxString value = e.GetString(); @@ -77,25 +125,65 @@ SettingsPanel::SettingsPanel(MainFrame* frame, wxWindow* parent) } snprintf(boot_args, sizeof(boot_args), "%s", value.ToStdString().c_str()); }); + rootType->Bind(wxEVT_RADIOBOX, [setupMode](wxCommandEvent& e) + { + palerain_flags &= ~(palerain_option_rootless | palerain_option_rootful); + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); - root->Add(bootArgs, 0, wxLEFT | wxRIGHT | wxEXPAND, 30); - root->Add(optionRevert, 0, wxLEFT | wxRIGHT | wxTOP, 10); - root->Add(optionDarkBlockchain, 0, wxLEFT | wxRIGHT | wxTOP, 10); + setupMode->Enable(e.GetSelection() == 1); + setupMode->SetSelection(0); - root->AddStretchSpacer(); + if (e.GetSelection() == 0) + palerain_flags |= palerain_option_rootless; + else + palerain_flags |= palerain_option_rootful; + }); + + setupMode->Bind(wxEVT_RADIOBOX, [&](wxCommandEvent& e) + { + palerain_flags &= ~(palerain_option_setup_rootful | palerain_option_setup_partial_root); + + if (e.GetSelection() == 1) + palerain_flags |= palerain_option_setup_rootful; + else if (e.GetSelection() == 2) + palerain_flags |= palerain_option_setup_partial_root; + }); + + auto* contentRow = new wxBoxSizer(wxHORIZONTAL); + auto* leftColumn = new wxBoxSizer(wxVERTICAL); + auto* rightColumn = new wxBoxSizer(wxVERTICAL); + + leftColumn->Add(rootType, 0, wxALL | wxEXPAND, 5); + leftColumn->Add(setupMode, 0, wxALL | wxEXPAND, 5); + + rightColumn->Add(optionSafemode, 0, wxALL, 5); + rightColumn->Add(optionVerbose, 0, wxALL, 5); + rightColumn->Add(optionRevert, 0, wxALL, 5); + rightColumn->Add(optionDarkBlockchain, 0, wxALL, 5); + rightColumn->AddStretchSpacer(); + + contentRow->Add(leftColumn, 1, wxEXPAND | wxRIGHT, 10); + contentRow->Add(rightColumn, 1, wxEXPAND | wxLEFT, 10); + + left->Add(contentRow, 1, wxEXPAND | wxLEFT | wxRIGHT, 10); + left->Add(new wxStaticText(this, wxID_ANY, "Boot Arguments:"), 0, wxALL, 15); + left->Add(bootArgs, 0, wxLEFT | wxRIGHT | wxEXPAND, 30); + left->AddStretchSpacer(); auto* back = new wxButton(this, wxID_ANY, "Back"); + right->Add(logo, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP | wxLEFT | wxRIGHT, 10); + right->Add(logoText, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, 5); + right->AddStretchSpacer(); + right->Add(back, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); back->Bind(wxEVT_BUTTON, [frame](wxCommandEvent&) { frame->ShowMain(); }); - auto* bottomRow = new wxBoxSizer(wxHORIZONTAL); - bottomRow->AddStretchSpacer(); - bottomRow->Add(back, 0, wxRIGHT | wxLEFT | wxBOTTOM, 12); - - root->Add(bottomRow, 0, wxEXPAND); + root->Add(left, 1, wxEXPAND); + root->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0, wxEXPAND | wxTOP | wxBOTTOM, 10); + root->Add(right, 0, wxEXPAND); SetSizer(root); }