diff --git a/original-src/binary_protocol.rs b/original-src/binary_protocol.rs deleted file mode 100644 index fa34b85..0000000 --- a/original-src/binary_protocol.rs +++ /dev/null @@ -1,259 +0,0 @@ -/// KaSe Binary CDC Protocol v2 -/// Frame: KS(2) + cmd(1) + len(2 LE) + payload(N) + crc8(1) -/// Response: KR(2) + cmd(1) + status(1) + len(2 LE) + payload(N) + crc8(1) - -#[allow(dead_code)] -pub mod cmd { - // System - pub const VERSION: u8 = 0x01; - pub const FEATURES: u8 = 0x02; - pub const DFU: u8 = 0x03; - pub const PING: u8 = 0x04; - - // Keymap - pub const SETLAYER: u8 = 0x10; - pub const SETKEY: u8 = 0x11; - pub const KEYMAP_CURRENT: u8 = 0x12; - pub const KEYMAP_GET: u8 = 0x13; - pub const LAYER_INDEX: u8 = 0x14; - pub const LAYER_NAME: u8 = 0x15; - - // Layout - pub const SET_LAYOUT_NAME: u8 = 0x20; - pub const LIST_LAYOUTS: u8 = 0x21; - pub const GET_LAYOUT_JSON: u8 = 0x22; - - // Macros - pub const LIST_MACROS: u8 = 0x30; - pub const MACRO_ADD: u8 = 0x31; - pub const MACRO_ADD_SEQ: u8 = 0x32; - pub const MACRO_DELETE: u8 = 0x33; - - // Statistics - pub const KEYSTATS_BIN: u8 = 0x40; - pub const KEYSTATS_RESET: u8 = 0x42; - pub const BIGRAMS_BIN: u8 = 0x43; - pub const BIGRAMS_RESET: u8 = 0x45; - - // Tap Dance - pub const TD_SET: u8 = 0x50; - pub const TD_LIST: u8 = 0x51; - pub const TD_DELETE: u8 = 0x52; - - // Combos - pub const COMBO_SET: u8 = 0x60; - pub const COMBO_LIST: u8 = 0x61; - pub const COMBO_DELETE: u8 = 0x62; - - // Leader - pub const LEADER_SET: u8 = 0x70; - pub const LEADER_LIST: u8 = 0x71; - pub const LEADER_DELETE: u8 = 0x72; - - // Bluetooth - pub const BT_QUERY: u8 = 0x80; - pub const BT_SWITCH: u8 = 0x81; - pub const BT_PAIR: u8 = 0x82; - pub const BT_DISCONNECT: u8 = 0x83; - pub const BT_NEXT: u8 = 0x84; - pub const BT_PREV: u8 = 0x85; - - // Features - pub const AUTOSHIFT_TOGGLE: u8 = 0x90; - pub const KO_SET: u8 = 0x91; - pub const KO_LIST: u8 = 0x92; - pub const KO_DELETE: u8 = 0x93; - pub const WPM_QUERY: u8 = 0x94; - pub const TRILAYER_SET: u8 = 0x94; - - // Tamagotchi - pub const TAMA_QUERY: u8 = 0xA0; - pub const TAMA_ENABLE: u8 = 0xA1; - pub const TAMA_DISABLE: u8 = 0xA2; - pub const TAMA_FEED: u8 = 0xA3; - pub const TAMA_PLAY: u8 = 0xA4; - pub const TAMA_SLEEP: u8 = 0xA5; - pub const TAMA_MEDICINE: u8 = 0xA6; - pub const TAMA_SAVE: u8 = 0xA7; - - // OTA - pub const OTA_START: u8 = 0xF0; - pub const OTA_DATA: u8 = 0xF1; - pub const OTA_ABORT: u8 = 0xF2; -} - -#[allow(dead_code)] -pub mod status { - pub const OK: u8 = 0x00; - pub const ERR_UNKNOWN: u8 = 0x01; - pub const ERR_CRC: u8 = 0x02; - pub const ERR_INVALID: u8 = 0x03; - pub const ERR_RANGE: u8 = 0x04; - pub const ERR_BUSY: u8 = 0x05; - pub const ERR_OVERFLOW: u8 = 0x06; -} - -/// CRC-8/MAXIM (polynomial 0x31, init 0x00) -pub fn crc8(data: &[u8]) -> u8 { - let mut crc: u8 = 0x00; - for &b in data { - crc ^= b; - for _ in 0..8 { - crc = if crc & 0x80 != 0 { - (crc << 1) ^ 0x31 - } else { - crc << 1 - }; - } - } - crc -} - -/// Build a KS request frame. -pub fn ks_frame(cmd_id: u8, payload: &[u8]) -> Vec { - let len = payload.len() as u16; - let mut frame = Vec::with_capacity(6 + payload.len()); - frame.push(0x4B); // 'K' - frame.push(0x53); // 'S' - frame.push(cmd_id); - frame.push((len & 0xFF) as u8); - frame.push((len >> 8) as u8); - frame.extend_from_slice(payload); - frame.push(crc8(payload)); - frame -} - -/// Build MACRO_ADD_SEQ payload: [slot][name_len][name...][step_count][{kc,mod}...] -pub fn macro_add_seq_payload(slot: u8, name: &str, steps_hex: &str) -> Vec { - let name_bytes = name.as_bytes(); - let name_len = name_bytes.len().min(255) as u8; - - // Parse hex steps "06:01,FF:0A,19:01" into (kc, mod) pairs - let mut step_pairs: Vec<(u8, u8)> = Vec::new(); - if !steps_hex.is_empty() { - for part in steps_hex.split(',') { - let trimmed = part.trim(); - let kv: Vec<&str> = trimmed.split(':').collect(); - let has_two = kv.len() == 2; - if !has_two { - continue; - } - let kc = u8::from_str_radix(kv[0].trim(), 16).unwrap_or(0); - let md = u8::from_str_radix(kv[1].trim(), 16).unwrap_or(0); - step_pairs.push((kc, md)); - } - } - let step_count = step_pairs.len().min(255) as u8; - - let mut payload = Vec::new(); - payload.push(slot); - payload.push(name_len); - payload.extend_from_slice(&name_bytes[..name_len as usize]); - payload.push(step_count); - for (kc, md) in &step_pairs { - payload.push(*kc); - payload.push(*md); - } - payload -} - -/// Build MACRO_DELETE payload: [slot] -pub fn macro_delete_payload(slot: u8) -> Vec { - vec![slot] -} - -/// Build COMBO_SET payload: [index][r1][c1][r2][c2][result] -pub fn combo_set_payload(index: u8, r1: u8, c1: u8, r2: u8, c2: u8, result: u8) -> Vec { - vec![index, r1, c1, r2, c2, result] -} - -/// Build TD_SET payload: [index][a1][a2][a3][a4] -pub fn td_set_payload(index: u8, actions: &[u16; 4]) -> Vec { - vec![index, actions[0] as u8, actions[1] as u8, actions[2] as u8, actions[3] as u8] -} - -/// Build KO_SET payload: [index][trigger_key][trigger_mod][result_key][result_mod] -pub fn ko_set_payload(index: u8, trig_key: u8, trig_mod: u8, res_key: u8, res_mod: u8) -> Vec { - vec![index, trig_key, trig_mod, res_key, res_mod] -} - -/// Build LEADER_SET payload: [index][seq_len][seq...][result][result_mod] -pub fn leader_set_payload(index: u8, sequence: &[u8], result: u8, result_mod: u8) -> Vec { - let seq_len = sequence.len().min(4) as u8; - let mut payload = Vec::with_capacity(4 + sequence.len()); - payload.push(index); - payload.push(seq_len); - payload.extend_from_slice(&sequence[..seq_len as usize]); - payload.push(result); - payload.push(result_mod); - payload -} - -/// Parsed KR response. -#[derive(Debug)] -pub struct KrResponse { - #[allow(dead_code)] - pub cmd: u8, - pub status: u8, - pub payload: Vec, -} - -impl KrResponse { - pub fn is_ok(&self) -> bool { - self.status == status::OK - } - - pub fn status_name(&self) -> &str { - match self.status { - status::OK => "OK", - status::ERR_UNKNOWN => "ERR_UNKNOWN", - status::ERR_CRC => "ERR_CRC", - status::ERR_INVALID => "ERR_INVALID", - status::ERR_RANGE => "ERR_RANGE", - status::ERR_BUSY => "ERR_BUSY", - status::ERR_OVERFLOW => "ERR_OVERFLOW", - _ => "UNKNOWN", - } - } -} - -/// Parse a KR response from raw bytes. Returns (response, bytes_consumed). -pub fn parse_kr(data: &[u8]) -> Result<(KrResponse, usize), String> { - // Find KR magic - let pos = data - .windows(2) - .position(|w| w[0] == 0x4B && w[1] == 0x52) - .ok_or("No KR header found")?; - - if data.len() < pos + 7 { - return Err("Response too short".into()); - } - - let cmd = data[pos + 2]; - let status = data[pos + 3]; - let plen = data[pos + 4] as u16 | ((data[pos + 5] as u16) << 8); - let payload_start = pos + 6; - let payload_end = payload_start + plen as usize; - - if data.len() < payload_end + 1 { - return Err(format!( - "Incomplete response: need {} bytes, got {}", - payload_end + 1, - data.len() - )); - } - - let payload = data[payload_start..payload_end].to_vec(); - let expected_crc = data[payload_end]; - let actual_crc = crc8(&payload); - - if expected_crc != actual_crc { - return Err(format!( - "CRC mismatch: expected 0x{:02X}, got 0x{:02X}", - expected_crc, actual_crc - )); - } - - let consumed = payload_end + 1 - pos; - Ok((KrResponse { cmd, status, payload }, consumed)) -} diff --git a/original-src/flasher.rs b/original-src/flasher.rs deleted file mode 100644 index 8a688f8..0000000 --- a/original-src/flasher.rs +++ /dev/null @@ -1,501 +0,0 @@ -/// ESP32 ROM bootloader flasher via serial (CH340/CP2102 programming port). -/// Implements minimal SLIP-framed bootloader protocol for firmware flashing -/// without requiring esptool. - -#[cfg(not(target_arch = "wasm32"))] -use serialport::SerialPort; -#[cfg(not(target_arch = "wasm32"))] -use std::sync::mpsc; -#[cfg(not(target_arch = "wasm32"))] -use std::time::{Duration, Instant}; - -// ==================== SLIP framing ==================== - -const SLIP_END: u8 = 0xC0; -const SLIP_ESC: u8 = 0xDB; -const SLIP_ESC_END: u8 = 0xDC; -const SLIP_ESC_ESC: u8 = 0xDD; - -#[cfg(not(target_arch = "wasm32"))] -fn slip_encode(data: &[u8]) -> Vec { - let mut frame = Vec::with_capacity(data.len() + 10); - frame.push(SLIP_END); - for &byte in data { - match byte { - SLIP_END => { - frame.push(SLIP_ESC); - frame.push(SLIP_ESC_END); - } - SLIP_ESC => { - frame.push(SLIP_ESC); - frame.push(SLIP_ESC_ESC); - } - _ => frame.push(byte), - } - } - frame.push(SLIP_END); - frame -} - -#[cfg(not(target_arch = "wasm32"))] -fn slip_decode(frame: &[u8]) -> Vec { - let mut data = Vec::with_capacity(frame.len()); - let mut escaped = false; - for &byte in frame { - if escaped { - match byte { - SLIP_ESC_END => data.push(SLIP_END), - SLIP_ESC_ESC => data.push(SLIP_ESC), - _ => data.push(byte), - } - escaped = false; - } else if byte == SLIP_ESC { - escaped = true; - } else if byte != SLIP_END { - data.push(byte); - } - } - data -} - -// ==================== Bootloader commands ==================== - -const CMD_SYNC: u8 = 0x08; -const CMD_CHANGE_BAUDRATE: u8 = 0x0F; -const CMD_SPI_ATTACH: u8 = 0x0D; -const CMD_FLASH_BEGIN: u8 = 0x02; -const CMD_FLASH_DATA: u8 = 0x03; -const CMD_FLASH_END: u8 = 0x04; - -const FLASH_BLOCK_SIZE: u32 = 1024; -const INITIAL_BAUD: u32 = 115200; -const FLASH_BAUD: u32 = 460800; - -#[cfg(not(target_arch = "wasm32"))] -fn xor_checksum(data: &[u8]) -> u32 { - let mut chk: u8 = 0xEF; - for &b in data { - chk ^= b; - } - chk as u32 -} - -/// Build a bootloader command packet (before SLIP encoding). -/// Format: [0x00][cmd][size:u16 LE][checksum:u32 LE][data...] -#[cfg(not(target_arch = "wasm32"))] -fn build_command(cmd: u8, data: &[u8], checksum: u32) -> Vec { - let size = data.len() as u16; - let mut pkt = Vec::with_capacity(8 + data.len()); - pkt.push(0x00); // direction: command - pkt.push(cmd); - pkt.push((size & 0xFF) as u8); - pkt.push((size >> 8) as u8); - pkt.push((checksum & 0xFF) as u8); - pkt.push(((checksum >> 8) & 0xFF) as u8); - pkt.push(((checksum >> 16) & 0xFF) as u8); - pkt.push(((checksum >> 24) & 0xFF) as u8); - pkt.extend_from_slice(data); - pkt -} - -/// Extract complete SLIP frames from a byte buffer. -/// Returns (frames, remaining_bytes_not_consumed). -#[cfg(not(target_arch = "wasm32"))] -fn extract_slip_frames(raw: &[u8]) -> Vec> { - let mut frames = Vec::new(); - let mut in_frame = false; - let mut current = Vec::new(); - - for &byte in raw { - if byte == SLIP_END { - if in_frame && !current.is_empty() { - // End of frame - frames.push(current.clone()); - current.clear(); - in_frame = false; - } else { - // Start of frame (or consecutive 0xC0) - in_frame = true; - current.clear(); - } - } else if in_frame { - current.push(byte); - } - // If !in_frame and byte != SLIP_END, it's garbage — skip - } - frames -} - -/// Send a command and receive a valid response. -/// Handles boot log garbage and multiple SYNC responses. -#[cfg(not(target_arch = "wasm32"))] -fn send_command( - port: &mut Box, - cmd: u8, - data: &[u8], - checksum: u32, - timeout_ms: u64, -) -> Result, String> { - let pkt = build_command(cmd, data, checksum); - let frame = slip_encode(&pkt); - - port.write_all(&frame) - .map_err(|e| format!("Write error: {}", e))?; - port.flush() - .map_err(|e| format!("Flush error: {}", e))?; - - // Read bytes and extract SLIP frames, looking for a valid response - let mut raw = Vec::new(); - let mut buf = [0u8; 512]; - let start = Instant::now(); - let timeout = Duration::from_millis(timeout_ms); - - loop { - let elapsed = start.elapsed(); - if elapsed > timeout { - let got = if raw.is_empty() { - "nothing".to_string() - } else { - format!("{} raw bytes, no valid response", raw.len()) - }; - return Err(format!("Response timeout (got {})", got)); - } - - let read_result = port.read(&mut buf); - match read_result { - Ok(n) if n > 0 => { - raw.extend_from_slice(&buf[..n]); - } - _ => { - std::thread::sleep(Duration::from_millis(1)); - if raw.is_empty() { - continue; - } - } - } - - // Try to find a valid response in accumulated data - let frames = extract_slip_frames(&raw); - for slip_data in &frames { - let decoded = slip_decode(slip_data); - - if decoded.len() < 8 { - continue; - } - - let direction = decoded[0]; - let resp_cmd = decoded[1]; - - if direction != 0x01 || resp_cmd != cmd { - continue; - } - - // ROM bootloader status is at offset 8 (right after 8-byte header) - // Format: [dir][cmd][size:u16][value:u32][status][error][pad][pad] - if decoded.len() >= 10 { - let status = decoded[8]; - let error = decoded[9]; - if status != 0 { - return Err(format!("Bootloader error: cmd=0x{:02X} status={}, error={} (0x{:02X})", - cmd, status, error, error)); - } - } - - return Ok(decoded); - } - } -} - -// ==================== Bootloader entry ==================== - -/// Toggle DTR/RTS to reset ESP32 into bootloader mode. -/// Standard auto-reset circuit: DTR→EN, RTS→GPIO0. -#[cfg(not(target_arch = "wasm32"))] -fn enter_bootloader(port: &mut Box) -> Result<(), String> { - // Hold GPIO0 low (RTS=true) while pulsing EN (DTR) - port.write_data_terminal_ready(false) - .map_err(|e| format!("DTR error: {}", e))?; - port.write_request_to_send(true) - .map_err(|e| format!("RTS error: {}", e))?; - std::thread::sleep(Duration::from_millis(100)); - - // Release EN (DTR=true) while keeping GPIO0 low - port.write_data_terminal_ready(true) - .map_err(|e| format!("DTR error: {}", e))?; - port.write_request_to_send(false) - .map_err(|e| format!("RTS error: {}", e))?; - std::thread::sleep(Duration::from_millis(50)); - - // Release all - port.write_data_terminal_ready(false) - .map_err(|e| format!("DTR error: {}", e))?; - - // Drain any boot message - let _ = port.clear(serialport::ClearBuffer::All); - std::thread::sleep(Duration::from_millis(200)); - - Ok(()) -} - -// ==================== High-level commands ==================== - -#[cfg(not(target_arch = "wasm32"))] -fn sync(port: &mut Box) -> Result<(), String> { - // SYNC payload: [0x07, 0x07, 0x12, 0x20] + 32 x 0x55 - let mut payload = vec![0x07, 0x07, 0x12, 0x20]; - payload.extend_from_slice(&[0x55; 32]); - - for attempt in 0..10 { - let result = send_command(port, CMD_SYNC, &payload, 0, 500); - match result { - Ok(_) => return Ok(()), - Err(_) if attempt < 9 => { - // Drain any pending data before retry - let _ = port.clear(serialport::ClearBuffer::Input); - std::thread::sleep(Duration::from_millis(50)); - } - Err(e) => return Err(format!("SYNC failed after 10 attempts: {}", e)), - } - } - Err("SYNC failed".into()) -} - -/// Tell the bootloader to switch to a faster baud rate, then reconnect. -#[cfg(not(target_arch = "wasm32"))] -fn change_baudrate(port: &mut Box, new_baud: u32) -> Result<(), String> { - // Payload: [new_baud:u32 LE][old_baud:u32 LE] (old_baud=0 means "current") - let mut payload = Vec::with_capacity(8); - payload.extend_from_slice(&new_baud.to_le_bytes()); - payload.extend_from_slice(&0u32.to_le_bytes()); - - send_command(port, CMD_CHANGE_BAUDRATE, &payload, 0, 3000)?; - - // Switch host side to new baud - port.set_baud_rate(new_baud) - .map_err(|e| format!("Set baud error: {}", e))?; - - // Small delay for baud switch to take effect - std::thread::sleep(Duration::from_millis(50)); - let _ = port.clear(serialport::ClearBuffer::All); - - Ok(()) -} - -#[cfg(not(target_arch = "wasm32"))] -fn spi_attach(port: &mut Box) -> Result<(), String> { - let payload = [0u8; 8]; - send_command(port, CMD_SPI_ATTACH, &payload, 0, 3000)?; - Ok(()) -} - -#[cfg(not(target_arch = "wasm32"))] -fn flash_begin( - port: &mut Box, - offset: u32, - total_size: u32, - block_size: u32, -) -> Result<(), String> { - let num_blocks = (total_size + block_size - 1) / block_size; - - let mut payload = Vec::with_capacity(20); - // erase_size - payload.extend_from_slice(&total_size.to_le_bytes()); - // num_blocks - payload.extend_from_slice(&num_blocks.to_le_bytes()); - // block_size - payload.extend_from_slice(&block_size.to_le_bytes()); - // offset - payload.extend_from_slice(&offset.to_le_bytes()); - // encrypted (ESP32-S3 requires this 5th field — 0 = not encrypted) - payload.extend_from_slice(&0u32.to_le_bytes()); - - // FLASH_BEGIN can take a while (flash erase) — long timeout - send_command(port, CMD_FLASH_BEGIN, &payload, 0, 30_000)?; - Ok(()) -} - -#[cfg(not(target_arch = "wasm32"))] -fn flash_data( - port: &mut Box, - seq: u32, - data: &[u8], -) -> Result<(), String> { - let data_len = data.len() as u32; - - let mut payload = Vec::with_capacity(16 + data.len()); - // data length - payload.extend_from_slice(&data_len.to_le_bytes()); - // sequence number - payload.extend_from_slice(&seq.to_le_bytes()); - // reserved (2 x u32) - payload.extend_from_slice(&0u32.to_le_bytes()); - payload.extend_from_slice(&0u32.to_le_bytes()); - // data - payload.extend_from_slice(data); - - let checksum = xor_checksum(data); - send_command(port, CMD_FLASH_DATA, &payload, checksum, 10_000)?; - Ok(()) -} - -#[cfg(not(target_arch = "wasm32"))] -fn flash_end(port: &mut Box, reboot: bool) -> Result<(), String> { - let flag: u32 = if reboot { 0 } else { 1 }; - let payload = flag.to_le_bytes(); - // FLASH_END might not get a response if device reboots - let _ = send_command(port, CMD_FLASH_END, &payload, 0, 2000); - - if reboot { - // Hard reset: toggle RTS to pulse EN pin (like esptool --after hard_reset) - std::thread::sleep(Duration::from_millis(100)); - port.write_request_to_send(true) - .map_err(|e| format!("RTS error: {}", e))?; - std::thread::sleep(Duration::from_millis(100)); - port.write_request_to_send(false) - .map_err(|e| format!("RTS error: {}", e))?; - } - - Ok(()) -} - -// ==================== Main entry point ==================== - -/// Flash firmware to ESP32 via programming port (CH340/CP2102). -/// Sends progress updates via the channel as (progress_0_to_1, status_message). -#[cfg(not(target_arch = "wasm32"))] -pub fn flash_firmware( - port_name: &str, - firmware: &[u8], - offset: u32, - tx: &mpsc::Sender, -) -> Result<(), String> { - let send_progress = |progress: f32, msg: String| { - let _ = tx.send(super::ui::BgResult::OtaProgress(progress, msg)); - }; - - send_progress(0.0, "Opening port...".into()); - - let builder = serialport::new(port_name, INITIAL_BAUD); - let builder_timeout = builder.timeout(Duration::from_millis(500)); - let mut port = builder_timeout.open() - .map_err(|e| format!("Cannot open {}: {}", port_name, e))?; - - // Step 1: Enter bootloader - send_progress(0.0, "Resetting into bootloader...".into()); - enter_bootloader(&mut port)?; - - // Step 2: Sync at 115200 - send_progress(0.0, "Syncing with bootloader...".into()); - sync(&mut port)?; - send_progress(0.02, "Bootloader sync OK".into()); - - // Step 3: Switch to 460800 baud for faster flashing - send_progress(0.03, format!("Switching to {} baud...", FLASH_BAUD)); - change_baudrate(&mut port, FLASH_BAUD)?; - send_progress(0.04, format!("Baud: {}", FLASH_BAUD)); - - // Step 4: SPI attach - send_progress(0.05, "Attaching SPI flash...".into()); - spi_attach(&mut port)?; - - // Step 5: Flash begin (this erases the flash — can take several seconds) - let total_size = firmware.len() as u32; - let num_blocks = (total_size + FLASH_BLOCK_SIZE - 1) / FLASH_BLOCK_SIZE; - send_progress(0.05, format!("Erasing flash ({} KB)...", total_size / 1024)); - flash_begin(&mut port, offset, total_size, FLASH_BLOCK_SIZE)?; - send_progress(0.10, "Flash erased, writing...".into()); - - // Step 6: Flash data blocks - for (i, chunk) in firmware.chunks(FLASH_BLOCK_SIZE as usize).enumerate() { - // Pad last block to block_size - let mut block = chunk.to_vec(); - let pad_needed = FLASH_BLOCK_SIZE as usize - block.len(); - if pad_needed > 0 { - block.extend(std::iter::repeat(0xFF).take(pad_needed)); - } - - flash_data(&mut port, i as u32, &block)?; - - let blocks_done = (i + 1) as f32; - let total_blocks = num_blocks as f32; - let progress = 0.10 + 0.85 * (blocks_done / total_blocks); - let msg = format!("Writing block {}/{} ({} KB / {} KB)", - i + 1, num_blocks, - ((i + 1) as u32 * FLASH_BLOCK_SIZE).min(total_size) / 1024, - total_size / 1024); - send_progress(progress, msg); - } - - // Step 7: Flash end + reboot - send_progress(0.97, "Finalizing...".into()); - flash_end(&mut port, true)?; - - send_progress(1.0, format!("Flash OK — {} KB written at 0x{:X}", total_size / 1024, offset)); - Ok(()) -} - -// ==================== Tests ==================== - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn slip_encode_no_special() { - let data = vec![0x01, 0x02, 0x03]; - let encoded = slip_encode(&data); - assert_eq!(encoded, vec![0xC0, 0x01, 0x02, 0x03, 0xC0]); - } - - #[test] - fn slip_encode_with_end_byte() { - let data = vec![0x01, 0xC0, 0x03]; - let encoded = slip_encode(&data); - assert_eq!(encoded, vec![0xC0, 0x01, 0xDB, 0xDC, 0x03, 0xC0]); - } - - #[test] - fn slip_encode_with_esc_byte() { - let data = vec![0x01, 0xDB, 0x03]; - let encoded = slip_encode(&data); - assert_eq!(encoded, vec![0xC0, 0x01, 0xDB, 0xDD, 0x03, 0xC0]); - } - - #[test] - fn slip_roundtrip() { - let original = vec![0xC0, 0xDB, 0x00, 0xFF, 0xC0]; - let encoded = slip_encode(&original); - let decoded = slip_decode(&encoded); - assert_eq!(decoded, original); - } - - #[test] - fn xor_checksum_basic() { - let data = vec![0x01, 0x02, 0x03]; - let chk = xor_checksum(&data); - let expected = 0xEF ^ 0x01 ^ 0x02 ^ 0x03; - assert_eq!(chk, expected as u32); - } - - #[test] - fn xor_checksum_empty() { - let chk = xor_checksum(&[]); - assert_eq!(chk, 0xEF); - } - - #[test] - fn build_command_format() { - let data = vec![0xAA, 0xBB]; - let pkt = build_command(0x08, &data, 0x12345678); - assert_eq!(pkt[0], 0x00); // direction - assert_eq!(pkt[1], 0x08); // command - assert_eq!(pkt[2], 0x02); // size low - assert_eq!(pkt[3], 0x00); // size high - assert_eq!(pkt[4], 0x78); // checksum byte 0 - assert_eq!(pkt[5], 0x56); // checksum byte 1 - assert_eq!(pkt[6], 0x34); // checksum byte 2 - assert_eq!(pkt[7], 0x12); // checksum byte 3 - assert_eq!(pkt[8], 0xAA); // data - assert_eq!(pkt[9], 0xBB); - } -} diff --git a/original-src/keycode.rs b/original-src/keycode.rs deleted file mode 100644 index 6709ee3..0000000 --- a/original-src/keycode.rs +++ /dev/null @@ -1,390 +0,0 @@ -/// Decode a raw 16-bit keycode into a human-readable string. -/// -/// Covers all KaSe firmware keycode ranges: HID basic keys, layer switches, -/// macros, Bluetooth, one-shot, mod-tap, layer-tap, tap-dance, and more. -pub fn decode_keycode(raw: u16) -> String { - // --- HID basic keycodes 0x00..=0xE7 --- - if raw <= 0x00E7 { - return hid_key_name(raw as u8); - } - - // --- MO (Momentary Layer): 0x0100..=0x0A00, low byte == 0 --- - if raw >= 0x0100 && raw <= 0x0A00 && (raw & 0xFF) == 0 { - let layer = (raw >> 8) - 1; - return format!("MO {layer}"); - } - - // --- TO (Toggle Layer): 0x0B00..=0x1400, low byte == 0 --- - if raw >= 0x0B00 && raw <= 0x1400 && (raw & 0xFF) == 0 { - let layer = (raw >> 8) - 0x0B; - return format!("TO {layer}"); - } - - // --- MACRO: 0x1500..=0x2800, low byte == 0 --- - if raw >= 0x1500 && raw <= 0x2800 && (raw & 0xFF) == 0 { - let idx = (raw >> 8) - 0x14; - return format!("M{idx}"); - } - - // --- BT keycodes --- - match raw { - 0x2900 => return "BT Next".into(), - 0x2A00 => return "BT Prev".into(), - 0x2B00 => return "BT Pair".into(), - 0x2C00 => return "BT Disc".into(), - 0x2E00 => return "USB/BT".into(), - 0x2F00 => return "BT On/Off".into(), - _ => {} - } - - // --- OSM (One-Shot Mod): 0x3000..=0x30FF --- - if raw >= 0x3000 && raw <= 0x30FF { - let mods = (raw & 0xFF) as u8; - return format!("OSM {}", mod_name(mods)); - } - - // --- OSL (One-Shot Layer): 0x3100..=0x310F --- - if raw >= 0x3100 && raw <= 0x310F { - let layer = raw & 0x0F; - return format!("OSL {layer}"); - } - - // --- Fixed special codes --- - match raw { - 0x3200 => return "Caps Word".into(), - 0x3300 => return "Repeat".into(), - 0x3400 => return "Leader".into(), - 0x3500 => return "Feed".into(), - 0x3600 => return "Play".into(), - 0x3700 => return "Sleep".into(), - 0x3800 => return "Meds".into(), - 0x3900 => return "GEsc".into(), - 0x3A00 => return "Layer Lock".into(), - 0x3C00 => return "AS Toggle".into(), - _ => {} - } - - // --- KO (Key Override) slots: 0x3D00..=0x3DFF --- - if raw >= 0x3D00 && raw <= 0x3DFF { - let slot = raw & 0xFF; - return format!("KO {slot}"); - } - - // --- LT (Layer-Tap): 0x4000..=0x4FFF --- - // layout: 0x4LKK where L = layer (0..F), KK = HID keycode - if raw >= 0x4000 && raw <= 0x4FFF { - let layer = (raw >> 8) & 0x0F; - let kc = (raw & 0xFF) as u8; - return format!("LT {} {}", layer, hid_key_name(kc)); - } - - // --- MT (Mod-Tap): 0x5000..=0x5FFF --- - // layout: 0x5MKK where M = mod nibble (4 bits), KK = HID keycode - if raw >= 0x5000 && raw <= 0x5FFF { - let mods = ((raw >> 8) & 0x0F) as u8; - let kc = (raw & 0xFF) as u8; - return format!("MT {} {}", mod_name(mods), hid_key_name(kc)); - } - - // --- TD (Tap Dance): 0x6000..=0x6FFF --- - if raw >= 0x6000 && raw <= 0x6FFF { - let index = (raw >> 8) & 0x0F; - return format!("TD {index}"); - } - - // --- Unknown --- - format!("0x{raw:04X}") -} - -/// Decode a modifier bitmask into a human-readable string. -/// -/// Bits: 0x01=Ctrl, 0x02=Shift, 0x04=Alt, 0x08=GUI, -/// 0x10=RCtrl, 0x20=RShift, 0x40=RAlt, 0x80=RGUI. -/// Multiple modifiers are joined with "+". -pub fn mod_name(mod_mask: u8) -> String { - let mut parts = Vec::new(); - if mod_mask & 0x01 != 0 { parts.push("Ctrl"); } - if mod_mask & 0x02 != 0 { parts.push("Shift"); } - if mod_mask & 0x04 != 0 { parts.push("Alt"); } - if mod_mask & 0x08 != 0 { parts.push("GUI"); } - if mod_mask & 0x10 != 0 { parts.push("RCtrl"); } - if mod_mask & 0x20 != 0 { parts.push("RShift"); } - if mod_mask & 0x40 != 0 { parts.push("RAlt"); } - if mod_mask & 0x80 != 0 { parts.push("RGUI"); } - if parts.is_empty() { - format!("0x{mod_mask:02X}") - } else { - parts.join("+") - } -} - -/// Map a single HID usage code (0x00..=0xE7) to a short readable name. -pub fn hid_key_name(code: u8) -> String { - match code { - // No key / transparent - 0x00 => "None", - // 0x01 = ErrorRollOver, 0x02 = POSTFail, 0x03 = ErrorUndefined (not user-facing) - 0x01 => "ErrRollOver", - 0x02 => "POSTFail", - 0x03 => "ErrUndef", - - // Letters - 0x04 => "A", - 0x05 => "B", - 0x06 => "C", - 0x07 => "D", - 0x08 => "E", - 0x09 => "F", - 0x0A => "G", - 0x0B => "H", - 0x0C => "I", - 0x0D => "J", - 0x0E => "K", - 0x0F => "L", - 0x10 => "M", - 0x11 => "N", - 0x12 => "O", - 0x13 => "P", - 0x14 => "Q", - 0x15 => "R", - 0x16 => "S", - 0x17 => "T", - 0x18 => "U", - 0x19 => "V", - 0x1A => "W", - 0x1B => "X", - 0x1C => "Y", - 0x1D => "Z", - - // Number row - 0x1E => "1", - 0x1F => "2", - 0x20 => "3", - 0x21 => "4", - 0x22 => "5", - 0x23 => "6", - 0x24 => "7", - 0x25 => "8", - 0x26 => "9", - 0x27 => "0", - - // Common control keys - 0x28 => "Enter", - 0x29 => "Esc", - 0x2A => "Backspace", - 0x2B => "Tab", - 0x2C => "Space", - - // Punctuation / symbols - 0x2D => "-", - 0x2E => "=", - 0x2F => "[", - 0x30 => "]", - 0x31 => "\\", - 0x32 => "Europe1", - 0x33 => ";", - 0x34 => "'", - 0x35 => "`", - 0x36 => ",", - 0x37 => ".", - 0x38 => "/", - - // Caps Lock - 0x39 => "Caps Lock", - - // Function keys - 0x3A => "F1", - 0x3B => "F2", - 0x3C => "F3", - 0x3D => "F4", - 0x3E => "F5", - 0x3F => "F6", - 0x40 => "F7", - 0x41 => "F8", - 0x42 => "F9", - 0x43 => "F10", - 0x44 => "F11", - 0x45 => "F12", - - // Navigation / editing cluster - 0x46 => "PrtSc", - 0x47 => "ScrLk", - 0x48 => "Pause", - 0x49 => "Ins", - 0x4A => "Home", - 0x4B => "PgUp", - 0x4C => "Del", - 0x4D => "End", - 0x4E => "PgDn", - - // Arrow keys - 0x4F => "Right", - 0x50 => "Left", - 0x51 => "Down", - 0x52 => "Up", - - // Keypad - 0x53 => "NumLk", - 0x54 => "Num /", - 0x55 => "Num *", - 0x56 => "Num -", - 0x57 => "Num +", - 0x58 => "Num Enter", - 0x59 => "Num 1", - 0x5A => "Num 2", - 0x5B => "Num 3", - 0x5C => "Num 4", - 0x5D => "Num 5", - 0x5E => "Num 6", - 0x5F => "Num 7", - 0x60 => "Num 8", - 0x61 => "Num 9", - 0x62 => "Num 0", - 0x63 => "Num .", - 0x64 => "Europe2", - 0x65 => "Menu", - 0x66 => "Power", - 0x67 => "Num =", - - // F13-F24 - 0x68 => "F13", - 0x69 => "F14", - 0x6A => "F15", - 0x6B => "F16", - 0x6C => "F17", - 0x6D => "F18", - 0x6E => "F19", - 0x6F => "F20", - 0x70 => "F21", - 0x71 => "F22", - 0x72 => "F23", - 0x73 => "F24", - - // Misc system keys - 0x74 => "Execute", - 0x75 => "Help", - 0x76 => "Menu2", - 0x77 => "Select", - 0x78 => "Stop", - 0x79 => "Again", - 0x7A => "Undo", - 0x7B => "Cut", - 0x7C => "Copy", - 0x7D => "Paste", - 0x7E => "Find", - 0x7F => "Mute", - 0x80 => "Vol Up", - 0x81 => "Vol Down", - - // Locking keys - 0x82 => "Lock Caps", - 0x83 => "Lock Num", - 0x84 => "Lock Scroll", - - // Keypad extras - 0x85 => "Num ,", - 0x86 => "Num =2", - - // International / Kanji - 0x87 => "Kanji1", - 0x88 => "Kanji2", - 0x89 => "Kanji3", - 0x8A => "Kanji4", - 0x8B => "Kanji5", - 0x8C => "Kanji6", - 0x8D => "Kanji7", - 0x8E => "Kanji8", - 0x8F => "Kanji9", - - // Language keys - 0x90 => "Lang1", - 0x91 => "Lang2", - 0x92 => "Lang3", - 0x93 => "Lang4", - 0x94 => "Lang5", - 0x95 => "Lang6", - 0x96 => "Lang7", - 0x97 => "Lang8", - 0x98 => "Lang9", - - // Rare system keys - 0x99 => "Alt Erase", - 0x9A => "SysReq", - 0x9B => "Cancel", - 0x9C => "Clear", - 0x9D => "Prior", - 0x9E => "Return", - 0x9F => "Separator", - 0xA0 => "Out", - 0xA1 => "Oper", - 0xA2 => "Clear Again", - 0xA3 => "CrSel", - 0xA4 => "ExSel", - - // 0xA5..=0xAF reserved / not defined in standard HID tables - - // Extended keypad - 0xB0 => "Num 00", - 0xB1 => "Num 000", - 0xB2 => "Thousands Sep", - 0xB3 => "Decimal Sep", - 0xB4 => "Currency", - 0xB5 => "Currency Sub", - 0xB6 => "Num (", - 0xB7 => "Num )", - 0xB8 => "Num {", - 0xB9 => "Num }", - 0xBA => "Num Tab", - 0xBB => "Num Bksp", - 0xBC => "Num A", - 0xBD => "Num B", - 0xBE => "Num C", - 0xBF => "Num D", - 0xC0 => "Num E", - 0xC1 => "Num F", - 0xC2 => "Num XOR", - 0xC3 => "Num ^", - 0xC4 => "Num %", - 0xC5 => "Num <", - 0xC6 => "Num >", - 0xC7 => "Num &", - 0xC8 => "Num &&", - 0xC9 => "Num |", - 0xCA => "Num ||", - 0xCB => "Num :", - 0xCC => "Num #", - 0xCD => "Num Space", - 0xCE => "Num @", - 0xCF => "Num !", - 0xD0 => "Num M Store", - 0xD1 => "Num M Recall", - 0xD2 => "Num M Clear", - 0xD3 => "Num M+", - 0xD4 => "Num M-", - 0xD5 => "Num M*", - 0xD6 => "Num M/", - 0xD7 => "Num +/-", - 0xD8 => "Num Clear", - 0xD9 => "Num ClrEntry", - 0xDA => "Num Binary", - 0xDB => "Num Octal", - 0xDC => "Num Decimal", - 0xDD => "Num Hex", - - // 0xDE..=0xDF reserved - - // Modifier keys - 0xE0 => "LCtrl", - 0xE1 => "LShift", - 0xE2 => "LAlt", - 0xE3 => "LGUI", - 0xE4 => "RCtrl", - 0xE5 => "RShift", - 0xE6 => "RAlt", - 0xE7 => "RGUI", - - // Anything else in 0x00..=0xFF not covered above - _ => return format!("0x{code:02X}"), - } - .into() -} diff --git a/original-src/layout.rs b/original-src/layout.rs deleted file mode 100644 index 6197a31..0000000 --- a/original-src/layout.rs +++ /dev/null @@ -1,286 +0,0 @@ -use serde_json::Value; - -/// A keycap with computed absolute position. -#[derive(Clone, Debug, PartialEq)] -pub struct KeycapPos { - pub row: usize, - pub col: usize, - pub x: f32, - pub y: f32, - pub w: f32, - pub h: f32, - pub angle: f32, // degrees -} - -const KEY_SIZE: f32 = 50.0; -const KEY_GAP: f32 = 4.0; - -/// Parse a layout JSON string into absolute key positions. -pub fn parse_json(json: &str) -> Result, String> { - let val: Value = serde_json::from_str(json) - .map_err(|e| format!("Invalid layout JSON: {}", e))?; - let mut keys = Vec::new(); - walk(&val, 0.0, 0.0, 0.0, &mut keys); - if keys.is_empty() { - return Err("No keys found in layout".into()); - } - Ok(keys) -} - -/// Default layout embedded at compile time. -pub fn default_layout() -> Vec { - let json = include_str!("../default.json"); - parse_json(json).unwrap_or_default() -} - -fn walk(node: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { - let obj = match node.as_object() { - Some(o) => o, - None => return, - }; - - for (key, val) in obj { - let key_str = key.as_str(); - match key_str { - "Group" => walk_group(val, ox, oy, parent_angle, out), - "Line" => walk_line(val, ox, oy, parent_angle, out), - "Keycap" => walk_keycap(val, ox, oy, parent_angle, out), - _ => {} - } - } -} - -fn parse_margin(val: &Value) -> (f32, f32, f32, f32) { - let as_str = val.as_str(); - if let Some(s) = as_str { - let split = s.split(','); - let parts: Vec = split - .filter_map(|p| { - let trimmed = p.trim(); - let parsed = trimmed.parse().ok(); - parsed - }) - .collect(); - let has_four_parts = parts.len() == 4; - if has_four_parts { - return (parts[0], parts[1], parts[2], parts[3]); - } - } - (0.0, 0.0, 0.0, 0.0) -} - -fn parse_angle(val: &Value) -> f32 { - let rotate_transform = val.get("RotateTransform"); - let angle_val = rotate_transform - .and_then(|rt| rt.get("Angle")); - let angle_f64 = angle_val - .and_then(|a| a.as_f64()); - let angle = angle_f64.unwrap_or(0.0) as f32; - angle -} - -fn walk_group(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { - let obj = match val.as_object() { - Some(o) => o, - None => return, - }; - - let margin_val = obj.get("Margin"); - let (ml, mt, _, _) = margin_val - .map(parse_margin) - .unwrap_or_default(); - let transform_val = obj.get("RenderTransform"); - let angle = transform_val - .map(parse_angle) - .unwrap_or(0.0); - - let gx = ox + ml; - let gy = oy + mt; - - let children_val = obj.get("Children"); - let children_array = children_val - .and_then(|c| c.as_array()); - if let Some(children) = children_array { - let combined_angle = parent_angle + angle; - for child in children { - walk(child, gx, gy, combined_angle, out); - } - } -} - -fn walk_line(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { - let obj = match val.as_object() { - Some(o) => o, - None => return, - }; - - let margin_val = obj.get("Margin"); - let (ml, mt, _, _) = margin_val - .map(parse_margin) - .unwrap_or_default(); - let transform_val = obj.get("RenderTransform"); - let angle = transform_val - .map(parse_angle) - .unwrap_or(0.0); - let total_angle = parent_angle + angle; - - let orientation_val = obj.get("Orientation"); - let orientation_str = orientation_val - .and_then(|o| o.as_str()) - .unwrap_or("Vertical"); - let horiz = orientation_str == "Horizontal"; - - let lx = ox + ml; - let ly = oy + mt; - - let rad = total_angle.to_radians(); - let cos_a = rad.cos(); - let sin_a = rad.sin(); - - let mut cursor = 0.0f32; - - let children_val = obj.get("Children"); - let children_array = children_val - .and_then(|c| c.as_array()); - if let Some(children) = children_array { - for child in children { - let (cx, cy) = if horiz { - let x = lx + cursor * cos_a; - let y = ly + cursor * sin_a; - (x, y) - } else { - let x = lx - cursor * sin_a; - let y = ly + cursor * cos_a; - (x, y) - }; - - let child_size = measure(child, horiz); - walk(child, cx, cy, total_angle, out); - cursor += child_size; - } - } -} - -/// Measure a child's extent along the parent's main axis. -fn measure(node: &Value, horiz: bool) -> f32 { - let obj = match node.as_object() { - Some(o) => o, - None => return 0.0, - }; - - for (key, val) in obj { - let key_str = key.as_str(); - match key_str { - "Keycap" => { - let width_val = val.get("Width"); - let width_f64 = width_val - .and_then(|v| v.as_f64()); - let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32; - let extent = if horiz { - w + KEY_GAP - } else { - KEY_SIZE + KEY_GAP - }; - return extent; - } - "Line" => { - let sub = match val.as_object() { - Some(o) => o, - None => return 0.0, - }; - let sub_orientation = sub.get("Orientation"); - let sub_orient_str = sub_orientation - .and_then(|o| o.as_str()) - .unwrap_or("Vertical"); - let sub_horiz = sub_orient_str == "Horizontal"; - - let sub_children_val = sub.get("Children"); - let sub_children_array = sub_children_val - .and_then(|c| c.as_array()); - let children = sub_children_array - .map(|a| a.as_slice()) - .unwrap_or(&[]); - - let same_direction = sub_horiz == horiz; - let content: f32 = if same_direction { - // Same direction: sum - children - .iter() - .map(|c| measure(c, sub_horiz)) - .sum() - } else { - // Cross direction: max - children - .iter() - .map(|c| measure(c, horiz)) - .fold(0.0f32, f32::max) - }; - - return content; - } - "Group" => { - let sub = match val.as_object() { - Some(o) => o, - None => return 0.0, - }; - let sub_children_val = sub.get("Children"); - let sub_children_array = sub_children_val - .and_then(|c| c.as_array()); - let children = sub_children_array - .map(|a| a.as_slice()) - .unwrap_or(&[]); - let max_extent = children - .iter() - .map(|c| measure(c, horiz)) - .fold(0.0f32, f32::max); - return max_extent; - } - _ => {} - } - } - 0.0 -} - -fn walk_keycap(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { - let obj = match val.as_object() { - Some(o) => o, - None => return, - }; - - let col_val = obj.get("Column"); - let col_u64 = col_val - .and_then(|v| v.as_u64()); - let col = col_u64.unwrap_or(0) as usize; - - let row_val = obj.get("Row"); - let row_u64 = row_val - .and_then(|v| v.as_u64()); - let row = row_u64.unwrap_or(0) as usize; - - let width_val = obj.get("Width"); - let width_f64 = width_val - .and_then(|v| v.as_f64()); - let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32; - - let margin_val = obj.get("Margin"); - let (ml, mt, _, _) = margin_val - .map(parse_margin) - .unwrap_or_default(); - - let transform_val = obj.get("RenderTransform"); - let angle = transform_val - .map(parse_angle) - .unwrap_or(0.0); - - let total_angle = parent_angle + angle; - - out.push(KeycapPos { - row, - col, - x: ox + ml, - y: oy + mt, - w, - h: KEY_SIZE, - angle: total_angle, - }); -} diff --git a/original-src/parsers.rs b/original-src/parsers.rs deleted file mode 100644 index 6fed54a..0000000 --- a/original-src/parsers.rs +++ /dev/null @@ -1,900 +0,0 @@ -/// Parsing functions for firmware text and binary responses. -/// Separated for testability. - -/// Keyboard physical dimensions (must match firmware). -pub const ROWS: usize = 5; -pub const COLS: usize = 13; - -/// Parse "TD0: 04,05,06,29" lines into an array of 8 tap dance slots. -/// Each slot has 4 actions: [1-tap, 2-tap, 3-tap, hold]. -pub fn parse_td_lines(lines: &[String]) -> Vec<[u16; 4]> { - let mut result = vec![[0u16; 4]; 8]; - - for line in lines { - // Only process lines starting with "TD" - let starts_with_td = line.starts_with("TD"); - if !starts_with_td { - continue; - } - - // Find the colon separator: "TD0: ..." - let colon = match line.find(':') { - Some(i) => i, - None => continue, - }; - - // Extract the index between "TD" and ":" - let index_str = &line[2..colon]; - let idx: usize = match index_str.parse() { - Ok(i) if i < 8 => i, - _ => continue, - }; - - // Parse the comma-separated hex values after the colon - let after_colon = &line[colon + 1..]; - let trimmed_values = after_colon.trim(); - let split_parts = trimmed_values.split(','); - let vals: Vec = split_parts - .filter_map(|s| { - let trimmed_part = s.trim(); - u16::from_str_radix(trimmed_part, 16).ok() - }) - .collect(); - - // We need exactly 4 values - let has_four_values = vals.len() == 4; - if has_four_values { - result[idx] = [vals[0], vals[1], vals[2], vals[3]]; - } - } - - result -} - -/// Parse KO (Key Override) lines into arrays of [trigger, mod, result, res_mod]. -/// Format: "KO0: trigger=2A mod=02 -> result=4C resmod=00" -pub fn parse_ko_lines(lines: &[String]) -> Vec<[u8; 4]> { - let mut result = Vec::new(); - - for line in lines { - // Only process lines starting with "KO" - let starts_with_ko = line.starts_with("KO"); - if !starts_with_ko { - continue; - } - - // Helper: extract hex value after a keyword like "trigger=" - let parse_hex = |key: &str| -> u8 { - let key_position = line.find(key); - - let after_key = match key_position { - Some(i) => { - let rest = &line[i + key.len()..]; - let first_token = rest.split_whitespace().next(); - first_token - } - None => None, - }; - - let parsed_value = match after_key { - Some(s) => { - let without_prefix = s.trim_start_matches("0x"); - u8::from_str_radix(without_prefix, 16).ok() - } - None => None, - }; - - parsed_value.unwrap_or(0) - }; - - let trigger = parse_hex("trigger="); - let modifier = parse_hex("mod="); - let result_key = parse_hex("result="); - let result_mod = parse_hex("resmod="); - - result.push([trigger, modifier, result_key, result_mod]); - } - - result -} - -/// Parse heatmap lines (KEYSTATS? response). -/// Format: "R0: 100 50 30 20 10 5 0 15 25 35 45 55 65" -/// Returns (data[5][13], max_value). -pub fn parse_heatmap_lines(lines: &[String]) -> (Vec>, u32) { - let mut data: Vec> = vec![vec![0u32; COLS]; ROWS]; - let mut max = 0u32; - - for line in lines { - let trimmed = line.trim(); - - // Only process lines starting with "R" - if !trimmed.starts_with('R') { - continue; - } - - // Find the colon - let colon = match trimmed.find(':') { - Some(i) => i, - None => continue, - }; - - // Extract row number between "R" and ":" - let row_str = &trimmed[1..colon]; - let row: usize = match row_str.parse() { - Ok(r) if r < ROWS => r, - _ => continue, - }; - - // Parse space-separated values after the colon - let values_str = &trimmed[colon + 1..]; - for (col, token) in values_str.split_whitespace().enumerate() { - if col >= COLS { - break; - } - let count: u32 = token.parse().unwrap_or(0); - data[row][col] = count; - if count > max { - max = count; - } - } - } - - (data, max) -} - -/// Parsed combo: [index, row1, col1, row2, col2, result_keycode] -#[derive(Clone)] -pub struct ComboEntry { - pub index: u8, - pub r1: u8, - pub c1: u8, - pub r2: u8, - pub c2: u8, - pub result: u16, -} - -/// Parse "COMBO0: r3c3+r3c4=29" lines. -pub fn parse_combo_lines(lines: &[String]) -> Vec { - let mut result = Vec::new(); - - for line in lines { - let starts_with_combo = line.starts_with("COMBO"); - if !starts_with_combo { - continue; - } - - // Find colon: "COMBO0: ..." - let colon = match line.find(':') { - Some(i) => i, - None => continue, - }; - - // Index between "COMBO" and ":" - let index_str = &line[5..colon]; - let index: u8 = match index_str.parse() { - Ok(i) => i, - _ => continue, - }; - - // After colon: "r3c3+r3c4=29" - let rest = line[colon + 1..].trim(); - - // Split by "=" - let eq_parts: Vec<&str> = rest.split('=').collect(); - let has_two_parts = eq_parts.len() == 2; - if !has_two_parts { - continue; - } - - // Split left side by "+" - let key_parts: Vec<&str> = eq_parts[0].split('+').collect(); - let has_two_keys = key_parts.len() == 2; - if !has_two_keys { - continue; - } - - // Parse "r3c4" format - let pos1 = parse_rc(key_parts[0].trim()); - let pos2 = parse_rc(key_parts[1].trim()); - - let (r1, c1) = match pos1 { - Some(rc) => rc, - None => continue, - }; - let (r2, c2) = match pos2 { - Some(rc) => rc, - None => continue, - }; - - // Parse result keycode (hex) - let result_str = eq_parts[1].trim(); - let result_code: u16 = match u16::from_str_radix(result_str, 16) { - Ok(v) => v, - _ => continue, - }; - - result.push(ComboEntry { - index, - r1, - c1, - r2, - c2, - result: result_code, - }); - } - - result -} - -/// Parse "r3c4" into (row, col). -fn parse_rc(s: &str) -> Option<(u8, u8)> { - let lower = s.to_lowercase(); - - let r_pos = lower.find('r'); - let c_pos = lower.find('c'); - - let r_idx = match r_pos { - Some(i) => i, - None => return None, - }; - let c_idx = match c_pos { - Some(i) => i, - None => return None, - }; - - let row_str = &lower[r_idx + 1..c_idx]; - let col_str = &lower[c_idx + 1..]; - - let row: u8 = match row_str.parse() { - Ok(v) => v, - _ => return None, - }; - let col: u8 = match col_str.parse() { - Ok(v) => v, - _ => return None, - }; - - Some((row, col)) -} - -/// Parsed leader sequence entry. -#[derive(Clone)] -pub struct LeaderEntry { - pub index: u8, - pub sequence: Vec, // HID keycodes - pub result: u8, - pub result_mod: u8, -} - -/// Parse "LEADER0: 04,->29+00" lines. -pub fn parse_leader_lines(lines: &[String]) -> Vec { - let mut result = Vec::new(); - - for line in lines { - let starts_with_leader = line.starts_with("LEADER"); - if !starts_with_leader { - continue; - } - - let colon = match line.find(':') { - Some(i) => i, - None => continue, - }; - - let index_str = &line[6..colon]; - let index: u8 = match index_str.parse() { - Ok(i) => i, - _ => continue, - }; - - // After colon: "04,->29+00" - let rest = line[colon + 1..].trim(); - - // Split by "->" - let arrow_parts: Vec<&str> = rest.split("->").collect(); - let has_two_parts = arrow_parts.len() == 2; - if !has_two_parts { - continue; - } - - // Sequence: comma-separated hex keycodes (trailing comma OK) - let seq_str = arrow_parts[0].trim().trim_end_matches(','); - let sequence: Vec = seq_str - .split(',') - .filter_map(|s| { - let trimmed = s.trim(); - u8::from_str_radix(trimmed, 16).ok() - }) - .collect(); - - // Result: "29+00" = keycode + modifier - let result_parts: Vec<&str> = arrow_parts[1].trim().split('+').collect(); - let has_result = result_parts.len() == 2; - if !has_result { - continue; - } - - let result_key = match u8::from_str_radix(result_parts[0].trim(), 16) { - Ok(v) => v, - _ => continue, - }; - let result_mod = match u8::from_str_radix(result_parts[1].trim(), 16) { - Ok(v) => v, - _ => continue, - }; - - result.push(LeaderEntry { - index, - sequence, - result: result_key, - result_mod, - }); - } - - result -} - -/// A single macro step: keycode + modifier, or delay. -#[derive(Clone)] -pub struct MacroStep { - pub keycode: u8, - pub modifier: u8, -} - -impl MacroStep { - /// Returns true if this step is a delay (keycode 0xFF). - pub fn is_delay(&self) -> bool { - self.keycode == 0xFF - } - - /// Delay in milliseconds (modifier * 10). - pub fn delay_ms(&self) -> u32 { - self.modifier as u32 * 10 - } -} - -/// A parsed macro entry. -#[derive(Clone)] -pub struct MacroEntry { - pub slot: u8, - pub name: String, - pub steps: Vec, -} - -/// Parse MACROS? text response. -/// Lines can be like: -/// "MACRO 0: CopyPaste [06:01,FF:0A,19:01]" -/// "M0: name=CopyPaste steps=06:01,FF:0A,19:01" -/// or just raw text lines -pub fn parse_macro_lines(lines: &[String]) -> Vec { - let mut result = Vec::new(); - - for line in lines { - // Try format: "MACRO 0: name [steps]" or "M0: ..." - let trimmed = line.trim(); - - // Skip empty or header lines - let is_empty = trimmed.is_empty(); - if is_empty { - continue; - } - - // Try to find slot number - let has_macro_prefix = trimmed.starts_with("MACRO") || trimmed.starts_with("M"); - if !has_macro_prefix { - continue; - } - - let colon = match trimmed.find(':') { - Some(i) => i, - None => continue, - }; - - // Extract slot number from prefix - let prefix_end = trimmed[..colon].trim(); - let digits_start = prefix_end - .find(|c: char| c.is_ascii_digit()) - .unwrap_or(prefix_end.len()); - let slot_str = &prefix_end[digits_start..]; - let slot: u8 = match slot_str.trim().parse() { - Ok(s) => s, - _ => continue, - }; - - let after_colon = trimmed[colon + 1..].trim(); - - // Try to parse name and steps from brackets: "CopyPaste [06:01,FF:0A,19:01]" - let bracket_start = after_colon.find('['); - let bracket_end = after_colon.find(']'); - - let (name, steps_str) = match (bracket_start, bracket_end) { - (Some(bs), Some(be)) => { - let name_part = after_colon[..bs].trim().to_string(); - let steps_part = &after_colon[bs + 1..be]; - (name_part, steps_part.to_string()) - } - _ => { - // Try "name=X steps=Y" format - let name_eq = after_colon.find("name="); - let steps_eq = after_colon.find("steps="); - match (name_eq, steps_eq) { - (Some(ni), Some(si)) => { - let name_start = ni + 5; - let name_end = si; - let n = after_colon[name_start..name_end].trim().to_string(); - let s = after_colon[si + 6..].trim().to_string(); - (n, s) - } - _ => { - // Just use the whole thing as name, no steps - (after_colon.to_string(), String::new()) - } - } - } - }; - - // Parse steps: "06:01,FF:0A,19:01" - let mut steps = Vec::new(); - let has_steps = !steps_str.is_empty(); - if has_steps { - let step_parts = steps_str.split(','); - for part in step_parts { - let step_trimmed = part.trim(); - let kv: Vec<&str> = step_trimmed.split(':').collect(); - let has_two = kv.len() == 2; - if !has_two { - continue; - } - let key_byte = u8::from_str_radix(kv[0].trim(), 16).unwrap_or(0); - let mod_byte = u8::from_str_radix(kv[1].trim(), 16).unwrap_or(0); - steps.push(MacroStep { - keycode: key_byte, - modifier: mod_byte, - }); - } - } - - result.push(MacroEntry { - slot, - name, - steps, - }); - } - - result -} - -// --------------------------------------------------------------------------- -// Binary payload parsers (protocol v2) -// --------------------------------------------------------------------------- -// These functions parse the *payload* bytes extracted from a KR response frame. -// They produce the same data types as the text parsers above. - -/// Parse TD_LIST (0x51) binary payload. -/// Format: [count:u8] then count entries of [idx:u8][a1:u8][a2:u8][a3:u8][a4:u8]. -/// Returns Vec<[u16; 4]> with 8 slots (same shape as parse_td_lines). -pub fn parse_td_binary(payload: &[u8]) -> Vec<[u16; 4]> { - let mut result = vec![[0u16; 4]; 8]; - - // Need at least 1 byte for count - let has_count = !payload.is_empty(); - if !has_count { - return result; - } - - let count = payload[0] as usize; - let entry_size = 5; // idx(1) + actions(4) - let mut offset = 1; - - for _ in 0..count { - // Bounds check: need 5 bytes for this entry - let remaining = payload.len().saturating_sub(offset); - let enough_bytes = remaining >= entry_size; - if !enough_bytes { - break; - } - - let idx = payload[offset] as usize; - let action1 = payload[offset + 1] as u16; - let action2 = payload[offset + 2] as u16; - let action3 = payload[offset + 3] as u16; - let action4 = payload[offset + 4] as u16; - - let valid_index = idx < 8; - if valid_index { - result[idx] = [action1, action2, action3, action4]; - } - - offset += entry_size; - } - - result -} - -/// Parse COMBO_LIST (0x61) binary payload. -/// Format: [count:u8] then count entries of [idx:u8][r1:u8][c1:u8][r2:u8][c2:u8][result:u8]. -pub fn parse_combo_binary(payload: &[u8]) -> Vec { - let mut result = Vec::new(); - - let has_count = !payload.is_empty(); - if !has_count { - return result; - } - - let count = payload[0] as usize; - let entry_size = 6; // idx + r1 + c1 + r2 + c2 + result - let mut offset = 1; - - for _ in 0..count { - let remaining = payload.len().saturating_sub(offset); - let enough_bytes = remaining >= entry_size; - if !enough_bytes { - break; - } - - let index = payload[offset]; - let r1 = payload[offset + 1]; - let c1 = payload[offset + 2]; - let r2 = payload[offset + 3]; - let c2 = payload[offset + 4]; - let result_code = payload[offset + 5] as u16; - - result.push(ComboEntry { - index, - r1, - c1, - r2, - c2, - result: result_code, - }); - - offset += entry_size; - } - - result -} - -/// Parse LEADER_LIST (0x71) binary payload. -/// Format: [count:u8] then per entry: [idx:u8][seq_len:u8][seq: seq_len bytes][result:u8][result_mod:u8]. -pub fn parse_leader_binary(payload: &[u8]) -> Vec { - let mut result = Vec::new(); - - let has_count = !payload.is_empty(); - if !has_count { - return result; - } - - let count = payload[0] as usize; - let mut offset = 1; - - for _ in 0..count { - // Need at least idx(1) + seq_len(1) - let remaining = payload.len().saturating_sub(offset); - let enough_for_header = remaining >= 2; - if !enough_for_header { - break; - } - - let index = payload[offset]; - let seq_len = payload[offset + 1] as usize; - offset += 2; - - // Need seq_len bytes for sequence + 2 bytes for result+result_mod - let remaining_after_header = payload.len().saturating_sub(offset); - let enough_for_body = remaining_after_header >= seq_len + 2; - if !enough_for_body { - break; - } - - let sequence = payload[offset..offset + seq_len].to_vec(); - offset += seq_len; - - let result_key = payload[offset]; - let result_mod = payload[offset + 1]; - offset += 2; - - result.push(LeaderEntry { - index, - sequence, - result: result_key, - result_mod, - }); - } - - result -} - -/// Parse KO_LIST (0x92) binary payload. -/// Format: [count:u8] then count entries of [idx:u8][trigger_key:u8][trigger_mod:u8][result_key:u8][result_mod:u8]. -/// Returns Vec<[u8; 4]> = [trigger_key, trigger_mod, result_key, result_mod] (same as parse_ko_lines). -pub fn parse_ko_binary(payload: &[u8]) -> Vec<[u8; 4]> { - let mut result = Vec::new(); - - let has_count = !payload.is_empty(); - if !has_count { - return result; - } - - let count = payload[0] as usize; - let entry_size = 5; // idx + trigger_key + trigger_mod + result_key + result_mod - let mut offset = 1; - - for _ in 0..count { - let remaining = payload.len().saturating_sub(offset); - let enough_bytes = remaining >= entry_size; - if !enough_bytes { - break; - } - - // idx is payload[offset], but we skip it (not stored in output) - let trigger_key = payload[offset + 1]; - let trigger_mod = payload[offset + 2]; - let result_key = payload[offset + 3]; - let result_mod = payload[offset + 4]; - - result.push([trigger_key, trigger_mod, result_key, result_mod]); - - offset += entry_size; - } - - result -} - -/// Parse BT_QUERY (0x80) binary payload. -/// Format: [active_slot:u8][initialized:u8][connected:u8][pairing:u8] -/// then 3 slot entries: [slot_idx:u8][valid:u8][addr:6 bytes][name_len:u8][name: name_len bytes] -/// Returns Vec of text lines compatible with the UI (same shape as legacy text parsing). -pub fn parse_bt_binary(payload: &[u8]) -> Vec { - let mut lines = Vec::new(); - - // Need at least 4 bytes for the global state header - let enough_for_header = payload.len() >= 4; - if !enough_for_header { - return lines; - } - - let active_slot = payload[0]; - let initialized = payload[1]; - let connected = payload[2]; - let pairing = payload[3]; - - let status_line = format!( - "BT: slot={} init={} conn={} pairing={}", - active_slot, initialized, connected, pairing - ); - lines.push(status_line); - - let mut offset = 4; - let slot_count = 3; - - for _ in 0..slot_count { - // Each slot: slot_idx(1) + valid(1) + addr(6) + name_len(1) = 9 bytes minimum - let remaining = payload.len().saturating_sub(offset); - let enough_for_slot_header = remaining >= 9; - if !enough_for_slot_header { - break; - } - - let slot_idx = payload[offset]; - let valid = payload[offset + 1]; - let addr_bytes = &payload[offset + 2..offset + 8]; - let name_len = payload[offset + 8] as usize; - offset += 9; - - // Read the name string - let remaining_for_name = payload.len().saturating_sub(offset); - let enough_for_name = remaining_for_name >= name_len; - if !enough_for_name { - break; - } - - let name_bytes = &payload[offset..offset + name_len]; - let name = String::from_utf8_lossy(name_bytes).to_string(); - offset += name_len; - - // Format address as "XX:XX:XX:XX:XX:XX" - let addr_str = format!( - "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", - addr_bytes[0], addr_bytes[1], addr_bytes[2], - addr_bytes[3], addr_bytes[4], addr_bytes[5] - ); - - let slot_line = format!( - "BT slot {}: valid={} addr={} name={}", - slot_idx, valid, addr_str, name - ); - lines.push(slot_line); - } - - lines -} - -/// Parse TAMA_QUERY (0xA0) binary payload (22 bytes fixed). -/// Format: [enabled:u8][state:u8][hunger:u16 LE][happiness:u16 LE][energy:u16 LE] -/// [health:u16 LE][level:u16 LE][xp:u16 LE][total_keys:u32 LE][max_kpm:u32 LE] -/// Returns Vec with one summary line. -pub fn parse_tama_binary(payload: &[u8]) -> Vec { - let expected_size = 22; - let enough_bytes = payload.len() >= expected_size; - if !enough_bytes { - return vec!["TAMA: invalid payload".to_string()]; - } - - let enabled = payload[0]; - let _state = payload[1]; - - let hunger = u16::from_le_bytes([payload[2], payload[3]]); - let happiness = u16::from_le_bytes([payload[4], payload[5]]); - let energy = u16::from_le_bytes([payload[6], payload[7]]); - let health = u16::from_le_bytes([payload[8], payload[9]]); - let level = u16::from_le_bytes([payload[10], payload[11]]); - let _xp = u16::from_le_bytes([payload[12], payload[13]]); - let total_keys = u32::from_le_bytes([payload[14], payload[15], payload[16], payload[17]]); - let _max_kpm = u32::from_le_bytes([payload[18], payload[19], payload[20], payload[21]]); - - let line = format!( - "TAMA: Lv{} hunger={} happy={} energy={} health={} keys={} enabled={}", - level, hunger, happiness, energy, health, total_keys, enabled - ); - - vec![line] -} - -/// Parse WPM_QUERY (0x93) binary payload (2 bytes fixed). -/// Format: [wpm:u16 LE] -pub fn parse_wpm_binary(payload: &[u8]) -> String { - let enough_bytes = payload.len() >= 2; - if !enough_bytes { - return "WPM: 0".to_string(); - } - - let wpm = u16::from_le_bytes([payload[0], payload[1]]); - - format!("WPM: {}", wpm) -} - -/// Parse LIST_MACROS (0x30) binary payload. -/// Format: [count:u8] then per entry: -/// [idx:u8][keycode:u16 LE][name_len:u8][name: name_len bytes] -/// [keys_len:u8][keys: keys_len bytes][step_count:u8][{kc:u8,mod:u8}... step_count*2 bytes] -pub fn parse_macros_binary(payload: &[u8]) -> Vec { - let mut result = Vec::new(); - - let has_count = !payload.is_empty(); - if !has_count { - return result; - } - - let count = payload[0] as usize; - let mut offset = 1; - - for _ in 0..count { - // Need at least: idx(1) + keycode(2) + name_len(1) = 4 - let remaining = payload.len().saturating_sub(offset); - let enough_for_prefix = remaining >= 4; - if !enough_for_prefix { - break; - } - - let slot = payload[offset]; - // keycode is stored but not used in MacroEntry (it's the trigger keycode) - let _keycode = u16::from_le_bytes([payload[offset + 1], payload[offset + 2]]); - let name_len = payload[offset + 3] as usize; - offset += 4; - - // Read name - let remaining_for_name = payload.len().saturating_sub(offset); - let enough_for_name = remaining_for_name >= name_len; - if !enough_for_name { - break; - } - - let name_bytes = &payload[offset..offset + name_len]; - let name = String::from_utf8_lossy(name_bytes).to_string(); - offset += name_len; - - // Read keys_len + keys (raw key bytes, skipped for MacroEntry) - let remaining_for_keys_len = payload.len().saturating_sub(offset); - let enough_for_keys_len = remaining_for_keys_len >= 1; - if !enough_for_keys_len { - break; - } - - let keys_len = payload[offset] as usize; - offset += 1; - - let remaining_for_keys = payload.len().saturating_sub(offset); - let enough_for_keys = remaining_for_keys >= keys_len; - if !enough_for_keys { - break; - } - - // Skip the raw keys bytes - offset += keys_len; - - // Read step_count + steps - let remaining_for_step_count = payload.len().saturating_sub(offset); - let enough_for_step_count = remaining_for_step_count >= 1; - if !enough_for_step_count { - break; - } - - let step_count = payload[offset] as usize; - offset += 1; - - let steps_byte_size = step_count * 2; - let remaining_for_steps = payload.len().saturating_sub(offset); - let enough_for_steps = remaining_for_steps >= steps_byte_size; - if !enough_for_steps { - break; - } - - let mut steps = Vec::with_capacity(step_count); - for i in 0..step_count { - let step_offset = offset + i * 2; - let kc = payload[step_offset]; - let md = payload[step_offset + 1]; - steps.push(MacroStep { - keycode: kc, - modifier: md, - }); - } - offset += steps_byte_size; - - result.push(MacroEntry { - slot, - name, - steps, - }); - } - - result -} - -/// Parse KEYSTATS_BIN (0x40) binary payload. -/// Format: [rows:u8][cols:u8][counts: rows*cols * u32 LE] -/// Returns (heatmap_data, max_value) — same shape as parse_heatmap_lines. -pub fn parse_keystats_binary(payload: &[u8]) -> (Vec>, u32) { - // Need at least 2 bytes for rows and cols - let enough_for_header = payload.len() >= 2; - if !enough_for_header { - return (vec![], 0); - } - - let rows = payload[0] as usize; - let cols = payload[1] as usize; - let total_cells = rows * cols; - let data_byte_size = total_cells * 4; // each count is u32 LE - - let remaining = payload.len().saturating_sub(2); - let enough_for_data = remaining >= data_byte_size; - if !enough_for_data { - return (vec![], 0); - } - - let mut data: Vec> = vec![vec![0u32; cols]; rows]; - let mut max_value = 0u32; - let mut offset = 2; - - for row in 0..rows { - for col in 0..cols { - let count = u32::from_le_bytes([ - payload[offset], - payload[offset + 1], - payload[offset + 2], - payload[offset + 3], - ]); - data[row][col] = count; - - let is_new_max = count > max_value; - if is_new_max { - max_value = count; - } - - offset += 4; - } - } - - (data, max_value) -} diff --git a/original-src/serial/native.rs b/original-src/serial/native.rs deleted file mode 100644 index ebc91ec..0000000 --- a/original-src/serial/native.rs +++ /dev/null @@ -1,559 +0,0 @@ -use serialport::SerialPort; -use std::io::{BufRead, BufReader, Read, Write}; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; - -use crate::binary_protocol::{self as bp, KrResponse}; -use crate::parsers::{ROWS, COLS}; - -const BAUD_RATE: u32 = 115200; -const CONNECT_TIMEOUT_MS: u64 = 300; -const QUERY_TIMEOUT_MS: u64 = 800; -const BINARY_READ_TIMEOUT_MS: u64 = 1500; -const LEGACY_BINARY_SETTLE_MS: u64 = 50; -const BINARY_SETTLE_MS: u64 = 30; -const JSON_TIMEOUT_SECS: u64 = 3; - -pub struct SerialManager { - port: Option>, - pub port_name: String, - pub connected: bool, - pub v2: bool, // true if firmware supports binary protocol v2 -} - -impl SerialManager { - pub fn new() -> Self { - Self { - port: None, - port_name: String::new(), - connected: false, - v2: false, - } - } - - #[allow(dead_code)] - pub fn list_ports() -> Vec { - let available = serialport::available_ports(); - let ports = available.unwrap_or_default(); - let port_iter = ports.into_iter(); - let name_iter = port_iter.map(|p| p.port_name); - let names: Vec = name_iter.collect(); - names - } - - pub fn connect(&mut self, port_name: &str) -> Result<(), String> { - let builder = serialport::new(port_name, BAUD_RATE); - let builder_with_timeout = builder.timeout(Duration::from_millis(CONNECT_TIMEOUT_MS)); - let open_result = builder_with_timeout.open(); - let port = open_result.map_err(|e| format!("Failed to open {}: {}", port_name, e))?; - - self.port = Some(port); - self.port_name = port_name.to_string(); - self.connected = true; - self.v2 = false; - - // Detect v2: try PING - if let Some(p) = self.port.as_mut() { - let _ = p.clear(serialport::ClearBuffer::All); - } - std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); - - let ping_result = self.send_binary(bp::cmd::PING, &[]); - if ping_result.is_ok() { - self.v2 = true; - } - - Ok(()) - } - - pub fn auto_connect(&mut self) -> Result { - let port_name = Self::find_kase_port()?; - self.connect(&port_name)?; - Ok(port_name) - } - - pub fn find_kase_port() -> Result { - const TARGET_VID: u16 = 0xCAFE; - const TARGET_PID: u16 = 0x4001; - - let available = serialport::available_ports(); - let ports = available.unwrap_or_default(); - if ports.is_empty() { - return Err("No serial ports found".into()); - } - - // First pass: check USB VID/PID and product name - for port in &ports { - let port_type = &port.port_type; - match port_type { - serialport::SerialPortType::UsbPort(usb) => { - let vid_matches = usb.vid == TARGET_VID; - let pid_matches = usb.pid == TARGET_PID; - if vid_matches && pid_matches { - return Ok(port.port_name.clone()); - } - - match &usb.product { - Some(product) => { - let is_kase = product.contains("KaSe"); - let is_kesp = product.contains("KeSp"); - if is_kase || is_kesp { - return Ok(port.port_name.clone()); - } - } - None => {} - } - } - _ => {} - } - } - - // Second pass (Linux only): check udevadm info - #[cfg(target_os = "linux")] - for port in &ports { - let udevadm_result = std::process::Command::new("udevadm") - .args(["info", "-n", &port.port_name]) - .output(); - - match udevadm_result { - Ok(output) => { - let stdout_bytes = &output.stdout; - let text = String::from_utf8_lossy(stdout_bytes); - let has_kase = text.contains("KaSe"); - let has_kesp = text.contains("KeSp"); - if has_kase || has_kesp { - return Ok(port.port_name.clone()); - } - } - Err(_) => {} - } - } - - let scanned_count = ports.len(); - Err(format!("No KaSe keyboard found ({} port(s) scanned)", scanned_count)) - } - - pub fn port_mut(&mut self) -> Option<&mut Box> { - self.port.as_mut() - } - - pub fn disconnect(&mut self) { - self.port = None; - self.port_name.clear(); - self.connected = false; - self.v2 = false; - } - - // ==================== LOW-LEVEL: ASCII LEGACY ==================== - - pub fn send_command(&mut self, cmd: &str) -> Result<(), String> { - let port = self.port.as_mut().ok_or("Not connected")?; - let data = format!("{}\r\n", cmd); - let bytes = data.as_bytes(); - - let write_result = port.write_all(bytes); - write_result.map_err(|e| format!("Write: {}", e))?; - - let flush_result = port.flush(); - flush_result.map_err(|e| format!("Flush: {}", e))?; - - Ok(()) - } - - pub fn query_command(&mut self, cmd: &str) -> Result, String> { - self.send_command(cmd)?; - - let port = self.port.as_mut().ok_or("Not connected")?; - let cloned_port = port.try_clone(); - let port_clone = cloned_port.map_err(|e| e.to_string())?; - let mut reader = BufReader::new(port_clone); - - let mut lines = Vec::new(); - let start = Instant::now(); - let max_wait = Duration::from_millis(QUERY_TIMEOUT_MS); - - loop { - let elapsed = start.elapsed(); - if elapsed > max_wait { - break; - } - - let mut line = String::new(); - let read_result = reader.read_line(&mut line); - - match read_result { - Ok(0) => break, - Ok(_) => { - let trimmed = line.trim().to_string(); - let is_terminal = trimmed == "OK" || trimmed == "ERROR"; - if is_terminal { - break; - } - let is_not_empty = !trimmed.is_empty(); - if is_not_empty { - lines.push(trimmed); - } - } - Err(_) => break, - } - } - - Ok(lines) - } - - fn read_raw(&mut self, timeout_ms: u64) -> Result, String> { - let port = self.port.as_mut().ok_or("Not connected")?; - let mut buf = vec![0u8; 4096]; - let mut result = Vec::new(); - let start = Instant::now(); - let timeout = Duration::from_millis(timeout_ms); - - while start.elapsed() < timeout { - let read_result = port.read(&mut buf); - - match read_result { - Ok(n) if n > 0 => { - let received_bytes = &buf[..n]; - result.extend_from_slice(received_bytes); - std::thread::sleep(Duration::from_millis(5)); - } - _ => { - let got_something = !result.is_empty(); - if got_something { - break; - } - std::thread::sleep(Duration::from_millis(5)); - } - } - } - - Ok(result) - } - - /// Legacy C> binary protocol - fn query_legacy_binary(&mut self, cmd: &str) -> Result<(u8, Vec), String> { - self.send_command(cmd)?; - std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); - let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; - - // Look for the "C>" header in the raw bytes - let mut windows = raw.windows(2); - let header_search = windows.position(|w| w == b"C>"); - let pos = header_search.ok_or("No C> header found")?; - - let min_packet_size = pos + 5; - if raw.len() < min_packet_size { - return Err("Packet too short".into()); - } - - let cmd_type = raw[pos + 2]; - let low_byte = raw[pos + 3] as u16; - let high_byte = (raw[pos + 4] as u16) << 8; - let data_len = low_byte | high_byte; - - let data_start = pos + 5; - let data_end = data_start.checked_add(data_len as usize) - .ok_or("Data length overflow")?; - - if raw.len() < data_end { - return Err(format!("Incomplete: need {}, got {}", data_end, raw.len())); - } - - let payload = raw[data_start..data_end].to_vec(); - Ok((cmd_type, payload)) - } - - // ==================== LOW-LEVEL: BINARY V2 ==================== - - /// Send a KS frame, read KR response. - pub fn send_binary(&mut self, cmd_id: u8, payload: &[u8]) -> Result { - let frame = bp::ks_frame(cmd_id, payload); - let port = self.port.as_mut().ok_or("Not connected")?; - - let write_result = port.write_all(&frame); - write_result.map_err(|e| format!("Write: {}", e))?; - - let flush_result = port.flush(); - flush_result.map_err(|e| format!("Flush: {}", e))?; - - std::thread::sleep(Duration::from_millis(BINARY_SETTLE_MS)); - let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; - - let (resp, _remaining) = bp::parse_kr(&raw)?; - let firmware_ok = resp.is_ok(); - if !firmware_ok { - let status = resp.status_name(); - return Err(format!("Firmware error: {}", status)); - } - - Ok(resp) - } - - // ==================== HIGH-LEVEL: AUTO V2/LEGACY ==================== - - pub fn get_firmware_version(&mut self) -> Option { - // Try v2 binary first - if self.v2 { - let binary_result = self.send_binary(bp::cmd::VERSION, &[]); - match binary_result { - Ok(resp) => { - let raw_bytes = &resp.payload; - let lossy_string = String::from_utf8_lossy(raw_bytes); - let version = lossy_string.to_string(); - return Some(version); - } - Err(_) => {} - } - } - - // Legacy fallback - let query_result = self.query_command("VERSION?"); - let lines = match query_result { - Ok(l) => l, - Err(_) => return None, - }; - let mut line_iter = lines.into_iter(); - let first_line = line_iter.next(); - first_line - } - - pub fn get_keymap(&mut self, layer: u8) -> Result>, String> { - // Try v2 binary first - if self.v2 { - let resp = self.send_binary(bp::cmd::KEYMAP_GET, &[layer])?; - let keymap = self.parse_keymap_payload(&resp.payload)?; - return Ok(keymap); - } - - // Legacy - let cmd = format!("KEYMAP{}", layer); - let (cmd_type, data) = self.query_legacy_binary(&cmd)?; - - if cmd_type != 1 { - return Err(format!("Unexpected cmd type: {}", cmd_type)); - } - if data.len() < 2 { - return Err("Data too short".into()); - } - - // skip 2-byte layer index in legacy - let data_without_header = &data[2..]; - self.parse_keymap_payload(data_without_header) - } - - fn parse_keymap_payload(&self, data: &[u8]) -> Result>, String> { - // v2: [layer:u8][keycodes...] -- skip first byte - // legacy: already stripped - let expected_with_layer_byte = 1 + ROWS * COLS * 2; - let has_layer_byte = data.len() >= expected_with_layer_byte; - let offset = if has_layer_byte { 1 } else { 0 }; - let kc_data = &data[offset..]; - - let needed_bytes = ROWS * COLS * 2; - if kc_data.len() < needed_bytes { - return Err(format!("Keymap data too short: {} bytes (need {})", kc_data.len(), needed_bytes)); - } - - let mut keymap = Vec::with_capacity(ROWS); - - for row_index in 0..ROWS { - let mut row = Vec::with_capacity(COLS); - - for col_index in 0..COLS { - let idx = (row_index * COLS + col_index) * 2; - let low_byte = kc_data[idx] as u16; - let high_byte = (kc_data[idx + 1] as u16) << 8; - let keycode = low_byte | high_byte; - row.push(keycode); - } - - keymap.push(row); - } - - Ok(keymap) - } - - pub fn get_layer_names(&mut self) -> Result, String> { - // Try v2 binary first - if self.v2 { - let resp = self.send_binary(bp::cmd::LIST_LAYOUTS, &[])?; - - let payload = &resp.payload; - if payload.is_empty() { - return Err("Empty response".into()); - } - - let count = payload[0] as usize; - let mut names = Vec::with_capacity(count); - let mut i = 1; - - for _ in 0..count { - let remaining = payload.len(); - let need_header = i + 2; - if need_header > remaining { - break; - } - - let _layer_index = payload[i]; - let name_len = payload[i + 1] as usize; - i += 2; - - let need_name = i + name_len; - if need_name > payload.len() { - break; - } - - let name_bytes = &payload[i..i + name_len]; - let name_lossy = String::from_utf8_lossy(name_bytes); - let name = name_lossy.to_string(); - names.push(name); - i += name_len; - } - - let found_names = !names.is_empty(); - if found_names { - return Ok(names); - } - } - - // Legacy fallback: try C> binary protocol - let legacy_result = self.query_legacy_binary("LAYOUTS?"); - match legacy_result { - Ok((cmd_type, data)) => { - let is_layout_type = cmd_type == 4; - let has_data = !data.is_empty(); - - if is_layout_type && has_data { - let text = String::from_utf8_lossy(&data); - let parts = text.split(';'); - let non_empty = parts.filter(|s| !s.is_empty()); - let trimmed_names = non_empty.map(|s| { - let long_enough = s.len() > 1; - if long_enough { - s[1..].to_string() - } else { - s.to_string() - } - }); - let names: Vec = trimmed_names.collect(); - - let found_names = !names.is_empty(); - if found_names { - return Ok(names); - } - } - } - Err(_) => {} - } - - // Last resort: raw text - self.send_command("LAYOUTS?")?; - std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS * 2)); - let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; - - let text = String::from_utf8_lossy(&raw); - let split_by_delimiters = text.split(|c: char| c == ';' || c == '\n'); - - let cleaned = split_by_delimiters.map(|s| { - let step1 = s.trim(); - let step2 = step1.trim_matches(|c: char| c.is_control() || c == '"'); - step2 - }); - - let valid_names = cleaned.filter(|s| { - let is_not_empty = !s.is_empty(); - let is_short_enough = s.len() < 30; - let no_header_marker = !s.contains("C>"); - let not_ok = *s != "OK"; - is_not_empty && is_short_enough && no_header_marker && not_ok - }); - - let as_strings = valid_names.map(|s| s.to_string()); - let names: Vec = as_strings.collect(); - - let found_any = !names.is_empty(); - if found_any { - Ok(names) - } else { - Err("No layer names found".into()) - } - } - - pub fn get_layout_json(&mut self) -> Result { - // Try v2 binary first - if self.v2 { - let resp = self.send_binary(bp::cmd::GET_LAYOUT_JSON, &[])?; - let has_payload = !resp.payload.is_empty(); - if has_payload { - let raw_bytes = &resp.payload; - let lossy_string = String::from_utf8_lossy(raw_bytes); - let json = lossy_string.to_string(); - return Ok(json); - } - } - - // Legacy: text brace-counting - self.send_command("LAYOUT?")?; - std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); - - let port = self.port.as_mut().ok_or("Not connected")?; - let mut result = String::new(); - let mut buf = [0u8; 4096]; - let start = Instant::now(); - let max_wait = Duration::from_secs(JSON_TIMEOUT_SECS); - let mut brace_count: i32 = 0; - let mut started = false; - - while start.elapsed() < max_wait { - let read_result = port.read(&mut buf); - - match read_result { - Ok(n) if n > 0 => { - let received_bytes = &buf[..n]; - let chunk = String::from_utf8_lossy(received_bytes); - - for ch in chunk.chars() { - let is_open_brace = ch == '{'; - if is_open_brace { - started = true; - brace_count += 1; - } - - if started { - result.push(ch); - } - - let is_close_brace = ch == '}'; - if is_close_brace && started { - brace_count -= 1; - let json_complete = brace_count == 0; - if json_complete { - return Ok(result); - } - } - } - } - _ => { - std::thread::sleep(Duration::from_millis(10)); - } - } - } - - let got_nothing = result.is_empty(); - if got_nothing { - Err("No JSON".into()) - } else { - Err("Incomplete JSON".into()) - } - } -} - -/// Thread-safe wrapper -pub type SharedSerial = Arc>; - -pub fn new_shared() -> SharedSerial { - let manager = SerialManager::new(); - let mutex = Mutex::new(manager); - let shared = Arc::new(mutex); - shared -} diff --git a/original-src/stats_analyzer.rs b/original-src/stats_analyzer.rs deleted file mode 100644 index a14e755..0000000 --- a/original-src/stats_analyzer.rs +++ /dev/null @@ -1,335 +0,0 @@ -/// Stats analysis: hand balance, finger load, row usage, top keys, bigrams. -/// Transforms raw heatmap data into structured analysis for the stats tab. - -use crate::keycode; -use crate::parsers::ROWS; - -/// Which hand a column belongs to. -#[derive(Clone, Copy, PartialEq)] -pub enum Hand { - Left, - Right, -} - -/// Which finger a column belongs to. -#[derive(Clone, Copy, PartialEq)] -pub enum Finger { - Pinky, - Ring, - Middle, - Index, - Thumb, -} - -/// Row names for the 5 rows. -const ROW_NAMES: [&str; 5] = ["Number", "Upper", "Home", "Lower", "Thumb"]; - -/// Finger names (French). -const FINGER_NAMES: [&str; 5] = ["Pinky", "Ring", "Middle", "Index", "Thumb"]; - -/// Map column index → (Hand, Finger). -/// KaSe layout: cols 0-5 = left hand, cols 6 (gap), cols 7-12 = right hand. -fn col_to_hand_finger(col: usize) -> (Hand, Finger) { - match col { - 0 => (Hand::Left, Finger::Pinky), - 1 => (Hand::Left, Finger::Ring), - 2 => (Hand::Left, Finger::Middle), - 3 => (Hand::Left, Finger::Index), - 4 => (Hand::Left, Finger::Index), // inner column, still index - 5 => (Hand::Left, Finger::Thumb), - 6 => (Hand::Left, Finger::Thumb), // center / gap - 7 => (Hand::Right, Finger::Thumb), - 8 => (Hand::Right, Finger::Index), - 9 => (Hand::Right, Finger::Index), // inner column - 10 => (Hand::Right, Finger::Middle), - 11 => (Hand::Right, Finger::Ring), - 12 => (Hand::Right, Finger::Pinky), - _ => (Hand::Left, Finger::Pinky), - } -} - -/// Hand balance result. -#[allow(dead_code)] -pub struct HandBalance { - pub left_count: u32, - pub right_count: u32, - pub total: u32, - pub left_pct: f32, - pub right_pct: f32, -} - -/// Finger load for one finger. -#[allow(dead_code)] -pub struct FingerLoad { - pub name: String, - pub hand: Hand, - pub count: u32, - pub pct: f32, -} - -/// Row usage for one row. -#[allow(dead_code)] -pub struct RowUsage { - pub name: String, - pub row: usize, - pub count: u32, - pub pct: f32, -} - -/// A key in the top keys ranking. -#[allow(dead_code)] -pub struct TopKey { - pub name: String, - pub finger: String, - pub count: u32, - pub pct: f32, -} - -/// Compute hand balance from heatmap data. -pub fn hand_balance(heatmap: &[Vec]) -> HandBalance { - let mut left: u32 = 0; - let mut right: u32 = 0; - - for row in heatmap { - for (c, &count) in row.iter().enumerate() { - let (hand, _) = col_to_hand_finger(c); - match hand { - Hand::Left => left += count, - Hand::Right => right += count, - } - } - } - - let total = left + right; - let left_pct = if total > 0 { left as f32 / total as f32 * 100.0 } else { 0.0 }; - let right_pct = if total > 0 { right as f32 / total as f32 * 100.0 } else { 0.0 }; - - HandBalance { left_count: left, right_count: right, total, left_pct, right_pct } -} - -/// Compute finger load (10 fingers: 5 left + 5 right). -pub fn finger_load(heatmap: &[Vec]) -> Vec { - let mut counts = [[0u32; 5]; 2]; // [hand][finger] - - for row in heatmap { - for (c, &count) in row.iter().enumerate() { - let (hand, finger) = col_to_hand_finger(c); - let hi = if hand == Hand::Left { 0 } else { 1 }; - let fi = match finger { - Finger::Pinky => 0, - Finger::Ring => 1, - Finger::Middle => 2, - Finger::Index => 3, - Finger::Thumb => 4, - }; - counts[hi][fi] += count; - } - } - - let total: u32 = counts[0].iter().sum::() + counts[1].iter().sum::(); - let mut result = Vec::with_capacity(10); - - // Left hand fingers - for fi in 0..5 { - let count = counts[0][fi]; - let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; - let name = format!("{} L", FINGER_NAMES[fi]); - result.push(FingerLoad { name, hand: Hand::Left, count, pct }); - } - - // Right hand fingers - for fi in 0..5 { - let count = counts[1][fi]; - let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; - let name = format!("{} R", FINGER_NAMES[fi]); - result.push(FingerLoad { name, hand: Hand::Right, count, pct }); - } - - result -} - -/// Compute row usage. -pub fn row_usage(heatmap: &[Vec]) -> Vec { - let mut row_counts = [0u32; ROWS]; - - for (r, row) in heatmap.iter().enumerate() { - if r >= ROWS { break; } - let row_sum: u32 = row.iter().sum(); - row_counts[r] = row_sum; - } - - let total: u32 = row_counts.iter().sum(); - let mut result = Vec::with_capacity(ROWS); - - for r in 0..ROWS { - let count = row_counts[r]; - let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; - let name = ROW_NAMES[r].to_string(); - result.push(RowUsage { name, row: r, count, pct }); - } - - result -} - -/// Compute top N keys by press count. -pub fn top_keys(heatmap: &[Vec], keymap: &[Vec], n: usize) -> Vec { - let mut all_keys: Vec<(u32, usize, usize)> = Vec::new(); // (count, row, col) - - for (r, row) in heatmap.iter().enumerate() { - for (c, &count) in row.iter().enumerate() { - if count > 0 { - all_keys.push((count, r, c)); - } - } - } - - all_keys.sort_by(|a, b| b.0.cmp(&a.0)); - all_keys.truncate(n); - - let total: u32 = heatmap.iter().flat_map(|r| r.iter()).sum(); - - let mut result = Vec::with_capacity(n); - for (count, r, c) in all_keys { - let code = keymap.get(r).and_then(|row| row.get(c)).copied().unwrap_or(0); - let name = keycode::decode_keycode(code); - let (hand, finger) = col_to_hand_finger(c); - let hand_str = if hand == Hand::Left { "L" } else { "R" }; - let finger_str = match finger { - Finger::Pinky => "Pinky", - Finger::Ring => "Ring", - Finger::Middle => "Middle", - Finger::Index => "Index", - Finger::Thumb => "Thumb", - }; - let finger_label = format!("{} {}", finger_str, hand_str); - let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; - result.push(TopKey { name, finger: finger_label, count, pct }); - } - - result -} - -/// Find keys that have never been pressed (count = 0, keycode != 0). -pub fn dead_keys(heatmap: &[Vec], keymap: &[Vec]) -> Vec { - let mut result = Vec::new(); - for (r, row) in heatmap.iter().enumerate() { - for (c, &count) in row.iter().enumerate() { - if count > 0 { continue; } - let code = keymap.get(r).and_then(|row| row.get(c)).copied().unwrap_or(0); - let is_mapped = code != 0; - if is_mapped { - let name = keycode::decode_keycode(code); - result.push(name); - } - } - } - result -} - -// ==================== Bigram analysis ==================== - -/// A parsed bigram entry. -pub struct BigramEntry { - pub from_row: u8, - pub from_col: u8, - pub to_row: u8, - pub to_col: u8, - pub count: u32, -} - -/// Bigram analysis results. -#[allow(dead_code)] -pub struct BigramAnalysis { - pub total: u32, - pub alt_hand: u32, - pub same_hand: u32, - pub sfb: u32, - pub alt_hand_pct: f32, - pub same_hand_pct: f32, - pub sfb_pct: f32, -} - -/// Parse bigram text lines from firmware. -/// Format: " R2C3 -> R2C4 : 150" -pub fn parse_bigram_lines(lines: &[String]) -> Vec { - let mut entries = Vec::new(); - - for line in lines { - let trimmed = line.trim(); - let has_arrow = trimmed.contains("->"); - if !has_arrow { continue; } - - let parts: Vec<&str> = trimmed.split("->").collect(); - if parts.len() != 2 { continue; } - - let left = parts[0].trim(); - let right_and_count = parts[1].trim(); - - let right_parts: Vec<&str> = right_and_count.split(':').collect(); - if right_parts.len() != 2 { continue; } - - let right = right_parts[0].trim(); - let count_str = right_parts[1].trim(); - - let from = parse_rc(left); - let to = parse_rc(right); - let count: u32 = count_str.parse().unwrap_or(0); - - if let (Some((fr, fc)), Some((tr, tc))) = (from, to) { - entries.push(BigramEntry { - from_row: fr, from_col: fc, - to_row: tr, to_col: tc, - count, - }); - } - } - - entries -} - -/// Parse "R2C3" into (row, col). -fn parse_rc(s: &str) -> Option<(u8, u8)> { - let s = s.trim(); - let r_pos = s.find('R')?; - let c_pos = s.find('C')?; - if c_pos <= r_pos { return None; } - - let row_str = &s[r_pos + 1..c_pos]; - let col_str = &s[c_pos + 1..]; - let row: u8 = row_str.parse().ok()?; - let col: u8 = col_str.parse().ok()?; - Some((row, col)) -} - -/// Analyze bigram entries for hand alternation and SFB. -pub fn analyze_bigrams(entries: &[BigramEntry]) -> BigramAnalysis { - let mut alt_hand: u32 = 0; - let mut same_hand: u32 = 0; - let mut sfb: u32 = 0; - let mut total: u32 = 0; - - for entry in entries { - let (hand_from, finger_from) = col_to_hand_finger(entry.from_col as usize); - let (hand_to, finger_to) = col_to_hand_finger(entry.to_col as usize); - - total += entry.count; - - if hand_from != hand_to { - alt_hand += entry.count; - } else { - same_hand += entry.count; - if finger_from == finger_to { - sfb += entry.count; - } - } - } - - let alt_hand_pct = if total > 0 { alt_hand as f32 / total as f32 * 100.0 } else { 0.0 }; - let same_hand_pct = if total > 0 { same_hand as f32 / total as f32 * 100.0 } else { 0.0 }; - let sfb_pct = if total > 0 { sfb as f32 / total as f32 * 100.0 } else { 0.0 }; - - BigramAnalysis { - total, alt_hand, same_hand, sfb, - alt_hand_pct, same_hand_pct, sfb_pct, - } -} diff --git a/original-src/ui_background.rs b/original-src/ui_background.rs deleted file mode 100644 index 1c92af9..0000000 --- a/original-src/ui_background.rs +++ /dev/null @@ -1,263 +0,0 @@ -use super::BgResult; - -/// Map a query tag to its binary command ID. -#[cfg(target_arch = "wasm32")] -use crate::binary_protocol as bp; -#[cfg(target_arch = "wasm32")] -fn tag_to_binary_cmd(tag: &str) -> Option { - match tag { - "td" => Some(bp::cmd::TD_LIST), - "combo" => Some(bp::cmd::COMBO_LIST), - "leader" => Some(bp::cmd::LEADER_LIST), - "ko" => Some(bp::cmd::KO_LIST), - "bt" => Some(bp::cmd::BT_QUERY), - "tama" => Some(bp::cmd::TAMA_QUERY), - "wpm" => Some(bp::cmd::WPM_QUERY), - "macros" => Some(bp::cmd::LIST_MACROS), - "keystats" => Some(bp::cmd::KEYSTATS_BIN), - "features" => Some(bp::cmd::FEATURES), - _ => None, - } -} - -impl super::KaSeApp { - // ---- Background helpers ---- - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn bg_query(&mut self, tag: &str, cmd: &str) { - self.busy = true; - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let tag = tag.to_string(); - let cmd = cmd.to_string(); - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); - let lines = ser.query_command(&cmd).unwrap_or_default(); - let _ = tx.send(BgResult::TextLines(tag, lines)); - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn bg_query(&mut self, tag: &str, cmd: &str) { - if self.web_busy.get() { return; } - self.busy = true; - self.web_busy.set(true); - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let web_busy = self.web_busy.clone(); - let tag = tag.to_string(); - let cmd = cmd.to_string(); - - wasm_bindgen_futures::spawn_local(async move { - let v2 = serial.borrow().v2; - let handles = serial.borrow().io_handles(); - if let Ok((reader, writer)) = handles { - if let Some(cmd_id) = v2.then(|| tag_to_binary_cmd(&tag)).flatten() { - match crate::serial::send_binary(&reader, &writer, cmd_id, &[]).await { - Ok(resp) => { - let _ = tx.send(BgResult::BinaryPayload(tag, resp.payload)); - } - Err(_) => {} - } - } else { - let result = crate::serial::query_command(&reader, &writer, &cmd).await; - match result { - Ok(lines) => { - let _ = tx.send(BgResult::TextLines(tag, lines)); - } - Err(e) => { - if e == "timeout_refresh" { - serial.borrow_mut().refresh_reader(); - } - } - } - } - } - web_busy.set(false); - }); - } - - /// Run multiple queries sequentially (avoids mutex contention). - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn bg_query_batch(&mut self, queries: &[(&str, &str)]) { - self.busy = true; - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - - let owned_queries: Vec<(String, String)> = queries - .iter() - .map(|(t, c)| (t.to_string(), c.to_string())) - .collect(); - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); - - for (tag, cmd) in owned_queries { - let lines = ser.query_command(&cmd).unwrap_or_default(); - let _ = tx.send(BgResult::TextLines(tag, lines)); - } - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn bg_query_batch(&mut self, queries: &[(&str, &str)]) { - if self.web_busy.get() { return; } - self.busy = true; - self.web_busy.set(true); - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let web_busy = self.web_busy.clone(); - - let owned: Vec<(String, String)> = queries - .iter() - .map(|(t, c)| (t.to_string(), c.to_string())) - .collect(); - - wasm_bindgen_futures::spawn_local(async move { - let v2 = serial.borrow().v2; - - for (tag, text_cmd) in &owned { - let handles = serial.borrow().io_handles(); - let (reader, writer) = match handles { - Ok(h) => h, - Err(_) => break, - }; - - if let Some(cmd_id) = v2.then(|| tag_to_binary_cmd(tag)).flatten() { - match crate::serial::send_binary(&reader, &writer, cmd_id, &[]).await { - Ok(resp) => { - let _ = tx.send(BgResult::BinaryPayload(tag.clone(), resp.payload)); - } - Err(_) => {} - } - } else { - match crate::serial::query_command(&reader, &writer, text_cmd).await { - Ok(lines) => { - let _ = tx.send(BgResult::TextLines(tag.clone(), lines)); - } - Err(e) => { - if e == "timeout_refresh" { - serial.borrow_mut().refresh_reader(); - } - } - } - } - } - web_busy.set(false); - }); - } - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn bg_send(&self, cmd: &str) { - let serial = self.serial.clone(); - let cmd = cmd.to_string(); - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); - let _ = ser.send_command(&cmd); - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn bg_send(&self, cmd: &str) { - let serial = self.serial.clone(); - let cmd = cmd.to_string(); - - wasm_bindgen_futures::spawn_local(async move { - let handles = serial.borrow().io_handles(); - if let Ok((_reader, writer)) = handles { - let _ = crate::serial::send_command(&writer, &cmd).await; - } - }); - } - - // ---- Data helpers ---- - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn load_keymap(&mut self) { - self.busy = true; - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let layer = self.current_layer as u8; - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); - - match ser.get_keymap(layer) { - Ok(km) => { let _ = tx.send(BgResult::Keymap(km)); } - Err(e) => { let _ = tx.send(BgResult::Error(e)); } - } - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn load_keymap(&mut self) { - if self.web_busy.get() { return; } - self.busy = true; - self.web_busy.set(true); - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let web_busy = self.web_busy.clone(); - let layer = self.current_layer as u8; - - wasm_bindgen_futures::spawn_local(async move { - let ser = serial.borrow(); - let v2 = ser.v2; - let handles = ser.io_handles(); - drop(ser); - match handles { - Ok((reader, writer)) => { - match crate::serial::get_keymap(&reader, &writer, layer, v2).await { - Ok(km) => { let _ = tx.send(BgResult::Keymap(km)); } - Err(e) => { let _ = tx.send(BgResult::Error(e)); } - } - } - Err(e) => { let _ = tx.send(BgResult::Error(e)); } - } - web_busy.set(false); - }); - } - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn load_layer_names(&self) { - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); - - match ser.get_layer_names() { - Ok(names) if !names.is_empty() => { - let _ = tx.send(BgResult::LayerNames(names)); - } - _ => {} - } - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn load_layer_names(&self) { - if self.web_busy.get() { return; } - self.web_busy.set(true); - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let web_busy = self.web_busy.clone(); - - wasm_bindgen_futures::spawn_local(async move { - let ser = serial.borrow(); - let v2 = ser.v2; - let handles = ser.io_handles(); - drop(ser); - if let Ok((reader, writer)) = handles { - match crate::serial::get_layer_names(&reader, &writer, v2).await { - Ok(names) if !names.is_empty() => { - let _ = tx.send(BgResult::LayerNames(names)); - } - _ => {} - } - } - web_busy.set(false); - }); - } -} diff --git a/original-src/ui_connection.rs b/original-src/ui_connection.rs deleted file mode 100644 index a8376ba..0000000 --- a/original-src/ui_connection.rs +++ /dev/null @@ -1,139 +0,0 @@ -use super::BgResult; - -#[cfg(target_arch = "wasm32")] -use eframe::egui; - -impl super::KaSeApp { - // ---- Connection helpers ---- - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn is_connected(&self) -> bool { - // Utilise le cache — PAS de serial.lock() ici ! - // Sinon le thread UI bloque quand un thread background - // (OTA, query batch) tient le lock. - self.connected_cache - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn is_connected(&self) -> bool { - let ser = self.serial.borrow(); - ser.connected - } - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn connect(&mut self) { - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - self.busy = true; - self.status_msg = "Scanning ports...".into(); - - std::thread::spawn(move || { - let mut ser = match serial.lock() { - Ok(g) => g, - Err(poisoned) => poisoned.into_inner(), - }; - - match ser.auto_connect() { - Ok(port_name) => { - let fw = ser.get_firmware_version().unwrap_or_default(); - let names = ser.get_layer_names().unwrap_or_default(); - let km = ser.get_keymap(0).unwrap_or_default(); - let _ = tx.send(BgResult::Connected(port_name, fw, names, km)); - - // Try to fetch physical layout from firmware - let layout = ser.get_layout_json() - .ok() - .and_then(|json| crate::layout::parse_json(&json).ok()); - if let Some(keys) = layout { - let _ = tx.send(BgResult::LayoutJson(keys)); - } - } - Err(e) => { - let _ = tx.send(BgResult::ConnectError(e)); - } - } - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn connect_web(&mut self, ctx: &egui::Context) { - self.busy = true; - self.web_busy.set(true); - self.status_msg = "Selecting port...".into(); - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let web_busy = self.web_busy.clone(); - let ctx = ctx.clone(); - - wasm_bindgen_futures::spawn_local(async move { - // Step 1: async port selection (no borrow held) - let conn = match crate::serial::request_port().await { - Ok(c) => c, - Err(e) => { - let _ = tx.send(BgResult::ConnectError(e)); - web_busy.set(false); - ctx.request_repaint(); - return; - } - }; - - // Step 2: extract handles before moving conn - let reader = conn.reader.clone(); - let writer = conn.writer.clone(); - let v2 = conn.v2; - - // Step 3: store connection - serial.borrow_mut().apply_connection(conn); - - // Step 4: async queries (no borrow held) - let fw = crate::serial::get_firmware_version(&reader, &writer, v2) - .await - .unwrap_or_default(); - let names = crate::serial::get_layer_names(&reader, &writer, v2) - .await - .unwrap_or_default(); - let km = crate::serial::get_keymap(&reader, &writer, 0, v2) - .await - .unwrap_or_default(); - - let _ = tx.send(BgResult::Connected("WebSerial".into(), fw, names, km)); - - // Try to fetch physical layout from firmware - let layout_json = crate::serial::get_layout_json(&reader, &writer, v2).await; - if let Ok(json) = layout_json { - if let Ok(keys) = crate::layout::parse_json(&json) { - let _ = tx.send(BgResult::LayoutJson(keys)); - } - } - - web_busy.set(false); - ctx.request_repaint(); - }); - } - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn disconnect(&mut self) { - let mut guard = match self.serial.lock() { - Ok(g) => g, - Err(poisoned) => poisoned.into_inner(), - }; - guard.disconnect(); - drop(guard); - - self.connected_cache = false; - self.firmware_version.clear(); - self.keymap.clear(); - self.layer_names = vec!["Layer 0".into()]; - self.status_msg = "Disconnected".into(); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn disconnect(&mut self) { - self.serial.borrow_mut().disconnect(); - - self.firmware_version.clear(); - self.keymap.clear(); - self.layer_names = vec!["Layer 0".into()]; - self.status_msg = "Disconnected".into(); - } -} diff --git a/original-src/ui_helpers.rs b/original-src/ui_helpers.rs deleted file mode 100644 index b697e21..0000000 --- a/original-src/ui_helpers.rs +++ /dev/null @@ -1,218 +0,0 @@ -use super::{BgResult, Instant}; - -impl super::KaSeApp { - #[allow(dead_code)] - pub(super) fn notify(&mut self, msg: &str) { - let timestamp = Instant::now(); - let entry = (msg.to_string(), timestamp); - self.notifications.push(entry); - } - - pub(super) fn get_heatmap_intensity(&self, row: usize, col: usize) -> f32 { - if !self.heatmap_on || self.heatmap_max == 0 { - return 0.0; - } - - let row_data = self.heatmap_data.get(row); - let cell_option = row_data.and_then(|r| r.get(col)); - let count = cell_option.copied().unwrap_or(0); - - let count_float = count as f32; - let max_float = self.heatmap_max as f32; - let intensity = count_float / max_float; - intensity - } - - #[cfg(not(target_arch = "wasm32"))] - pub(super) fn load_heatmap(&mut self) { - self.busy = true; - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); - let lines = ser.query_command("KEYSTATS?").unwrap_or_default(); - let (data, max) = crate::parsers::parse_heatmap_lines(&lines); - let _ = tx.send(BgResult::HeatmapData(data, max)); - }); - } - - #[cfg(target_arch = "wasm32")] - pub(super) fn load_heatmap(&mut self) { - if self.web_busy.get() { return; } - self.busy = true; - self.web_busy.set(true); - let serial = self.serial.clone(); - let tx = self.bg_tx.clone(); - let web_busy = self.web_busy.clone(); - - wasm_bindgen_futures::spawn_local(async move { - let handles = serial.borrow().io_handles(); - if let Ok((reader, writer)) = handles { - let lines = crate::serial::query_command(&reader, &writer, "KEYSTATS?") - .await - .unwrap_or_default(); - let (data, max) = crate::parsers::parse_heatmap_lines(&lines); - let _ = tx.send(BgResult::HeatmapData(data, max)); - } - web_busy.set(false); - }); - } - - - /// Apply a key selection from the key selector. - /// row == 951: new KO result key - /// row == 950: new KO trigger key - /// row 900-949: KO edit (900 + idx*10 + 0=trig, +1=result) - /// row >= 800: new leader result - /// row >= 700: new leader sequence key (append) - /// row >= 600: leader result edit (row-600 = leader index) - /// row >= 500: leader sequence key edit (row = 500 + idx*10 + seq_pos) - /// row >= 400: new combo result mode - /// row >= 300: combo result edit (row-300 = combo index) - /// row >= 200: macro step mode (add key as step) - /// row >= 100: TD mode (row-100 = td index, col = action slot) - /// row < 100: keymap mode - pub(super) fn apply_key_selection(&mut self, row: usize, col: usize, code: u16) { - if row == 951 { - // New KO result key - self.ko_new_res_key = code as u8; - self.ko_new_res_set = true; - self.status_msg = format!("KO result = {}", crate::keycode::hid_key_name(code as u8)); - return; - } - - if row == 950 { - // New KO trigger key - self.ko_new_trig_key = code as u8; - self.ko_new_trig_set = true; - self.status_msg = format!("KO trigger = {}", crate::keycode::hid_key_name(code as u8)); - return; - } - - if row >= 900 { - // KO edit: row = 900 + idx*10 + field (0=trig, 1=result) - let offset = row - 900; - let ko_idx = offset / 10; - let field = offset % 10; - let idx_valid = ko_idx < self.ko_data.len(); - if idx_valid { - if field == 0 { - self.ko_data[ko_idx][0] = code as u8; - self.status_msg = format!("KO #{} trigger = 0x{:02X}", ko_idx, code); - } else { - self.ko_data[ko_idx][2] = code as u8; - self.status_msg = format!("KO #{} result = 0x{:02X}", ko_idx, code); - } - } - return; - } - - if row >= 800 { - // New leader result - self.leader_new_result = code as u8; - self.leader_new_result_set = true; - self.status_msg = format!("Leader result = {}", crate::keycode::hid_key_name(code as u8)); - return; - } - - if row >= 700 { - // New leader sequence key (append) - let seq_not_full = self.leader_new_seq.len() < 4; - if seq_not_full { - self.leader_new_seq.push(code as u8); - let key_name = crate::keycode::hid_key_name(code as u8); - self.status_msg = format!("Leader seq + {}", key_name); - } - return; - } - - if row >= 600 { - // Leader result edit (existing) - let leader_idx = row - 600; - let idx_valid = leader_idx < self.leader_data.len(); - if idx_valid { - self.leader_data[leader_idx].result = code as u8; - self.status_msg = format!("Leader #{} result = 0x{:02X}", leader_idx, code); - } - return; - } - - if row >= 500 { - // Leader sequence key edit (existing) - // row = 500 + leader_idx*10 + seq_pos - let offset = row - 500; - let leader_idx = offset / 10; - let seq_pos = offset % 10; - let idx_valid = leader_idx < self.leader_data.len(); - if idx_valid { - let seq_valid = seq_pos < self.leader_data[leader_idx].sequence.len(); - if seq_valid { - self.leader_data[leader_idx].sequence[seq_pos] = code as u8; - self.status_msg = format!("Leader #{} key {} = 0x{:02X}", leader_idx, seq_pos, code); - } - } - return; - } - - if row >= 400 { - // New combo result mode - self.combo_new_result = code; - self.status_msg = format!("New combo result = 0x{:04X}", code); - return; - } - - if row >= 300 { - // Combo result edit mode - let combo_idx = row - 300; - let idx_valid = combo_idx < self.combo_data.len(); - if idx_valid { - self.combo_data[combo_idx].result = code; - self.status_msg = format!("Combo #{} result = 0x{:04X}", combo_idx, code); - } - return; - } - - if row >= 200 { - // Macro step mode - self.apply_macro_step(code); - return; - } - - if row >= 100 { - // TD mode - let td_idx = row - 100; - let idx_valid = td_idx < self.td_data.len(); - let col_valid = col < 4; - - if idx_valid && col_valid { - self.td_data[td_idx][col] = code; - self.status_msg = format!("TD {} action {} = 0x{:04X}", td_idx, col, code); - } - } else { - // Keymap mode - validate bounds BEFORE sending - let row_valid = row < self.keymap.len(); - let col_valid = row_valid && col < self.keymap[row].len(); - - if col_valid { - let layer = self.current_layer as u8; - let row_byte = row as u8; - let col_byte = col as u8; - let cmd = crate::protocol::cmd_set_key(layer, row_byte, col_byte, code); - self.bg_send(&cmd); - - self.keymap[row][col] = code; - self.status_msg = format!("[{},{}] = 0x{:04X}", row, col, code); - } else { - self.status_msg = format!("Invalid key position [{},{}]", row, col); - } - } - } - - pub(super) fn get_key(&self, row: usize, col: usize) -> u16 { - let row_data = self.keymap.get(row); - let cell_option = row_data.and_then(|r| r.get(col)); - let value = cell_option.copied(); - value.unwrap_or(0) - } -} diff --git a/original-src/ui_mod.rs b/original-src/ui_mod.rs deleted file mode 100644 index ea634fa..0000000 --- a/original-src/ui_mod.rs +++ /dev/null @@ -1,277 +0,0 @@ -mod background; -mod connection; -mod geometry; -mod helpers; -mod key_selector; -mod render; -mod tab_advanced; -mod tab_keymap; -mod tab_macros; -mod tab_settings; -mod tab_stats; -mod update; - -use std::sync::mpsc; -use crate::serial::{self, SharedSerial}; -use crate::layout::{self, KeycapPos}; -use crate::layout_remap::KeyboardLayout; - -// Instant doesn't exist in WASM - use a wrapper -#[cfg(not(target_arch = "wasm32"))] -type Instant = std::time::Instant; - -#[cfg(target_arch = "wasm32")] -#[derive(Clone, Copy)] -struct Instant(f64); - -#[cfg(target_arch = "wasm32")] -impl Instant { - fn now() -> Self { - let window = web_sys::window().unwrap(); - let performance = window.performance().unwrap(); - Instant(performance.now()) - } - fn elapsed(&self) -> std::time::Duration { - let now = Self::now(); - let ms = now.0 - self.0; - std::time::Duration::from_millis(ms as u64) - } -} - -/// Results from background serial operations. -pub(super) enum BgResult { - Connected(String, String, Vec, Vec>), - ConnectError(String), - Keymap(Vec>), - LayerNames(Vec), - TextLines(String, Vec), - #[allow(dead_code)] // constructed only in WASM builds - BinaryPayload(String, Vec), // tag, raw KR payload - LayoutJson(Vec), // physical key positions from firmware - OtaProgress(f32, String), // progress 0-1, status message - HeatmapData(Vec>, u32), // counts, max - Error(String), -} - -#[derive(PartialEq)] -pub(super) enum Tab { - Keymap, - Advanced, - Macros, - Stats, - Settings, -} - -pub struct KaSeApp { - pub(super) serial: SharedSerial, - pub(super) bg_tx: mpsc::Sender, - pub(super) bg_rx: mpsc::Receiver, - pub(super) busy: bool, - #[cfg(target_arch = "wasm32")] - pub(super) web_busy: std::rc::Rc>, - pub(super) tab: Tab, - - // Connection - /// Cached connection status — avoids serial.lock() in views. - /// Updated in poll_bg() when Connected/ConnectError is received. - pub(super) connected_cache: bool, - pub(super) firmware_version: String, - - // Keymap - pub(super) current_layer: usize, - pub(super) layer_names: Vec, - pub(super) keymap: Vec>, // rows x cols - pub(super) key_layout: Vec, - pub(super) editing_key: Option<(usize, usize)>, // (row, col) being edited - - // Keymap editing - pub(super) layer_rename: String, - - // Advanced - pub(super) td_lines: Vec, - pub(super) td_data: Vec<[u16; 4]>, // parsed tap dance slots - pub(super) combo_lines: Vec, - pub(super) combo_data: Vec, - /// None = pas de picking en cours. Some((combo_idx, slot)) = on attend un clic clavier. - /// combo_idx: usize::MAX = nouveau combo. slot: 0 = key1, 1 = key2. - pub(super) combo_picking: Option<(usize, u8)>, - pub(super) combo_new_r1: u8, - pub(super) combo_new_c1: u8, - pub(super) combo_new_r2: u8, - pub(super) combo_new_c2: u8, - pub(super) combo_new_result: u16, - pub(super) combo_new_key1_set: bool, - pub(super) combo_new_key2_set: bool, - pub(super) leader_lines: Vec, - pub(super) leader_data: Vec, - // Leader editing: new sequence being built - pub(super) leader_new_seq: Vec, // HID keycodes for the sequence - pub(super) leader_new_result: u8, - pub(super) leader_new_mod: u8, - pub(super) leader_new_result_set: bool, - pub(super) ko_lines: Vec, - pub(super) ko_data: Vec<[u8; 4]>, // parsed: [trigger, trig_mod, result, res_mod] - pub(super) ko_new_trig_key: u8, - pub(super) ko_new_trig_mod: u8, - pub(super) ko_new_res_key: u8, - pub(super) ko_new_res_mod: u8, - pub(super) ko_new_trig_set: bool, - pub(super) ko_new_res_set: bool, - pub(super) bt_lines: Vec, - pub(super) tama_lines: Vec, - pub(super) wpm_text: String, - pub(super) autoshift_status: String, - pub(super) tri_l1: String, - pub(super) tri_l2: String, - pub(super) tri_l3: String, - - // Macros - pub(super) macro_lines: Vec, - pub(super) macro_data: Vec, - pub(super) macro_slot: String, - pub(super) macro_name: String, - pub(super) macro_steps: String, - - // Stats / Heatmap - pub(super) keystats_lines: Vec, - pub(super) bigrams_lines: Vec, - pub(super) heatmap_data: Vec>, // rows x cols press counts - pub(super) heatmap_max: u32, - pub(super) heatmap_on: bool, - pub(super) heatmap_selected: Option<(usize, usize)>, // selected key for bigram view - pub(super) stats_dirty: bool, - - // Key selector - pub(super) key_search: String, - pub(super) mt_mod: u8, - pub(super) mt_key: u8, - pub(super) lt_layer: u8, - pub(super) lt_key: u8, - pub(super) hex_input: String, - - // Settings - pub(super) keyboard_layout: crate::layout_remap::KeyboardLayout, - - // OTA - pub(super) ota_path: String, - pub(super) ota_status: String, - pub(super) ota_firmware_data: Vec, - pub(super) ota_progress: f32, - pub(super) ota_releases: Vec<(String, String)>, // (tag_name, asset_url) - pub(super) ota_selected_release: usize, - - // Prog port flasher (native only) - #[cfg(not(target_arch = "wasm32"))] - pub(super) prog_port: String, - #[cfg(not(target_arch = "wasm32"))] - pub(super) prog_path: String, - #[cfg(not(target_arch = "wasm32"))] - pub(super) prog_ports_list: Vec, - - // Notifications - pub(super) notifications: Vec<(String, Instant)>, - - // Status - pub(super) status_msg: String, - pub(super) last_reconnect_poll: Instant, - pub(super) last_port_check: Instant, - pub(super) last_wpm_poll: Instant, - pub(super) last_stats_refresh: Instant, -} - -impl KaSeApp { - pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - let (bg_tx, bg_rx) = mpsc::channel(); - - let saved_settings = crate::settings::load(); - let keyboard_layout = KeyboardLayout::from_name(&saved_settings.keyboard_layout); - - Self { - serial: serial::new_shared(), - bg_tx, - bg_rx, - busy: false, - connected_cache: false, - #[cfg(target_arch = "wasm32")] - web_busy: std::rc::Rc::new(std::cell::Cell::new(false)), - tab: Tab::Keymap, - firmware_version: String::new(), - current_layer: 0, - layer_names: vec!["Layer 0".into()], - keymap: Vec::new(), - key_layout: layout::default_layout(), - editing_key: None, - layer_rename: String::new(), - td_lines: Vec::new(), - td_data: Vec::new(), - combo_lines: Vec::new(), - combo_data: Vec::new(), - combo_picking: None, - combo_new_r1: 0, - combo_new_c1: 0, - combo_new_r2: 0, - combo_new_c2: 0, - combo_new_result: 0, - combo_new_key1_set: false, - combo_new_key2_set: false, - leader_new_seq: Vec::new(), - leader_new_result: 0, - leader_new_mod: 0, - leader_new_result_set: false, - leader_lines: Vec::new(), - leader_data: Vec::new(), - ko_lines: Vec::new(), - ko_data: Vec::new(), - ko_new_trig_key: 0, - ko_new_trig_mod: 0, - ko_new_res_key: 0, - ko_new_res_mod: 0, - ko_new_trig_set: false, - ko_new_res_set: false, - bt_lines: Vec::new(), - macro_lines: Vec::new(), - macro_data: Vec::new(), - tama_lines: Vec::new(), - wpm_text: String::new(), - autoshift_status: String::new(), - tri_l1: "1".into(), - tri_l2: "2".into(), - tri_l3: "3".into(), - macro_slot: "0".into(), - macro_name: String::new(), - macro_steps: String::new(), - keystats_lines: Vec::new(), - bigrams_lines: Vec::new(), - heatmap_data: Vec::new(), - heatmap_max: 0, - heatmap_on: false, - heatmap_selected: None, - stats_dirty: true, - key_search: String::new(), - mt_mod: 0x02, // HID Left Shift - mt_key: 0x04, // HID 'A' - lt_layer: 1, - lt_key: 0x2C, // HID Space - hex_input: String::new(), - keyboard_layout, - #[cfg(not(target_arch = "wasm32"))] - prog_port: String::new(), - #[cfg(not(target_arch = "wasm32"))] - prog_path: String::new(), - #[cfg(not(target_arch = "wasm32"))] - prog_ports_list: Vec::new(), - notifications: Vec::new(), - ota_path: String::new(), - ota_status: String::new(), - ota_firmware_data: Vec::new(), - ota_progress: 0.0, - ota_releases: Vec::new(), - ota_selected_release: 0, - status_msg: "Searching KeSp...".into(), - last_reconnect_poll: Instant::now(), - last_port_check: Instant::now(), - last_wpm_poll: Instant::now(), - last_stats_refresh: Instant::now(), - } - } -} diff --git a/src/advanced.rs b/src/advanced.rs new file mode 100644 index 0000000..d788070 --- /dev/null +++ b/src/advanced.rs @@ -0,0 +1,283 @@ +use crate::context::{mod_idx_to_byte, AppContext, BgMsg}; +use crate::protocol; +use crate::{AdvancedBridge, AppState, MainWindow}; +use slint::{ComponentHandle, Model, SharedString}; + +/// Wire up all advanced callbacks: refresh, delete combo/leader/KO, +/// set trilayer, BT switch, TAMA action, toggle autoshift, create combo/KO/leader. +pub fn setup(window: &MainWindow, ctx: &AppContext) { + // --- Advanced: refresh --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_refresh_advanced(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(r) = ser.send_binary(cmd::TD_LIST, &[]) { + let _ = tx.send(BgMsg::TdList(protocol::parsers::parse_td_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { + let _ = tx.send(BgMsg::ComboList(protocol::parsers::parse_combo_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { + let _ = tx.send(BgMsg::LeaderList(protocol::parsers::parse_leader_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { + let _ = tx.send(BgMsg::KoList(protocol::parsers::parse_ko_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::BT_QUERY, &[]) { + let _ = tx.send(BgMsg::BtStatus(protocol::parsers::parse_bt_binary(&r.payload))); + } + }); + }); + } + + // --- Advanced: delete combo --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_delete_combo(move |idx| { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::COMBO_DELETE, &[idx as u8]); + if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { + let _ = tx.send(BgMsg::ComboList(protocol::parsers::parse_combo_binary(&r.payload))); + } + }); + }); + } + + // --- Advanced: delete leader --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_delete_leader(move |idx| { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::LEADER_DELETE, &[idx as u8]); + if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { + let _ = tx.send(BgMsg::LeaderList(protocol::parsers::parse_leader_binary(&r.payload))); + } + }); + }); + } + + // --- Advanced: delete KO --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_delete_ko(move |idx| { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::KO_DELETE, &[idx as u8]); + if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { + let _ = tx.send(BgMsg::KoList(protocol::parsers::parse_ko_binary(&r.payload))); + } + }); + }); + } + + // --- Advanced: set trilayer --- + { + let serial = ctx.serial.clone(); + let window_weak = window.as_weak(); + + window.global::().on_set_trilayer(move |l1, l2, l3| { + let payload = vec![l1 as u8, l2 as u8, l3 as u8]; + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(protocol::binary::cmd::TRILAYER_SET, &payload); + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text( + SharedString::from(format!("Tri-layer: {} + {} → {}", l1, l2, l3)) + ); + } + }); + } + + // --- Advanced: BT switch --- + { + let serial = ctx.serial.clone(); + + window.global::().on_bt_switch(move |slot| { + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(protocol::binary::cmd::BT_SWITCH, &[slot as u8]); + }); + }); + } + + // --- Advanced: TAMA action --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_tama_action(move |action| { + use protocol::binary::cmd; + let action_cmd = match action.as_str() { + "feed" => cmd::TAMA_FEED, + "play" => cmd::TAMA_PLAY, + "sleep" => cmd::TAMA_SLEEP, + "meds" => cmd::TAMA_MEDICINE, + "toggle" => cmd::TAMA_ENABLE, // toggle handled by firmware + _ => return, + }; + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(action_cmd, &[]); + if let Ok(r) = ser.send_binary(cmd::TAMA_QUERY, &[]) { + let _ = tx.send(BgMsg::TamaStatus(protocol::parsers::parse_tama_binary(&r.payload))); + } + }); + }); + } + + // --- Advanced: toggle autoshift --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_toggle_autoshift(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.send_binary(protocol::binary::cmd::AUTOSHIFT_TOGGLE, &[]) { + Ok(r) => { + let enabled = r.payload.first().copied().unwrap_or(0); + let status = if enabled != 0 { "Autoshift: ON" } else { "Autoshift: OFF" }; + let _ = tx.send(BgMsg::AutoshiftStatus(status.to_string())); + } + Err(e) => { + let _ = tx.send(BgMsg::AutoshiftStatus(format!("Error: {}", e))); + } + } + }); + }); + } + + // --- Advanced: create combo --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_create_combo(move || { + let Some(w) = window_weak.upgrade() else { return }; + let adv = w.global::(); + let r1 = adv.get_new_combo_r1() as u8; + let c1 = adv.get_new_combo_c1() as u8; + let r2 = adv.get_new_combo_r2() as u8; + let c2 = adv.get_new_combo_c2() as u8; + let result = adv.get_new_combo_result_code() as u8; + let key1_name = adv.get_new_combo_key1_name(); + let key2_name = adv.get_new_combo_key2_name(); + if key1_name == "Pick..." || key2_name == "Pick..." { + w.global::().set_status_text("Pick both keys first".into()); + return; + } + let next_idx = adv.get_combos().row_count() as u8; + let payload = protocol::binary::combo_set_payload(next_idx, r1, c1, r2, c2, result); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::COMBO_SET, &payload); + if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { + let _ = tx.send(BgMsg::ComboList(protocol::parsers::parse_combo_binary(&r.payload))); + } + }); + w.global::().set_status_text("Creating combo...".into()); + }); + } + + // --- Advanced: create KO --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_create_ko(move || { + let Some(w) = window_weak.upgrade() else { return }; + let adv = w.global::(); + let trig = adv.get_new_ko_trigger_code() as u8; + let trig_mod = (adv.get_new_ko_trig_ctrl() as u8) + | ((adv.get_new_ko_trig_shift() as u8) << 1) + | ((adv.get_new_ko_trig_alt() as u8) << 2); + let result = adv.get_new_ko_result_code() as u8; + let res_mod = (adv.get_new_ko_res_ctrl() as u8) + | ((adv.get_new_ko_res_shift() as u8) << 1) + | ((adv.get_new_ko_res_alt() as u8) << 2); + let next_idx = adv.get_key_overrides().row_count() as u8; + let payload = protocol::binary::ko_set_payload(next_idx, trig, trig_mod, result, res_mod); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::KO_SET, &payload); + if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { + let _ = tx.send(BgMsg::KoList(protocol::parsers::parse_ko_binary(&r.payload))); + } + }); + w.global::().set_status_text("Creating key override...".into()); + }); + } + + // --- Advanced: create leader --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_create_leader(move |result_code: i32, mod_idx: i32| { + let Some(w) = window_weak.upgrade() else { return }; + let adv = w.global::(); + let count = adv.get_new_leader_seq_count() as usize; + let mut sequence = Vec::new(); + if count > 0 { sequence.push(adv.get_new_leader_seq0_code() as u8); } + if count > 1 { sequence.push(adv.get_new_leader_seq1_code() as u8); } + if count > 2 { sequence.push(adv.get_new_leader_seq2_code() as u8); } + if count > 3 { sequence.push(adv.get_new_leader_seq3_code() as u8); } + let result = result_code as u8; + let result_mod = mod_idx_to_byte(mod_idx); + let next_idx = adv.get_leaders().row_count() as u8; + let payload = protocol::binary::leader_set_payload(next_idx, &sequence, result, result_mod); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::LEADER_SET, &payload); + if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { + let _ = tx.send(BgMsg::LeaderList(protocol::parsers::parse_leader_binary(&r.payload))); + } + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text("Creating leader key...".into()); + } + }); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..fe9a170 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,211 @@ +use crate::context::BgMsg; +use crate::protocol; +use crate::protocol::serial::SerialManager; +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; + +/// Export all keyboard config to JSON file via rfd save dialog. +pub fn export_config( + serial: &Arc>, + tx: &mpsc::Sender, +) -> Result { + use protocol::binary::cmd; + use protocol::config_io::*; + + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + + let _ = tx.send(BgMsg::ConfigProgress(0.05, "Reading layer names...".into())); + let layer_names = ser.get_layer_names().unwrap_or_default(); + let num_layers = layer_names.len().max(1); + + let mut keymaps = Vec::new(); + for layer in 0..num_layers { + let progress = 0.05 + (layer as f32 / num_layers as f32) * 0.30; + let _ = tx.send(BgMsg::ConfigProgress(progress, format!("Reading layer {}...", layer))); + let km = ser.get_keymap(layer as u8).unwrap_or_default(); + keymaps.push(km); + } + + let _ = tx.send(BgMsg::ConfigProgress(0.40, "Reading tap dances...".into())); + let tap_dances = match ser.send_binary(cmd::TD_LIST, &[]) { + Ok(resp) => { + let td_raw = protocol::parsers::parse_td_binary(&resp.payload); + td_raw.iter().enumerate() + .filter(|(_, actions)| actions.iter().any(|&a| a != 0)) + .map(|(i, actions)| TdConfig { index: i as u8, actions: *actions }) + .collect() + } + Err(_) => Vec::new(), + }; + + let _ = tx.send(BgMsg::ConfigProgress(0.50, "Reading combos...".into())); + let combos = match ser.send_binary(cmd::COMBO_LIST, &[]) { + Ok(resp) => { + protocol::parsers::parse_combo_binary(&resp.payload).iter().map(|c| ComboConfig { + index: c.index, r1: c.r1, c1: c.c1, r2: c.r2, c2: c.c2, result: c.result, + }).collect() + } + Err(_) => Vec::new(), + }; + + let _ = tx.send(BgMsg::ConfigProgress(0.60, "Reading key overrides...".into())); + let key_overrides = match ser.send_binary(cmd::KO_LIST, &[]) { + Ok(resp) => { + protocol::parsers::parse_ko_binary(&resp.payload).iter().map(|ko| KoConfig { + trigger_key: ko[0], trigger_mod: ko[1], result_key: ko[2], result_mod: ko[3], + }).collect() + } + Err(_) => Vec::new(), + }; + + let _ = tx.send(BgMsg::ConfigProgress(0.70, "Reading leaders...".into())); + let leaders = match ser.send_binary(cmd::LEADER_LIST, &[]) { + Ok(resp) => { + protocol::parsers::parse_leader_binary(&resp.payload).iter().map(|l| LeaderConfig { + index: l.index, sequence: l.sequence.clone(), result: l.result, result_mod: l.result_mod, + }).collect() + } + Err(_) => Vec::new(), + }; + + let _ = tx.send(BgMsg::ConfigProgress(0.80, "Reading macros...".into())); + let macros = match ser.send_binary(cmd::LIST_MACROS, &[]) { + Ok(resp) => { + protocol::parsers::parse_macros_binary(&resp.payload).iter().map(|m| { + let steps_str: Vec = m.steps.iter() + .map(|s| format!("{:02X}:{:02X}", s.keycode, s.modifier)) + .collect(); + MacroConfig { slot: m.slot, name: m.name.clone(), steps: steps_str.join(",") } + }).collect() + } + Err(_) => Vec::new(), + }; + + drop(ser); + + let _ = tx.send(BgMsg::ConfigProgress(0.90, "Saving file...".into())); + + let config = KeyboardConfig { + version: 1, + layer_names, + keymaps, + tap_dances, + combos, + key_overrides, + leaders, + macros, + }; + + let json = config.to_json()?; + + let file = rfd::FileDialog::new() + .add_filter("KeSp Config", &["json"]) + .set_file_name("kesp_config.json") + .save_file(); + + match file { + Some(path) => { + std::fs::write(&path, &json).map_err(|e| format!("Write error: {}", e))?; + Ok(format!("Exported to {}", path.display())) + } + None => Ok("Export cancelled".into()), + } +} + +/// Import keyboard config using binary protocol v2. +pub fn import_config( + serial: &Arc>, + tx: &mpsc::Sender, + config: &protocol::config_io::KeyboardConfig, +) -> Result { + use protocol::binary as bp; + + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let mut errors = 0usize; + + let total_steps = (config.layer_names.len() + + config.keymaps.len() + + config.tap_dances.len() + + config.combos.len() + + config.key_overrides.len() + + config.leaders.len() + + config.macros.len()) + .max(1) as f32; + let mut done = 0usize; + + let _ = tx.send(BgMsg::ConfigProgress(0.0, "Setting layer names...".into())); + for (i, name) in config.layer_names.iter().enumerate() { + let payload = bp::set_layout_name_payload(i as u8, name); + if ser.send_binary(bp::cmd::SET_LAYOUT_NAME, &payload).is_err() { errors += 1; } + done += 1; + } + + for (layer, km) in config.keymaps.iter().enumerate() { + let progress = done as f32 / total_steps; + let _ = tx.send(BgMsg::ConfigProgress(progress, format!("Writing layer {}...", layer))); + let payload = bp::setlayer_payload(layer as u8, km); + if let Err(e) = ser.send_binary(bp::cmd::SETLAYER, &payload) { + eprintln!("SETLAYER {} failed: {}", layer, e); + errors += 1; + } + done += 1; + } + + let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting tap dances...".into())); + for td in &config.tap_dances { + let payload = bp::td_set_payload(td.index, &td.actions); + if ser.send_binary(bp::cmd::TD_SET, &payload).is_err() { errors += 1; } + done += 1; + } + + let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting combos...".into())); + for combo in &config.combos { + let payload = bp::combo_set_payload(combo.index, combo.r1, combo.c1, combo.r2, combo.c2, combo.result as u8); + if ser.send_binary(bp::cmd::COMBO_SET, &payload).is_err() { errors += 1; } + done += 1; + } + + let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting key overrides...".into())); + for (i, ko) in config.key_overrides.iter().enumerate() { + let payload = bp::ko_set_payload(i as u8, ko.trigger_key, ko.trigger_mod, ko.result_key, ko.result_mod); + if ser.send_binary(bp::cmd::KO_SET, &payload).is_err() { errors += 1; } + done += 1; + } + + let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting leaders...".into())); + for leader in &config.leaders { + let payload = bp::leader_set_payload(leader.index, &leader.sequence, leader.result, leader.result_mod); + if ser.send_binary(bp::cmd::LEADER_SET, &payload).is_err() { errors += 1; } + done += 1; + } + + let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting macros...".into())); + for m in &config.macros { + let payload = bp::macro_add_seq_payload(m.slot, &m.name, &m.steps); + if ser.send_binary(bp::cmd::MACRO_ADD_SEQ, &payload).is_err() { errors += 1; } + } + + let _ = tx.send(BgMsg::ConfigProgress(0.95, "Refreshing...".into())); + let names = ser.get_layer_names().unwrap_or_default(); + let km = ser.get_keymap(0).unwrap_or_default(); + let _ = tx.send(BgMsg::LayerNames(names)); + let _ = tx.send(BgMsg::Keymap(km)); + + let total_keys: usize = config.keymaps.iter() + .map(|l| l.iter().map(|r| r.len()).sum::()) + .sum(); + + if errors > 0 { + Ok(format!("Import done with {} errors (check stderr)", errors)) + } else { + Ok(format!("Imported: {} layers, {} keys, {} TD, {} combos, {} KO, {} leaders, {} macros", + config.layer_names.len(), + total_keys, + config.tap_dances.len(), + config.combos.len(), + config.key_overrides.len(), + config.leaders.len(), + config.macros.len(), + )) + } +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..c32f8c4 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,155 @@ +use crate::context::{AppContext, BgMsg}; +use crate::protocol; +use crate::{AppState, ConnectionBridge, ConnectionState, MainWindow}; +use slint::ComponentHandle; + +/// Auto-connect to the keyboard on startup. +pub fn auto_connect(window: &MainWindow, ctx: &AppContext) { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + window.global::().set_status_text("Scanning ports...".into()); + window.global::().set_connection(ConnectionState::Connecting); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.auto_connect() { + Ok(port_name) => { + let fw = ser.get_firmware_version().unwrap_or_default(); + let names = ser.get_layer_names().unwrap_or_default(); + let km = ser.get_keymap(0).unwrap_or_default(); + let _ = tx.send(BgMsg::Connected(port_name, fw, names, km)); + // Fetch physical layout from firmware + match ser.get_layout_json() { + Ok(json) => { + match protocol::layout::parse_json(&json) { + Ok(keys) => { let _ = tx.send(BgMsg::LayoutJson(keys)); } + Err(e) => eprintln!("Layout parse error: {}", e), + } + } + Err(e) => eprintln!("get_layout_json error: {}", e), + } + } + Err(e) => { + let _ = tx.send(BgMsg::ConnectError(e)); + } + } + }); +} + +/// Wire up Connect, Disconnect, refresh_ports, and tab-change auto-refresh. +pub fn setup(window: &MainWindow, ctx: &AppContext) { + // --- Connect callback --- + { + let serial_c = ctx.serial.clone(); + let tx_c = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + window.global::().on_connect(move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text("Scanning ports...".into()); + w.global::().set_connection(ConnectionState::Connecting); + } + let serial = serial_c.clone(); + let tx = tx_c.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.auto_connect() { + Ok(port_name) => { + let fw = ser.get_firmware_version().unwrap_or_default(); + let names = ser.get_layer_names().unwrap_or_default(); + let km = ser.get_keymap(0).unwrap_or_default(); + let _ = tx.send(BgMsg::Connected(port_name, fw, names, km)); + match ser.get_layout_json() { + Ok(json) => { + match protocol::layout::parse_json(&json) { + Ok(keys) => { let _ = tx.send(BgMsg::LayoutJson(keys)); } + Err(e) => eprintln!("Layout parse error: {}", e), + } + } + Err(e) => eprintln!("get_layout_json error: {}", e), + } + } + Err(e) => { let _ = tx.send(BgMsg::ConnectError(e)); } + } + }); + }); + } + + // --- Disconnect callback --- + { + let serial_d = ctx.serial.clone(); + let tx_d = ctx.bg_tx.clone(); + window.global::().on_disconnect(move || { + let mut ser = serial_d.lock().unwrap_or_else(|e| e.into_inner()); + ser.disconnect(); + let _ = tx_d.send(BgMsg::Disconnected); + }); + } + + window.global::().on_refresh_ports(|| {}); + + // --- Auto-refresh on tab change --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_tab_changed(move |tab_idx| { + let Some(w) = window_weak.upgrade() else { return }; + if w.global::().get_connection() != ConnectionState::Connected { return; } + + let serial = serial.clone(); + let tx = tx.clone(); + match tab_idx { + 1 => { + // Advanced: refresh TD, combo, leader, KO, BT via binary + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(r) = ser.send_binary(cmd::TD_LIST, &[]) { + let _ = tx.send(BgMsg::TdList(protocol::parsers::parse_td_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { + let _ = tx.send(BgMsg::ComboList(protocol::parsers::parse_combo_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { + let _ = tx.send(BgMsg::LeaderList(protocol::parsers::parse_leader_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { + let _ = tx.send(BgMsg::KoList(protocol::parsers::parse_ko_binary(&r.payload))); + } + if let Ok(r) = ser.send_binary(cmd::BT_QUERY, &[]) { + let _ = tx.send(BgMsg::BtStatus(protocol::parsers::parse_bt_binary(&r.payload))); + } + }); + } + 2 => { + // Macros: refresh via binary + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(resp) = ser.send_binary(protocol::binary::cmd::LIST_MACROS, &[]) { + let macros = protocol::parsers::parse_macros_binary(&resp.payload); + let _ = tx.send(BgMsg::MacroList(macros)); + } + }); + } + 3 => { + // Stats: refresh heatmap + bigrams via binary + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(r) = ser.send_binary(cmd::KEYSTATS_BIN, &[]) { + let (data, max) = protocol::parsers::parse_keystats_binary(&r.payload); + let _ = tx.send(BgMsg::HeatmapData(data, max)); + } + // Bigrams: keep text query (binary format needs dedicated parser) + let bigram_lines = if let Ok(r) = ser.send_binary(protocol::binary::cmd::BIGRAMS_TEXT, &[]) { + String::from_utf8_lossy(&r.payload).lines().map(|l| l.to_string()).collect() + } else { Vec::new() }; + let _ = tx.send(BgMsg::BigramLines(bigram_lines)); + }); + } + _ => {} + } + }); + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..8d8292c --- /dev/null +++ b/src/context.rs @@ -0,0 +1,78 @@ +use crate::protocol::layout::KeycapPos; +use crate::protocol::layout_remap::KeyboardLayout; +use crate::protocol::parsers; +use crate::protocol::serial::SerialManager; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; + +/// Shared application state passed to all bridge setup functions. +/// +/// Thread-safe fields (`serial`, `bg_tx`) are cloned into background threads. +/// Main-thread fields (`Rc>`) stay on the UI thread only. +pub struct AppContext { + pub serial: Arc>, + pub bg_tx: mpsc::Sender, + pub keys: Rc>>, + pub current_keymap: Rc>>>, + pub current_layer: Rc>, + pub keyboard_layout: Rc>, + pub heatmap_data: Rc>>>, + pub macro_steps: Rc>>, +} + +/// Messages sent from background serial threads to the UI event loop. +pub enum BgMsg { + Connected(String, String, Vec, Vec>), // port, fw_version, layer_names, keymap + ConnectError(String), + Keymap(Vec>), + LayerNames(Vec), + Disconnected, + #[allow(dead_code)] + TextLines(String, Vec), + HeatmapData(Vec>, u32), // counts, max + BigramLines(Vec), + LayoutJson(Vec), + MacroList(Vec), + TdList(Vec<[u16; 4]>), + ComboList(Vec), + LeaderList(Vec), + KoList(Vec<[u8; 4]>), + BtStatus(Vec), + TamaStatus(Vec), + AutoshiftStatus(String), + Wpm(u16), + FlashProgress(f32, String), + FlashDone(Result<(), String>), + OtaProgress(f32, String), + OtaDone(Result<(), String>), + ConfigProgress(f32, String), + ConfigDone(Result), +} + +/// Spawn a background thread that locks the serial port and runs `f`. +#[allow(dead_code)] +/// +/// Eliminates the 4-line clone+spawn+lock boilerplate repeated 30+ times. +pub fn serial_spawn(ctx: &AppContext, f: F) +where + F: FnOnce(&mut SerialManager, &mpsc::Sender) + Send + 'static, +{ + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + f(&mut ser, &tx); + }); +} + +/// Map ComboBox modifier index to HID modifier byte. +/// [None, Ctrl, Shift, Alt, GUI, RCtrl, RShift, RAlt, RGUI] +pub fn mod_idx_to_byte(idx: i32) -> u8 { + match idx { + 1 => 0x01, 2 => 0x02, 3 => 0x04, 4 => 0x08, + 5 => 0x10, 6 => 0x20, 7 => 0x40, 8 => 0x80, + _ => 0x00, + } +} diff --git a/src/dispatch.rs b/src/dispatch.rs new file mode 100644 index 0000000..384567c --- /dev/null +++ b/src/dispatch.rs @@ -0,0 +1,364 @@ +use crate::context::{AppContext, BgMsg}; +use crate::protocol::{self as protocol, keycode}; +use crate::models; +use crate::{ + AdvancedBridge, AppState, BigramData, ComboData, ConnectionState, FingerLoadData, + FlasherBridge, HandBalanceData, KeyOverrideData, KeymapBridge, LayoutBridge, LeaderData, + MacroBridge, MacroData, MainWindow, RowUsageData, SettingsBridge, StatsBridge, + TapDanceAction, TapDanceData, TopKeyData, LayerInfo, +}; +use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; +use std::rc::Rc; +use std::sync::mpsc; + +/// Start the event loop with BgMsg polling, WPM timer, and layout auto-refresh. +pub fn run(window: &MainWindow, ctx: &AppContext, bg_rx: mpsc::Receiver) { + let window_weak = window.as_weak(); + let keys_arc = ctx.keys.clone(); + let current_keymap = ctx.current_keymap.clone(); + let keyboard_layout = ctx.keyboard_layout.clone(); + let heatmap_data = ctx.heatmap_data.clone(); + + let timer = slint::Timer::default(); + timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_millis(50), + move || { + let Some(window) = window_weak.upgrade() else { return }; + + while let Ok(msg) = bg_rx.try_recv() { + handle_msg(&window, msg, &keys_arc, ¤t_keymap, &keyboard_layout, &heatmap_data); + } + }, + ); + + // WPM polling timer (5s) + let wpm_timer = slint::Timer::default(); + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak2 = window.as_weak(); + + wpm_timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_secs(5), + move || { + let Some(w) = window_weak2.upgrade() else { return }; + if w.global::().get_connection() != ConnectionState::Connected { return; } + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let Ok(mut ser) = serial.try_lock() else { return }; + if let Ok(r) = ser.send_binary(protocol::binary::cmd::WPM_QUERY, &[]) { + let wpm = if r.payload.len() >= 2 { + u16::from_le_bytes([r.payload[0], r.payload[1]]) + } else { 0 }; + let _ = tx.send(BgMsg::Wpm(wpm)); + } + }); + }, + ); + } + + // Layout auto-refresh timer (5s) + let layout_timer = slint::Timer::default(); + { + let window_weak3 = window.as_weak(); + layout_timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_secs(5), + move || { + let Some(w) = window_weak3.upgrade() else { return }; + let lb = w.global::(); + if !lb.get_auto_refresh() { return; } + let path = lb.get_file_path().to_string(); + if path.is_empty() { return; } + if let Ok(json) = std::fs::read_to_string(&path) { + models::populate_layout_preview(&w, &json); + } + }, + ); + } + + let _keep_timer = timer; + let _keep_wpm = wpm_timer; + let _keep_layout = layout_timer; + window.run().unwrap(); +} + +fn handle_msg( + window: &MainWindow, + msg: BgMsg, + keys_arc: &Rc>>, + current_keymap: &Rc>>>, + keyboard_layout: &Rc>, + heatmap_data: &Rc>>>, +) { + match msg { + BgMsg::Connected(port, fw, names, km) => { + let app = window.global::(); + app.set_connection(ConnectionState::Connected); + app.set_firmware_version(SharedString::from(&fw)); + app.set_status_text(SharedString::from(format!("Connected to {}", port))); + + let new_layers = models::build_layer_model(&names); + window.global::().set_layers(ModelRc::from(new_layers)); + + *current_keymap.borrow_mut() = km.clone(); + let keycaps = window.global::().get_keycaps(); + let layout = keyboard_layout.borrow(); + let keys = keys_arc.borrow(); + models::update_keycap_labels(&keycaps, &keys, &km, &layout); + } + BgMsg::ConnectError(e) => { + let app = window.global::(); + app.set_connection(ConnectionState::Disconnected); + app.set_status_text(SharedString::from(format!("Error: {}", e))); + } + BgMsg::Keymap(km) => { + *current_keymap.borrow_mut() = km.clone(); + let keycaps = window.global::().get_keycaps(); + let layout = keyboard_layout.borrow(); + let keys = keys_arc.borrow(); + models::update_keycap_labels(&keycaps, &keys, &km, &layout); + window.global::().set_status_text("Keymap loaded".into()); + } + BgMsg::LayerNames(names) => { + let active = window.global::().get_active_layer() as usize; + let layers: Vec = names.iter().enumerate().map(|(i, name)| LayerInfo { + index: i as i32, + name: SharedString::from(name.as_str()), + active: i == active, + }).collect(); + window.global::().set_layers( + ModelRc::from(Rc::new(VecModel::from(layers))) + ); + } + BgMsg::Disconnected => { + let app = window.global::(); + app.set_connection(ConnectionState::Disconnected); + app.set_firmware_version(SharedString::default()); + app.set_status_text("Disconnected".into()); + } + BgMsg::LayoutJson(new_keys) => { + *keys_arc.borrow_mut() = new_keys.clone(); + let new_model = models::build_keycap_model(&new_keys); + let km = current_keymap.borrow(); + if !km.is_empty() { + let layout = keyboard_layout.borrow(); + models::update_keycap_labels(&new_model, &new_keys, &km, &layout); + } + let mut max_x: f32 = 0.0; + let mut max_y: f32 = 0.0; + for kp in &new_keys { + if kp.x + kp.w > max_x { max_x = kp.x + kp.w; } + if kp.y + kp.h > max_y { max_y = kp.y + kp.h; } + } + let bridge = window.global::(); + bridge.set_content_width(max_x); + bridge.set_content_height(max_y); + bridge.set_keycaps(ModelRc::from(new_model)); + window.global::().set_status_text( + SharedString::from(format!("Layout loaded ({} keys)", new_keys.len())) + ); + } + BgMsg::BigramLines(lines) => { + let entries = protocol::stats::parse_bigram_lines(&lines); + let analysis = protocol::stats::analyze_bigrams(&entries); + window.global::().set_bigrams(BigramData { + alt_hand_pct: analysis.alt_hand_pct, + same_hand_pct: analysis.same_hand_pct, + sfb_pct: analysis.sfb_pct, + total: analysis.total as i32, + }); + } + BgMsg::FlashProgress(progress, msg) => { + let f = window.global::(); + f.set_flash_progress(progress); + f.set_flash_status(SharedString::from(msg)); + } + BgMsg::FlashDone(result) => { + let f = window.global::(); + f.set_flashing(false); + match result { + Ok(()) => { + f.set_flash_progress(1.0); + f.set_flash_status(SharedString::from("Flash complete!")); + window.global::().set_status_text("Flash complete!".into()); + } + Err(e) => { + f.set_flash_status(SharedString::from(format!("Error: {}", e))); + window.global::().set_status_text( + SharedString::from(format!("Flash error: {}", e)) + ); + } + } + } + BgMsg::HeatmapData(data, max) => { + *heatmap_data.borrow_mut() = data.clone(); + let keycaps = window.global::().get_keycaps(); + let keys = keys_arc.borrow(); + for i in 0..keycaps.row_count() { + if i >= keys.len() { break; } + let mut item = keycaps.row_data(i).unwrap(); + let kp = &keys[i]; + let count = data.get(kp.row).and_then(|r| r.get(kp.col)).copied().unwrap_or(0); + item.heat = if max > 0 { count as f32 / max as f32 } else { 0.0 }; + keycaps.set_row_data(i, item); + } + drop(keys); + + let km = current_keymap.borrow(); + let balance = protocol::stats::hand_balance(&data); + let fingers = protocol::stats::finger_load(&data); + let rows = protocol::stats::row_usage(&data); + let top = protocol::stats::top_keys(&data, &km, 10); + let dead = protocol::stats::dead_keys(&data, &km); + + let stats = window.global::(); + stats.set_hand_balance(HandBalanceData { + left_pct: balance.left_pct, right_pct: balance.right_pct, total: balance.total as i32, + }); + stats.set_total_presses(balance.total as i32); + stats.set_finger_load(ModelRc::from(Rc::new(VecModel::from( + fingers.iter().map(|f| FingerLoadData { + name: SharedString::from(&f.name), pct: f.pct, count: f.count as i32, + }).collect::>() + )))); + stats.set_row_usage(ModelRc::from(Rc::new(VecModel::from( + rows.iter().map(|r| RowUsageData { + name: SharedString::from(&r.name), pct: r.pct, count: r.count as i32, + }).collect::>() + )))); + stats.set_top_keys(ModelRc::from(Rc::new(VecModel::from( + top.iter().map(|t| TopKeyData { + name: SharedString::from(&t.name), finger: SharedString::from(&t.finger), + count: t.count as i32, pct: t.pct, + }).collect::>() + )))); + stats.set_dead_keys(ModelRc::from(Rc::new(VecModel::from( + dead.iter().map(|d| SharedString::from(d.as_str())).collect::>() + )))); + window.global::().set_status_text( + SharedString::from(format!("Stats loaded ({} total presses, max {})", balance.total, max)) + ); + } + BgMsg::TextLines(_tag, _lines) => {} + BgMsg::TdList(td_data) => { + let model: Vec = td_data.iter().enumerate() + .filter(|(_, actions)| actions.iter().any(|&a| a != 0)) + .map(|(i, actions)| TapDanceData { + index: i as i32, + actions: ModelRc::from(Rc::new(VecModel::from( + actions.iter().map(|&a| TapDanceAction { + name: SharedString::from(keycode::decode_keycode(a)), + code: a as i32, + }).collect::>() + ))), + }).collect(); + window.global::().set_tap_dances( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + BgMsg::ComboList(combo_data) => { + let model: Vec = combo_data.iter().map(|c| ComboData { + index: c.index as i32, + key1: SharedString::from(format!("R{}C{}", c.r1, c.c1)), + key2: SharedString::from(format!("R{}C{}", c.r2, c.c2)), + result: SharedString::from(keycode::decode_keycode(c.result)), + }).collect(); + window.global::().set_combos( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + BgMsg::LeaderList(leader_data) => { + let model: Vec = leader_data.iter().map(|l| { + let seq: Vec = l.sequence.iter().map(|&k| keycode::hid_key_name(k)).collect(); + LeaderData { + index: l.index as i32, + sequence: SharedString::from(seq.join(" → ")), + result: SharedString::from(keycode::hid_key_name(l.result)), + } + }).collect(); + window.global::().set_leaders( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + BgMsg::KoList(ko_data) => { + let model: Vec = ko_data.iter().enumerate().map(|(i, ko)| { + let trig_key = keycode::hid_key_name(ko[0]); + let trig_mod = keycode::mod_name(ko[1]); + let res_key = keycode::hid_key_name(ko[2]); + let res_mod = keycode::mod_name(ko[3]); + let trigger = if ko[1] != 0 { format!("{}+{}", trig_mod, trig_key) } else { trig_key }; + let result = if ko[3] != 0 { format!("{}+{}", res_mod, res_key) } else { res_key }; + KeyOverrideData { + index: i as i32, + trigger: SharedString::from(trigger), + result: SharedString::from(result), + } + }).collect(); + window.global::().set_key_overrides( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + BgMsg::BtStatus(lines) => { + window.global::().set_bt_status(SharedString::from(lines.join("\n"))); + } + BgMsg::TamaStatus(lines) => { + window.global::().set_tama_status(SharedString::from(lines.join("\n"))); + } + BgMsg::AutoshiftStatus(text) => { + window.global::().set_autoshift_status(SharedString::from(text)); + } + BgMsg::Wpm(wpm) => { + window.global::().set_wpm(wpm as i32); + } + BgMsg::OtaProgress(progress, msg) => { + let s = window.global::(); + s.set_ota_progress(progress); + s.set_ota_status(SharedString::from(msg)); + } + BgMsg::OtaDone(result) => { + let s = window.global::(); + s.set_ota_flashing(false); + match result { + Ok(()) => { s.set_ota_progress(1.0); s.set_ota_status("OTA complete!".into()); } + Err(e) => { s.set_ota_status(SharedString::from(format!("OTA error: {}", e))); } + } + } + BgMsg::ConfigProgress(progress, msg) => { + let s = window.global::(); + s.set_config_progress(progress); + s.set_config_status(SharedString::from(msg)); + } + BgMsg::ConfigDone(result) => { + let s = window.global::(); + s.set_config_busy(false); + match result { + Ok(msg) => { s.set_config_progress(1.0); s.set_config_status(SharedString::from(msg)); } + Err(e) => { s.set_config_progress(0.0); s.set_config_status(SharedString::from(format!("Error: {}", e))); } + } + } + BgMsg::MacroList(macros) => { + let model: Vec = macros.iter().map(|m| { + let steps_str: Vec = m.steps.iter().map(|s| { + if s.is_delay() { format!("T({})", s.delay_ms()) } + else { keycode::hid_key_name(s.keycode).to_string() } + }).collect(); + MacroData { + slot: m.slot as i32, + name: SharedString::from(&m.name), + steps: SharedString::from(steps_str.join(" ")), + } + }).collect(); + let max_slot = model.iter().fold(-1i32, |acc, m| acc.max(m.slot)); + let mb = window.global::(); + mb.set_macros(ModelRc::from(Rc::new(VecModel::from(model)))); + let next_slot = max_slot + 1; + if next_slot > mb.get_new_slot_idx() { + mb.set_new_slot_idx(next_slot); + } + } + } +} diff --git a/src/flasher.rs b/src/flasher.rs new file mode 100644 index 0000000..924f494 --- /dev/null +++ b/src/flasher.rs @@ -0,0 +1,118 @@ +use crate::context::{AppContext, BgMsg}; +use crate::protocol; +use crate::protocol::serial::SerialManager; +use crate::{FlasherBridge, MainWindow}; +use slint::{ComponentHandle, ModelRc, SharedString, VecModel}; +use std::rc::Rc; +use std::sync::mpsc; + +pub fn setup(window: &MainWindow, ctx: &AppContext) { + init_prog_ports(window); + setup_refresh_prog_ports(window); + setup_browse_firmware(window); + setup_flash(window, ctx); +} + +// Init prog ports list on startup +fn init_prog_ports(window: &MainWindow) { + let ports = SerialManager::list_prog_ports(); + if let Some(first) = ports.first() { + window.global::().set_selected_prog_port(SharedString::from(first.as_str())); + } + let model: Vec = ports.iter().map(|p| SharedString::from(p.as_str())).collect(); + window.global::().set_prog_ports( + ModelRc::from(Rc::new(VecModel::from(model))) + ); +} + +// --- Flasher: refresh prog ports --- +fn setup_refresh_prog_ports(window: &MainWindow) { + let window_weak = window.as_weak(); + + window.global::().on_refresh_prog_ports(move || { + let ports = SerialManager::list_prog_ports(); + if let Some(w) = window_weak.upgrade() { + if let Some(first) = ports.first() { + w.global::().set_selected_prog_port(SharedString::from(first.as_str())); + } + let model: Vec = ports.iter().map(|p| SharedString::from(p.as_str())).collect(); + w.global::().set_prog_ports( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + }); +} + +// --- Flasher: browse firmware --- +fn setup_browse_firmware(window: &MainWindow) { + let window_weak = window.as_weak(); + + window.global::().on_browse_firmware(move || { + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let file = rfd::FileDialog::new() + .add_filter("Firmware", &["bin"]) + .pick_file(); + if let Some(path) = file { + let path_str = path.to_string_lossy().to_string(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_firmware_path( + SharedString::from(path_str.as_str()) + ); + } + }); + } + }); + }); +} + +// --- Flasher: flash --- +fn setup_flash(window: &MainWindow, ctx: &AppContext) { + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_flash(move || { + let Some(w) = window_weak.upgrade() else { return }; + let flasher = w.global::(); + let port = flasher.get_selected_prog_port().to_string(); + let path = flasher.get_firmware_path().to_string(); + let offset: u32 = match flasher.get_flash_offset_index() { + 0 => 0x20000, // factory + 1 => 0x220000, // ota_0 + _ => 0x20000, + }; + + if port.is_empty() || path.is_empty() { return; } + + // Read firmware file + let firmware = match std::fs::read(&path) { + Ok(data) => data, + Err(e) => { + let _ = tx.send(BgMsg::FlashDone(Err(format!("Cannot read {}: {}", path, e)))); + return; + } + }; + + flasher.set_flashing(true); + flasher.set_flash_progress(0.0); + flasher.set_flash_status(SharedString::from("Starting...")); + + let tx = tx.clone(); + std::thread::spawn(move || { + let (ftx, frx) = mpsc::channel(); + // Forward flash progress to main bg channel + let tx2 = tx.clone(); + let progress_thread = std::thread::spawn(move || { + while let Ok(protocol::flasher::FlashProgress::OtaProgress(p, msg)) = frx.recv() { + let _ = tx2.send(BgMsg::FlashProgress(p, msg)); + } + }); + + let result = protocol::flasher::flash_firmware(&port, &firmware, offset, &ftx); + drop(ftx); // close channel so progress_thread exits + let _ = progress_thread.join(); + let _ = tx.send(BgMsg::FlashDone(result.map_err(|e| e.to_string()))); + }); + }); +} diff --git a/src/key_selector.rs b/src/key_selector.rs new file mode 100644 index 0000000..9b04348 --- /dev/null +++ b/src/key_selector.rs @@ -0,0 +1,245 @@ +use crate::context::AppContext; +use crate::protocol::{self as protocol, keycode}; +use crate::models; +use crate::{ + AdvancedBridge, AppState, KeySelectorBridge, KeymapBridge, MainWindow, +}; +use slint::{ComponentHandle, Model, SharedString}; +use std::rc::Rc; + +pub fn setup(window: &MainWindow, ctx: &AppContext) { + setup_filter(window); + let apply_keycode = build_apply_keycode(window, ctx); + let refresh_macro_display = crate::macros::make_refresh_display(window, ctx); + let dispatch_keycode = build_dispatch_keycode(window, ctx, apply_keycode, refresh_macro_display); + setup_callbacks(window, dispatch_keycode); +} + +fn setup_filter(window: &MainWindow) { + let all_keys = models::build_key_entries(); + let window_weak = window.as_weak(); + window.global::().on_apply_filter(move |search| { + if let Some(w) = window_weak.upgrade() { + models::populate_key_categories(&w, &all_keys, &search); + } + }); +} + +fn build_apply_keycode(window: &MainWindow, ctx: &AppContext) -> Rc { + let serial = ctx.serial.clone(); + let keys_arc = ctx.keys.clone(); + let current_keymap = ctx.current_keymap.clone(); + let current_layer = ctx.current_layer.clone(); + let keyboard_layout = ctx.keyboard_layout.clone(); + let window_weak = window.as_weak(); + + Rc::new(move |code: u16| { + let Some(w) = window_weak.upgrade() else { return }; + let key_idx = w.global::().get_selected_key_index(); + if key_idx < 0 { return; } + let key_idx = key_idx as usize; + let keys = keys_arc.borrow(); + if key_idx >= keys.len() { return; } + + let kp = &keys[key_idx]; + let row = kp.row; + let col = kp.col; + drop(keys); + let layer = current_layer.get() as u8; + + { + let mut km = current_keymap.borrow_mut(); + if row < km.len() && col < km[row].len() { + km[row][col] = code; + } + } + + let layout = *keyboard_layout.borrow(); + let km = current_keymap.borrow().clone(); + let keys = keys_arc.borrow().clone(); + let keycaps = w.global::().get_keycaps(); + models::update_keycap_labels(&keycaps, &keys, &km, &layout); + + let payload = protocol::binary::setkey_payload(layer, row as u8, col as u8, code); + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(protocol::binary::cmd::SETKEY, &payload); + }); + + w.global::().set_status_text( + SharedString::from(format!("[{},{}] = 0x{:04X}", row, col, code)) + ); + }) +} + +fn build_dispatch_keycode( + window: &MainWindow, + ctx: &AppContext, + apply_keycode: Rc, + refresh_macro_display: Rc, +) -> Rc { + let keys_arc = ctx.keys.clone(); + let serial = ctx.serial.clone(); + let macro_steps = ctx.macro_steps.clone(); + let window_weak = window.as_weak(); + + Rc::new(move |code: u16| { + let Some(w) = window_weak.upgrade() else { return }; + let target = w.global::().get_selector_target(); + let name = SharedString::from(keycode::decode_keycode(code)); + + match target.as_str() { + "keymap" => { apply_keycode(code); } + "combo-result" => { + let adv = w.global::(); + adv.set_new_combo_result_code(code as i32); + adv.set_new_combo_result_name(name); + } + "ko-trigger" => { + let adv = w.global::(); + adv.set_new_ko_trigger_code(code as i32); + adv.set_new_ko_trigger_name(name); + } + "ko-result" => { + let adv = w.global::(); + adv.set_new_ko_result_code(code as i32); + adv.set_new_ko_result_name(name); + } + "leader-result" => { + let adv = w.global::(); + adv.set_new_leader_result_code(code as i32); + adv.set_new_leader_result_name(name); + } + "combo-key1" | "combo-key2" => { + let keys = keys_arc.borrow(); + let idx = code as usize; + if idx < keys.len() { + let kp = &keys[idx]; + let adv = w.global::(); + let label = SharedString::from(format!("R{}C{}", kp.row, kp.col)); + if target.as_str() == "combo-key1" { + adv.set_new_combo_r1(kp.row as i32); + adv.set_new_combo_c1(kp.col as i32); + adv.set_new_combo_key1_name(label); + } else { + adv.set_new_combo_r2(kp.row as i32); + adv.set_new_combo_c2(kp.col as i32); + adv.set_new_combo_key2_name(label); + } + } + } + "leader-seq" => { + let adv = w.global::(); + let count = adv.get_new_leader_seq_count(); + match count { + 0 => { adv.set_new_leader_seq0_code(code as i32); adv.set_new_leader_seq0_name(name); } + 1 => { adv.set_new_leader_seq1_code(code as i32); adv.set_new_leader_seq1_name(name); } + 2 => { adv.set_new_leader_seq2_code(code as i32); adv.set_new_leader_seq2_name(name); } + 3 => { adv.set_new_leader_seq3_code(code as i32); adv.set_new_leader_seq3_name(name); } + _ => {} + } + if count < 4 { adv.set_new_leader_seq_count(count + 1); } + } + "td-action" => { + let adv = w.global::(); + let td_idx = adv.get_editing_td_index(); + let slot = adv.get_editing_td_slot() as usize; + if td_idx >= 0 && slot < 4 { + let tds = adv.get_tap_dances(); + for i in 0..tds.row_count() { + let td = tds.row_data(i).unwrap(); + if td.index == td_idx { + let actions = td.actions; + let mut a = actions.row_data(slot).unwrap(); + a.name = name.clone(); + a.code = code as i32; + actions.set_row_data(slot, a); + + let mut codes = [0u16; 4]; + for (j, code) in codes.iter_mut().enumerate().take(4.min(actions.row_count())) { + *code = actions.row_data(j).unwrap().code as u16; + } + let payload = protocol::binary::td_set_payload(td_idx as u8, &codes); + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(protocol::binary::cmd::TD_SET, &payload); + }); + w.global::().set_status_text( + SharedString::from(format!("TD{} slot {} = {}", td_idx, slot, name)) + ); + break; + } + } + } + } + "macro-step" => { + let mut steps = macro_steps.borrow_mut(); + steps.push((code as u8, 0x00)); + drop(steps); + refresh_macro_display(); + } + _ => { apply_keycode(code); } + } + }) +} + +fn setup_callbacks(window: &MainWindow, dispatch_keycode: Rc) { + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_select_keycode(move |code| { + dispatch(code as u16); + }); + } + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_apply_hex(move |hex_str| { + if let Ok(code) = u16::from_str_radix(hex_str.trim(), 16) { + dispatch(code); + } + }); + } + { + let window_weak = window.as_weak(); + window.global::().on_preview_hex(move |hex_str| { + let preview = u16::from_str_radix(hex_str.trim(), 16) + .map(keycode::decode_keycode) + .unwrap_or_default(); + if let Some(w) = window_weak.upgrade() { + w.global::().set_hex_preview(SharedString::from(preview)); + } + }); + } + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_apply_mt(move |mod_idx, key_idx| { + let mod_nibble: u16 = match mod_idx { + 0 => 0x01, 1 => 0x02, 2 => 0x04, 3 => 0x08, + 4 => 0x10, 5 => 0x20, 6 => 0x40, 7 => 0x80, + _ => 0x02, + }; + let hid: u16 = match key_idx { + 0..=25 => 0x04 + key_idx as u16, + 26..=35 => 0x1E + (key_idx - 26) as u16, + 36 => 0x2C, 37 => 0x28, 38 => 0x29, 39 => 0x2B, 40 => 0x2A, + _ => 0x04, + }; + let code = 0x5000 | (mod_nibble << 8) | hid; + dispatch(code); + }); + } + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_apply_lt(move |layer_idx, key_idx| { + let layer = (layer_idx as u16) & 0x0F; + let hid: u16 = match key_idx { + 0 => 0x2C, 1 => 0x28, 2 => 0x29, 3 => 0x2A, 4 => 0x2B, + 5..=9 => 0x04 + (key_idx - 5) as u16, + _ => 0x2C, + }; + let code = 0x4000 | (layer << 8) | hid; + dispatch(code); + }); + } +} diff --git a/src/keymap.rs b/src/keymap.rs new file mode 100644 index 0000000..bb9d0c0 --- /dev/null +++ b/src/keymap.rs @@ -0,0 +1,110 @@ +use crate::context::{AppContext, BgMsg}; +use crate::protocol; +use crate::{AppState, KeymapBridge, MainWindow}; +use slint::{ComponentHandle, Model, SharedString}; + +/// Wire up key selection, layer switch, layer rename, and heatmap toggle. +pub fn setup(window: &MainWindow, ctx: &AppContext) { + // --- Key selection callback --- + { + let window_weak = window.as_weak(); + window.global::().on_select_key(move |key_index| { + let Some(w) = window_weak.upgrade() else { return }; + let keycaps = w.global::().get_keycaps(); + let idx = key_index as usize; + if idx >= keycaps.row_count() { return; } + for i in 0..keycaps.row_count() { + let mut item = keycaps.row_data(i).unwrap(); + let should_select = i == idx; + if item.selected != should_select { + item.selected = should_select; + keycaps.set_row_data(i, item); + } + } + let bridge = w.global::(); + bridge.set_selected_key_index(key_index); + let item = keycaps.row_data(idx).unwrap(); + bridge.set_selected_key_label(item.label.clone()); + }); + } + + // --- Layer switch callback --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let current_layer = ctx.current_layer.clone(); + let window_weak = window.as_weak(); + + window.global::().on_switch_layer(move |layer_index| { + let idx = layer_index as usize; + current_layer.set(idx); + + // Update active flag on the CURRENT model (not a captured stale ref) + if let Some(w) = window_weak.upgrade() { + let layers = w.global::().get_layers(); + for i in 0..layers.row_count() { + let mut item = layers.row_data(i).unwrap(); + let should_be_active = item.index == layer_index; + if item.active != should_be_active { + item.active = should_be_active; + layers.set_row_data(i, item); + } + } + w.global::().set_active_layer(layer_index); + w.global::().set_status_text(SharedString::from(format!("Loading layer {}...", idx))); + } + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.get_keymap(idx as u8) { + Ok(km) => { let _ = tx.send(BgMsg::Keymap(km)); } + Err(e) => { let _ = tx.send(BgMsg::ConnectError(e)); } + } + }); + }); + } + + // --- Layer rename callback --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_rename_layer(move |layer_idx, new_name| { + let payload = protocol::binary::set_layout_name_payload(layer_idx as u8, &new_name); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(protocol::binary::cmd::SET_LAYOUT_NAME, &payload); + if let Ok(names) = ser.get_layer_names() { + let _ = tx.send(BgMsg::LayerNames(names)); + } + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text( + SharedString::from(format!("Renamed layer {} → {}", layer_idx, new_name)) + ); + } + }); + } + + // --- Heatmap toggle: auto-load data --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_toggle_heatmap(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(resp) = ser.send_binary(protocol::binary::cmd::KEYSTATS_BIN, &[]) { + let (data, max) = protocol::parsers::parse_keystats_binary(&resp.payload); + let _ = tx.send(BgMsg::HeatmapData(data, max)); + } + }); + }); + } +} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..f2dd710 --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,113 @@ +use crate::context::AppContext; +use crate::models; +use crate::{LayoutBridge, MainWindow}; +use slint::{ComponentHandle, SharedString}; + +pub fn setup(window: &MainWindow, ctx: &AppContext) { + setup_load_from_file(window); + setup_load_from_keyboard(window, ctx); + setup_export_json(window); +} + +// --- Layout preview: load from file --- +fn setup_load_from_file(window: &MainWindow) { + let window_weak = window.as_weak(); + window.global::().on_load_from_file(move || { + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let file = rfd::FileDialog::new() + .add_filter("Layout JSON", &["json"]) + .pick_file(); + let Some(path) = file else { return }; + let json = match std::fs::read_to_string(&path) { + Ok(j) => j, + Err(e) => { + let err = format!("Read error: {}", e); + let _ = slint::invoke_from_event_loop({ + let window_weak = window_weak.clone(); + move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_status(SharedString::from(err)); + } + } + }); + return; + } + }; + let path_str = path.to_string_lossy().to_string(); + let _ = slint::invoke_from_event_loop({ + let window_weak = window_weak.clone(); + move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_file_path(SharedString::from(&path_str)); + models::populate_layout_preview(&w, &json); + } + } + }); + }); + }); +} + +// --- Layout preview: load from keyboard --- +fn setup_load_from_keyboard(window: &MainWindow, ctx: &AppContext) { + let serial = ctx.serial.clone(); + let window_weak = window.as_weak(); + window.global::().on_load_from_keyboard(move || { + let serial = serial.clone(); + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let json = match ser.get_layout_json() { + Ok(j) => j, + Err(e) => { + let err = format!("Error: {}", e); + let _ = slint::invoke_from_event_loop({ + let window_weak = window_weak.clone(); + move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_status(SharedString::from(err)); + } + } + }); + return; + } + }; + let _ = slint::invoke_from_event_loop({ + let window_weak = window_weak.clone(); + move || { + if let Some(w) = window_weak.upgrade() { + models::populate_layout_preview(&w, &json); + } + } + }); + }); + }); +} + +// --- Layout preview: export JSON --- +fn setup_export_json(window: &MainWindow) { + let window_weak = window.as_weak(); + window.global::().on_export_json(move || { + let Some(w) = window_weak.upgrade() else { return }; + let json = w.global::().get_json_text().to_string(); + if json.is_empty() { return; } + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let file = rfd::FileDialog::new() + .add_filter("Layout JSON", &["json"]) + .set_file_name("layout.json") + .save_file(); + if let Some(path) = file { + let msg = match std::fs::write(&path, &json) { + Ok(()) => format!("Exported to {}", path.display()), + Err(e) => format!("Write error: {}", e), + }; + let _ = slint::invoke_from_event_loop(move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_status(SharedString::from(msg)); + } + }); + } + }); + }); +} diff --git a/src/logic/layout_remap.rs b/src/logic/layout_remap.rs deleted file mode 100644 index 023f97d..0000000 --- a/src/logic/layout_remap.rs +++ /dev/null @@ -1,339 +0,0 @@ -/// Remaps HID key names to their visual representation based on the user's -/// keyboard layout (language). Ported from the C# `KeyConverter.LayoutOverrides`. - -// Display implementation for the keyboard layout picker in settings. -impl std::fmt::Display for KeyboardLayout { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KeyboardLayout { - Qwerty, - Azerty, - Qwertz, - Dvorak, - Colemak, - Bepo, - QwertyEs, - QwertyPt, - QwertyIt, - QwertyNordic, - QwertyBr, - QwertyTr, - QwertyUk, -} - -impl KeyboardLayout { - /// All known layout variants. - pub fn all() -> &'static [KeyboardLayout] { - &[ - KeyboardLayout::Qwerty, - KeyboardLayout::Azerty, - KeyboardLayout::Qwertz, - KeyboardLayout::Dvorak, - KeyboardLayout::Colemak, - KeyboardLayout::Bepo, - KeyboardLayout::QwertyEs, - KeyboardLayout::QwertyPt, - KeyboardLayout::QwertyIt, - KeyboardLayout::QwertyNordic, - KeyboardLayout::QwertyBr, - KeyboardLayout::QwertyTr, - KeyboardLayout::QwertyUk, - ] - } - - /// Human-readable display name. - pub fn name(&self) -> &str { - match self { - KeyboardLayout::Qwerty => "QWERTY", - KeyboardLayout::Azerty => "AZERTY", - KeyboardLayout::Qwertz => "QWERTZ", - KeyboardLayout::Dvorak => "DVORAK", - KeyboardLayout::Colemak => "COLEMAK", - KeyboardLayout::Bepo => "BEPO", - KeyboardLayout::QwertyEs => "QWERTY_ES", - KeyboardLayout::QwertyPt => "QWERTY_PT", - KeyboardLayout::QwertyIt => "QWERTY_IT", - KeyboardLayout::QwertyNordic => "QWERTY_NORDIC", - KeyboardLayout::QwertyBr => "QWERTY_BR", - KeyboardLayout::QwertyTr => "QWERTY_TR", - KeyboardLayout::QwertyUk => "QWERTY_UK", - } - } - - /// Parse a layout name (case-insensitive). Falls back to `Qwerty`. - pub fn from_name(s: &str) -> Self { - match s.to_ascii_uppercase().as_str() { - "QWERTY" => KeyboardLayout::Qwerty, - "AZERTY" => KeyboardLayout::Azerty, - "QWERTZ" => KeyboardLayout::Qwertz, - "DVORAK" => KeyboardLayout::Dvorak, - "COLEMAK" => KeyboardLayout::Colemak, - "BEPO" | "BÉPO" => KeyboardLayout::Bepo, - "QWERTY_ES" => KeyboardLayout::QwertyEs, - "QWERTY_PT" => KeyboardLayout::QwertyPt, - "QWERTY_IT" => KeyboardLayout::QwertyIt, - "QWERTY_NORDIC" => KeyboardLayout::QwertyNordic, - "QWERTY_BR" => KeyboardLayout::QwertyBr, - "QWERTY_TR" => KeyboardLayout::QwertyTr, - "QWERTY_UK" => KeyboardLayout::QwertyUk, - _ => KeyboardLayout::Qwerty, - } - } -} - -/// Given a `layout` and an HID key name (e.g. `"A"`, `"COMMA"`, `"SEMICOLON"`), -/// returns the visual label for that key on the given layout, or `None` when no -/// override exists (meaning the default / QWERTY label applies). -/// -/// The lookup is **case-insensitive** on `hid_name`. -pub fn remap_key_label(layout: &KeyboardLayout, hid_name: &str) -> Option<&'static str> { - // Normalise to uppercase for matching. - let key = hid_name.to_ascii_uppercase(); - let key = key.as_str(); - - match layout { - // QWERTY has no overrides — it *is* the reference layout. - KeyboardLayout::Qwerty => None, - - KeyboardLayout::Azerty => match key { - "COMMA" | "COMM" | "COMA" => Some(";"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some(","), - "PERIOD" | "DOT" => Some(":"), - "SLASH" | "SLSH" | "/" => Some("!"), - "M" => Some(","), - "W" => Some("Z"), - "Z" => Some("W"), - "Q" => Some("A"), - "A" => Some("Q"), - "," => Some(";"), - "." => Some(":"), - ";" => Some("M"), - "-" | "MINUS" | "MIN" => Some(")"), - "BRACKET_LEFT" | "LBRCKT" | "LBRC" | "[" => Some("^"), - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" | "]" => Some("$"), - "BACKSLASH" | "BSLSH" | "\\" => Some("<"), - "APOSTROPHE" | "APO" | "QUOT" | "'" => Some("\u{00f9}"), // ù - "1" => Some("& 1"), - "2" => Some("\u{00e9} 2 ~"), // é 2 ~ - "3" => Some("\" 3 #"), - "4" => Some("' 4 }"), - "5" => Some("( 5 ["), - "6" => Some("- 6 |"), - "7" => Some("\u{00e8} 7 `"), // è 7 ` - "8" => Some("_ 8 \\"), - "9" => Some("\u{00e7} 9 ^"), // ç 9 ^ - "0" => Some("\u{00e0} 0 @"), // à 0 @ - _ => None, - }, - - KeyboardLayout::Qwertz => match key { - "Y" => Some("Z"), - "Z" => Some("Y"), - "MINUS" | "MIN" => Some("\u{00df}"), // ß - "EQUAL" | "EQL" => Some("'"), - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00fc}"), // ü - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f6}"), // ö - "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e4}"), // ä - "GRAVE" | "GRV" => Some("^"), - "SLASH" | "SLSH" => Some("-"), - "BACKSLASH" | "BSLSH" => Some("#"), - _ => None, - }, - - KeyboardLayout::Dvorak => match key { - "Q" => Some("'"), - "W" => Some(","), - "E" => Some("."), - "R" => Some("P"), - "T" => Some("Y"), - "Y" => Some("F"), - "U" => Some("G"), - "I" => Some("C"), - "O" => Some("R"), - "P" => Some("L"), - "A" => Some("A"), - "S" => Some("O"), - "D" => Some("E"), - "F" => Some("U"), - "G" => Some("I"), - "H" => Some("D"), - "J" => Some("H"), - "K" => Some("T"), - "L" => Some("N"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("S"), - "APOSTROPHE" | "APO" | "QUOT" => Some("-"), - "Z" => Some(";"), - "X" => Some("Q"), - "C" => Some("J"), - "V" => Some("K"), - "B" => Some("X"), - "N" => Some("B"), - "M" => Some("M"), - "COMMA" | "COMM" | "COMA" => Some("W"), - "PERIOD" | "DOT" => Some("V"), - "SLASH" | "SLSH" => Some("Z"), - "MINUS" | "MIN" => Some("["), - "EQUAL" | "EQL" => Some("]"), - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("/"), - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("="), - _ => None, - }, - - KeyboardLayout::Colemak => match key { - "E" => Some("F"), - "R" => Some("P"), - "T" => Some("G"), - "Y" => Some("J"), - "U" => Some("L"), - "I" => Some("U"), - "O" => Some("Y"), - "P" => Some(";"), - "S" => Some("R"), - "D" => Some("S"), - "F" => Some("T"), - "G" => Some("D"), - "J" => Some("N"), - "K" => Some("E"), - "L" => Some("I"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("O"), - "N" => Some("K"), - _ => None, - }, - - KeyboardLayout::Bepo => match key { - "Q" => Some("B"), - "W" => Some("E"), - "E" => Some("P"), - "R" => Some("O"), - "T" => Some("E"), - "Y" => Some("^"), - "U" => Some("V"), - "I" => Some("D"), - "O" => Some("L"), - "P" => Some("J"), - "A" => Some("A"), - "S" => Some("U"), - "D" => Some("I"), - "F" => Some("E"), - "G" => Some(","), - "H" => Some("C"), - "J" => Some("T"), - "K" => Some("S"), - "L" => Some("R"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("N"), - "Z" => Some("A"), - "X" => Some("Y"), - "C" => Some("X"), - "V" => Some("."), - "B" => Some("K"), - "N" => Some("'"), - "M" => Some("Q"), - "COMMA" | "COMM" | "COMA" => Some("G"), - "PERIOD" | "DOT" => Some("H"), - "SLASH" | "SLSH" => Some("F"), - "1" => Some("\""), - "2" => Some("<"), - "3" => Some(">"), - "4" => Some("("), - "5" => Some(")"), - "6" => Some("@"), - "7" => Some("+"), - "8" => Some("-"), - "9" => Some("/"), - "0" => Some("*"), - _ => None, - }, - - KeyboardLayout::QwertyEs => match key { - "MINUS" | "MIN" => Some("'"), - "EQUAL" | "EQL" => Some("\u{00a1}"), // ¡ - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("`"), - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f1}"), // ñ - "APOSTROPHE" | "APO" | "QUOT" => Some("'"), - "GRAVE" | "GRV" => Some("\u{00ba}"), // º - "SLASH" | "SLSH" => Some("-"), - "BACKSLASH" | "BSLSH" => Some("\u{00e7}"), // ç - _ => None, - }, - - KeyboardLayout::QwertyPt => match key { - "MINUS" | "MIN" => Some("'"), - "EQUAL" | "EQL" => Some("\u{00ab}"), // « - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("+"), - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("'"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00e7}"), // ç - "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00ba}"), // º - "GRAVE" | "GRV" => Some("\\"), - "SLASH" | "SLSH" => Some("-"), - "BACKSLASH" | "BSLSH" => Some("~"), - _ => None, - }, - - KeyboardLayout::QwertyIt => match key { - "MINUS" | "MIN" => Some("'"), - "EQUAL" | "EQL" => Some("\u{00ec}"), // ì - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00e8}"), // è - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f2}"), // ò - "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e0}"), // à - "GRAVE" | "GRV" => Some("\\"), - "SLASH" | "SLSH" => Some("-"), - "BACKSLASH" | "BSLSH" => Some("\u{00f9}"), // ù - _ => None, - }, - - KeyboardLayout::QwertyNordic => match key { - "MINUS" | "MIN" => Some("+"), - "EQUAL" | "EQL" => Some("'"), - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00e5}"), // å - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("\u{00a8}"), // ¨ - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f6}"), // ö - "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e4}"), // ä - "GRAVE" | "GRV" => Some("\u{00a7}"), // § - "SLASH" | "SLSH" => Some("-"), - "BACKSLASH" | "BSLSH" => Some("'"), - _ => None, - }, - - KeyboardLayout::QwertyBr => match key { - "MINUS" | "MIN" => Some("-"), - "EQUAL" | "EQL" => Some("="), - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("'"), - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("["), - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00e7}"), // ç - "APOSTROPHE" | "APO" | "QUOT" => Some("~"), - "GRAVE" | "GRV" => Some("'"), - "SLASH" | "SLSH" => Some(";"), - "BACKSLASH" | "BSLSH" => Some("]"), - _ => None, - }, - - KeyboardLayout::QwertyTr => match key { - "MINUS" | "MIN" => Some("*"), - "EQUAL" | "EQL" => Some("-"), - "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{011f}"), // ğ - "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("\u{00fc}"), // ü - "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{015f}"), // ş - "APOSTROPHE" | "APO" | "QUOT" => Some("i"), - "GRAVE" | "GRV" => Some("\""), - "SLASH" | "SLSH" => Some("."), - "BACKSLASH" | "BSLSH" => Some(","), - _ => None, - }, - - KeyboardLayout::QwertyUk => match key { - "GRAVE" | "GRV" => Some("`"), - "MINUS" | "MIN" => Some("-"), - "EQUAL" | "EQL" => Some("="), - "BACKSLASH" | "BSLSH" => Some("#"), - "APOSTROPHE" | "APO" | "QUOT" => Some("'"), - _ => None, - }, - } -} diff --git a/src/logic/protocol.rs b/src/logic/protocol.rs deleted file mode 100644 index 03f9b63..0000000 --- a/src/logic/protocol.rs +++ /dev/null @@ -1,65 +0,0 @@ -#![allow(dead_code)] -/// CDC protocol command helpers for KaSe keyboard firmware. - -// Text-based query commands -pub const CMD_TAP_DANCE: &str = "TD?"; -pub const CMD_COMBOS: &str = "COMBO?"; -pub const CMD_LEADER: &str = "LEADER?"; -pub const CMD_KEY_OVERRIDE: &str = "KO?"; -pub const CMD_BT_STATUS: &str = "BT?"; -pub const CMD_WPM: &str = "WPM?"; -pub const CMD_TAMA: &str = "TAMA?"; -pub const CMD_MACROS_TEXT: &str = "MACROS?"; -pub const CMD_FEATURES: &str = "FEATURES?"; -pub const CMD_KEYSTATS: &str = "KEYSTATS?"; -pub const CMD_BIGRAMS: &str = "BIGRAMS?"; - -pub fn cmd_set_key(layer: u8, row: u8, col: u8, keycode: u16) -> String { - format!("SETKEY {},{},{},{:04X}", layer, row, col, keycode) -} - -pub fn cmd_set_layer_name(layer: u8, name: &str) -> String { - format!("LAYOUTNAME{}:{}", layer, name) -} - -pub fn cmd_bt_switch(slot: u8) -> String { - format!("BT SWITCH {}", slot) -} - -pub fn cmd_trilayer(l1: u8, l2: u8, l3: u8) -> String { - format!("TRILAYER {},{},{}", l1, l2, l3) -} - -pub fn cmd_macroseq(slot: u8, name: &str, steps: &str) -> String { - format!("MACROSEQ {};{};{}", slot, name, steps) -} - -pub fn cmd_macro_del(slot: u8) -> String { - format!("MACRODEL {}", slot) -} - -pub fn cmd_comboset(index: u8, r1: u8, c1: u8, r2: u8, c2: u8, result: u8) -> String { - format!("COMBOSET {};{},{},{},{},{:02X}", index, r1, c1, r2, c2, result) -} - -pub fn cmd_combodel(index: u8) -> String { - format!("COMBODEL {}", index) -} - -pub fn cmd_koset(index: u8, trig_key: u8, trig_mod: u8, res_key: u8, res_mod: u8) -> String { - format!("KOSET {};{:02X},{:02X},{:02X},{:02X}", index, trig_key, trig_mod, res_key, res_mod) -} - -pub fn cmd_kodel(index: u8) -> String { - format!("KODEL {}", index) -} - -pub fn cmd_leaderset(index: u8, sequence: &[u8], result: u8, result_mod: u8) -> String { - let seq_hex: Vec = sequence.iter().map(|k| format!("{:02X}", k)).collect(); - let seq_str = seq_hex.join(","); - format!("LEADERSET {};{};{:02X},{:02X}", index, seq_str, result, result_mod) -} - -pub fn cmd_leaderdel(index: u8) -> String { - format!("LEADERDEL {}", index) -} diff --git a/src/logic/serial/mod.rs b/src/logic/serial/mod.rs deleted file mode 100644 index cfc2f59..0000000 --- a/src/logic/serial/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// Serial communication module. -/// Dispatches to native (serialport crate) or web (WebSerial API) -/// depending on the target architecture. - -#[cfg(not(target_arch = "wasm32"))] -mod native; - -#[cfg(target_arch = "wasm32")] -mod web; - -#[cfg(not(target_arch = "wasm32"))] -pub use native::*; - -#[cfg(target_arch = "wasm32")] -pub use web::*; diff --git a/src/logic/settings.rs b/src/logic/settings.rs deleted file mode 100644 index 531200f..0000000 --- a/src/logic/settings.rs +++ /dev/null @@ -1,118 +0,0 @@ -/// Persistent settings for KaSe Controller. -/// -/// - **Native**: `kase_settings.json` next to the executable. -/// - **WASM**: `localStorage` under key `"kase_settings"`. -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Settings { - #[serde(default = "default_layout")] - pub keyboard_layout: String, -} - -fn default_layout() -> String { - "QWERTY".to_string() -} - -impl Default for Settings { - fn default() -> Self { - Self { - keyboard_layout: default_layout(), - } - } -} - -// ============================================================================= -// Native: JSON file next to executable -// ============================================================================= - -#[cfg(not(target_arch = "wasm32"))] -mod native_settings { - use super::Settings; - use std::path::PathBuf; - - /// Config file path: next to the executable. - fn settings_path() -> PathBuf { - let exe = std::env::current_exe().unwrap_or_default(); - let parent_dir = exe.parent().unwrap_or(std::path::Path::new(".")); - parent_dir.join("kase_settings.json") - } - - pub fn load() -> Settings { - let path = settings_path(); - let json_content = std::fs::read_to_string(&path).ok(); - let parsed = json_content.and_then(|s| serde_json::from_str(&s).ok()); - parsed.unwrap_or_default() - } - - pub fn save(settings: &Settings) { - let path = settings_path(); - let json_result = serde_json::to_string_pretty(settings); - if let Ok(json) = json_result { - let _ = std::fs::write(path, json); - } - } -} - -// ============================================================================= -// WASM: browser localStorage -// ============================================================================= - -#[cfg(target_arch = "wasm32")] -mod web_settings { - use super::Settings; - - const STORAGE_KEY: &str = "kase_settings"; - - /// Get localStorage. Returns None if not in a browser context. - fn get_storage() -> Option { - let window = web_sys::window()?; - window.local_storage().ok().flatten() - } - - pub fn load() -> Settings { - let storage = match get_storage() { - Some(s) => s, - None => return Settings::default(), - }; - - let json_option = storage.get_item(STORAGE_KEY).ok().flatten(); - let parsed = json_option.and_then(|s| serde_json::from_str(&s).ok()); - parsed.unwrap_or_default() - } - - pub fn save(settings: &Settings) { - let storage = match get_storage() { - Some(s) => s, - None => return, - }; - - let json_result = serde_json::to_string(settings); - if let Ok(json) = json_result { - let _ = storage.set_item(STORAGE_KEY, &json); - } - } -} - -// ============================================================================= -// Public interface -// ============================================================================= - -/// Load settings from persistent storage. -/// Returns `Settings::default()` if none found. -pub fn load() -> Settings { - #[cfg(not(target_arch = "wasm32"))] - return native_settings::load(); - - #[cfg(target_arch = "wasm32")] - return web_settings::load(); -} - -/// Save settings to persistent storage. Fails silently. -pub fn save(settings: &Settings) { - #[cfg(not(target_arch = "wasm32"))] - native_settings::save(settings); - - #[cfg(target_arch = "wasm32")] - web_settings::save(settings); -} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..a0f6c94 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,182 @@ +use crate::context::{AppContext, BgMsg}; +use crate::protocol; +use crate::protocol::keycode; +use crate::{AppState, MacroBridge, MacroStepDisplay, MainWindow}; +use slint::{ComponentHandle, ModelRc, SharedString, VecModel}; +use std::rc::Rc; + +/// Build a closure that refreshes the macro step display from `ctx.macro_steps`. +pub fn make_refresh_display(window: &MainWindow, ctx: &AppContext) -> Rc { + let macro_steps = ctx.macro_steps.clone(); + let window_weak = window.as_weak(); + Rc::new(move || { + let Some(w) = window_weak.upgrade() else { return }; + let steps = macro_steps.borrow(); + let display: Vec = steps.iter().map(|&(kc, _md)| { + if kc == 0xFF { + MacroStepDisplay { + label: SharedString::from(format!("T {}ms", _md as u32 * 10)), + is_delay: true, + } + } else { + MacroStepDisplay { + label: SharedString::from(keycode::hid_key_name(kc)), + is_delay: false, + } + } + }).collect(); + let text: Vec = steps.iter().map(|&(kc, md)| { + if kc == 0xFF { format!("T({})", md as u32 * 10) } + else { format!("D({:02X})", kc) } + }).collect(); + let mb = w.global::(); + mb.set_new_steps(ModelRc::from(Rc::new(VecModel::from(display)))); + mb.set_new_steps_text(SharedString::from(text.join(" "))); + }) +} + +/// Wire up all macro callbacks: refresh, add delay, add shortcut, remove last, clear, save, delete. +pub fn setup(window: &MainWindow, ctx: &AppContext) { + // --- Macros: refresh --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_refresh_macros(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if ser.v2 { + if let Ok(resp) = ser.send_binary(protocol::binary::cmd::LIST_MACROS, &[]) { + let macros = protocol::parsers::parse_macros_binary(&resp.payload); + let _ = tx.send(BgMsg::MacroList(macros)); + } + } else { + // Legacy fallback — should not happen with v2 firmware + let _ = ser.send_binary(protocol::binary::cmd::LIST_MACROS, &[]); + } + }); + }); + } + + // --- Macros: add delay step --- + { + let macro_steps = ctx.macro_steps.clone(); + let refresh = make_refresh_display(window, ctx); + + window.global::().on_add_delay_step(move |ms| { + let units = (ms as u8) / 10; + macro_steps.borrow_mut().push((0xFF, units)); + refresh(); + }); + } + + // --- Macros: add shortcut preset --- + { + let macro_steps = ctx.macro_steps.clone(); + let refresh = make_refresh_display(window, ctx); + + window.global::().on_add_shortcut(move |shortcut| { + let ctrl = |key: u8| vec![(0xE0u8, 0u8), (key, 0)]; + let steps: Vec<(u8, u8)> = match shortcut.as_str() { + "ctrl+c" => ctrl(0x06), + "ctrl+v" => ctrl(0x19), + "ctrl+x" => ctrl(0x1B), + "ctrl+z" => ctrl(0x1D), + "ctrl+y" => ctrl(0x1C), + "ctrl+s" => ctrl(0x16), + "ctrl+a" => ctrl(0x04), + "alt+f4" => vec![(0xE2, 0), (0x3D, 0)], + _ => return, + }; + macro_steps.borrow_mut().extend(steps); + refresh(); + }); + } + + // --- Macros: remove last step --- + { + let macro_steps = ctx.macro_steps.clone(); + let refresh = make_refresh_display(window, ctx); + + window.global::().on_remove_last_step(move || { + macro_steps.borrow_mut().pop(); + refresh(); + }); + } + + // --- Macros: clear steps --- + { + let macro_steps = ctx.macro_steps.clone(); + let refresh = make_refresh_display(window, ctx); + + window.global::().on_clear_steps(move || { + macro_steps.borrow_mut().clear(); + refresh(); + }); + } + + // --- Macros: save --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let macro_steps = ctx.macro_steps.clone(); + let window_weak = window.as_weak(); + + window.global::().on_save_macro(move || { + let Some(w) = window_weak.upgrade() else { return }; + let mb = w.global::(); + let slot_num = mb.get_new_slot_idx() as u8; + mb.set_new_slot_idx(slot_num as i32 + 1); + let name = mb.get_new_name().to_string(); + let steps = macro_steps.borrow(); + let steps_str: Vec = steps.iter().map(|&(kc, md)| { + format!("{:02X}:{:02X}", kc, md) + }).collect(); + let steps_text = steps_str.join(","); + drop(steps); + let payload = protocol::binary::macro_add_seq_payload(slot_num, &name, &steps_text); + + macro_steps.borrow_mut().clear(); + mb.set_new_name(SharedString::default()); + mb.set_new_steps(ModelRc::from(Rc::new(VecModel::::default()))); + mb.set_new_steps_text(SharedString::default()); + + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::MACRO_ADD_SEQ, &payload); + if let Ok(resp) = ser.send_binary(cmd::LIST_MACROS, &[]) { + let macros = protocol::parsers::parse_macros_binary(&resp.payload); + let _ = tx.send(BgMsg::MacroList(macros)); + } + }); + w.global::().set_status_text( + SharedString::from(format!("Saving macro #{}...", slot_num)) + ); + }); + } + + // --- Macros: delete --- + { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_delete_macro(move |slot| { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_binary(cmd::MACRO_DELETE, &protocol::binary::macro_delete_payload(slot as u8)); + if let Ok(resp) = ser.send_binary(cmd::LIST_MACROS, &[]) { + let macros = protocol::parsers::parse_macros_binary(&resp.payload); + let _ = tx.send(BgMsg::MacroList(macros)); + } + }); + }); + } +} diff --git a/src/main.rs b/src/main.rs index ddd3c6a..437727a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,2443 +1,58 @@ -mod logic; +mod protocol; +mod context; +mod models; +mod config; +mod dispatch; +mod keymap; +mod macros; +mod advanced; +mod stats; +mod settings; +mod flasher; +mod layout; +mod connection; +mod key_selector; slint::include_modules!(); -use logic::keycode; -use logic::layout::KeycapPos; -use logic::serial::SerialManager; -use slint::{Model, ModelRc, SharedString, VecModel}; +use context::{AppContext, BgMsg}; use std::sync::mpsc; use std::sync::{Arc, Mutex}; use std::rc::Rc; -// Messages from background serial thread to UI -enum BgMsg { - Connected(String, String, Vec, Vec>), // port, fw_version, layer_names, keymap - ConnectError(String), - Keymap(Vec>), - LayerNames(Vec), - Disconnected, - #[allow(dead_code)] - TextLines(String, Vec), // kept for OTA legacy compatibility - HeatmapData(Vec>, u32), // counts, max - BigramLines(Vec), - LayoutJson(Vec), - MacroList(Vec), - TdList(Vec<[u16; 4]>), - ComboList(Vec), - LeaderList(Vec), - KoList(Vec<[u8; 4]>), - BtStatus(Vec), - TamaStatus(Vec), - AutoshiftStatus(String), - Wpm(u16), - FlashProgress(f32, String), - FlashDone(Result<(), String>), - OtaProgress(f32, String), - OtaDone(Result<(), String>), - ConfigProgress(f32, String), - ConfigDone(Result), -} - -fn build_keycap_model(keys: &[KeycapPos]) -> Rc> { - let keycaps: Vec = keys - .iter() - .enumerate() - .map(|(idx, kp)| KeycapData { - x: kp.x, - y: kp.y, - w: kp.w, - h: kp.h, - rotation: kp.angle, - rotation_cx: kp.w / 2.0, - rotation_cy: kp.h / 2.0, - label: SharedString::from(format!("{},{}", kp.col, kp.row)), - sublabel: SharedString::default(), - keycode: 0, - color: slint::Color::from_argb_u8(255, 0x44, 0x47, 0x5a), - heat: 0.0, - selected: false, - index: idx as i32, - }) - .collect(); - Rc::new(VecModel::from(keycaps)) -} - -fn build_layer_model(names: &[String]) -> Rc> { - let layers: Vec = names - .iter() - .enumerate() - .map(|(i, name)| LayerInfo { - index: i as i32, - name: SharedString::from(name.as_str()), - active: i == 0, - }) - .collect(); - Rc::new(VecModel::from(layers)) -} - -/// Update keycap labels from keymap data (row x col -> keycode -> label) -fn update_keycap_labels( - keycap_model: &impl Model, - keys: &[KeycapPos], - keymap: &[Vec], - layout: &logic::layout_remap::KeyboardLayout, -) { - for i in 0..keycap_model.row_count() { - if i >= keys.len() { break; } - let mut item = keycap_model.row_data(i).unwrap(); - let kp = &keys[i]; - let row = kp.row as usize; - let col = kp.col as usize; - - if row < keymap.len() && col < keymap[row].len() { - let code = keymap[row][col]; - let decoded = keycode::decode_keycode(code); - let remapped = logic::layout_remap::remap_key_label(layout, &decoded); - let label = remapped.unwrap_or(&decoded).to_string(); - item.keycode = code as i32; - item.label = SharedString::from(label); - item.sublabel = if decoded != format!("0x{:04X}", code) { - SharedString::default() - } else { - SharedString::from(format!("0x{:04X}", code)) - }; - } - keycap_model.set_row_data(i, item); - } -} - -/// Build the list of all selectable HID keycodes for the key selector -fn build_key_entries() -> Rc> { - let mut entries = Vec::new(); - - // Letters - for code in 0x04u16..=0x1D { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Letter"), - }); - } - // Numbers - for code in 0x1Eu16..=0x27 { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Number"), - }); - } - // Control keys - for code in [0x28u16, 0x29, 0x2A, 0x2B, 0x2C] { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Control"), - }); - } - // Punctuation - for code in 0x2Du16..=0x38 { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Symbol"), - }); - } - // F keys - for code in 0x3Au16..=0x45 { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Function"), - }); - } - // Navigation - for code in [0x46u16, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52] { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Navigation"), - }); - } - // Modifiers - for code in 0xE0u16..=0xE7 { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Modifier"), - }); - } - // Caps Lock - entries.push(KeyEntry { - name: SharedString::from("Caps Lock"), - code: 0x39, - category: SharedString::from("Control"), - }); - // Keypad - for code in 0x53u16..=0x63 { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Keypad"), - }); - } - // F13-F24 - for code in 0x68u16..=0x73 { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Function"), - }); - } - // Media - for code in [0x7Fu16, 0x80, 0x81] { - entries.push(KeyEntry { - name: SharedString::from(keycode::hid_key_name(code as u8)), - code: code as i32, - category: SharedString::from("Media"), - }); - } - // BT keys - for (code, name) in [ - (0x2900u16, "BT Next"), (0x2A00, "BT Prev"), (0x2B00, "BT Pair"), - (0x2C00, "BT Disc"), (0x2E00, "USB/BT"), (0x2F00, "BT On/Off"), - ] { - entries.push(KeyEntry { - name: SharedString::from(name), - code: code as i32, - category: SharedString::from("Bluetooth"), - }); - } - // Tap Dance 0..7 - for i in 0u16..=7 { - let code = (0x60 | i) << 8; - entries.push(KeyEntry { - name: SharedString::from(format!("TD {}", i)), - code: code as i32, - category: SharedString::from("Tap Dance"), - }); - } - // Macro 0..9 - for i in 0u16..=9 { - let code = (i + 0x15) << 8; - entries.push(KeyEntry { - name: SharedString::from(format!("M{}", i)), - code: code as i32, - category: SharedString::from("Macro"), - }); - } - // OSL 0..9 - for i in 0u16..=9 { - entries.push(KeyEntry { - name: SharedString::from(format!("OSL {}", i)), - code: 0x3100 + i as i32, - category: SharedString::from("Layer"), - }); - } - // Layer: MO 0..9 - for layer in 0u16..=9 { - let code = (layer + 1) << 8; - entries.push(KeyEntry { - name: SharedString::from(format!("MO {}", layer)), - code: code as i32, - category: SharedString::from("Layer"), - }); - } - // Layer: TO 0..9 - for layer in 0u16..=9 { - let code = (layer + 0x0B) << 8; - entries.push(KeyEntry { - name: SharedString::from(format!("TO {}", layer)), - code: code as i32, - category: SharedString::from("Layer"), - }); - } - // Special KaSe firmware keys - for (code, name) in [ - (0x3200u16, "Caps Word"), - (0x3300, "Repeat"), - (0x3400, "Leader"), - (0x3900, "GEsc"), - (0x3A00, "Layer Lock"), - (0x3C00, "AS Toggle"), - ] { - entries.push(KeyEntry { - name: SharedString::from(name), - code: code as i32, - category: SharedString::from("Special"), - }); - } - // None - entries.insert(0, KeyEntry { - name: SharedString::from("None"), - code: 0, - category: SharedString::from("Special"), - }); - - Rc::new(VecModel::from(entries)) -} - -fn populate_key_categories(window: &MainWindow, all_keys: &VecModel, search: &str) { - let search_lower = search.to_lowercase(); - let filter = |cat: &str| -> Vec { - (0..all_keys.row_count()) - .filter_map(|i| { - let e = all_keys.row_data(i).unwrap(); - let cat_match = e.category.as_str() == cat - || (cat == "Navigation" && (e.category.as_str() == "Control" || e.category.as_str() == "Navigation")) - || (cat == "Special" && (e.category.as_str() == "Special" || e.category.as_str() == "Bluetooth" || e.category.as_str() == "Media")) - || (cat == "TDMacro" && (e.category.as_str() == "Tap Dance" || e.category.as_str() == "Macro")); - let search_match = search_lower.is_empty() - || e.name.to_lowercase().contains(&search_lower) - || e.category.to_lowercase().contains(&search_lower); - if cat_match && search_match { Some(e) } else { None } - }) - .collect() - }; - let set = |model: Vec| ModelRc::from(Rc::new(VecModel::from(model))); - let ks = window.global::(); - ks.set_cat_letters(set(filter("Letter"))); - ks.set_cat_numbers(set(filter("Number"))); - ks.set_cat_modifiers(set(filter("Modifier"))); - ks.set_cat_nav(set(filter("Navigation"))); - ks.set_cat_function(set(filter("Function"))); - ks.set_cat_symbols(set(filter("Symbol"))); - ks.set_cat_layers(set(filter("Layer"))); - ks.set_cat_special(set(filter("Special"))); - ks.set_cat_td_macro(set(filter("TDMacro"))); -} - -/// Map ComboBox index [None,Ctrl,Shift,Alt,GUI,RCtrl,RShift,RAlt,RGUI] to HID mod byte -fn mod_idx_to_byte(idx: i32) -> u8 { - match idx { - 1 => 0x01, // Ctrl - 2 => 0x02, // Shift - 3 => 0x04, // Alt - 4 => 0x08, // GUI - 5 => 0x10, // RCtrl - 6 => 0x20, // RShift - 7 => 0x40, // RAlt - 8 => 0x80, // RGUI - _ => 0x00, // None - } -} - -/// Export all keyboard config to JSON file via rfd save dialog. -/// Uses binary protocol v2 for all queries (fast, no text parsing). -fn export_config( - serial: &Arc>, - tx: &mpsc::Sender, -) -> Result { - use logic::binary_protocol::cmd; - use logic::config_io::*; - - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - - // 1. Layer names (binary LIST_LAYOUTS 0x21) - let _ = tx.send(BgMsg::ConfigProgress(0.05, "Reading layer names...".into())); - let layer_names = ser.get_layer_names().unwrap_or_default(); - let num_layers = layer_names.len().max(1); - - // 2. Keymaps — binary KEYMAP_GET per layer - let mut keymaps = Vec::new(); - for layer in 0..num_layers { - let progress = 0.05 + (layer as f32 / num_layers as f32) * 0.30; - let _ = tx.send(BgMsg::ConfigProgress(progress, format!("Reading layer {}...", layer))); - let km = ser.get_keymap(layer as u8).unwrap_or_default(); - keymaps.push(km); - } - - // 3. Tap dances — binary TD_LIST (0x51) - let _ = tx.send(BgMsg::ConfigProgress(0.40, "Reading tap dances...".into())); - let tap_dances = match ser.send_binary(cmd::TD_LIST, &[]) { - Ok(resp) => { - let td_raw = logic::parsers::parse_td_binary(&resp.payload); - td_raw.iter().enumerate() - .filter(|(_, actions)| actions.iter().any(|&a| a != 0)) - .map(|(i, actions)| TdConfig { index: i as u8, actions: *actions }) - .collect() - } - Err(_) => Vec::new(), - }; - - // 4. Combos — binary COMBO_LIST (0x61) - let _ = tx.send(BgMsg::ConfigProgress(0.50, "Reading combos...".into())); - let combos = match ser.send_binary(cmd::COMBO_LIST, &[]) { - Ok(resp) => { - logic::parsers::parse_combo_binary(&resp.payload).iter().map(|c| ComboConfig { - index: c.index, r1: c.r1, c1: c.c1, r2: c.r2, c2: c.c2, result: c.result, - }).collect() - } - Err(_) => Vec::new(), - }; - - // 5. Key overrides — binary KO_LIST (0x92) - let _ = tx.send(BgMsg::ConfigProgress(0.60, "Reading key overrides...".into())); - let key_overrides = match ser.send_binary(cmd::KO_LIST, &[]) { - Ok(resp) => { - logic::parsers::parse_ko_binary(&resp.payload).iter().map(|ko| KoConfig { - trigger_key: ko[0], trigger_mod: ko[1], result_key: ko[2], result_mod: ko[3], - }).collect() - } - Err(_) => Vec::new(), - }; - - // 6. Leaders — binary LEADER_LIST (0x71) - let _ = tx.send(BgMsg::ConfigProgress(0.70, "Reading leaders...".into())); - let leaders = match ser.send_binary(cmd::LEADER_LIST, &[]) { - Ok(resp) => { - logic::parsers::parse_leader_binary(&resp.payload).iter().map(|l| LeaderConfig { - index: l.index, sequence: l.sequence.clone(), result: l.result, result_mod: l.result_mod, - }).collect() - } - Err(_) => Vec::new(), - }; - - // 7. Macros — binary LIST_MACROS (0x30) - let _ = tx.send(BgMsg::ConfigProgress(0.80, "Reading macros...".into())); - let macros = match ser.send_binary(cmd::LIST_MACROS, &[]) { - Ok(resp) => { - logic::parsers::parse_macros_binary(&resp.payload).iter().map(|m| { - let steps_str: Vec = m.steps.iter() - .map(|s| format!("{:02X}:{:02X}", s.keycode, s.modifier)) - .collect(); - MacroConfig { slot: m.slot, name: m.name.clone(), steps: steps_str.join(",") } - }).collect() - } - Err(_) => Vec::new(), - }; - - drop(ser); // Release serial lock before file dialog - - let _ = tx.send(BgMsg::ConfigProgress(0.90, "Saving file...".into())); - - let config = KeyboardConfig { - version: 1, - layer_names, - keymaps, - tap_dances, - combos, - key_overrides, - leaders, - macros, - }; - - let json = config.to_json()?; - - let file = rfd::FileDialog::new() - .add_filter("KeSp Config", &["json"]) - .set_file_name("kesp_config.json") - .save_file(); - - match file { - Some(path) => { - std::fs::write(&path, &json).map_err(|e| format!("Write error: {}", e))?; - Ok(format!("Exported to {}", path.display())) - } - None => Ok("Export cancelled".into()), - } -} - -/// Import keyboard config using binary protocol v2. -/// SETLAYER sends a full layer in one frame (~131 bytes) instead of 65 individual SETKEY. -fn import_config( - serial: &Arc>, - tx: &mpsc::Sender, - config: &logic::config_io::KeyboardConfig, -) -> Result { - use logic::binary_protocol as bp; - - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let mut errors = 0usize; - - let total_steps = (config.layer_names.len() - + config.keymaps.len() // 1 SETLAYER per layer (not per key!) - + config.tap_dances.len() - + config.combos.len() - + config.key_overrides.len() - + config.leaders.len() - + config.macros.len()) - .max(1) as f32; - let mut done = 0usize; - - // 1. Layer names — binary SET_LAYOUT_NAME (0x20) - let _ = tx.send(BgMsg::ConfigProgress(0.0, "Setting layer names...".into())); - for (i, name) in config.layer_names.iter().enumerate() { - let payload = bp::set_layout_name_payload(i as u8, name); - if ser.send_binary(bp::cmd::SET_LAYOUT_NAME, &payload).is_err() { errors += 1; } - done += 1; - } - - // 2. Keymaps — binary SETLAYER (0x10): one frame per layer! - for (layer, km) in config.keymaps.iter().enumerate() { - let progress = done as f32 / total_steps; - let _ = tx.send(BgMsg::ConfigProgress(progress, format!("Writing layer {}...", layer))); - let payload = bp::setlayer_payload(layer as u8, km); - if let Err(e) = ser.send_binary(bp::cmd::SETLAYER, &payload) { - eprintln!("SETLAYER {} failed: {}", layer, e); - errors += 1; - } - done += 1; - } - - // 3. Tap dances — binary TD_SET (0x50) - let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting tap dances...".into())); - for td in &config.tap_dances { - let payload = bp::td_set_payload(td.index, &td.actions); - if ser.send_binary(bp::cmd::TD_SET, &payload).is_err() { errors += 1; } - done += 1; - } - - // 4. Combos — binary COMBO_SET (0x60) - let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting combos...".into())); - for combo in &config.combos { - let payload = bp::combo_set_payload(combo.index, combo.r1, combo.c1, combo.r2, combo.c2, combo.result as u8); - if ser.send_binary(bp::cmd::COMBO_SET, &payload).is_err() { errors += 1; } - done += 1; - } - - // 5. Key overrides — binary KO_SET (0x91) - let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting key overrides...".into())); - for (i, ko) in config.key_overrides.iter().enumerate() { - let payload = bp::ko_set_payload(i as u8, ko.trigger_key, ko.trigger_mod, ko.result_key, ko.result_mod); - if ser.send_binary(bp::cmd::KO_SET, &payload).is_err() { errors += 1; } - done += 1; - } - - // 6. Leaders — binary LEADER_SET (0x70) - let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting leaders...".into())); - for leader in &config.leaders { - let payload = bp::leader_set_payload(leader.index, &leader.sequence, leader.result, leader.result_mod); - if ser.send_binary(bp::cmd::LEADER_SET, &payload).is_err() { errors += 1; } - done += 1; - } - - // 7. Macros — binary MACRO_ADD_SEQ (0x32) - let _ = tx.send(BgMsg::ConfigProgress(done as f32 / total_steps, "Setting macros...".into())); - for m in &config.macros { - let payload = bp::macro_add_seq_payload(m.slot, &m.name, &m.steps); - if ser.send_binary(bp::cmd::MACRO_ADD_SEQ, &payload).is_err() { errors += 1; } - } - - // 8. Refresh UI - let _ = tx.send(BgMsg::ConfigProgress(0.95, "Refreshing...".into())); - let names = ser.get_layer_names().unwrap_or_default(); - let km = ser.get_keymap(0).unwrap_or_default(); - let _ = tx.send(BgMsg::LayerNames(names)); - let _ = tx.send(BgMsg::Keymap(km)); - - let total_keys: usize = config.keymaps.iter() - .map(|l| l.iter().map(|r| r.len()).sum::()) - .sum(); - - if errors > 0 { - Ok(format!("Import done with {} errors (check stderr)", errors)) - } else { - Ok(format!("Imported: {} layers, {} keys, {} TD, {} combos, {} KO, {} leaders, {} macros", - config.layer_names.len(), - total_keys, - config.tap_dances.len(), - config.combos.len(), - config.key_overrides.len(), - config.leaders.len(), - config.macros.len(), - )) - } -} - -/// Populate LayoutBridge with parsed JSON layout for preview. -fn populate_layout_preview(window: &MainWindow, json: &str) { - let lb = window.global::(); - match logic::layout::parse_json(json) { - Ok(keys) => { - let keycaps: Vec = keys.iter().enumerate().map(|(idx, kp)| KeycapData { - x: kp.x, y: kp.y, w: kp.w, h: kp.h, - rotation: kp.angle, - rotation_cx: kp.w / 2.0, rotation_cy: kp.h / 2.0, - label: SharedString::from(format!("R{}C{}", kp.row, kp.col)), - sublabel: SharedString::default(), - keycode: 0, color: slint::Color::from_argb_u8(255, 0x44, 0x47, 0x5a), - heat: 0.0, selected: false, index: idx as i32, - }).collect(); - // Compute content bounds - let max_x = keys.iter().map(|k| k.x + k.w).fold(0.0f32, f32::max); - let max_y = keys.iter().map(|k| k.y + k.h).fold(0.0f32, f32::max); - lb.set_content_width(max_x + 20.0); - lb.set_content_height(max_y + 20.0); - lb.set_keycaps(ModelRc::from(Rc::new(VecModel::from(keycaps)))); - lb.set_status(SharedString::from(format!("{} keys loaded", keys.len()))); - // Pretty-print JSON for display and export - let pretty_json = serde_json::from_str::(json) - .and_then(|v| serde_json::to_string_pretty(&v)) - .unwrap_or_else(|_| json.to_string()); - lb.set_json_text(SharedString::from(pretty_json)); - } - Err(e) => { - lb.set_status(SharedString::from(format!("Parse error: {}", e))); - lb.set_json_text(SharedString::from(json)); - } - } -} - fn main() { - let keys = logic::layout::default_layout(); - let keys_arc: Rc>> = Rc::new(std::cell::RefCell::new(keys.clone())); + let keys = protocol::layout::default_layout(); let window = MainWindow::new().unwrap(); - // Set up initial keymap models - let keymap_bridge = window.global::(); - keymap_bridge.set_keycaps(ModelRc::from(build_keycap_model(&keys))); - keymap_bridge.set_layers(ModelRc::from(build_layer_model(&[ - "Layer 0".into(), "Layer 1".into(), "Layer 2".into(), "Layer 3".into(), - ]))); - // Compute initial content bounds - { - let mut max_x: f32 = 0.0; - let mut max_y: f32 = 0.0; - for kp in &keys { - let right = kp.x + kp.w; - let bottom = kp.y + kp.h; - if right > max_x { max_x = right; } - if bottom > max_y { max_y = bottom; } - } - keymap_bridge.set_content_width(max_x); - keymap_bridge.set_content_height(max_y); - } + let saved_settings = protocol::settings::load(); + models::init_models(&window, &keys, &saved_settings); - // Set up settings bridge - { - let layouts: Vec = logic::layout_remap::KeyboardLayout::all() - .iter() - .map(|l| SharedString::from(l.name())) - .collect(); - let layout_model = Rc::new(VecModel::from(layouts)); - window.global::().set_available_layouts(ModelRc::from(layout_model)); - } - - // Set up key selector - let all_keys = build_key_entries(); - window.global::().set_all_keys(ModelRc::from(all_keys.clone())); - populate_key_categories(&window, &all_keys, ""); - - // Serial manager shared between threads - let serial: Arc> = Arc::new(Mutex::new(SerialManager::new())); let (bg_tx, bg_rx) = mpsc::channel::(); - // Current state - let current_keymap: Rc>>> = Rc::new(std::cell::RefCell::new(Vec::new())); - let current_layer: Rc> = Rc::new(std::cell::Cell::new(0)); - let saved_settings = logic::settings::load(); - let keyboard_layout = Rc::new(std::cell::RefCell::new( - logic::layout_remap::KeyboardLayout::from_name(&saved_settings.keyboard_layout), - )); - - // Set initial layout index - { - let all_layouts = logic::layout_remap::KeyboardLayout::all(); - let current = *keyboard_layout.borrow(); - let idx = all_layouts.iter().position(|l| *l == current).unwrap_or(0); - window.global::().set_selected_layout_index(idx as i32); - } - - // Heatmap data (for stats) - let heatmap_data: Rc>>> = Rc::new(std::cell::RefCell::new(Vec::new())); - - // --- Auto-connect on startup --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - window.global::().set_status_text("Scanning ports...".into()); - window.global::().set_connection(ConnectionState::Connecting); - - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - match ser.auto_connect() { - Ok(port_name) => { - let fw = ser.get_firmware_version().unwrap_or_default(); - let names = ser.get_layer_names().unwrap_or_default(); - let km = ser.get_keymap(0).unwrap_or_default(); - let _ = tx.send(BgMsg::Connected(port_name, fw, names, km)); - // Fetch physical layout from firmware - match ser.get_layout_json() { - Ok(json) => { - match logic::layout::parse_json(&json) { - Ok(keys) => { let _ = tx.send(BgMsg::LayoutJson(keys)); } - Err(e) => eprintln!("Layout parse error: {}", e), - } - } - Err(e) => eprintln!("get_layout_json error: {}", e), - } - } - Err(e) => { - let _ = tx.send(BgMsg::ConnectError(e)); - } - } - }); - } - - // --- Key selection callback --- - { - let window_weak = window.as_weak(); - keymap_bridge.on_select_key(move |key_index| { - let Some(w) = window_weak.upgrade() else { return }; - let keycaps = w.global::().get_keycaps(); - let idx = key_index as usize; - if idx >= keycaps.row_count() { return; } - for i in 0..keycaps.row_count() { - let mut item = keycaps.row_data(i).unwrap(); - let should_select = i == idx; - if item.selected != should_select { - item.selected = should_select; - keycaps.set_row_data(i, item); - } - } - let bridge = w.global::(); - bridge.set_selected_key_index(key_index); - let item = keycaps.row_data(idx).unwrap(); - bridge.set_selected_key_label(item.label.clone()); - }); - } - - // --- Layer switch callback --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let current_layer = current_layer.clone(); - let window_weak = window.as_weak(); - - keymap_bridge.on_switch_layer(move |layer_index| { - let idx = layer_index as usize; - current_layer.set(idx); - - // Update active flag on the CURRENT model (not a captured stale ref) - if let Some(w) = window_weak.upgrade() { - let layers = w.global::().get_layers(); - for i in 0..layers.row_count() { - let mut item = layers.row_data(i).unwrap(); - let should_be_active = item.index == layer_index; - if item.active != should_be_active { - item.active = should_be_active; - layers.set_row_data(i, item); - } - } - w.global::().set_active_layer(layer_index); - w.global::().set_status_text(SharedString::from(format!("Loading layer {}...", idx))); - } - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - match ser.get_keymap(idx as u8) { - Ok(km) => { let _ = tx.send(BgMsg::Keymap(km)); } - Err(e) => { let _ = tx.send(BgMsg::ConnectError(e)); } - } - }); - }); - } - - // --- Layer rename callback --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - keymap_bridge.on_rename_layer(move |layer_idx, new_name| { - let payload = logic::binary_protocol::set_layout_name_payload(layer_idx as u8, &new_name); - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(logic::binary_protocol::cmd::SET_LAYOUT_NAME, &payload); - if let Ok(names) = ser.get_layer_names() { - let _ = tx.send(BgMsg::LayerNames(names)); - } - }); - if let Some(w) = window_weak.upgrade() { - w.global::().set_status_text( - SharedString::from(format!("Renamed layer {} → {}", layer_idx, new_name)) - ); - } - }); - } - - // --- Heatmap toggle: auto-load data --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - keymap_bridge.on_toggle_heatmap(move || { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if let Ok(resp) = ser.send_binary(logic::binary_protocol::cmd::KEYSTATS_BIN, &[]) { - let (data, max) = logic::parsers::parse_keystats_binary(&resp.payload); - let _ = tx.send(BgMsg::HeatmapData(data, max)); - } - }); - }); - } - - // --- Connect/Disconnect callbacks --- - { - let serial_c = serial.clone(); - let tx_c = bg_tx.clone(); - let window_weak = window.as_weak(); - window.global::().on_connect(move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_status_text("Scanning ports...".into()); - w.global::().set_connection(ConnectionState::Connecting); - } - let serial = serial_c.clone(); - let tx = tx_c.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - match ser.auto_connect() { - Ok(port_name) => { - let fw = ser.get_firmware_version().unwrap_or_default(); - let names = ser.get_layer_names().unwrap_or_default(); - let km = ser.get_keymap(0).unwrap_or_default(); - let _ = tx.send(BgMsg::Connected(port_name, fw, names, km)); - match ser.get_layout_json() { - Ok(json) => { - match logic::layout::parse_json(&json) { - Ok(keys) => { let _ = tx.send(BgMsg::LayoutJson(keys)); } - Err(e) => eprintln!("Layout parse error: {}", e), - } - } - Err(e) => eprintln!("get_layout_json error: {}", e), - } - } - Err(e) => { let _ = tx.send(BgMsg::ConnectError(e)); } - } - }); - }); - } - - { - let serial_d = serial.clone(); - let tx_d = bg_tx.clone(); - window.global::().on_disconnect(move || { - let mut ser = serial_d.lock().unwrap_or_else(|e| e.into_inner()); - ser.disconnect(); - let _ = tx_d.send(BgMsg::Disconnected); - }); - } - - window.global::().on_refresh_ports(|| {}); - - // --- Auto-refresh on tab change --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_tab_changed(move |tab_idx| { - let Some(w) = window_weak.upgrade() else { return }; - if w.global::().get_connection() != ConnectionState::Connected { return; } - - let serial = serial.clone(); - let tx = tx.clone(); - match tab_idx { - 1 => { - // Advanced: refresh TD, combo, leader, KO, BT via binary - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if let Ok(r) = ser.send_binary(cmd::TD_LIST, &[]) { - let _ = tx.send(BgMsg::TdList(logic::parsers::parse_td_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { - let _ = tx.send(BgMsg::ComboList(logic::parsers::parse_combo_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { - let _ = tx.send(BgMsg::LeaderList(logic::parsers::parse_leader_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { - let _ = tx.send(BgMsg::KoList(logic::parsers::parse_ko_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::BT_QUERY, &[]) { - let _ = tx.send(BgMsg::BtStatus(logic::parsers::parse_bt_binary(&r.payload))); - } - }); - } - 2 => { - // Macros: refresh via binary - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if let Ok(resp) = ser.send_binary(logic::binary_protocol::cmd::LIST_MACROS, &[]) { - let macros = logic::parsers::parse_macros_binary(&resp.payload); - let _ = tx.send(BgMsg::MacroList(macros)); - } - }); - } - 3 => { - // Stats: refresh heatmap + bigrams via binary - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if let Ok(r) = ser.send_binary(cmd::KEYSTATS_BIN, &[]) { - let (data, max) = logic::parsers::parse_keystats_binary(&r.payload); - let _ = tx.send(BgMsg::HeatmapData(data, max)); - } - // Bigrams: keep text query (binary format needs dedicated parser) - let bigram_lines = if let Ok(r) = ser.send_binary(logic::binary_protocol::cmd::BIGRAMS_TEXT, &[]) { - String::from_utf8_lossy(&r.payload).lines().map(|l| l.to_string()).collect() - } else { Vec::new() }; - let _ = tx.send(BgMsg::BigramLines(bigram_lines)); - }); - } - _ => {} - } - }); - } - - // --- Settings: change layout --- - { - let keyboard_layout = keyboard_layout.clone(); - let keys_arc = keys_arc.clone(); - let current_keymap = current_keymap.clone(); - let window_weak = window.as_weak(); - - window.global::().on_change_layout(move |idx| { - let all_layouts = logic::layout_remap::KeyboardLayout::all(); - let idx = idx as usize; - if idx >= all_layouts.len() { return; } - - let new_layout = all_layouts[idx]; - *keyboard_layout.borrow_mut() = new_layout; - - let settings = logic::settings::Settings { - keyboard_layout: new_layout.name().to_string(), - }; - logic::settings::save(&settings); - - let km = current_keymap.borrow(); - let keys = keys_arc.borrow(); - if let Some(w) = window_weak.upgrade() { - if !km.is_empty() { - let keycaps = w.global::().get_keycaps(); - update_keycap_labels(&keycaps, &keys, &km, &new_layout); - } - } - - if let Some(w) = window_weak.upgrade() { - w.global::().set_status_text( - SharedString::from(format!("Layout: {}", new_layout.name())) - ); - } - }); - } - - // --- Key selector: filter --- - { - let all_keys = all_keys.clone(); - let window_weak = window.as_weak(); - - window.global::().on_apply_filter(move |search| { - if let Some(w) = window_weak.upgrade() { - populate_key_categories(&w, &all_keys, &search); - } - }); - } - - // --- Key selector: shared apply logic --- - // Wraps keycode application in a closure shared by all key selector actions. - let apply_keycode = { - let serial = serial.clone(); - let keys_arc = keys_arc.clone(); - let current_keymap = current_keymap.clone(); - let current_layer = current_layer.clone(); - let keyboard_layout = keyboard_layout.clone(); - let window_weak = window.as_weak(); - - Rc::new(move |code: u16| { - let Some(w) = window_weak.upgrade() else { return }; - let key_idx = w.global::().get_selected_key_index(); - if key_idx < 0 { return; } - let key_idx = key_idx as usize; - let keys = keys_arc.borrow(); - if key_idx >= keys.len() { return; } - - let kp = &keys[key_idx]; - let row = kp.row as usize; - let col = kp.col as usize; - drop(keys); - let layer = current_layer.get() as u8; - - { - let mut km = current_keymap.borrow_mut(); - if row < km.len() && col < km[row].len() { - km[row][col] = code; - } - } - - // Clone out of RefCells to avoid holding borrows across bridge calls - let layout = *keyboard_layout.borrow(); - let km = current_keymap.borrow().clone(); - let keys = keys_arc.borrow().clone(); - let keycaps = w.global::().get_keycaps(); - update_keycap_labels(&keycaps, &keys, &km, &layout); - - let payload = logic::binary_protocol::setkey_payload(layer, row as u8, col as u8, code); - let serial = serial.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(logic::binary_protocol::cmd::SETKEY, &payload); - }); - - w.global::().set_status_text( - SharedString::from(format!("[{},{}] = 0x{:04X}", row, col, code)) - ); - }) + let ctx = AppContext { + serial: Arc::new(Mutex::new(protocol::serial::SerialManager::new())), + bg_tx, + keys: Rc::new(std::cell::RefCell::new(keys)), + current_keymap: Rc::new(std::cell::RefCell::new(Vec::new())), + current_layer: Rc::new(std::cell::Cell::new(0)), + keyboard_layout: Rc::new(std::cell::RefCell::new( + protocol::layout_remap::KeyboardLayout::from_name(&saved_settings.keyboard_layout), + )), + heatmap_data: Rc::new(std::cell::RefCell::new(Vec::new())), + macro_steps: Rc::new(std::cell::RefCell::new(Vec::new())), }; - // Macro steps state (needed by dispatch_keycode) - let macro_steps: Rc>> = Rc::new(std::cell::RefCell::new(Vec::new())); - let refresh_macro_display = { - let macro_steps = macro_steps.clone(); - let window_weak = window.as_weak(); - Rc::new(move || { - let Some(w) = window_weak.upgrade() else { return }; - let steps = macro_steps.borrow(); - let display: Vec = steps.iter().map(|&(kc, _md)| { - if kc == 0xFF { - MacroStepDisplay { - label: SharedString::from(format!("T {}ms", _md as u32 * 10)), - is_delay: true, - } - } else { - MacroStepDisplay { - label: SharedString::from(keycode::hid_key_name(kc)), - is_delay: false, - } - } - }).collect(); - let text: Vec = steps.iter().map(|&(kc, md)| { - if kc == 0xFF { format!("T({})", md as u32 * 10) } - else { format!("D({:02X})", kc) } - }).collect(); - let mb = w.global::(); - mb.set_new_steps(ModelRc::from(Rc::new(VecModel::from(display)))); - mb.set_new_steps_text(SharedString::from(text.join(" "))); - }) - }; - - // Dispatch key selection based on target - let dispatch_keycode = { - let apply_keycode = apply_keycode.clone(); - let keys_arc = keys_arc.clone(); - let serial = serial.clone(); - let macro_steps = macro_steps.clone(); - let refresh_macro_display = refresh_macro_display.clone(); - let window_weak = window.as_weak(); - - Rc::new(move |code: u16| { - let Some(w) = window_weak.upgrade() else { return }; - let target = w.global::().get_selector_target(); - let name = SharedString::from(keycode::decode_keycode(code)); - - match target.as_str() { - "keymap" => { apply_keycode(code); } - "combo-result" => { - let adv = w.global::(); - adv.set_new_combo_result_code(code as i32); - adv.set_new_combo_result_name(name); - } - "ko-trigger" => { - let adv = w.global::(); - adv.set_new_ko_trigger_code(code as i32); - adv.set_new_ko_trigger_name(name); - } - "ko-result" => { - let adv = w.global::(); - adv.set_new_ko_result_code(code as i32); - adv.set_new_ko_result_name(name); - } - "leader-result" => { - let adv = w.global::(); - adv.set_new_leader_result_code(code as i32); - adv.set_new_leader_result_name(name); - } - "combo-key1" | "combo-key2" => { - // code = key index from the mini keyboard in the popup - let keys = keys_arc.borrow(); - let idx = code as usize; - if idx < keys.len() { - let kp = &keys[idx]; - let adv = w.global::(); - let label = SharedString::from(format!("R{}C{}", kp.row, kp.col)); - if target.as_str() == "combo-key1" { - adv.set_new_combo_r1(kp.row as i32); - adv.set_new_combo_c1(kp.col as i32); - adv.set_new_combo_key1_name(label); - } else { - adv.set_new_combo_r2(kp.row as i32); - adv.set_new_combo_c2(kp.col as i32); - adv.set_new_combo_key2_name(label); - } - } - } - "leader-seq" => { - let adv = w.global::(); - let count = adv.get_new_leader_seq_count(); - match count { - 0 => { adv.set_new_leader_seq0_code(code as i32); adv.set_new_leader_seq0_name(name); } - 1 => { adv.set_new_leader_seq1_code(code as i32); adv.set_new_leader_seq1_name(name); } - 2 => { adv.set_new_leader_seq2_code(code as i32); adv.set_new_leader_seq2_name(name); } - 3 => { adv.set_new_leader_seq3_code(code as i32); adv.set_new_leader_seq3_name(name); } - _ => {} - } - if count < 4 { adv.set_new_leader_seq_count(count + 1); } - } - "td-action" => { - let adv = w.global::(); - let td_idx = adv.get_editing_td_index(); - let slot = adv.get_editing_td_slot() as usize; - if td_idx >= 0 && slot < 4 { - // Update model in place - let tds = adv.get_tap_dances(); - for i in 0..tds.row_count() { - let td = tds.row_data(i).unwrap(); - if td.index == td_idx { - let actions = td.actions; - let mut a = actions.row_data(slot).unwrap(); - a.name = name.clone(); - a.code = code as i32; - actions.set_row_data(slot, a); - - // Collect all 4 action codes and send to firmware - let mut codes = [0u16; 4]; - for j in 0..4.min(actions.row_count()) { - codes[j] = actions.row_data(j).unwrap().code as u16; - } - let payload = logic::binary_protocol::td_set_payload(td_idx as u8, &codes); - let serial = serial.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(logic::binary_protocol::cmd::TD_SET, &payload); - }); - w.global::().set_status_text( - SharedString::from(format!("TD{} slot {} = {}", td_idx, slot, name)) - ); - break; - } - } - } - } - "macro-step" => { - // Add key press (Down + Up) to macro steps - let mut steps = macro_steps.borrow_mut(); - steps.push((code as u8, 0x00)); // D(key) - drop(steps); - refresh_macro_display(); - } - _ => { apply_keycode(code); } - } - }) - }; - - // Key from list - { - let dispatch = dispatch_keycode.clone(); - window.global::().on_select_keycode(move |code| { - dispatch(code as u16); - }); - } - - // Hex input - { - let dispatch = dispatch_keycode.clone(); - window.global::().on_apply_hex(move |hex_str| { - if let Ok(code) = u16::from_str_radix(hex_str.trim(), 16) { - dispatch(code); - } - }); - } - - // Hex preview: decode keycode and show human-readable name - { - let window_weak = window.as_weak(); - window.global::().on_preview_hex(move |hex_str| { - let preview = u16::from_str_radix(hex_str.trim(), 16) - .map(|code| keycode::decode_keycode(code)) - .unwrap_or_default(); - if let Some(w) = window_weak.upgrade() { - w.global::().set_hex_preview(SharedString::from(preview)); - } - }); - } - - // MT builder: mod_combo_index maps to modifier nibble, key_combo_index maps to HID code - { - let dispatch = dispatch_keycode.clone(); - window.global::().on_apply_mt(move |mod_idx, key_idx| { - let mod_nibble: u16 = match mod_idx { - 0 => 0x01, // Ctrl - 1 => 0x02, // Shift - 2 => 0x04, // Alt - 3 => 0x08, // GUI - 4 => 0x10, // RCtrl - 5 => 0x20, // RShift - 6 => 0x40, // RAlt - 7 => 0x80, // RGUI - _ => 0x02, - }; - // ComboBox order: A-Z (0-25), 1-0 (26-35), Space(36), Enter(37), Esc(38), Tab(39), Bksp(40) - let hid: u16 = match key_idx { - 0..=25 => 0x04 + key_idx as u16, // A-Z - 26..=35 => 0x1E + (key_idx - 26) as u16, // 1-0 - 36 => 0x2C, // Space - 37 => 0x28, // Enter - 38 => 0x29, // Esc - 39 => 0x2B, // Tab - 40 => 0x2A, // Backspace - _ => 0x04, - }; - let code = 0x5000 | (mod_nibble << 8) | hid; - dispatch(code); - }); - } - - // LT builder: layer_combo_index = layer (0-9), key_combo_index maps to HID code - { - let dispatch = dispatch_keycode.clone(); - window.global::().on_apply_lt(move |layer_idx, key_idx| { - let layer = (layer_idx as u16) & 0x0F; - // ComboBox order: Space(0), Enter(1), Esc(2), Bksp(3), Tab(4), A-E(5-9) - let hid: u16 = match key_idx { - 0 => 0x2C, // Space - 1 => 0x28, // Enter - 2 => 0x29, // Esc - 3 => 0x2A, // Backspace - 4 => 0x2B, // Tab - 5..=9 => 0x04 + (key_idx - 5) as u16, // A-E - _ => 0x2C, - }; - let code = 0x4000 | (layer << 8) | hid; - dispatch(code); - }); - } - - // --- Stats: refresh --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_refresh_stats(move || { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if let Ok(r) = ser.send_binary(cmd::KEYSTATS_BIN, &[]) { - let (data, max) = logic::parsers::parse_keystats_binary(&r.payload); - let _ = tx.send(BgMsg::HeatmapData(data, max)); - } - let bigram_lines = if let Ok(r) = ser.send_binary(logic::binary_protocol::cmd::BIGRAMS_TEXT, &[]) { - String::from_utf8_lossy(&r.payload).lines().map(|l| l.to_string()).collect() - } else { Vec::new() }; - let _ = tx.send(BgMsg::BigramLines(bigram_lines)); - }); - }); - } - - // --- Advanced: refresh --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_refresh_advanced(move || { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if let Ok(r) = ser.send_binary(cmd::TD_LIST, &[]) { - let _ = tx.send(BgMsg::TdList(logic::parsers::parse_td_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { - let _ = tx.send(BgMsg::ComboList(logic::parsers::parse_combo_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { - let _ = tx.send(BgMsg::LeaderList(logic::parsers::parse_leader_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { - let _ = tx.send(BgMsg::KoList(logic::parsers::parse_ko_binary(&r.payload))); - } - if let Ok(r) = ser.send_binary(cmd::BT_QUERY, &[]) { - let _ = tx.send(BgMsg::BtStatus(logic::parsers::parse_bt_binary(&r.payload))); - } - }); - }); - } - - // --- Advanced: delete combo --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_delete_combo(move |idx| { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::COMBO_DELETE, &[idx as u8]); - if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { - let _ = tx.send(BgMsg::ComboList(logic::parsers::parse_combo_binary(&r.payload))); - } - }); - }); - } - - // --- Advanced: delete leader --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_delete_leader(move |idx| { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::LEADER_DELETE, &[idx as u8]); - if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { - let _ = tx.send(BgMsg::LeaderList(logic::parsers::parse_leader_binary(&r.payload))); - } - }); - }); - } - - // --- Advanced: delete KO --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_delete_ko(move |idx| { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::KO_DELETE, &[idx as u8]); - if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { - let _ = tx.send(BgMsg::KoList(logic::parsers::parse_ko_binary(&r.payload))); - } - }); - }); - } - - // --- Advanced: set trilayer --- - { - let serial = serial.clone(); - let window_weak = window.as_weak(); - - window.global::().on_set_trilayer(move |l1, l2, l3| { - let payload = vec![l1 as u8, l2 as u8, l3 as u8]; - let serial = serial.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(logic::binary_protocol::cmd::TRILAYER_SET, &payload); - }); - if let Some(w) = window_weak.upgrade() { - w.global::().set_status_text( - SharedString::from(format!("Tri-layer: {} + {} → {}", l1, l2, l3)) - ); - } - }); - } - - // --- Advanced: BT switch --- - { - let serial = serial.clone(); - - window.global::().on_bt_switch(move |slot| { - let serial = serial.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(logic::binary_protocol::cmd::BT_SWITCH, &[slot as u8]); - }); - }); - } - - // --- Advanced: TAMA action --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_tama_action(move |action| { - use logic::binary_protocol::cmd; - let action_cmd = match action.as_str() { - "feed" => cmd::TAMA_FEED, - "play" => cmd::TAMA_PLAY, - "sleep" => cmd::TAMA_SLEEP, - "meds" => cmd::TAMA_MEDICINE, - "toggle" => cmd::TAMA_ENABLE, // toggle handled by firmware - _ => return, - }; - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(action_cmd, &[]); - if let Ok(r) = ser.send_binary(cmd::TAMA_QUERY, &[]) { - let _ = tx.send(BgMsg::TamaStatus(logic::parsers::parse_tama_binary(&r.payload))); - } - }); - }); - } - - // --- Advanced: toggle autoshift --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_toggle_autoshift(move || { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - match ser.send_binary(logic::binary_protocol::cmd::AUTOSHIFT_TOGGLE, &[]) { - Ok(r) => { - let enabled = r.payload.first().copied().unwrap_or(0); - let status = if enabled != 0 { "Autoshift: ON" } else { "Autoshift: OFF" }; - let _ = tx.send(BgMsg::AutoshiftStatus(status.to_string())); - } - Err(e) => { - let _ = tx.send(BgMsg::AutoshiftStatus(format!("Error: {}", e))); - } - } - }); - }); - } - - // --- Advanced: create combo --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_create_combo(move || { - let Some(w) = window_weak.upgrade() else { return }; - let adv = w.global::(); - let r1 = adv.get_new_combo_r1() as u8; - let c1 = adv.get_new_combo_c1() as u8; - let r2 = adv.get_new_combo_r2() as u8; - let c2 = adv.get_new_combo_c2() as u8; - let result = adv.get_new_combo_result_code() as u8; - let key1_name = adv.get_new_combo_key1_name(); - let key2_name = adv.get_new_combo_key2_name(); - if key1_name == "Pick..." || key2_name == "Pick..." { - w.global::().set_status_text("Pick both keys first".into()); - return; - } - let next_idx = adv.get_combos().row_count() as u8; - let payload = logic::binary_protocol::combo_set_payload(next_idx, r1, c1, r2, c2, result); - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::COMBO_SET, &payload); - if let Ok(r) = ser.send_binary(cmd::COMBO_LIST, &[]) { - let _ = tx.send(BgMsg::ComboList(logic::parsers::parse_combo_binary(&r.payload))); - } - }); - w.global::().set_status_text("Creating combo...".into()); - }); - } - - // --- Advanced: create KO --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_create_ko(move || { - let Some(w) = window_weak.upgrade() else { return }; - let adv = w.global::(); - let trig = adv.get_new_ko_trigger_code() as u8; - let trig_mod = (adv.get_new_ko_trig_ctrl() as u8) - | ((adv.get_new_ko_trig_shift() as u8) << 1) - | ((adv.get_new_ko_trig_alt() as u8) << 2); - let result = adv.get_new_ko_result_code() as u8; - let res_mod = (adv.get_new_ko_res_ctrl() as u8) - | ((adv.get_new_ko_res_shift() as u8) << 1) - | ((adv.get_new_ko_res_alt() as u8) << 2); - let next_idx = adv.get_key_overrides().row_count() as u8; - let payload = logic::binary_protocol::ko_set_payload(next_idx, trig, trig_mod, result, res_mod); - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::KO_SET, &payload); - if let Ok(r) = ser.send_binary(cmd::KO_LIST, &[]) { - let _ = tx.send(BgMsg::KoList(logic::parsers::parse_ko_binary(&r.payload))); - } - }); - w.global::().set_status_text("Creating key override...".into()); - }); - } - - // --- Advanced: create leader --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_create_leader(move |result_code: i32, mod_idx: i32| { - let Some(w) = window_weak.upgrade() else { return }; - let adv = w.global::(); - let count = adv.get_new_leader_seq_count() as usize; - let mut sequence = Vec::new(); - if count > 0 { sequence.push(adv.get_new_leader_seq0_code() as u8); } - if count > 1 { sequence.push(adv.get_new_leader_seq1_code() as u8); } - if count > 2 { sequence.push(adv.get_new_leader_seq2_code() as u8); } - if count > 3 { sequence.push(adv.get_new_leader_seq3_code() as u8); } - let result = result_code as u8; - let result_mod = mod_idx_to_byte(mod_idx); - let next_idx = adv.get_leaders().row_count() as u8; - let payload = logic::binary_protocol::leader_set_payload(next_idx, &sequence, result, result_mod); - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::LEADER_SET, &payload); - if let Ok(r) = ser.send_binary(cmd::LEADER_LIST, &[]) { - let _ = tx.send(BgMsg::LeaderList(logic::parsers::parse_leader_binary(&r.payload))); - } - }); - if let Some(w) = window_weak.upgrade() { - w.global::().set_status_text("Creating leader key...".into()); - } - }); - } - - // --- Macros: refresh --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_refresh_macros(move || { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - if ser.v2 { - if let Ok(resp) = ser.send_binary(logic::binary_protocol::cmd::LIST_MACROS, &[]) { - let macros = logic::parsers::parse_macros_binary(&resp.payload); - let _ = tx.send(BgMsg::MacroList(macros)); - } - } else { - // Legacy fallback — should not happen with v2 firmware - let _ = ser.send_binary(logic::binary_protocol::cmd::LIST_MACROS, &[]); - } - }); - }); - } - - // --- Macros: add delay step --- - { - let macro_steps = macro_steps.clone(); - let refresh = refresh_macro_display.clone(); - - window.global::().on_add_delay_step(move |ms| { - let units = (ms as u8) / 10; - macro_steps.borrow_mut().push((0xFF, units)); - refresh(); - }); - } - - // --- Macros: remove last step --- - { - let macro_steps = macro_steps.clone(); - let refresh = refresh_macro_display.clone(); - - window.global::().on_remove_last_step(move || { - macro_steps.borrow_mut().pop(); - refresh(); - }); - } - - // --- Macros: clear steps --- - { - let macro_steps = macro_steps.clone(); - let refresh = refresh_macro_display.clone(); - - window.global::().on_clear_steps(move || { - macro_steps.borrow_mut().clear(); - refresh(); - }); - } - - // --- Macros: save --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let macro_steps = macro_steps.clone(); - let window_weak = window.as_weak(); - - window.global::().on_save_macro(move || { - let Some(w) = window_weak.upgrade() else { return }; - let mb = w.global::(); - let slot_num = mb.get_macros().row_count() as u8; - let name = mb.get_new_name().to_string(); - let steps = macro_steps.borrow(); - let steps_str: Vec = steps.iter().map(|&(kc, md)| { - if kc == 0xFF { format!("{:02X}:{:02X}", kc, md) } - else { format!("{:02X}:{:02X}", kc, md) } - }).collect(); - let steps_text = steps_str.join(","); - drop(steps); - let payload = logic::binary_protocol::macro_add_seq_payload(slot_num, &name, &steps_text); - - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::MACRO_ADD_SEQ, &payload); - if let Ok(resp) = ser.send_binary(cmd::LIST_MACROS, &[]) { - let macros = logic::parsers::parse_macros_binary(&resp.payload); - let _ = tx.send(BgMsg::MacroList(macros)); - } - }); - w.global::().set_status_text( - SharedString::from(format!("Saving macro #{}...", slot_num)) - ); - }); - } - - // --- Macros: delete --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - - window.global::().on_delete_macro(move |slot| { - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use logic::binary_protocol::cmd; - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let _ = ser.send_binary(cmd::MACRO_DELETE, &logic::binary_protocol::macro_delete_payload(slot as u8)); - if let Ok(resp) = ser.send_binary(cmd::LIST_MACROS, &[]) { - let macros = logic::parsers::parse_macros_binary(&resp.payload); - let _ = tx.send(BgMsg::MacroList(macros)); - } - }); - }); - } - - // --- OTA: browse --- - { - let window_weak = window.as_weak(); - window.global::().on_ota_browse(move || { - let window_weak = window_weak.clone(); - std::thread::spawn(move || { - let file = rfd::FileDialog::new() - .add_filter("Firmware", &["bin"]) - .pick_file(); - if let Some(path) = file { - let path_str = path.to_string_lossy().to_string(); - let _ = slint::invoke_from_event_loop(move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_ota_path(SharedString::from(path_str.as_str())); - } - }); - } - }); - }); - } - - // --- OTA: start --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_ota_start(move || { - let Some(w) = window_weak.upgrade() else { return }; - let settings = w.global::(); - let path = settings.get_ota_path().to_string(); - if path.is_empty() { return; } - - let firmware = match std::fs::read(&path) { - Ok(data) => data, - Err(e) => { - let _ = tx.send(BgMsg::OtaDone(Err(format!("Cannot read {}: {}", path, e)))); - return; - } - }; - - settings.set_ota_flashing(true); - settings.set_ota_progress(0.0); - settings.set_ota_status(SharedString::from("Starting OTA...")); - - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - use std::io::{Read, Write, BufRead, BufReader}; - - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let total = firmware.len(); - let chunk_size = 4096usize; - - // Step 1: Send OTA command - let cmd = format!("OTA {}", total); - if let Err(e) = ser.send_command(&cmd) { - let _ = tx.send(BgMsg::OtaDone(Err(format!("Send OTA cmd failed: {}", e)))); - return; - } - - // Step 2: Wait for OTA_READY (read lines with timeout) - let _ = tx.send(BgMsg::OtaProgress(0.0, "Waiting for OTA_READY...".into())); - let port = match ser.port_mut() { - Some(p) => p, - None => { - let _ = tx.send(BgMsg::OtaDone(Err("Port not available".into()))); - return; - } - }; - - // Read until we get OTA_READY or timeout - let old_timeout = port.timeout(); - let _ = port.set_timeout(std::time::Duration::from_secs(5)); - let mut got_ready = false; - let port_clone = port.try_clone().unwrap(); - let mut reader = BufReader::new(port_clone); - for _ in 0..20 { - let mut line = String::new(); - if reader.read_line(&mut line).is_ok() { - if line.contains("OTA_READY") { - got_ready = true; - break; - } - } - } - drop(reader); - - if !got_ready { - let _ = port.set_timeout(old_timeout); - let _ = tx.send(BgMsg::OtaDone(Err("Firmware did not respond OTA_READY".into()))); - return; - } - - // Step 3: Send chunks and wait for ACK after each - let num_chunks = (total + chunk_size - 1) / chunk_size; - let _ = port.set_timeout(std::time::Duration::from_secs(5)); - - for (i, chunk) in firmware.chunks(chunk_size).enumerate() { - // Send chunk - if let Err(e) = port.write_all(chunk) { - let _ = port.set_timeout(old_timeout); - let _ = tx.send(BgMsg::OtaDone(Err(format!("Write chunk {} failed: {}", i, e)))); - return; - } - let _ = port.flush(); - - let progress = (i + 1) as f32 / num_chunks as f32; - let _ = tx.send(BgMsg::OtaProgress(progress * 0.95, format!( - "Chunk {}/{} ({} KB / {} KB)", - i + 1, num_chunks, - ((i + 1) * chunk_size).min(total) / 1024, - total / 1024 - ))); - - // Wait for ACK line - let mut ack_buf = [0u8; 256]; - let mut ack = String::new(); - let start = std::time::Instant::now(); - while start.elapsed() < std::time::Duration::from_secs(5) { - match port.read(&mut ack_buf) { - Ok(n) if n > 0 => { - ack.push_str(&String::from_utf8_lossy(&ack_buf[..n])); - if ack.contains("OTA_OK") || ack.contains("OTA_DONE") || ack.contains("OTA_FAIL") { - break; - } - } - _ => std::thread::sleep(std::time::Duration::from_millis(10)), - } - } - - if ack.contains("OTA_FAIL") { - let _ = port.set_timeout(old_timeout); - let _ = tx.send(BgMsg::OtaDone(Err(format!("Firmware error: {}", ack.trim())))); - return; - } - if ack.contains("OTA_DONE") { - break; // Firmware signals all received - } - } - - let _ = port.set_timeout(old_timeout); - let _ = tx.send(BgMsg::OtaProgress(1.0, "OTA complete, rebooting...".into())); - let _ = tx.send(BgMsg::OtaDone(Ok(()))); - }); - }); - } - - // --- Config Export --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_config_export(move || { - let Some(w) = window_weak.upgrade() else { return }; - w.global::().set_config_busy(true); - w.global::().set_config_progress(0.0); - w.global::().set_config_status(SharedString::from("Reading config...")); - - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let result = export_config(&serial, &tx); - let _ = tx.send(BgMsg::ConfigDone(result)); - }); - }); - } - - // --- Config Import --- - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_config_import(move || { - let serial = serial.clone(); - let tx = tx.clone(); - let window_weak = window_weak.clone(); - std::thread::spawn(move || { - let file = rfd::FileDialog::new() - .add_filter("KeSp Config", &["json"]) - .pick_file(); - let Some(path) = file else { return }; - - let _ = slint::invoke_from_event_loop({ - let window_weak = window_weak.clone(); - move || { - if let Some(w) = window_weak.upgrade() { - let s = w.global::(); - s.set_config_busy(true); - s.set_config_progress(0.0); - s.set_config_status(SharedString::from("Importing config...")); - } - } - }); - - let json = match std::fs::read_to_string(&path) { - Ok(j) => j, - Err(e) => { - let _ = tx.send(BgMsg::ConfigDone(Err(format!("Read error: {}", e)))); - return; - } - }; - let config = match logic::config_io::KeyboardConfig::from_json(&json) { - Ok(c) => c, - Err(e) => { - let _ = tx.send(BgMsg::ConfigDone(Err(format!("Parse error: {}", e)))); - return; - } - }; - - let result = import_config(&serial, &tx, &config); - let _ = tx.send(BgMsg::ConfigDone(result)); - }); - }); - } - - // --- Flasher: refresh prog ports --- - { - let window_weak = window.as_weak(); - - window.global::().on_refresh_prog_ports(move || { - let ports = SerialManager::list_prog_ports(); - if let Some(w) = window_weak.upgrade() { - if let Some(first) = ports.first() { - w.global::().set_selected_prog_port(SharedString::from(first.as_str())); - } - let model: Vec = ports.iter().map(|p| SharedString::from(p.as_str())).collect(); - w.global::().set_prog_ports( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - }); - } - - // --- Flasher: browse firmware --- - { - let window_weak = window.as_weak(); - - window.global::().on_browse_firmware(move || { - let window_weak = window_weak.clone(); - std::thread::spawn(move || { - let file = rfd::FileDialog::new() - .add_filter("Firmware", &["bin"]) - .pick_file(); - if let Some(path) = file { - let path_str = path.to_string_lossy().to_string(); - let _ = slint::invoke_from_event_loop(move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_firmware_path( - SharedString::from(path_str.as_str()) - ); - } - }); - } - }); - }); - } - - // --- Flasher: flash --- - { - let tx = bg_tx.clone(); - let window_weak = window.as_weak(); - - window.global::().on_flash(move || { - let Some(w) = window_weak.upgrade() else { return }; - let flasher = w.global::(); - let port = flasher.get_selected_prog_port().to_string(); - let path = flasher.get_firmware_path().to_string(); - let offset: u32 = match flasher.get_flash_offset_index() { - 0 => 0x20000, // factory - 1 => 0x220000, // ota_0 - _ => 0x20000, - }; - - if port.is_empty() || path.is_empty() { return; } - - // Read firmware file - let firmware = match std::fs::read(&path) { - Ok(data) => data, - Err(e) => { - let _ = tx.send(BgMsg::FlashDone(Err(format!("Cannot read {}: {}", path, e)))); - return; - } - }; - - flasher.set_flashing(true); - flasher.set_flash_progress(0.0); - flasher.set_flash_status(SharedString::from("Starting...")); - - let tx = tx.clone(); - std::thread::spawn(move || { - let (ftx, frx) = mpsc::channel(); - // Forward flash progress to main bg channel - let tx2 = tx.clone(); - let progress_thread = std::thread::spawn(move || { - while let Ok(logic::flasher::FlashProgress::OtaProgress(p, msg)) = frx.recv() { - let _ = tx2.send(BgMsg::FlashProgress(p, msg)); - } - }); - - let result = logic::flasher::flash_firmware(&port, &firmware, offset, &ftx); - drop(ftx); // close channel so progress_thread exits - let _ = progress_thread.join(); - let _ = tx.send(BgMsg::FlashDone(result.map_err(|e| e.to_string()))); - }); - }); - } - - // Init prog ports list - { - let ports = SerialManager::list_prog_ports(); - if let Some(first) = ports.first() { - window.global::().set_selected_prog_port(SharedString::from(first.as_str())); - } - let model: Vec = ports.iter().map(|p| SharedString::from(p.as_str())).collect(); - window.global::().set_prog_ports( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - - // --- Layout preview: load from file --- - { - let window_weak = window.as_weak(); - window.global::().on_load_from_file(move || { - let window_weak = window_weak.clone(); - std::thread::spawn(move || { - let file = rfd::FileDialog::new() - .add_filter("Layout JSON", &["json"]) - .pick_file(); - let Some(path) = file else { return }; - let json = match std::fs::read_to_string(&path) { - Ok(j) => j, - Err(e) => { - let err = format!("Read error: {}", e); - let _ = slint::invoke_from_event_loop({ - let window_weak = window_weak.clone(); - move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_status(SharedString::from(err)); - } - } - }); - return; - } - }; - let path_str = path.to_string_lossy().to_string(); - let _ = slint::invoke_from_event_loop({ - let window_weak = window_weak.clone(); - move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_file_path(SharedString::from(&path_str)); - populate_layout_preview(&w, &json); - } - } - }); - }); - }); - } - - // --- Layout preview: load from keyboard --- - { - let serial = serial.clone(); - let window_weak = window.as_weak(); - window.global::().on_load_from_keyboard(move || { - let serial = serial.clone(); - let window_weak = window_weak.clone(); - std::thread::spawn(move || { - let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); - let json = match ser.get_layout_json() { - Ok(j) => j, - Err(e) => { - let err = format!("Error: {}", e); - let _ = slint::invoke_from_event_loop({ - let window_weak = window_weak.clone(); - move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_status(SharedString::from(err)); - } - } - }); - return; - } - }; - let _ = slint::invoke_from_event_loop({ - let window_weak = window_weak.clone(); - move || { - if let Some(w) = window_weak.upgrade() { - populate_layout_preview(&w, &json); - } - } - }); - }); - }); - } - - // --- Layout preview: export JSON --- - { - let window_weak = window.as_weak(); - window.global::().on_export_json(move || { - let Some(w) = window_weak.upgrade() else { return }; - let json = w.global::().get_json_text().to_string(); - if json.is_empty() { return; } - let window_weak = window_weak.clone(); - std::thread::spawn(move || { - let file = rfd::FileDialog::new() - .add_filter("Layout JSON", &["json"]) - .set_file_name("layout.json") - .save_file(); - if let Some(path) = file { - let msg = match std::fs::write(&path, &json) { - Ok(()) => format!("Exported to {}", path.display()), - Err(e) => format!("Write error: {}", e), - }; - let _ = slint::invoke_from_event_loop(move || { - if let Some(w) = window_weak.upgrade() { - w.global::().set_status(SharedString::from(msg)); - } - }); - } - }); - }); - } - - // --- Poll background messages via timer --- - { - let window_weak = window.as_weak(); - let keys_arc = keys_arc.clone(); - let current_keymap = current_keymap.clone(); - let keyboard_layout = keyboard_layout.clone(); - let heatmap_data = heatmap_data.clone(); - - let timer = slint::Timer::default(); - timer.start( - slint::TimerMode::Repeated, - std::time::Duration::from_millis(50), - move || { - let Some(window) = window_weak.upgrade() else { return }; - - while let Ok(msg) = bg_rx.try_recv() { - match msg { - BgMsg::Connected(port, fw, names, km) => { - let app = window.global::(); - app.set_connection(ConnectionState::Connected); - app.set_firmware_version(SharedString::from(&fw)); - app.set_status_text(SharedString::from(format!("Connected to {}", port))); - - let new_layers = build_layer_model(&names); - window.global::().set_layers(ModelRc::from(new_layers)); - - *current_keymap.borrow_mut() = km.clone(); - let keycaps = window.global::().get_keycaps(); - let layout = keyboard_layout.borrow(); - let keys = keys_arc.borrow(); - update_keycap_labels(&keycaps, &keys, &km, &layout); - } - BgMsg::ConnectError(e) => { - let app = window.global::(); - app.set_connection(ConnectionState::Disconnected); - app.set_status_text(SharedString::from(format!("Error: {}", e))); - } - BgMsg::Keymap(km) => { - *current_keymap.borrow_mut() = km.clone(); - let keycaps = window.global::().get_keycaps(); - let layout = keyboard_layout.borrow(); - let keys = keys_arc.borrow(); - update_keycap_labels(&keycaps, &keys, &km, &layout); - window.global::().set_status_text("Keymap loaded".into()); - } - BgMsg::LayerNames(names) => { - let active = window.global::().get_active_layer() as usize; - let layers: Vec = names.iter().enumerate().map(|(i, name)| LayerInfo { - index: i as i32, - name: SharedString::from(name.as_str()), - active: i == active, - }).collect(); - window.global::().set_layers( - ModelRc::from(Rc::new(VecModel::from(layers))) - ); - } - BgMsg::Disconnected => { - let app = window.global::(); - app.set_connection(ConnectionState::Disconnected); - app.set_firmware_version(SharedString::default()); - app.set_status_text("Disconnected".into()); - } - BgMsg::LayoutJson(new_keys) => { - *keys_arc.borrow_mut() = new_keys.clone(); - let new_model = build_keycap_model(&new_keys); - let km = current_keymap.borrow(); - if !km.is_empty() { - let layout = keyboard_layout.borrow(); - update_keycap_labels(&new_model, &new_keys, &km, &layout); - } - // Compute content bounds for responsive scaling - let mut max_x: f32 = 0.0; - let mut max_y: f32 = 0.0; - for kp in &new_keys { - let right = kp.x + kp.w; - let bottom = kp.y + kp.h; - if right > max_x { max_x = right; } - if bottom > max_y { max_y = bottom; } - } - let bridge = window.global::(); - bridge.set_content_width(max_x); - bridge.set_content_height(max_y); - bridge.set_keycaps(ModelRc::from(new_model)); - window.global::().set_status_text( - SharedString::from(format!("Layout loaded ({} keys)", new_keys.len())) - ); - } - BgMsg::BigramLines(lines) => { - let entries = logic::stats_analyzer::parse_bigram_lines(&lines); - let analysis = logic::stats_analyzer::analyze_bigrams(&entries); - window.global::().set_bigrams(BigramData { - alt_hand_pct: analysis.alt_hand_pct, - same_hand_pct: analysis.same_hand_pct, - sfb_pct: analysis.sfb_pct, - total: analysis.total as i32, - }); - } - BgMsg::FlashProgress(progress, msg) => { - let flasher = window.global::(); - flasher.set_flash_progress(progress); - flasher.set_flash_status(SharedString::from(msg)); - } - BgMsg::FlashDone(result) => { - let flasher = window.global::(); - flasher.set_flashing(false); - match result { - Ok(()) => { - flasher.set_flash_progress(1.0); - flasher.set_flash_status(SharedString::from("Flash complete!")); - window.global::().set_status_text("Flash complete!".into()); - } - Err(e) => { - flasher.set_flash_status(SharedString::from(format!("Error: {}", e))); - window.global::().set_status_text( - SharedString::from(format!("Flash error: {}", e)) - ); - } - } - } - BgMsg::HeatmapData(data, max) => { - *heatmap_data.borrow_mut() = data.clone(); - - // Update heat intensity on keycaps - let keycaps = window.global::().get_keycaps(); - let keys = keys_arc.borrow(); - for i in 0..keycaps.row_count() { - if i >= keys.len() { break; } - let mut item = keycaps.row_data(i).unwrap(); - let kp = &keys[i]; - let row = kp.row as usize; - let col = kp.col as usize; - let count = data.get(row) - .and_then(|r| r.get(col)) - .copied() - .unwrap_or(0); - item.heat = if max > 0 { count as f32 / max as f32 } else { 0.0 }; - keycaps.set_row_data(i, item); - } - drop(keys); - - let km = current_keymap.borrow(); - let balance = logic::stats_analyzer::hand_balance(&data); - let fingers = logic::stats_analyzer::finger_load(&data); - let rows = logic::stats_analyzer::row_usage(&data); - let top = logic::stats_analyzer::top_keys(&data, &km, 10); - let dead = logic::stats_analyzer::dead_keys(&data, &km); - - let stats = window.global::(); - stats.set_hand_balance(HandBalanceData { - left_pct: balance.left_pct, - right_pct: balance.right_pct, - total: balance.total as i32, - }); - stats.set_total_presses(balance.total as i32); - - let finger_model: Vec = fingers.iter().map(|f| FingerLoadData { - name: SharedString::from(&f.name), - pct: f.pct, - count: f.count as i32, - }).collect(); - stats.set_finger_load(ModelRc::from(Rc::new(VecModel::from(finger_model)))); - - let row_model: Vec = rows.iter().map(|r| RowUsageData { - name: SharedString::from(&r.name), - pct: r.pct, - count: r.count as i32, - }).collect(); - stats.set_row_usage(ModelRc::from(Rc::new(VecModel::from(row_model)))); - - let top_model: Vec = top.iter().map(|t| TopKeyData { - name: SharedString::from(&t.name), - finger: SharedString::from(&t.finger), - count: t.count as i32, - pct: t.pct, - }).collect(); - stats.set_top_keys(ModelRc::from(Rc::new(VecModel::from(top_model)))); - - let dead_model: Vec = dead.iter().map(|d| SharedString::from(d.as_str())).collect(); - stats.set_dead_keys(ModelRc::from(Rc::new(VecModel::from(dead_model)))); - - window.global::().set_status_text( - SharedString::from(format!("Stats loaded ({} total presses, max {})", balance.total, max)) - ); - } - BgMsg::TextLines(_tag, _lines) => { - // Legacy text handler — kept for OTA compatibility only - } - BgMsg::TdList(td_data) => { - let model: Vec = td_data.iter().enumerate() - .filter(|(_, actions)| actions.iter().any(|&a| a != 0)) - .map(|(i, actions)| TapDanceData { - index: i as i32, - actions: ModelRc::from(Rc::new(VecModel::from( - actions.iter().map(|&a| TapDanceAction { - name: SharedString::from(keycode::decode_keycode(a)), - code: a as i32, - }).collect::>() - ))), - }) - .collect(); - window.global::().set_tap_dances( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - BgMsg::ComboList(combo_data) => { - let model: Vec = combo_data.iter().map(|c| ComboData { - index: c.index as i32, - key1: SharedString::from(format!("R{}C{}", c.r1, c.c1)), - key2: SharedString::from(format!("R{}C{}", c.r2, c.c2)), - result: SharedString::from(keycode::decode_keycode(c.result)), - }).collect(); - window.global::().set_combos( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - BgMsg::LeaderList(leader_data) => { - let model: Vec = leader_data.iter().map(|l| { - let seq: Vec = l.sequence.iter() - .map(|&k| keycode::hid_key_name(k)) - .collect(); - LeaderData { - index: l.index as i32, - sequence: SharedString::from(seq.join(" → ")), - result: SharedString::from(keycode::hid_key_name(l.result)), - } - }).collect(); - window.global::().set_leaders( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - BgMsg::KoList(ko_data) => { - let model: Vec = ko_data.iter().enumerate().map(|(i, ko)| { - let trig_key = keycode::hid_key_name(ko[0]); - let trig_mod = keycode::mod_name(ko[1]); - let res_key = keycode::hid_key_name(ko[2]); - let res_mod = keycode::mod_name(ko[3]); - let trigger = if ko[1] != 0 { - format!("{}+{}", trig_mod, trig_key) - } else { - trig_key - }; - let result = if ko[3] != 0 { - format!("{}+{}", res_mod, res_key) - } else { - res_key - }; - KeyOverrideData { - index: i as i32, - trigger: SharedString::from(trigger), - result: SharedString::from(result), - } - }).collect(); - window.global::().set_key_overrides( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - BgMsg::BtStatus(lines) => { - let bt_text = lines.join("\n"); - window.global::().set_bt_status(SharedString::from(bt_text)); - } - BgMsg::TamaStatus(lines) => { - let text = lines.join("\n"); - window.global::().set_tama_status(SharedString::from(text)); - } - BgMsg::AutoshiftStatus(text) => { - window.global::().set_autoshift_status(SharedString::from(text)); - } - BgMsg::Wpm(wpm) => { - window.global::().set_wpm(wpm as i32); - } - BgMsg::OtaProgress(progress, msg) => { - let s = window.global::(); - s.set_ota_progress(progress); - s.set_ota_status(SharedString::from(msg)); - } - BgMsg::OtaDone(result) => { - let s = window.global::(); - s.set_ota_flashing(false); - match result { - Ok(()) => { - s.set_ota_progress(1.0); - s.set_ota_status(SharedString::from("OTA complete!")); - } - Err(e) => { - s.set_ota_status(SharedString::from(format!("OTA error: {}", e))); - } - } - } - BgMsg::ConfigProgress(progress, msg) => { - let s = window.global::(); - s.set_config_progress(progress); - s.set_config_status(SharedString::from(msg)); - } - BgMsg::ConfigDone(result) => { - let s = window.global::(); - s.set_config_busy(false); - match result { - Ok(msg) => { - s.set_config_progress(1.0); - s.set_config_status(SharedString::from(msg)); - } - Err(e) => { - s.set_config_progress(0.0); - s.set_config_status(SharedString::from(format!("Error: {}", e))); - } - } - } - BgMsg::MacroList(macros) => { - let model: Vec = macros.iter().map(|m| { - let steps_str: Vec = m.steps.iter().map(|s| { - if s.is_delay() { format!("T({})", s.delay_ms()) } - else { format!("{}", keycode::hid_key_name(s.keycode)) } - }).collect(); - MacroData { - slot: m.slot as i32, - name: SharedString::from(&m.name), - steps: SharedString::from(steps_str.join(" ")), - } - }).collect(); - window.global::().set_macros( - ModelRc::from(Rc::new(VecModel::from(model))) - ); - } - } - } - }, - ); - - // WPM polling timer (5s, non-blocking try_lock in background thread) - let wpm_timer = slint::Timer::default(); - { - let serial = serial.clone(); - let tx = bg_tx.clone(); - let window_weak2 = window.as_weak(); - - wpm_timer.start( - slint::TimerMode::Repeated, - std::time::Duration::from_secs(5), - move || { - let Some(w) = window_weak2.upgrade() else { return }; - if w.global::().get_connection() != ConnectionState::Connected { return; } - let serial = serial.clone(); - let tx = tx.clone(); - std::thread::spawn(move || { - let Ok(mut ser) = serial.try_lock() else { return }; - if let Ok(r) = ser.send_binary(logic::binary_protocol::cmd::WPM_QUERY, &[]) { - let wpm = if r.payload.len() >= 2 { - u16::from_le_bytes([r.payload[0], r.payload[1]]) - } else { 0 }; - let _ = tx.send(BgMsg::Wpm(wpm)); - } - }); - }, - ); - } - - // Keep timers alive - // Layout auto-refresh timer (5s) - let layout_timer = slint::Timer::default(); - { - let window_weak3 = window.as_weak(); - layout_timer.start( - slint::TimerMode::Repeated, - std::time::Duration::from_secs(5), - move || { - let Some(w) = window_weak3.upgrade() else { return }; - let lb = w.global::(); - if !lb.get_auto_refresh() { return; } - let path = lb.get_file_path().to_string(); - if path.is_empty() { return; } - if let Ok(json) = std::fs::read_to_string(&path) { - populate_layout_preview(&w, &json); - } - }, - ); - } - - let _keep_timer = timer; - let _keep_wpm = wpm_timer; - let _keep_layout = layout_timer; - window.run().unwrap(); - } + connection::auto_connect(&window, &ctx); + connection::setup(&window, &ctx); + keymap::setup(&window, &ctx); + stats::setup(&window, &ctx); + macros::setup(&window, &ctx); + advanced::setup(&window, &ctx); + key_selector::setup(&window, &ctx); + settings::setup(&window, &ctx); + flasher::setup(&window, &ctx); + layout::setup(&window, &ctx); + + dispatch::run(&window, &ctx, bg_rx); } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..b41ef44 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,321 @@ +use crate::protocol::{self as protocol, keycode, layout::KeycapPos, layout_remap}; +use crate::{ + KeycapData, KeyEntry, KeySelectorBridge, KeymapBridge, LayoutBridge, LayerInfo, MainWindow, + SettingsBridge, +}; +use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; +use std::rc::Rc; + +/// Initialize all UI models from default state. +pub fn init_models( + window: &MainWindow, + keys: &[KeycapPos], + saved_settings: &crate::protocol::settings::Settings, +) { + let kb = window.global::(); + kb.set_keycaps(ModelRc::from(build_keycap_model(keys))); + kb.set_layers(ModelRc::from(build_layer_model(&[ + "Layer 0".into(), "Layer 1".into(), "Layer 2".into(), "Layer 3".into(), + ]))); + let mut max_x: f32 = 0.0; + let mut max_y: f32 = 0.0; + for kp in keys { + if kp.x + kp.w > max_x { max_x = kp.x + kp.w; } + if kp.y + kp.h > max_y { max_y = kp.y + kp.h; } + } + kb.set_content_width(max_x); + kb.set_content_height(max_y); + + // Available keyboard layouts + let layouts: Vec = layout_remap::KeyboardLayout::all() + .iter() + .map(|l| SharedString::from(l.name())) + .collect(); + window.global::().set_available_layouts( + ModelRc::from(Rc::new(VecModel::from(layouts))), + ); + + // Initial layout index + let current = layout_remap::KeyboardLayout::from_name(&saved_settings.keyboard_layout); + let idx = layout_remap::KeyboardLayout::all() + .iter() + .position(|l| *l == current) + .unwrap_or(0); + window.global::().set_selected_layout_index(idx as i32); + + // Key selector + let all_keys = build_key_entries(); + window.global::().set_all_keys(ModelRc::from(all_keys.clone())); + populate_key_categories(window, &all_keys, ""); +} + +pub fn build_keycap_model(keys: &[KeycapPos]) -> Rc> { + let keycaps: Vec = keys + .iter() + .enumerate() + .map(|(idx, kp)| KeycapData { + x: kp.x, y: kp.y, w: kp.w, h: kp.h, + rotation: kp.angle, + rotation_cx: kp.w / 2.0, + rotation_cy: kp.h / 2.0, + label: SharedString::from(format!("{},{}", kp.col, kp.row)), + sublabel: SharedString::default(), + keycode: 0, + color: slint::Color::from_argb_u8(255, 0x44, 0x47, 0x5a), + heat: 0.0, + selected: false, + index: idx as i32, + }) + .collect(); + Rc::new(VecModel::from(keycaps)) +} + +pub fn build_layer_model(names: &[String]) -> Rc> { + let layers: Vec = names + .iter() + .enumerate() + .map(|(i, name)| LayerInfo { + index: i as i32, + name: SharedString::from(name.as_str()), + active: i == 0, + }) + .collect(); + Rc::new(VecModel::from(layers)) +} + +pub fn update_keycap_labels( + keycap_model: &impl Model, + keys: &[KeycapPos], + keymap: &[Vec], + layout: &layout_remap::KeyboardLayout, +) { + for i in 0..keycap_model.row_count() { + if i >= keys.len() { break; } + let mut item = keycap_model.row_data(i).unwrap(); + let kp = &keys[i]; + let row = kp.row; + let col = kp.col; + + if row < keymap.len() && col < keymap[row].len() { + let code = keymap[row][col]; + let decoded = keycode::decode_keycode(code); + let remapped = layout_remap::remap_key_label(layout, &decoded); + let label = remapped.unwrap_or(&decoded).to_string(); + item.keycode = code as i32; + item.label = SharedString::from(label); + item.sublabel = if decoded != format!("0x{:04X}", code) { + SharedString::default() + } else { + SharedString::from(format!("0x{:04X}", code)) + }; + } + keycap_model.set_row_data(i, item); + } +} + +pub fn build_key_entries() -> Rc> { + let mut entries = Vec::new(); + + for code in 0x04u16..=0x1D { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Letter"), + }); + } + for code in 0x1Eu16..=0x27 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Number"), + }); + } + for code in [0x28u16, 0x29, 0x2A, 0x2B, 0x2C] { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Control"), + }); + } + for code in 0x2Du16..=0x38 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Symbol"), + }); + } + for code in 0x3Au16..=0x45 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Function"), + }); + } + for code in [0x46u16, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52] { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Navigation"), + }); + } + for code in 0xE0u16..=0xE7 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Modifier"), + }); + } + entries.push(KeyEntry { + name: SharedString::from("Caps Lock"), + code: 0x39, + category: SharedString::from("Control"), + }); + for code in 0x53u16..=0x63 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Keypad"), + }); + } + for code in 0x68u16..=0x73 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Function"), + }); + } + for code in [0x7Fu16, 0x80, 0x81] { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Media"), + }); + } + for (code, name) in [ + (0x2900u16, "BT Next"), (0x2A00, "BT Prev"), (0x2B00, "BT Pair"), + (0x2C00, "BT Disc"), (0x2E00, "USB/BT"), (0x2F00, "BT On/Off"), + ] { + entries.push(KeyEntry { + name: SharedString::from(name), + code: code as i32, + category: SharedString::from("Bluetooth"), + }); + } + for i in 0u16..=7 { + let code = (0x60 | i) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("TD {}", i)), + code: code as i32, + category: SharedString::from("Tap Dance"), + }); + } + for i in 0u16..=9 { + let code = (i + 0x15) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("M{}", i)), + code: code as i32, + category: SharedString::from("Macro"), + }); + } + for i in 0u16..=9 { + entries.push(KeyEntry { + name: SharedString::from(format!("OSL {}", i)), + code: 0x3100 + i as i32, + category: SharedString::from("Layer"), + }); + } + for layer in 0u16..=9 { + let code = (layer + 1) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("MO {}", layer)), + code: code as i32, + category: SharedString::from("Layer"), + }); + } + for layer in 0u16..=9 { + let code = (layer + 0x0B) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("TO {}", layer)), + code: code as i32, + category: SharedString::from("Layer"), + }); + } + for (code, name) in [ + (0x3200u16, "Caps Word"), (0x3300, "Repeat"), (0x3400, "Leader"), + (0x3900, "GEsc"), (0x3A00, "Layer Lock"), (0x3C00, "AS Toggle"), + ] { + entries.push(KeyEntry { + name: SharedString::from(name), + code: code as i32, + category: SharedString::from("Special"), + }); + } + entries.insert(0, KeyEntry { + name: SharedString::from("None"), + code: 0, + category: SharedString::from("Special"), + }); + + Rc::new(VecModel::from(entries)) +} + +pub fn populate_key_categories(window: &MainWindow, all_keys: &VecModel, search: &str) { + let search_lower = search.to_lowercase(); + let filter = |cat: &str| -> Vec { + (0..all_keys.row_count()) + .filter_map(|i| { + let e = all_keys.row_data(i).unwrap(); + let cat_match = e.category.as_str() == cat + || (cat == "Navigation" && (e.category.as_str() == "Control" || e.category.as_str() == "Navigation")) + || (cat == "Special" && (e.category.as_str() == "Special" || e.category.as_str() == "Bluetooth" || e.category.as_str() == "Media")) + || (cat == "TDMacro" && (e.category.as_str() == "Tap Dance" || e.category.as_str() == "Macro")); + let search_match = search_lower.is_empty() + || e.name.to_lowercase().contains(&search_lower) + || e.category.to_lowercase().contains(&search_lower); + if cat_match && search_match { Some(e) } else { None } + }) + .collect() + }; + let set = |model: Vec| ModelRc::from(Rc::new(VecModel::from(model))); + let ks = window.global::(); + ks.set_cat_letters(set(filter("Letter"))); + ks.set_cat_numbers(set(filter("Number"))); + ks.set_cat_modifiers(set(filter("Modifier"))); + ks.set_cat_nav(set(filter("Navigation"))); + ks.set_cat_function(set(filter("Function"))); + ks.set_cat_symbols(set(filter("Symbol"))); + ks.set_cat_layers(set(filter("Layer"))); + ks.set_cat_special(set(filter("Special"))); + ks.set_cat_td_macro(set(filter("TDMacro"))); +} + +pub fn populate_layout_preview(window: &MainWindow, json: &str) { + let lb = window.global::(); + match protocol::layout::parse_json(json) { + Ok(keys) => { + let keycaps: Vec = keys.iter().enumerate().map(|(idx, kp)| KeycapData { + x: kp.x, y: kp.y, w: kp.w, h: kp.h, + rotation: kp.angle, + rotation_cx: kp.w / 2.0, rotation_cy: kp.h / 2.0, + label: SharedString::from(format!("R{}C{}", kp.row, kp.col)), + sublabel: SharedString::default(), + keycode: 0, color: slint::Color::from_argb_u8(255, 0x44, 0x47, 0x5a), + heat: 0.0, selected: false, index: idx as i32, + }).collect(); + let max_x = keys.iter().map(|k| k.x + k.w).fold(0.0f32, f32::max); + let max_y = keys.iter().map(|k| k.y + k.h).fold(0.0f32, f32::max); + lb.set_content_width(max_x + 20.0); + lb.set_content_height(max_y + 20.0); + lb.set_keycaps(ModelRc::from(std::rc::Rc::new(VecModel::from(keycaps)))); + lb.set_status(SharedString::from(format!("{} keys loaded", keys.len()))); + let pretty_json = serde_json::from_str::(json) + .and_then(|v| serde_json::to_string_pretty(&v)) + .unwrap_or_else(|_| json.to_string()); + lb.set_json_text(SharedString::from(pretty_json)); + } + Err(e) => { + lb.set_status(SharedString::from(format!("Parse error: {}", e))); + lb.set_json_text(SharedString::from(json)); + } + } +} diff --git a/src/logic/binary_protocol.rs b/src/protocol/binary.rs similarity index 100% rename from src/logic/binary_protocol.rs rename to src/protocol/binary.rs diff --git a/src/logic/config_io.rs b/src/protocol/config_io.rs similarity index 100% rename from src/logic/config_io.rs rename to src/protocol/config_io.rs diff --git a/src/logic/flasher.rs b/src/protocol/flasher.rs similarity index 100% rename from src/logic/flasher.rs rename to src/protocol/flasher.rs diff --git a/src/logic/keycode.rs b/src/protocol/keycode.rs similarity index 100% rename from src/logic/keycode.rs rename to src/protocol/keycode.rs diff --git a/src/logic/layout.rs b/src/protocol/layout.rs similarity index 100% rename from src/logic/layout.rs rename to src/protocol/layout.rs diff --git a/original-src/layout_remap.rs b/src/protocol/layout_remap.rs similarity index 100% rename from original-src/layout_remap.rs rename to src/protocol/layout_remap.rs diff --git a/src/logic/mod.rs b/src/protocol/mod.rs similarity index 82% rename from src/logic/mod.rs rename to src/protocol/mod.rs index 6b02cd7..92f86fe 100644 --- a/src/logic/mod.rs +++ b/src/protocol/mod.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -pub mod binary_protocol; +pub mod binary; pub mod config_io; #[allow(dead_code)] pub mod flasher; @@ -11,10 +11,10 @@ pub mod layout_remap; #[allow(dead_code)] pub mod parsers; #[allow(dead_code)] -pub mod protocol; +pub mod text_commands; #[allow(dead_code)] pub mod serial; #[allow(dead_code)] pub mod settings; #[allow(dead_code)] -pub mod stats_analyzer; +pub mod stats; diff --git a/src/logic/parsers.rs b/src/protocol/parsers.rs similarity index 100% rename from src/logic/parsers.rs rename to src/protocol/parsers.rs diff --git a/original-src/serial/mod.rs b/src/protocol/serial/mod.rs similarity index 100% rename from original-src/serial/mod.rs rename to src/protocol/serial/mod.rs diff --git a/src/logic/serial/native.rs b/src/protocol/serial/native.rs similarity index 99% rename from src/logic/serial/native.rs rename to src/protocol/serial/native.rs index efb2d86..6eb8552 100644 --- a/src/logic/serial/native.rs +++ b/src/protocol/serial/native.rs @@ -3,8 +3,8 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use crate::logic::binary_protocol::{self as bp, KrResponse}; -use crate::logic::parsers::{ROWS, COLS}; +use crate::protocol::binary::{self as bp, KrResponse}; +use crate::protocol::parsers::{ROWS, COLS}; const BAUD_RATE: u32 = 115200; const CONNECT_TIMEOUT_MS: u64 = 300; diff --git a/original-src/settings.rs b/src/protocol/settings.rs similarity index 100% rename from original-src/settings.rs rename to src/protocol/settings.rs diff --git a/src/logic/stats_analyzer.rs b/src/protocol/stats.rs similarity index 100% rename from src/logic/stats_analyzer.rs rename to src/protocol/stats.rs diff --git a/original-src/protocol.rs b/src/protocol/text_commands.rs similarity index 100% rename from original-src/protocol.rs rename to src/protocol/text_commands.rs diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..65a67e0 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,227 @@ +use crate::context::{AppContext, BgMsg}; +use crate::protocol; +use crate::{config, MainWindow, SettingsBridge}; +use slint::{ComponentHandle, SharedString}; + +pub fn setup(window: &MainWindow, ctx: &AppContext) { + setup_ota_browse(window); + setup_ota_start(window, ctx); + setup_config_export(window, ctx); + setup_config_import(window, ctx); +} + +// --- OTA: browse --- +fn setup_ota_browse(window: &MainWindow) { + let window_weak = window.as_weak(); + window.global::().on_ota_browse(move || { + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let file = rfd::FileDialog::new() + .add_filter("Firmware", &["bin"]) + .pick_file(); + if let Some(path) = file { + let path_str = path.to_string_lossy().to_string(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_ota_path(SharedString::from(path_str.as_str())); + } + }); + } + }); + }); +} + +// --- OTA: start --- +fn setup_ota_start(window: &MainWindow, ctx: &AppContext) { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_ota_start(move || { + let Some(w) = window_weak.upgrade() else { return }; + let settings = w.global::(); + let path = settings.get_ota_path().to_string(); + if path.is_empty() { return; } + + let firmware = match std::fs::read(&path) { + Ok(data) => data, + Err(e) => { + let _ = tx.send(BgMsg::OtaDone(Err(format!("Cannot read {}: {}", path, e)))); + return; + } + }; + + settings.set_ota_flashing(true); + settings.set_ota_progress(0.0); + settings.set_ota_status(SharedString::from("Starting OTA...")); + + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use std::io::{Read, Write, BufRead, BufReader}; + + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let total = firmware.len(); + let chunk_size = 4096usize; + + // Step 1: Send OTA command + let cmd = format!("OTA {}", total); + if let Err(e) = ser.send_command(&cmd) { + let _ = tx.send(BgMsg::OtaDone(Err(format!("Send OTA cmd failed: {}", e)))); + return; + } + + // Step 2: Wait for OTA_READY (read lines with timeout) + let _ = tx.send(BgMsg::OtaProgress(0.0, "Waiting for OTA_READY...".into())); + let port = match ser.port_mut() { + Some(p) => p, + None => { + let _ = tx.send(BgMsg::OtaDone(Err("Port not available".into()))); + return; + } + }; + + // Read until we get OTA_READY or timeout + let old_timeout = port.timeout(); + let _ = port.set_timeout(std::time::Duration::from_secs(5)); + let mut got_ready = false; + let port_clone = port.try_clone().unwrap(); + let mut reader = BufReader::new(port_clone); + for _ in 0..20 { + let mut line = String::new(); + if reader.read_line(&mut line).is_ok() && line.contains("OTA_READY") { + got_ready = true; + break; + } + } + drop(reader); + + if !got_ready { + let _ = port.set_timeout(old_timeout); + let _ = tx.send(BgMsg::OtaDone(Err("Firmware did not respond OTA_READY".into()))); + return; + } + + // Step 3: Send chunks and wait for ACK after each + let num_chunks = total.div_ceil(chunk_size); + let _ = port.set_timeout(std::time::Duration::from_secs(5)); + + for (i, chunk) in firmware.chunks(chunk_size).enumerate() { + // Send chunk + if let Err(e) = port.write_all(chunk) { + let _ = port.set_timeout(old_timeout); + let _ = tx.send(BgMsg::OtaDone(Err(format!("Write chunk {} failed: {}", i, e)))); + return; + } + let _ = port.flush(); + + let progress = (i + 1) as f32 / num_chunks as f32; + let _ = tx.send(BgMsg::OtaProgress(progress * 0.95, format!( + "Chunk {}/{} ({} KB / {} KB)", + i + 1, num_chunks, + ((i + 1) * chunk_size).min(total) / 1024, + total / 1024 + ))); + + // Wait for ACK line + let mut ack_buf = [0u8; 256]; + let mut ack = String::new(); + let start = std::time::Instant::now(); + while start.elapsed() < std::time::Duration::from_secs(5) { + match port.read(&mut ack_buf) { + Ok(n) if n > 0 => { + ack.push_str(&String::from_utf8_lossy(&ack_buf[..n])); + if ack.contains("OTA_OK") || ack.contains("OTA_DONE") || ack.contains("OTA_FAIL") { + break; + } + } + _ => std::thread::sleep(std::time::Duration::from_millis(10)), + } + } + + if ack.contains("OTA_FAIL") { + let _ = port.set_timeout(old_timeout); + let _ = tx.send(BgMsg::OtaDone(Err(format!("Firmware error: {}", ack.trim())))); + return; + } + if ack.contains("OTA_DONE") { + break; // Firmware signals all received + } + } + + let _ = port.set_timeout(old_timeout); + let _ = tx.send(BgMsg::OtaProgress(1.0, "OTA complete, rebooting...".into())); + let _ = tx.send(BgMsg::OtaDone(Ok(()))); + }); + }); +} + +// --- Config Export --- +fn setup_config_export(window: &MainWindow, ctx: &AppContext) { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_config_export(move || { + let Some(w) = window_weak.upgrade() else { return }; + w.global::().set_config_busy(true); + w.global::().set_config_progress(0.0); + w.global::().set_config_status(SharedString::from("Reading config...")); + + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let result = config::export_config(&serial, &tx); + let _ = tx.send(BgMsg::ConfigDone(result)); + }); + }); +} + +// --- Config Import --- +fn setup_config_import(window: &MainWindow, ctx: &AppContext) { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_config_import(move || { + let serial = serial.clone(); + let tx = tx.clone(); + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let file = rfd::FileDialog::new() + .add_filter("KeSp Config", &["json"]) + .pick_file(); + let Some(path) = file else { return }; + + let _ = slint::invoke_from_event_loop({ + let window_weak = window_weak.clone(); + move || { + if let Some(w) = window_weak.upgrade() { + let s = w.global::(); + s.set_config_busy(true); + s.set_config_progress(0.0); + s.set_config_status(SharedString::from("Importing config...")); + } + } + }); + + let json = match std::fs::read_to_string(&path) { + Ok(j) => j, + Err(e) => { + let _ = tx.send(BgMsg::ConfigDone(Err(format!("Read error: {}", e)))); + return; + } + }; + let config = match protocol::config_io::KeyboardConfig::from_json(&json) { + Ok(c) => c, + Err(e) => { + let _ = tx.send(BgMsg::ConfigDone(Err(format!("Parse error: {}", e)))); + return; + } + }; + + let result = config::import_config(&serial, &tx, &config); + let _ = tx.send(BgMsg::ConfigDone(result)); + }); + }); +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..9c8007b --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,28 @@ +use crate::context::AppContext; +use crate::{MainWindow, StatsBridge}; +use crate::protocol; +use slint::ComponentHandle; + +/// Wire up the stats refresh callback. +pub fn setup(window: &MainWindow, ctx: &AppContext) { + let serial = ctx.serial.clone(); + let tx = ctx.bg_tx.clone(); + + window.global::().on_refresh_stats(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + use protocol::binary::cmd; + use crate::context::BgMsg; + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(r) = ser.send_binary(cmd::KEYSTATS_BIN, &[]) { + let (data, max) = protocol::parsers::parse_keystats_binary(&r.payload); + let _ = tx.send(BgMsg::HeatmapData(data, max)); + } + let bigram_lines = if let Ok(r) = ser.send_binary(protocol::binary::cmd::BIGRAMS_TEXT, &[]) { + String::from_utf8_lossy(&r.payload).lines().map(|l| l.to_string()).collect() + } else { Vec::new() }; + let _ = tx.send(BgMsg::BigramLines(bigram_lines)); + }); + }); +} diff --git a/ui/components/dark_line_edit.slint b/ui/components/dark_line_edit.slint index e74769b..66adc66 100644 --- a/ui/components/dark_line_edit.slint +++ b/ui/components/dark_line_edit.slint @@ -1,4 +1,3 @@ -import { LineEdit } from "std-widgets.slint"; import { Theme } from "../theme.slint"; export component DarkLineEdit inherits Rectangle { @@ -9,15 +8,34 @@ export component DarkLineEdit inherits Rectangle { forward-focus: inner; height: 28px; + min-width: 60px; border-radius: 4px; border-width: 1px; border-color: inner.has-focus ? Theme.accent-purple : Theme.button-border; background: Theme.button-bg; - inner := LineEdit { + // Placeholder + if root.text == "" && !inner.has-focus : Text { + x: 6px; + y: 0; + width: root.width - 12px; + height: root.height; + text: root.placeholder-text; + color: Theme.fg-secondary; + font-size: 12px; + vertical-alignment: center; + } + + inner := TextInput { + x: 6px; + y: 0; + width: root.width - 12px; + height: root.height; text <=> root.text; - placeholder-text: root.placeholder-text; - accepted(t) => { root.accepted(t); } - edited(t) => { root.edited(t); } + color: Theme.fg-primary; + font-size: 12px; + vertical-alignment: center; + accepted => { root.accepted(root.text); } + edited => { root.edited(root.text); } } } diff --git a/ui/globals.slint b/ui/globals.slint index 0c87cca..60d1908 100644 --- a/ui/globals.slint +++ b/ui/globals.slint @@ -253,6 +253,7 @@ export global MacroBridge { callback save-macro(); // reads slot, name, steps from properties callback delete-macro(int); callback add-delay-step(int); // delay in ms (50, 100, 200, 500) + callback add-shortcut(string); // e.g. "ctrl+c", "ctrl+shift+z" callback remove-last-step(); callback clear-steps(); } diff --git a/ui/tabs/tab_keymap.slint b/ui/tabs/tab_keymap.slint index ee379db..a211a73 100644 --- a/ui/tabs/tab_keymap.slint +++ b/ui/tabs/tab_keymap.slint @@ -4,147 +4,187 @@ import { DarkButton } from "../components/dark_button.slint"; import { KeymapBridge, AppState, ConnectionState } from "../globals.slint"; import { KeyboardView } from "../components/keyboard_view.slint"; -export component TabKeymap inherits VerticalLayout { +export component TabKeymap inherits Rectangle { in-out property renaming: false; - padding: 8px; - spacing: 6px; - - // Main area: layers sidebar + keyboard - HorizontalLayout { - vertical-stretch: 1; + VerticalLayout { + padding: 8px; spacing: 6px; - // Layer sidebar (vertical) - VerticalLayout { - width: 90px; - spacing: 4px; - alignment: start; + // Main area: layers sidebar + keyboard + HorizontalLayout { + vertical-stretch: 1; + spacing: 6px; - Text { - text: "Layers"; - color: Theme.fg-secondary; - font-size: 11px; - horizontal-alignment: center; - } - - Flickable { - vertical-stretch: 1; - VerticalLayout { - spacing: 4px; - - for layer[idx] in KeymapBridge.layers : Rectangle { - height: 30px; - border-radius: 4px; - background: layer.active ? Theme.accent-purple : Theme.button-bg; + // Layer sidebar (vertical) + VerticalLayout { + width: 90px; + spacing: 4px; + alignment: start; Text { - text: layer.name; - color: Theme.fg-primary; + text: "Layers"; + color: Theme.fg-secondary; font-size: 11px; horizontal-alignment: center; + } + + Flickable { + vertical-stretch: 1; + VerticalLayout { + spacing: 4px; + + for layer[idx] in KeymapBridge.layers : Rectangle { + height: 30px; + border-radius: 4px; + background: layer.active ? Theme.accent-purple : Theme.button-bg; + + Text { + text: layer.name; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { KeymapBridge.switch-layer(layer.index); } + mouse-cursor: pointer; + } + } + } + } + + if !root.renaming && AppState.connection == ConnectionState.connected : Rectangle { + height: 24px; + border-radius: 4px; + background: rename-ta.has-hover ? Theme.button-hover : Theme.button-bg; + + Text { + text: "Rename"; + color: Theme.fg-secondary; + font-size: 10px; + horizontal-alignment: center; + vertical-alignment: center; + } + + rename-ta := TouchArea { + clicked => { root.renaming = true; } + mouse-cursor: pointer; + } + } + + Rectangle { vertical-stretch: 1; } + + // Heatmap toggle + Rectangle { + height: 30px; + border-radius: 4px; + background: KeymapBridge.heatmap-enabled ? Theme.accent-orange : Theme.button-bg; + + Text { + text: "Heatmap"; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { + KeymapBridge.heatmap-enabled = !KeymapBridge.heatmap-enabled; + if KeymapBridge.heatmap-enabled { KeymapBridge.toggle-heatmap(); } + } + mouse-cursor: pointer; + } + } + } + + // Keyboard view + KeyboardView { + horizontal-stretch: 1; + } + } + + // Selected key info bar + if KeymapBridge.selected-key-index >= 0 : Rectangle { + height: 36px; + background: Theme.bg-secondary; + border-radius: 4px; + + HorizontalLayout { + padding: 8px; + spacing: 12px; + + Text { + text: "Selected: " + KeymapBridge.selected-key-label; + color: Theme.accent-cyan; + font-size: 12px; vertical-alignment: center; } - TouchArea { - clicked => { KeymapBridge.switch-layer(layer.index); } - mouse-cursor: pointer; - } - } - } - } + Rectangle { horizontal-stretch: 1; } + + DarkButton { + text: "Change Key..."; + clicked => { + KeymapBridge.key-selector-open = true; + } + } + } + } + } + + // Rename popup overlay + if root.renaming : Rectangle { + background: #000000aa; + + TouchArea { + clicked => { root.renaming = false; } + } + + Rectangle { + width: 260px; + height: 120px; + border-radius: 8px; + background: Theme.bg-secondary; + + VerticalLayout { + padding: 16px; + spacing: 12px; + alignment: center; + + Text { + text: "Rename layer"; + color: Theme.fg-primary; + font-size: 13px; + horizontal-alignment: center; + } - // Rename - if root.renaming : VerticalLayout { - spacing: 4px; rename-input := DarkLineEdit { - max-width: 85px; placeholder-text: "New name"; accepted(text) => { KeymapBridge.rename-layer(KeymapBridge.active-layer, text); root.renaming = false; } } - DarkButton { - text: "Cancel"; - clicked => { root.renaming = false; } - } - } - if !root.renaming && AppState.connection == ConnectionState.connected : Rectangle { - height: 24px; - border-radius: 4px; - background: rename-ta.has-hover ? Theme.button-hover : Theme.button-bg; + HorizontalLayout { + spacing: 8px; + alignment: center; - Text { - text: "Rename"; - color: Theme.fg-secondary; - font-size: 10px; - horizontal-alignment: center; - vertical-alignment: center; - } + DarkButton { + text: "Cancel"; + clicked => { root.renaming = false; } + } - rename-ta := TouchArea { - clicked => { root.renaming = true; } - mouse-cursor: pointer; - } - } - - Rectangle { vertical-stretch: 1; } - - // Heatmap toggle - Rectangle { - height: 30px; - border-radius: 4px; - background: KeymapBridge.heatmap-enabled ? Theme.accent-orange : Theme.button-bg; - - Text { - text: "Heatmap"; - color: Theme.fg-primary; - font-size: 11px; - horizontal-alignment: center; - vertical-alignment: center; - } - - TouchArea { - clicked => { - KeymapBridge.heatmap-enabled = !KeymapBridge.heatmap-enabled; - if KeymapBridge.heatmap-enabled { KeymapBridge.toggle-heatmap(); } - } - mouse-cursor: pointer; - } - } - } - - // Keyboard view - KeyboardView { - horizontal-stretch: 1; - } - } - - // Selected key info bar - if KeymapBridge.selected-key-index >= 0 : Rectangle { - height: 36px; - background: Theme.bg-secondary; - border-radius: 4px; - - HorizontalLayout { - padding: 8px; - spacing: 12px; - - Text { - text: "Selected: " + KeymapBridge.selected-key-label; - color: Theme.accent-cyan; - font-size: 12px; - vertical-alignment: center; - } - - Rectangle { horizontal-stretch: 1; } - - DarkButton { - text: "Change Key..."; - clicked => { - KeymapBridge.key-selector-open = true; + DarkButton { + text: "Rename"; + clicked => { + KeymapBridge.rename-layer(KeymapBridge.active-layer, rename-input.text); + root.renaming = false; + } + } } } } diff --git a/ui/tabs/tab_macros.slint b/ui/tabs/tab_macros.slint index b358d83..c5bac5c 100644 --- a/ui/tabs/tab_macros.slint +++ b/ui/tabs/tab_macros.slint @@ -37,7 +37,9 @@ export component TabMacros inherits Rectangle { HorizontalLayout { spacing: 8px; - alignment: start; + + Text { text: "Slot:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } + Text { text: "#" + MacroBridge.new-slot-idx; color: Theme.accent-purple; font-size: 12px; font-weight: 600; vertical-alignment: center; } Text { text: "Name:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } DarkLineEdit { @@ -102,6 +104,23 @@ export component TabMacros inherits Rectangle { DarkButton { text: "Clear"; clicked => { MacroBridge.clear-steps(); } } } + // Common shortcuts + HorizontalLayout { + spacing: 6px; + alignment: start; + + Text { text: "Shortcuts:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + + DarkButton { text: "Ctrl+C"; clicked => { MacroBridge.add-shortcut("ctrl+c"); } } + DarkButton { text: "Ctrl+V"; clicked => { MacroBridge.add-shortcut("ctrl+v"); } } + DarkButton { text: "Ctrl+X"; clicked => { MacroBridge.add-shortcut("ctrl+x"); } } + DarkButton { text: "Ctrl+Z"; clicked => { MacroBridge.add-shortcut("ctrl+z"); } } + DarkButton { text: "Ctrl+Y"; clicked => { MacroBridge.add-shortcut("ctrl+y"); } } + DarkButton { text: "Ctrl+S"; clicked => { MacroBridge.add-shortcut("ctrl+s"); } } + DarkButton { text: "Ctrl+A"; clicked => { MacroBridge.add-shortcut("ctrl+a"); } } + DarkButton { text: "Alt+F4"; clicked => { MacroBridge.add-shortcut("alt+f4"); } } + } + // Save HorizontalLayout { spacing: 8px;