feat: Binary protocol v2, config import/export, Tools tab, new layout format
- Migrate all serial commands from ASCII text to binary KS/KR frame protocol
(SETLAYER, TD_LIST, COMBO_LIST, LEADER_LIST, KO_LIST, etc.)
- Add config import/export as JSON (keymaps, tap dances, combos, KO, leaders, macros)
- Merge Flash + Layout Preview into single Tools tab
- Replace WPF tree layout JSON format with flat groups+keys format:
- Top-level "keys" for absolute positioning (thumbs, isolated keys)
- "groups" with x/y/r transform, keys inside use local coordinates
- Coordinates in units (1u = 50px), w/h default 1u, r default 0
- Layout auto-refresh (5s timer) for live preview while editing externally
- Pretty-print JSON in layout preview and export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:30:04 +00:00
|
|
|
import { DarkLineEdit } from "../components/dark_line_edit.slint";
|
|
|
|
|
import { Theme } from "../theme.slint";
|
|
|
|
|
import { DarkButton } from "../components/dark_button.slint";
|
|
|
|
|
import { DarkCheckbox } from "../components/dark_checkbox.slint";
|
|
|
|
|
import { DarkComboBox } from "../components/dark_combo_box.slint";
|
2026-04-10 08:05:36 +00:00
|
|
|
import { FlasherBridge, LayoutBridge, ToolsBridge, KeycapData, AppState, ConnectionState } from "../globals.slint";
|
feat: Binary protocol v2, config import/export, Tools tab, new layout format
- Migrate all serial commands from ASCII text to binary KS/KR frame protocol
(SETLAYER, TD_LIST, COMBO_LIST, LEADER_LIST, KO_LIST, etc.)
- Add config import/export as JSON (keymaps, tap dances, combos, KO, leaders, macros)
- Merge Flash + Layout Preview into single Tools tab
- Replace WPF tree layout JSON format with flat groups+keys format:
- Top-level "keys" for absolute positioning (thumbs, isolated keys)
- "groups" with x/y/r transform, keys inside use local coordinates
- Coordinates in units (1u = 50px), w/h default 1u, r default 0
- Layout auto-refresh (5s timer) for live preview while editing externally
- Pretty-print JSON in layout preview and export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:30:04 +00:00
|
|
|
import { ScrollView } from "std-widgets.slint";
|
|
|
|
|
|
|
|
|
|
export component TabTools inherits Rectangle {
|
|
|
|
|
background: Theme.bg-primary;
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
VerticalLayout {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
spacing: 16px;
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: "Tools";
|
|
|
|
|
color: Theme.fg-primary;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== LAYOUT PREVIEW =====================
|
|
|
|
|
Rectangle {
|
|
|
|
|
background: Theme.bg-secondary;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
VerticalLayout {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
spacing: 10px;
|
|
|
|
|
|
|
|
|
|
HorizontalLayout {
|
|
|
|
|
spacing: 12px;
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: "Layout Preview";
|
|
|
|
|
color: Theme.accent-cyan;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle { horizontal-stretch: 1; }
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: "From keyboard";
|
|
|
|
|
primary: true;
|
|
|
|
|
enabled: AppState.connection == ConnectionState.connected;
|
|
|
|
|
clicked => { LayoutBridge.load-from-keyboard(); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: "Load file...";
|
|
|
|
|
clicked => { LayoutBridge.load-from-file(); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: "Export...";
|
|
|
|
|
enabled: LayoutBridge.json-text != "";
|
|
|
|
|
clicked => { LayoutBridge.export-json(); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if LayoutBridge.file-path != "" : HorizontalLayout {
|
|
|
|
|
spacing: 12px;
|
|
|
|
|
|
|
|
|
|
DarkCheckbox {
|
|
|
|
|
text: "Auto-refresh (5s)";
|
|
|
|
|
checked <=> LayoutBridge.auto-refresh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: LayoutBridge.file-path;
|
|
|
|
|
color: Theme.fg-secondary;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
overflow: elide;
|
|
|
|
|
horizontal-stretch: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if LayoutBridge.status != "" : Text {
|
|
|
|
|
text: LayoutBridge.status;
|
|
|
|
|
color: Theme.fg-secondary;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keyboard render
|
|
|
|
|
preview-area := Rectangle {
|
|
|
|
|
height: 300px;
|
|
|
|
|
background: Theme.bg-surface;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
clip: true;
|
|
|
|
|
|
|
|
|
|
property <float> scale-x: self.width / LayoutBridge.content-width;
|
|
|
|
|
property <float> scale-y: self.height / LayoutBridge.content-height;
|
|
|
|
|
property <float> scale: min(scale-x, scale-y) * 0.95;
|
|
|
|
|
property <length> offset-x: (self.width - LayoutBridge.content-width * scale) / 2;
|
|
|
|
|
property <length> offset-y: (self.height - LayoutBridge.content-height * scale) / 2;
|
|
|
|
|
|
|
|
|
|
if LayoutBridge.keycaps.length == 0 : Text {
|
|
|
|
|
text: "No layout loaded";
|
|
|
|
|
color: Theme.fg-secondary;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
horizontal-alignment: center;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for keycap[idx] in LayoutBridge.keycaps : Rectangle {
|
|
|
|
|
x: preview-area.offset-x + keycap.x * preview-area.scale;
|
|
|
|
|
y: preview-area.offset-y + keycap.y * preview-area.scale;
|
|
|
|
|
width: keycap.w * preview-area.scale;
|
|
|
|
|
height: keycap.h * preview-area.scale;
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: #44475a;
|
|
|
|
|
border-width: 1px;
|
|
|
|
|
border-color: Theme.bg-primary;
|
|
|
|
|
transform-rotation: keycap.rotation * 1deg;
|
|
|
|
|
transform-origin: {
|
|
|
|
|
x: self.width / 2,
|
|
|
|
|
y: self.height / 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: keycap.label;
|
|
|
|
|
color: Theme.fg-primary;
|
|
|
|
|
font-size: max(7px, 11px * preview-area.scale);
|
|
|
|
|
horizontal-alignment: center;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSON preview
|
|
|
|
|
Rectangle {
|
|
|
|
|
height: 100px;
|
|
|
|
|
background: Theme.bg-primary;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
clip: true;
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
Text {
|
|
|
|
|
text: LayoutBridge.json-text != "" ? LayoutBridge.json-text : "// JSON will appear here";
|
|
|
|
|
color: LayoutBridge.json-text != "" ? Theme.fg-primary : Theme.fg-secondary;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
font-family: "monospace";
|
|
|
|
|
wrap: word-wrap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 08:05:36 +00:00
|
|
|
// ===================== MATRIX TEST =====================
|
|
|
|
|
Rectangle {
|
|
|
|
|
background: Theme.bg-secondary;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
VerticalLayout {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
spacing: 10px;
|
|
|
|
|
|
|
|
|
|
HorizontalLayout {
|
|
|
|
|
spacing: 12px;
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: "Matrix Test";
|
|
|
|
|
color: Theme.accent-cyan;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle { horizontal-stretch: 1; }
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: ToolsBridge.matrix-test-active ? "Stop Test" : "Start Test";
|
|
|
|
|
primary: !ToolsBridge.matrix-test-active;
|
|
|
|
|
enabled: AppState.connection == ConnectionState.connected;
|
|
|
|
|
clicked => { ToolsBridge.toggle-matrix-test(); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ToolsBridge.matrix-test-status != "" : Text {
|
|
|
|
|
text: ToolsBridge.matrix-test-status;
|
|
|
|
|
color: ToolsBridge.matrix-test-active ? Theme.accent-green : Theme.fg-secondary;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keyboard render for matrix test
|
|
|
|
|
matrix-area := Rectangle {
|
|
|
|
|
height: 260px;
|
|
|
|
|
background: Theme.bg-surface;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
clip: true;
|
|
|
|
|
|
|
|
|
|
property <float> scale-x: self.width / ToolsBridge.matrix-content-width;
|
|
|
|
|
property <float> scale-y: self.height / ToolsBridge.matrix-content-height;
|
|
|
|
|
property <float> scale: min(scale-x, scale-y) * 0.95;
|
|
|
|
|
property <length> offset-x: (self.width - ToolsBridge.matrix-content-width * scale) / 2;
|
|
|
|
|
property <length> offset-y: (self.height - ToolsBridge.matrix-content-height * scale) / 2;
|
|
|
|
|
|
|
|
|
|
if ToolsBridge.matrix-keycaps.length == 0 : Text {
|
|
|
|
|
text: "Press \"Start Test\" to begin";
|
|
|
|
|
color: Theme.fg-secondary;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
horizontal-alignment: center;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for keycap[idx] in ToolsBridge.matrix-keycaps : Rectangle {
|
|
|
|
|
x: matrix-area.offset-x + keycap.x * matrix-area.scale;
|
|
|
|
|
y: matrix-area.offset-y + keycap.y * matrix-area.scale;
|
|
|
|
|
width: keycap.w * matrix-area.scale;
|
|
|
|
|
height: keycap.h * matrix-area.scale;
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: keycap.color;
|
|
|
|
|
border-width: 1px;
|
|
|
|
|
border-color: Theme.bg-primary;
|
|
|
|
|
transform-rotation: keycap.rotation * 1deg;
|
|
|
|
|
transform-origin: {
|
|
|
|
|
x: self.width / 2,
|
|
|
|
|
y: self.height / 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: keycap.label;
|
|
|
|
|
color: Theme.fg-primary;
|
|
|
|
|
font-size: max(7px, 10px * matrix-area.scale);
|
|
|
|
|
horizontal-alignment: center;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Binary protocol v2, config import/export, Tools tab, new layout format
- Migrate all serial commands from ASCII text to binary KS/KR frame protocol
(SETLAYER, TD_LIST, COMBO_LIST, LEADER_LIST, KO_LIST, etc.)
- Add config import/export as JSON (keymaps, tap dances, combos, KO, leaders, macros)
- Merge Flash + Layout Preview into single Tools tab
- Replace WPF tree layout JSON format with flat groups+keys format:
- Top-level "keys" for absolute positioning (thumbs, isolated keys)
- "groups" with x/y/r transform, keys inside use local coordinates
- Coordinates in units (1u = 50px), w/h default 1u, r default 0
- Layout auto-refresh (5s timer) for live preview while editing externally
- Pretty-print JSON in layout preview and export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:30:04 +00:00
|
|
|
// ===================== ESP32 FLASHER =====================
|
|
|
|
|
Rectangle {
|
|
|
|
|
background: Theme.bg-secondary;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
VerticalLayout {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
spacing: 10px;
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: "ESP32 Firmware Flasher";
|
|
|
|
|
color: Theme.accent-cyan;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
Text {
|
|
|
|
|
text: "Flash via programming port (CH340/CP2102)";
|
|
|
|
|
color: Theme.fg-secondary;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Port
|
|
|
|
|
HorizontalLayout {
|
|
|
|
|
spacing: 8px;
|
|
|
|
|
|
|
|
|
|
if FlasherBridge.prog-ports.length > 0 : DarkComboBox {
|
|
|
|
|
horizontal-stretch: 1;
|
|
|
|
|
model: FlasherBridge.prog-ports;
|
|
|
|
|
selected(value) => { FlasherBridge.selected-prog-port = value; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if FlasherBridge.prog-ports.length == 0 : DarkLineEdit {
|
|
|
|
|
horizontal-stretch: 1;
|
|
|
|
|
text <=> FlasherBridge.selected-prog-port;
|
|
|
|
|
placeholder-text: "/dev/ttyUSB0";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: "Refresh";
|
|
|
|
|
clicked => { FlasherBridge.refresh-prog-ports(); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if FlasherBridge.prog-ports.length == 0 : Text {
|
|
|
|
|
text: "No CH340/CP210x port detected. Enter path manually.";
|
|
|
|
|
color: Theme.accent-yellow;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
wrap: word-wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partition + firmware
|
|
|
|
|
HorizontalLayout {
|
|
|
|
|
spacing: 12px;
|
|
|
|
|
|
|
|
|
|
DarkComboBox {
|
|
|
|
|
width: 200px;
|
2026-04-08 19:20:03 +00:00
|
|
|
model: ["full (0x0)", "factory (0x20000)", "ota\\_0 (0x220000)"];
|
feat: Binary protocol v2, config import/export, Tools tab, new layout format
- Migrate all serial commands from ASCII text to binary KS/KR frame protocol
(SETLAYER, TD_LIST, COMBO_LIST, LEADER_LIST, KO_LIST, etc.)
- Add config import/export as JSON (keymaps, tap dances, combos, KO, leaders, macros)
- Merge Flash + Layout Preview into single Tools tab
- Replace WPF tree layout JSON format with flat groups+keys format:
- Top-level "keys" for absolute positioning (thumbs, isolated keys)
- "groups" with x/y/r transform, keys inside use local coordinates
- Coordinates in units (1u = 50px), w/h default 1u, r default 0
- Layout auto-refresh (5s timer) for live preview while editing externally
- Pretty-print JSON in layout preview and export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:30:04 +00:00
|
|
|
current-index <=> FlasherBridge.flash-offset-index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
horizontal-stretch: 1;
|
|
|
|
|
text: FlasherBridge.firmware-path != "" ? FlasherBridge.firmware-path : "No file selected";
|
|
|
|
|
color: FlasherBridge.firmware-path != "" ? Theme.fg-primary : Theme.fg-secondary;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
overflow: elide;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: "Browse...";
|
|
|
|
|
clicked => { FlasherBridge.browse-firmware(); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flash button + progress
|
|
|
|
|
HorizontalLayout {
|
|
|
|
|
spacing: 12px;
|
|
|
|
|
|
|
|
|
|
DarkButton {
|
|
|
|
|
text: FlasherBridge.flashing ? "Flashing..." : "Flash";
|
|
|
|
|
primary: true;
|
|
|
|
|
enabled: !FlasherBridge.flashing
|
|
|
|
|
&& FlasherBridge.firmware-path != ""
|
|
|
|
|
&& FlasherBridge.selected-prog-port != "";
|
|
|
|
|
clicked => { FlasherBridge.flash(); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: FlasherBridge.flash-status;
|
|
|
|
|
color: Theme.fg-primary;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
horizontal-stretch: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if FlasherBridge.flashing || FlasherBridge.flash-progress > 0 : Rectangle {
|
|
|
|
|
height: 20px;
|
|
|
|
|
background: Theme.bg-primary;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
x: 0;
|
|
|
|
|
width: parent.width * clamp(FlasherBridge.flash-progress, 0, 1);
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: FlasherBridge.flash-progress >= 1.0 ? Theme.accent-green : Theme.accent-purple;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: round(FlasherBridge.flash-progress * 100) + "%";
|
|
|
|
|
color: Theme.fg-primary;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
horizontal-alignment: center;
|
|
|
|
|
vertical-alignment: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Warning
|
|
|
|
|
Rectangle {
|
|
|
|
|
background: #ff555520;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border-width: 1px;
|
|
|
|
|
border-color: Theme.accent-red;
|
|
|
|
|
|
|
|
|
|
VerticalLayout {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
spacing: 4px;
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: "Warning";
|
|
|
|
|
color: Theme.accent-red;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
Text {
|
|
|
|
|
text: "Do not disconnect the keyboard during flashing. The device will reboot automatically when done.";
|
|
|
|
|
color: Theme.fg-secondary;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
wrap: word-wrap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|