From 16fe1ce319913c37b0537f15e2d6a71f5fcb53b8 Mon Sep 17 00:00:00 2001 From: timwata Date: Sat, 10 Oct 2026 22:29:56 +0900 Subject: [PATCH 1/2] fix: place the panel's pad keys on their physical columns The menu panel packed every row to the left, so the top row and the bottom row sat one column off from the pad. Pad.displayRows now spans the four-column grid with nil gaps (top row in the middle two columns, bottom row one column in), and both the panel and the emulator render a gap as an empty key-sized slot. Co-Authored-By: Claude Opus 5.5 --- Sources/WLKit/OAIProtocol.swift | 13 ++++++++----- Sources/WLMicroManager/EmulatorPanel.swift | 8 +++++++- Sources/WLMicroManager/MenuPanelView.swift | 13 +++++++++++-- Tests/WLKitTests/PlaceholderTests.swift | 7 ++++++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/Sources/WLKit/OAIProtocol.swift b/Sources/WLKit/OAIProtocol.swift index a6329a1..67e9156 100644 --- a/Sources/WLKit/OAIProtocol.swift +++ b/Sources/WLKit/OAIProtocol.swift @@ -156,13 +156,16 @@ public enum Pad { /// row is wired right to left — key 0 is the top-RIGHT key — so reading /// order starts 1, 0. The other rows run left to right in index order. public static let agentKeyIDs = [1, 0, 2, 3, 4, 5] - /// The rows as you look at the pad, for on-screen mirrors. `rows` stays in - /// firmware order because the keymap file is addressed that way. - public static let displayRows: [[Int]] = [ - [1, 0], + /// The rows as you look at the pad, for on-screen mirrors, on the pad's + /// four-column grid: nil is a column with no key in it. The top row sits + /// in the middle two columns and the bottom row starts one column in. + /// `rows` stays in firmware order because the keymap file is addressed + /// that way. + public static let displayRows: [[Int?]] = [ + [nil, 1, 0, nil], [2, 3, 4, 5], [6, 7, 8, 9], - [10, 11, 12], + [nil, 10, 11, 12], ] /// The agent slot a key answers for, or nil for non-agent keys. diff --git a/Sources/WLMicroManager/EmulatorPanel.swift b/Sources/WLMicroManager/EmulatorPanel.swift index 4969d36..51b11d7 100644 --- a/Sources/WLMicroManager/EmulatorPanel.swift +++ b/Sources/WLMicroManager/EmulatorPanel.swift @@ -77,7 +77,13 @@ struct EmulatorView: View { // Rows 0–2 are plain grids of single keys. ForEach(Array(Pad.displayRows.prefix(3).enumerated()), id: \.offset) { _, row in HStack(spacing: Self.gap) { - ForEach(row, id: \.self) { key in keyButton(key) } + ForEach(Array(row.enumerated()), id: \.offset) { _, key in + if let key { + keyButton(key) + } else { + Color.clear.frame(width: Self.keyW, height: Self.keyH) + } + } } } // The bottom row starts one column in, and its wide keycap diff --git a/Sources/WLMicroManager/MenuPanelView.swift b/Sources/WLMicroManager/MenuPanelView.swift index dd172d1..d458a2e 100644 --- a/Sources/WLMicroManager/MenuPanelView.swift +++ b/Sources/WLMicroManager/MenuPanelView.swift @@ -229,7 +229,13 @@ struct MenuPanelView: View { VStack(alignment: .leading, spacing: 6) { ForEach(Array(Pad.displayRows.enumerated()), id: \.offset) { _, row in HStack(spacing: 6) { - ForEach(row, id: \.self) { key in keyView(key) } + ForEach(Array(row.enumerated()), id: \.offset) { _, key in + if let key { + keyView(key) + } else { + Color.clear.frame(width: Self.keyW, height: Self.keyH) + } + } Spacer(minLength: 0) } } @@ -237,6 +243,9 @@ struct MenuPanelView: View { .padding(.horizontal, 14).padding(.vertical, 12) } + private static let keyW: CGFloat = 34 + private static let keyH: CGFloat = 26 + private func keyView(_ index: Int) -> some View { let color = bridge.keyColors[index] let isBound = Pad.boundKeyIDs.contains(index) @@ -269,7 +278,7 @@ struct MenuPanelView: View { } label: { RoundedRectangle(cornerRadius: 5) .fill(color ?? Color.secondary.opacity(isBound ? 0.16 : 0.07)) - .frame(width: 34, height: 26) + .frame(width: Self.keyW, height: Self.keyH) .overlay( RoundedRectangle(cornerRadius: 5) .strokeBorder(Color.secondary.opacity(0.25), lineWidth: 0.5) diff --git a/Tests/WLKitTests/PlaceholderTests.swift b/Tests/WLKitTests/PlaceholderTests.swift index 80ec7ee..1fe2a60 100644 --- a/Tests/WLKitTests/PlaceholderTests.swift +++ b/Tests/WLKitTests/PlaceholderTests.swift @@ -6,6 +6,11 @@ final class PlaceholderTests: XCTestCase { XCTAssertEqual(Pad.rows.flatMap { $0 }.count, Pad.keyCount) // Reading order: the top row is wired right to left. XCTAssertEqual(Pad.agentKeyIDs, [1, 0, 2, 3, 4, 5]) - XCTAssertEqual(Pad.displayRows.flatMap { $0 }.sorted(), Pad.rows.flatMap { $0 }.sorted()) + XCTAssertEqual(Pad.displayRows.flatMap { $0 }.compactMap { $0 }.sorted(), + Pad.rows.flatMap { $0 }.sorted()) + // Every display row spans the pad's four columns, so gaps line up. + XCTAssertEqual(Pad.displayRows.map(\.count), [4, 4, 4, 4]) + XCTAssertEqual(Pad.displayRows.first, [nil, 1, 0, nil]) + XCTAssertEqual(Pad.displayRows.last, [nil, 10, 11, 12]) } } From 3d165c7380f161d18e63a4b0713110ed58d211a6 Mon Sep 17 00:00:00 2001 From: timwata Date: Sat, 10 Oct 2026 22:35:39 +0900 Subject: [PATCH 2/2] fix: key 0 is the top-left key, not the top-right On the hardware the top-left key opened the second agent and the top-right key the first, so the top row is not wired right to left. Agent slots now take keys in plain index order, the panel and emulator show 0 on the left, and docs/hacking.md drops the reversed-row claim and draws the pad on its four-column grid. Co-Authored-By: Claude Opus 5.5 --- Sources/WLKit/OAIProtocol.swift | 10 +++++----- Sources/WLMicroManager/MenuPanelView.swift | 4 ++-- Tests/WLKitTests/PlaceholderTests.swift | 6 +++--- Tests/WLKitTests/StatusMapperTests.swift | 6 +++--- docs/hacking.md | 22 ++++++++++++---------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Sources/WLKit/OAIProtocol.swift b/Sources/WLKit/OAIProtocol.swift index 67e9156..8f4072a 100644 --- a/Sources/WLKit/OAIProtocol.swift +++ b/Sources/WLKit/OAIProtocol.swift @@ -152,17 +152,17 @@ public enum Pad { /// when turning everything off, not just the visible keys. public static let maxThreadID = 19 /// Agent slot N lights and answers to `agentKeyIDs[N]`, in READING order: - /// left to right, top to bottom as you look at the pad. The firmware's top - /// row is wired right to left — key 0 is the top-RIGHT key — so reading - /// order starts 1, 0. The other rows run left to right in index order. - public static let agentKeyIDs = [1, 0, 2, 3, 4, 5] + /// left to right, top to bottom as you look at the pad. Every row runs left + /// to right in index order, so this is plain index order: key 0 is the + /// top-LEFT key (checked on hardware — the top row is not reversed). + public static let agentKeyIDs = [0, 1, 2, 3, 4, 5] /// The rows as you look at the pad, for on-screen mirrors, on the pad's /// four-column grid: nil is a column with no key in it. The top row sits /// in the middle two columns and the bottom row starts one column in. /// `rows` stays in firmware order because the keymap file is addressed /// that way. public static let displayRows: [[Int?]] = [ - [nil, 1, 0, nil], + [nil, 0, 1, nil], [2, 3, 4, 5], [6, 7, 8, 9], [nil, 10, 11, 12], diff --git a/Sources/WLMicroManager/MenuPanelView.swift b/Sources/WLMicroManager/MenuPanelView.swift index d458a2e..94d8461 100644 --- a/Sources/WLMicroManager/MenuPanelView.swift +++ b/Sources/WLMicroManager/MenuPanelView.swift @@ -256,8 +256,8 @@ struct MenuPanelView: View { let isUnavailable = bridge.isRemote && (isStackKey || isLandKey) let macroText = bridge.keyBindings.text(for: index) let isVoiceKey = macroText == nil && Pad.voiceKeyIDs.contains(index) - // Key index and agent slot are different orderings — the top row is - // wired right to left — so the slot lookup goes through the pad map. + // Only some keys are agent keys, so the slot lookup goes through the + // pad map. let slot = Pad.agentSlot(for: index) let agent = slot.flatMap { $0 < bridge.agents.count ? bridge.agents[$0] : nil } diff --git a/Tests/WLKitTests/PlaceholderTests.swift b/Tests/WLKitTests/PlaceholderTests.swift index 1fe2a60..e6ab9fc 100644 --- a/Tests/WLKitTests/PlaceholderTests.swift +++ b/Tests/WLKitTests/PlaceholderTests.swift @@ -4,13 +4,13 @@ import XCTest final class PlaceholderTests: XCTestCase { func testPadGeometry() { XCTAssertEqual(Pad.rows.flatMap { $0 }.count, Pad.keyCount) - // Reading order: the top row is wired right to left. - XCTAssertEqual(Pad.agentKeyIDs, [1, 0, 2, 3, 4, 5]) + // Reading order is index order: key 0 is the top-left key. + XCTAssertEqual(Pad.agentKeyIDs, [0, 1, 2, 3, 4, 5]) XCTAssertEqual(Pad.displayRows.flatMap { $0 }.compactMap { $0 }.sorted(), Pad.rows.flatMap { $0 }.sorted()) // Every display row spans the pad's four columns, so gaps line up. XCTAssertEqual(Pad.displayRows.map(\.count), [4, 4, 4, 4]) - XCTAssertEqual(Pad.displayRows.first, [nil, 1, 0, nil]) + XCTAssertEqual(Pad.displayRows.first, [nil, 0, 1, nil]) XCTAssertEqual(Pad.displayRows.last, [nil, 10, 11, 12]) } } diff --git a/Tests/WLKitTests/StatusMapperTests.swift b/Tests/WLKitTests/StatusMapperTests.swift index 0df7c86..9db3b68 100644 --- a/Tests/WLKitTests/StatusMapperTests.swift +++ b/Tests/WLKitTests/StatusMapperTests.swift @@ -47,9 +47,9 @@ final class StatusMapperTests: XCTestCase { agent("working"), agent("blocked"), agent("idle"), ]) XCTAssertEqual(threads.count, 6, "one entry per agent key") - // The pad's top row is wired right to left, so reading order starts - // at key 1: the first agent lights the top-LEFT key. - XCTAssertEqual(threads.map(\.id), [1, 0, 2, 3, 4, 5], "keys in reading order") + // Every row runs left to right, so reading order is index order: the + // first agent lights key 0, the top-LEFT key. + XCTAssertEqual(threads.map(\.id), [0, 1, 2, 3, 4, 5], "keys in reading order") XCTAssertEqual(threads[0].color, 0xFFA000) XCTAssertEqual(threads[1].color, 0xFF2D2D) XCTAssertEqual(threads[2].color, 0x00C853) diff --git a/docs/hacking.md b/docs/hacking.md index e22b985..e87573c 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -375,23 +375,25 @@ to take a whole row's keycodes with it. ### Geometry -The matrix is `[2, 4, 4, 3]` and the key index runs row-major from 0. One -surprise: **the top row is wired right to left**, so index 0 is the top-**right** -key. +The matrix is `[2, 4, 4, 3]` and the key index runs row-major from 0, every +row left to right, on a four-column grid. The top row's two keys sit in the +middle columns, between the dial and the joystick; the bottom row starts one +column in. ``` - ┌─────────────┬─────────────┐ - │ 1 │ 0 │ row 0 <- reversed: 0 is on the RIGHT - ├──────┬──────┼──────┬──────┤ + ┌──────┬──────┐ + (dial) │ 0 │ 1 │ (joy) row 0 + ┌──────┼──────┼──────┼──────┐ │ 2 │ 3 │ 4 │ 5 │ row 1 ├──────┼──────┼──────┼──────┤ │ 6 │ 7 │ 8 │ 9 │ row 2 - ├──────┴──────┼──────┴──────┤ - │ 10 + 11 │ 12 │ row 3 - └─────────────┴─────────────┘ + └──────┼──────┴──────┼──────┤ + │ 10 + 11 │ 12 │ row 3 + └─────────────┴──────┘ ``` -Rows 1–3 run left to right in index order. Matrix positions 10 and 11 sit under +An earlier version of this guide had the top row wired right to left; on the +hardware, pressing the top-left key reports `AG00`. Matrix positions 10 and 11 sit under one wide keycap — the stock map leaves 11 as `KC_NONE`; bind and light both if you want the whole cap to glow evenly.