/* * main.cpp * MontaukOS Bluetooth Manager * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include extern "C" { #include #include } using namespace gui; // ============================================================================ // Constants // ============================================================================ static constexpr int WIN_W = 360; static constexpr int WIN_H = 420; static constexpr int ROW_H = 56; static constexpr int TAB_H = 36; static constexpr int PAD = 16; static constexpr int BTN_W = 90; static constexpr int BTN_H = 28; static constexpr int STATUS_H = 28; static constexpr int SCAN_BTN_W = 100; static constexpr int SCROLL_STEP = 20; static constexpr int FIELD_H = 32; static constexpr int A_PAD_TOP = 18; static constexpr int MAX_SCAN = 16; // Up to 4 active links + up to 8 stored bonds shown together in the Devices tab. static constexpr int MAX_CONNECTED = 16; static constexpr int FORGET_W = 64; static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33); static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66); static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0); static constexpr Color ROW_HOVER = Color::from_rgb(0xF0, 0xF4, 0xFF); static constexpr Color BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC); static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color GREEN = Color::from_rgb(0x2E, 0xA0, 0x43); static constexpr Color RED = Color::from_rgb(0xCC, 0x33, 0x33); // ============================================================================ // State // ============================================================================ enum Tab { TAB_DEVICES = 0, TAB_SCAN = 1, TAB_ADAPTER = 2, TAB_COUNT = 3, }; static const char* g_tab_labels[TAB_COUNT] = { "Devices", "Scan", "Adapter", }; static int g_win_w = WIN_W; static int g_win_h = WIN_H; static Tab g_tab = TAB_DEVICES; static int g_mouse_x = -1; static int g_mouse_y = -1; static int g_hover_row = -1; // Adapter info static montauk::abi::BtAdapterInfo g_adapter; static bool g_adapter_ok = false; // Connected/paired devices static montauk::abi::BtDevInfo g_devices[MAX_CONNECTED]; static char g_device_names[MAX_CONNECTED][64]; static int g_device_count = 0; // Scan results static montauk::abi::BtScanResult g_scan[MAX_SCAN]; static int g_scan_count = 0; static bool g_scanning = false; // Adapter MAC editing static char g_mac_value[24] = {}; static mtk::TextInputState g_mac_input = {}; static bool g_mac_focus = false; static uint64_t g_rng = 0; // Scroll static int g_scroll = 0; // Status message static char g_status[80]; static uint64_t g_status_time = 0; // ============================================================================ // Theme and layout // ============================================================================ static mtk::Theme bt_theme() { mtk::Theme theme = mtk::make_theme(); theme.window_bg = BG_COLOR; theme.surface = Color::from_rgb(0xF5, 0xF5, 0xF5); theme.surface_alt = Color::from_rgb(0xF8, 0xF8, 0xF8); theme.surface_hover = ROW_HOVER; theme.border = BORDER; theme.text = TEXT_COLOR; theme.text_muted = Color::from_rgb(0x88, 0x88, 0x88); theme.text_subtle = DIM_TEXT; theme.accent_soft = ROW_HOVER; theme.danger = RED; theme.danger_hover = mtk::darken(RED, 18); theme.disabled_bg = Color::from_rgb(0xE8, 0xE8, 0xE8); theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A); return theme; } static Rect tab_bar_rect() { return {0, 0, g_win_w, TAB_H}; } static Rect content_rect() { return {0, TAB_H, g_win_w, gui_max(g_win_h - TAB_H - STATUS_H, 0)}; } static Rect status_bar_rect() { return {0, g_win_h - STATUS_H, g_win_w, STATUS_H}; } static Rect scan_button_rect() { Rect content = content_rect(); return {(g_win_w - SCAN_BTN_W) / 2, content.y + 12, SCAN_BTN_W, BTN_H}; } static int devices_list_top() { return content_rect().y; } static int devices_list_bottom() { return status_bar_rect().y; } static int scan_list_top() { Rect scan_btn = scan_button_rect(); return scan_btn.y + scan_btn.h + 12; } static int scan_list_bottom() { return status_bar_rect().y; } static Rect devices_row_rect(int index) { return {0, devices_list_top() + index * ROW_H - g_scroll, g_win_w, ROW_H}; } static Rect scan_row_rect(int index) { return {0, scan_list_top() + index * ROW_H - g_scroll, g_win_w, ROW_H}; } static Rect row_action_button_rect(const Rect& row) { return {g_win_w - PAD - BTN_W, row.y + (ROW_H - BTN_H) / 2, BTN_W, BTN_H}; } // Secondary (Forget) button, left of the primary action button. Used for // paired-but-disconnected rows that show "Connect" + "Forget". static Rect row_forget_button_rect(const Rect& row) { Rect a = row_action_button_rect(row); return {a.x - 8 - FORGET_W, a.y, FORGET_W, BTN_H}; } static int current_row_count() { if (g_tab == TAB_DEVICES) return g_device_count; if (g_tab == TAB_SCAN) return g_scan_count; return 0; // Adapter tab is not a scrollable list } static int current_list_top() { return g_tab == TAB_DEVICES ? devices_list_top() : scan_list_top(); } static int current_list_bottom() { return g_tab == TAB_DEVICES ? devices_list_bottom() : scan_list_bottom(); } static int max_scroll() { int visible_h = gui_max(current_list_bottom() - current_list_top(), 0); int total_h = current_row_count() * ROW_H; if (total_h <= visible_h) return 0; return total_h - visible_h; } static void clamp_scroll() { g_scroll = gui_clamp(g_scroll, 0, max_scroll()); } static void draw_text_fit(Canvas& c, int x, int y, const char* text, int max_w, Color color) { if (!text || !text[0] || max_w <= 4) return; if (text_width(text) <= max_w) { c.text(x, y, text, color); return; } const char* ellipsis = "..."; int ell_w = text_width(ellipsis); if (ell_w >= max_w) return; char buf[96]; int out = 0; while (text[out] && out < (int)sizeof(buf) - 4) { buf[out] = text[out]; buf[out + 1] = '\0'; if (text_width(buf) + ell_w > max_w) break; out++; } if (out <= 0) return; buf[out] = '.'; buf[out + 1] = '.'; buf[out + 2] = '.'; buf[out + 3] = '\0'; c.text(x, y, buf, color); } static void draw_centered_text(Canvas& c, int area_y, int area_h, const char* text, Color color) { int tw = text_width(text); int th = system_font_height(); c.text((g_win_w - tw) / 2, area_y + (area_h - th) / 2, text, color); } static bool mouse_in_rect(const Rect& rect) { return rect.contains(g_mouse_x, g_mouse_y); } static mtk::WidgetState button_state(const Rect& rect, bool enabled, bool active = false) { return mtk::widget_state(active, mouse_in_rect(rect), enabled); } // ============================================================================ // Bluetooth helpers // ============================================================================ static void set_status(const char* msg) { snprintf(g_status, sizeof(g_status), "%s", msg); g_status_time = montauk::get_milliseconds(); } static void refresh_adapter() { montauk::memset(&g_adapter, 0, sizeof(g_adapter)); int r = montauk::bt_info(&g_adapter); g_adapter_ok = (r >= 0 && g_adapter.initialized); } static void refresh_devices() { int active = montauk::bt_list(g_devices, MAX_CONNECTED); if (active < 0) active = 0; g_device_count = active; // Append bonded (paired) devices that aren't currently connected, so the // Devices tab remembers them across sessions/reboots. montauk::abi::BtBondInfo bonds[8]; int nb = montauk::bt_bonds(bonds, 8); for (int i = 0; i < nb && g_device_count < MAX_CONNECTED; i++) { bool already = false; for (int j = 0; j < active; j++) { if (memcmp(g_devices[j].bdAddr, bonds[i].bdAddr, 6) == 0) { already = true; break; } } if (already) continue; montauk::abi::BtDevInfo d = {}; montauk::memcpy(d.bdAddr, bonds[i].bdAddr, 6); d.connected = 0; // paired but not connected d.encrypted = 0; d.handle = 0; d.linkType = 0x01; g_devices[g_device_count++] = d; } for (int i = 0; i < g_device_count; i++) { bool found = false; for (int j = 0; j < g_scan_count; j++) { if (memcmp(g_devices[i].bdAddr, g_scan[j].bdAddr, 6) == 0 && g_scan[j].name[0]) { montauk::memcpy(g_device_names[i], g_scan[j].name, 64); found = true; break; } } if (!found) { snprintf(g_device_names[i], 64, "%02X:%02X:%02X:%02X:%02X:%02X", g_devices[i].bdAddr[5], g_devices[i].bdAddr[4], g_devices[i].bdAddr[3], g_devices[i].bdAddr[2], g_devices[i].bdAddr[1], g_devices[i].bdAddr[0]); } } } static void do_scan() { g_scanning = true; g_scan_count = 0; g_scroll = 0; g_hover_row = -1; set_status("Scanning..."); } static void finish_scan() { int r = montauk::bt_scan(g_scan, MAX_SCAN, 3000); g_scan_count = r > 0 ? r : 0; g_scanning = false; clamp_scroll(); char msg[64]; snprintf(msg, sizeof(msg), "Found %d device%s", g_scan_count, g_scan_count == 1 ? "" : "s"); set_status(msg); } static void do_connect(const uint8_t* addr) { int r = montauk::bt_connect(addr); if (r >= 0) set_status("Connected"); else set_status("Connection failed"); refresh_devices(); clamp_scroll(); } static void do_disconnect(const uint8_t* addr) { int r = montauk::bt_disconnect(addr); if (r >= 0) set_status("Disconnected"); else set_status("Disconnect failed"); refresh_devices(); clamp_scroll(); } static void do_forget(const uint8_t* addr) { int r = montauk::bt_forget(addr); if (r >= 0) set_status("Device forgotten"); else set_status("Forget failed"); refresh_devices(); clamp_scroll(); } static bool is_connected(const uint8_t* addr) { for (int i = 0; i < g_device_count; i++) { if (memcmp(g_devices[i].bdAddr, addr, 6) == 0 && g_devices[i].connected) return true; } return false; } // ============================================================================ // Format helpers // ============================================================================ static const char* device_class_str(uint32_t cod) { uint32_t major = (cod >> 8) & 0x1F; switch (major) { case 1: return "Computer"; case 2: return "Phone"; case 3: return "Network"; case 4: return "Audio/Video"; case 5: return "Peripheral"; case 6: return "Imaging"; case 7: return "Wearable"; default: return "Device"; } } static void format_addr(char* buf, int len, const uint8_t* addr) { snprintf(buf, len, "%02X:%02X:%02X:%02X:%02X:%02X", addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); } static const char* rssi_bar(int8_t rssi) { if (rssi >= -50) return "Strong"; if (rssi >= -70) return "Good"; if (rssi >= -85) return "Weak"; return "Very weak"; } static void set_tab(Tab tab) { if (tab == g_tab) return; g_tab = tab; g_scroll = 0; g_hover_row = -1; if (tab != TAB_ADAPTER) g_mac_focus = false; clamp_scroll(); } // ============================================================================ // Adapter MAC address // ============================================================================ static bool is_mac_char(char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') || ch == ':'; } static bool accept_mac_char(char ch, void* userdata) { (void)userdata; return is_mac_char(ch); } static bool hex_nibble(char c, uint8_t& out) { if (c >= '0' && c <= '9') { out = (uint8_t)(c - '0'); return true; } if (c >= 'a' && c <= 'f') { out = (uint8_t)(c - 'a' + 10); return true; } if (c >= 'A' && c <= 'F') { out = (uint8_t)(c - 'A' + 10); return true; } return false; } // Parse "XX:XX:XX:XX:XX:XX" (':' or '-' separators, optional) into 6 bytes, // out[0] = first printed octet. Returns false unless exactly six octets and no // trailing junk remain. static bool parse_mac(const char* s, uint8_t out[6]) { int byteIdx = 0; const char* p = s; while (byteIdx < 6) { while (*p == ':' || *p == '-' || *p == ' ') p++; uint8_t hi = 0, lo = 0; if (!hex_nibble(p[0], hi) || !hex_nibble(p[1], lo)) return false; out[byteIdx++] = (uint8_t)((hi << 4) | lo); p += 2; } while (*p == ':' || *p == '-' || *p == ' ') p++; return *p == '\0'; } static uint32_t rng_next() { if (g_rng == 0) g_rng = montauk::get_milliseconds() | 1ULL; g_rng ^= g_rng << 13; g_rng ^= g_rng >> 7; g_rng ^= g_rng << 17; return (uint32_t)g_rng; } static void randomize_mac() { uint8_t b[6]; for (int i = 0; i < 6; i++) b[i] = (uint8_t)(rng_next() & 0xFF); // First printed octet: locally-administered + unicast (clear multicast bit). b[0] = (uint8_t)((b[0] & 0xFC) | 0x02); format_addr(g_mac_value, sizeof(g_mac_value), b); g_mac_focus = true; int len = (int)strlen(g_mac_value); mtk::text_input_set_cursor(g_mac_input, len, len, false); } // Prefill the editor from the saved config (if any), else from the live adapter. static void load_mac_config() { montauk::toml::Doc cfg = montauk::config::load("bluetooth"); const char* m = cfg.get_string("adapter.mac", ""); if (m && m[0]) { snprintf(g_mac_value, sizeof(g_mac_value), "%s", m); } else if (g_adapter_ok) { format_addr(g_mac_value, sizeof(g_mac_value), g_adapter.bdAddr); } else { g_mac_value[0] = '\0'; } cfg.destroy(); } static void apply_mac() { uint8_t bytes[6]; if (!parse_mac(g_mac_value, bytes)) { set_status("Invalid MAC address"); return; } if (montauk::bt_set_addr(bytes) < 0) { set_status("Failed to set MAC"); return; } // Normalize and persist to 0:/config/bluetooth.toml so it is re-applied // on every boot by the kernel. char norm[24]; format_addr(norm, sizeof(norm), bytes); montauk::toml::Doc cfg = montauk::config::load("bluetooth"); montauk::config::set_string(&cfg, "adapter.mac", norm); montauk::config::save("bluetooth", &cfg); cfg.destroy(); snprintf(g_mac_value, sizeof(g_mac_value), "%s", norm); g_mac_focus = false; set_status("MAC address changed"); refresh_adapter(); refresh_devices(); } static void use_default_mac() { montauk::toml::Doc cfg = montauk::config::load("bluetooth"); bool had = montauk::config::unset(&cfg, "adapter.mac"); montauk::config::save("bluetooth", &cfg); cfg.destroy(); set_status(had ? "Factory address restored on reboot" : "No custom MAC set"); } // ---- Adapter tab layout ---- static int a_cur_label_y() { return content_rect().y + A_PAD_TOP; } static int a_cur_value_y() { return a_cur_label_y() + system_font_height() + 4; } static int a_new_label_y() { return a_cur_value_y() + system_font_height() + 20; } static Rect adapter_input_rect() { int y = a_new_label_y() + system_font_height() + 6; return {PAD, y, g_win_w - PAD * 2, FIELD_H}; } static Rect adapter_apply_btn() { Rect f = adapter_input_rect(); return {g_win_w - PAD - BTN_W, f.y + f.h + 16, BTN_W, BTN_H}; } static Rect adapter_random_btn() { Rect a = adapter_apply_btn(); return {PAD, a.y, 80, BTN_H}; } static Rect adapter_default_btn() { Rect r = adapter_random_btn(); return {r.x + r.w + 8, r.y, 110, BTN_H}; } // ============================================================================ // Hover and hit testing // ============================================================================ static int get_hover_row(int mx, int my) { if (g_tab == TAB_DEVICES) { for (int i = 0; i < g_device_count; i++) { if (devices_row_rect(i).contains(mx, my)) return i; } } else if (g_tab == TAB_SCAN) { for (int i = 0; i < g_scan_count; i++) { if (scan_row_rect(i).contains(mx, my)) return i; } } return -1; } static bool handle_click(int mx, int my) { int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my); if (tab >= 0) { set_tab((Tab)tab); return true; } if (g_tab == TAB_SCAN) { Rect scan_btn = scan_button_rect(); if (!g_scanning && scan_btn.contains(mx, my)) { do_scan(); return true; } for (int i = 0; i < g_scan_count; i++) { Rect row = scan_row_rect(i); if (!row.contains(mx, my)) continue; Rect action = row_action_button_rect(row); if (action.contains(mx, my)) { if (is_connected(g_scan[i].bdAddr)) do_disconnect(g_scan[i].bdAddr); else do_connect(g_scan[i].bdAddr); return true; } } } else { for (int i = 0; i < g_device_count; i++) { Rect row = devices_row_rect(i); if (!row.contains(mx, my)) continue; Rect action = row_action_button_rect(row); if (g_devices[i].connected) { if (action.contains(mx, my)) { do_disconnect(g_devices[i].bdAddr); return true; } } else { // Paired but not connected: Connect (primary) + Forget. Rect forget = row_forget_button_rect(row); if (action.contains(mx, my)) { do_connect(g_devices[i].bdAddr); return true; } if (forget.contains(mx, my)) { do_forget(g_devices[i].bdAddr); return true; } } } } return false; } // Adapter-tab pointer handling: text field (focus/cursor/selection/context) // plus the Apply / Random / Use Default buttons. The tab bar is handled by the // caller before this runs. Returns true if the view should be redrawn. static bool handle_adapter_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons, int view_w, int view_h) { bool left_press = (buttons & 1) && !(prev_buttons & 1); Rect field = adapter_input_rect(); // While a context menu is open or a drag is in progress, let the widget // consume the event regardless of where the pointer is. if (g_mac_input.context.open || g_mac_input.dragging) { int res = mtk::text_input_handle_mouse( g_mac_input, field, g_mac_value, (int)sizeof(g_mac_value), mx, my, buttons, prev_buttons, view_w, view_h, g_mac_focus, false, accept_mac_char); return res != mtk::TEXT_INPUT_NONE; } if (left_press) { if (g_adapter_ok && adapter_apply_btn().contains(mx, my)) { apply_mac(); return true; } if (g_adapter_ok && adapter_random_btn().contains(mx, my)) { randomize_mac(); return true; } if (adapter_default_btn().contains(mx, my)) { use_default_mac(); return true; } } if (field.contains(mx, my)) { g_mac_focus = true; mtk::text_input_handle_mouse( g_mac_input, field, g_mac_value, (int)sizeof(g_mac_value), mx, my, buttons, prev_buttons, view_w, view_h, true, false, accept_mac_char); return true; } if (left_press) { g_mac_focus = false; return true; } return false; } static bool handle_adapter_key(const montauk::abi::KeyEvent& key) { if (key.ascii == '\n' || key.ascii == '\r') { apply_mac(); return true; } if (key.ascii == '\t') { g_mac_focus = !g_mac_focus; return true; } if (g_mac_focus) { int res = mtk::text_input_key(g_mac_input, g_mac_value, (int)sizeof(g_mac_value), key, accept_mac_char); return res != mtk::TEXT_INPUT_NONE; } // Not editing: number keys still switch tabs; any MAC char starts editing. if (key.ascii == '1') { set_tab(TAB_DEVICES); return true; } if (key.ascii == '2') { set_tab(TAB_SCAN); return true; } if (key.ascii == '3') { return true; } if (is_mac_char(key.ascii)) { g_mac_focus = true; int res = mtk::text_input_key(g_mac_input, g_mac_value, (int)sizeof(g_mac_value), key, accept_mac_char); return res != mtk::TEXT_INPUT_NONE; } return false; } // ============================================================================ // Render // ============================================================================ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) { Rect list = {0, devices_list_top(), g_win_w, gui_max(devices_list_bottom() - devices_list_top(), 0)}; int fh = system_font_height(); if (g_device_count == 0) { draw_centered_text(canvas, list.y, list.h, "No paired devices", theme.text_subtle); return; } for (int i = 0; i < g_device_count; i++) { Rect row = devices_row_rect(i); if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue; if (g_hover_row == i) canvas.fill_rect(row.x, row.y, row.w, row.h, theme.surface_hover); else mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme); bool connected = g_devices[i].connected; Color dot_color = connected ? GREEN : theme.text_subtle; fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color); Rect action = row_action_button_rect(row); Rect forget = row_forget_button_rect(row); int text_x = PAD + 20; // Connected rows have one button (Disconnect); paired rows have two // (Connect + Forget), so the text must stop before the leftmost button. int text_w_max = (connected ? action.x : forget.x) - text_x - 10; draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text); char addr_str[24]; format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr); draw_text_fit(canvas, text_x, row.y + 8 + fh + 2, addr_str, text_w_max, theme.text_subtle); if (connected) { mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER, button_state(action, true), theme); } else { mtk::draw_button(canvas, action, "Connect", mtk::BUTTON_PRIMARY, button_state(action, true), theme); mtk::draw_button(canvas, forget, "Forget", mtk::BUTTON_SECONDARY, button_state(forget, true), theme); } mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme); } } static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) { Rect scan_btn = scan_button_rect(); Rect list = {0, scan_list_top(), g_win_w, gui_max(scan_list_bottom() - scan_list_top(), 0)}; int fh = system_font_height(); if (g_scanning) { mtk::draw_button(canvas, scan_btn, "Scanning...", mtk::BUTTON_SECONDARY, button_state(scan_btn, false), theme); } else { mtk::draw_button(canvas, scan_btn, "Scan", mtk::BUTTON_PRIMARY, button_state(scan_btn, true), theme); } if (g_scan_count == 0 && !g_scanning) { draw_centered_text(canvas, list.y, list.h, "Press Scan to find devices", theme.text_subtle); return; } for (int i = 0; i < g_scan_count; i++) { Rect row = scan_row_rect(i); if (row.y + row.h <= list.y || row.y >= list.y + list.h) continue; if (g_hover_row == i) canvas.fill_rect(row.x, row.y, row.w, row.h, theme.surface_hover); else mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme); fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, theme.accent); Rect action = row_action_button_rect(row); int text_x = PAD + 20; int text_w_max = action.x - text_x - 10; const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device"; draw_text_fit(canvas, text_x, row.y + 4, display_name, text_w_max, theme.text); char detail[80]; char addr_str[24]; format_addr(addr_str, sizeof(addr_str), g_scan[i].bdAddr); snprintf(detail, sizeof(detail), "%s | %s | %s", device_class_str(g_scan[i].classOfDevice), addr_str, rssi_bar(g_scan[i].rssi)); draw_text_fit(canvas, text_x, row.y + 4 + fh + 2, detail, text_w_max, theme.text_subtle); bool conn = is_connected(g_scan[i].bdAddr); mtk::draw_button(canvas, action, conn ? "Disconnect" : "Connect", conn ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY, button_state(action, true), theme); mtk::draw_separator(canvas, PAD, row.y + ROW_H - 1, g_win_w - 2 * PAD, theme); } } static void render_adapter_tab(Canvas& canvas, const mtk::Theme& theme) { // Current address canvas.text(PAD, a_cur_label_y(), "Current Address", theme.text_subtle); if (g_adapter_ok) { char cur[24]; format_addr(cur, sizeof(cur), g_adapter.bdAddr); canvas.text(PAD, a_cur_value_y(), cur, theme.text); } else { canvas.text(PAD, a_cur_value_y(), "No adapter found", theme.danger); } // Editable new address canvas.text(PAD, a_new_label_y(), "New Address", theme.text_muted); Rect field = adapter_input_rect(); mtk::draw_text_field(canvas, field, g_mac_value, g_mac_input.cursor, g_mac_focus, false, theme, g_mac_input.selection_anchor); bool enabled = g_adapter_ok; Rect apply = adapter_apply_btn(); Rect rnd = adapter_random_btn(); Rect def = adapter_default_btn(); mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY, button_state(apply, enabled), theme); mtk::draw_button(canvas, rnd, "Random", mtk::BUTTON_SECONDARY, button_state(rnd, enabled), theme); mtk::draw_button(canvas, def, "Use Default", mtk::BUTTON_SECONDARY, button_state(def, true), theme); draw_text_fit(canvas, PAD, apply.y + BTN_H + 16, "Saved to config and re-applied on boot.", g_win_w - PAD * 2, theme.text_subtle); // Right-click context menu (cut/copy/paste) for the field, drawn last. mtk::draw_text_input_context_menu( canvas, g_mac_input, theme, mtk::text_input_has_selection(g_mac_input, g_mac_value, (int)sizeof(g_mac_value))); } static void render_status(Canvas& canvas, const mtk::Theme& theme) { Rect status = status_bar_rect(); int sy = status.y + (status.h - system_font_height()) / 2; uint64_t now = montauk::get_milliseconds(); canvas.fill_rect(status.x, status.y, status.w, status.h, theme.surface); mtk::draw_separator(canvas, 0, status.y, g_win_w, theme); int status_w = 0; bool show_status = g_status[0] && (now - g_status_time) < 5000; if (show_status) status_w = text_width(g_status); int left_max = g_win_w - PAD * 2 - (show_status ? status_w + 12 : 0); if (g_adapter_ok) { int connected = 0; for (int i = 0; i < g_device_count; i++) if (g_devices[i].connected) connected++; char adapter_info[96]; char addr_str[24]; format_addr(addr_str, sizeof(addr_str), g_adapter.bdAddr); const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter"; snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected", name, addr_str, connected); draw_text_fit(canvas, PAD, sy, adapter_info, left_max, theme.text_subtle); } else { draw_text_fit(canvas, PAD, sy, "No adapter found", left_max, theme.danger); } if (show_status) canvas.text(g_win_w - PAD - status_w, sy, g_status, theme.text_subtle); } static void render(Canvas& canvas) { mtk::Theme theme = bt_theme(); canvas.fill(theme.window_bg); mtk::draw_tab_bar(canvas, tab_bar_rect(), g_tab_labels, TAB_COUNT, g_tab, theme); if (g_tab == TAB_DEVICES) render_devices_tab(canvas, theme); else if (g_tab == TAB_SCAN) render_scan_tab(canvas, theme); else render_adapter_tab(canvas, theme); render_status(canvas, theme); } // ============================================================================ // Entry point // ============================================================================ extern "C" void _start() { if (!fonts::init()) montauk::exit(1); refresh_adapter(); refresh_devices(); load_mac_config(); clamp_scroll(); g_status[0] = 0; WsWindow win; if (!win.create("Bluetooth", WIN_W, WIN_H)) montauk::exit(1); mtk::StandaloneHost host(&win); Canvas canvas = host.canvas(); render(canvas); host.present(); uint64_t last_refresh = montauk::get_milliseconds(); while (true) { montauk::abi::WinEvent ev; int r = win.poll(&ev); if (r < 0) break; if (g_scanning) { canvas = host.canvas(); render(canvas); host.present(); finish_scan(); refresh_devices(); g_hover_row = get_hover_row(g_mouse_x, g_mouse_y); canvas = host.canvas(); render(canvas); host.present(); continue; } if (r == 0) { uint64_t now = montauk::get_milliseconds(); if (now - last_refresh >= 5000) { refresh_adapter(); refresh_devices(); clamp_scroll(); g_hover_row = get_hover_row(g_mouse_x, g_mouse_y); last_refresh = now; canvas = host.canvas(); render(canvas); host.present(); } montauk::sleep_ms(16); continue; } bool redraw = false; if (ev.type == 3) break; if (ev.type == 2) { g_win_w = win.width; g_win_h = win.height; clamp_scroll(); g_hover_row = get_hover_row(g_mouse_x, g_mouse_y); redraw = true; } if (ev.type == 0 && ev.key.pressed) { if (g_tab == TAB_ADAPTER) { // Esc defocuses the field if editing, otherwise closes. if (ev.key.scancode == 0x01 && g_mac_focus) { g_mac_focus = false; redraw = true; } else if (ev.key.scancode == 0x01) { break; } else if (handle_adapter_key(ev.key)) { redraw = true; } } else { if (ev.key.scancode == 0x01) break; if ((ev.key.ascii == 's' || ev.key.ascii == 'S') && !g_scanning) { do_scan(); redraw = true; } if (ev.key.ascii == '1') { set_tab(TAB_DEVICES); redraw = true; } if (ev.key.ascii == '2') { set_tab(TAB_SCAN); redraw = true; } if (ev.key.ascii == '3') { set_tab(TAB_ADAPTER); redraw = true; } } } if (ev.type == 1) { bool moved = (ev.mouse.x != g_mouse_x) || (ev.mouse.y != g_mouse_y); g_mouse_x = ev.mouse.x; g_mouse_y = ev.mouse.y; if (g_tab == TAB_ADAPTER) { if (moved) redraw = true; bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); int tab = clicked ? mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, g_mouse_x, g_mouse_y) : -1; if (tab >= 0) { set_tab((Tab)tab); redraw = true; } else if (handle_adapter_mouse(g_mouse_x, g_mouse_y, ev.mouse.buttons, ev.mouse.prev_buttons, win.width, win.height)) { redraw = true; } } else { int new_hover = get_hover_row(g_mouse_x, g_mouse_y); if (new_hover != g_hover_row) { g_hover_row = new_hover; redraw = true; } if (moved) redraw = true; bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (clicked && handle_click(ev.mouse.x, ev.mouse.y)) redraw = true; if (ev.mouse.scroll) { g_scroll -= ev.mouse.scroll * SCROLL_STEP; clamp_scroll(); g_hover_row = get_hover_row(g_mouse_x, g_mouse_y); redraw = true; } } } if (redraw) { canvas = host.canvas(); render(canvas); host.present(); } } win.destroy(); montauk::exit(0); }