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 :)
This commit is contained in:
C authored and GitHub committed 2026-07-06 04:05:57 -07:00
1 parent 89b6147e26
commit 18023c4037
42 files changed
+2895 -487

No files matched your search

+322 -143
View File
@@ -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 <wx/wx.h>
#include <chrono>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <libirecovery.h>
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<DeviceStateCallback> g_callbacks;
std::unordered_map<uint64_t, ManagedDevice> g_devices; // keyed by ECID
std::unordered_map<std::string, uint64_t> 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<DeviceStateCallback>& 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<MainFrame*>(user_data);
if (!frame || !event)
return;
#endif
DeviceState build_public_state_locked() {
DeviceState out;
out.connectedDeviceCount = static_cast<uint32_t>(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<DeviceStateCallback> callbacks;
DeviceState state;
{
std::lock_guard<std::mutex> 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<MainFrame*>(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<unsigned long long>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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