Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Apps/Keyty/Sources/Keyty/Domain/Events/InputEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,4 @@ public enum InputEvent {
}
}

public var displayString: String {
EventTransformer.shared.transform(self)
}
}
3 changes: 0 additions & 3 deletions Apps/Keyty/Sources/Keyty/Domain/Events/MediaKeyEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,4 @@ public struct MediaKeyEvent {
.mediaKey(self)
}

public var displayString: String {
EventTransformer.shared.transform(inputEvent)
}
}
4 changes: 0 additions & 4 deletions Apps/Keyty/Sources/Keyty/Domain/Events/MouseEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ public struct MouseEvent {
.mouse(self)
}

public var displayString: String {
EventTransformer.shared.transform(inputEvent)
}

public var hasZeroScrollDelta: Bool {
self.scrollingDeltaX == 0.0 && self.scrollingDeltaY == 0.0
}
Expand Down
14 changes: 10 additions & 4 deletions Apps/Keyty/Sources/Keyty/Domain/Events/StandardKeyEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ public struct StandardKeyEvent {
public let characters: String?
public let charactersIgnoringModifiers: String?

/// A key identified only by its code, for previews and samples that have no
/// event behind them.
public init(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags = []) {
self.type = .keyDown
self.modifierFlags = modifierFlags
self.keyCode = keyCode
self.characters = nil
self.charactersIgnoringModifiers = nil
}

public init(nsEvent event: NSEvent) {
self.type = event.type
self.modifierFlags = event.modifierFlags
Expand All @@ -27,10 +37,6 @@ public struct StandardKeyEvent {
.keystroke(self)
}

public var displayString: String {
EventTransformer.shared.transform(self.inputEvent)
}

public var isModified: Bool {
!self.modifierFlags.intersection([.control, .command, .option, .shift]).isEmpty
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ enum KeyboardGlyphCatalog {

static let tab = UnicodeToken.tab.string

/// Glyphs that can prefix a chord in display strings.
static let modifierSymbols: [String] = KeyboardModifierKey.Kind.allCases.map(\.glyph)

/// Physical key codes for left and right command, shift, option, and control keys.
static let modifierKeyCodes: Set<KeyboardKeyCode> = Set(KeyboardModifierKey.Kind.allCases.flatMap(\.keyCodes))

Expand All @@ -39,26 +36,4 @@ enum KeyboardGlyphCatalog {
key.displayText
}

/// Returns the normalized display symbol we show for fallback display text.
static func displaySymbol(for text: String) -> String {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
switch trimmed.uppercased() {
case "ESC":
return UnicodeToken.escape.string
case "DELETE", "BACKSPACE":
return UnicodeToken.delete.string
case "DEL", "FORWARD DELETE":
return UnicodeToken.forwardDelete.string
case "RETURN":
return UnicodeToken.returnKey.string
case "ENTER":
return UnicodeToken.keypadEnter.string
case "TAB":
return tab
case "SPACE":
return UnicodeToken.visibleSpace.string
default:
return trimmed.uppercased()
}
}
}
56 changes: 56 additions & 0 deletions Apps/Keyty/Sources/Keyty/Domain/Models/EventLegend.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// EventLegend.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import Foundation

/// The resolved visual form of a single input event.
///
/// Names what to draw without deciding how large to draw it: every case
/// identifies a resource, and renderers pick their own sizes, fonts, and colors.
struct EventLegend: Equatable {
/// Modifiers held for this event (in canonical display order)
let modifiers: [KeyboardModifierKey.Kind]

/// How the key itself should be drawn.
let kind: Kind

/// The key's textual form, always populated so it can stand in when `kind` cannot be rendered.
/// (e.g. the "tab" key will store `⇥` here).
let text: String

/// Secondary label for keycap styles that pair a symbol with a name.
/// (e.g. the "tab" key will store "tab" here).
let label: String?

init(
modifiers: [KeyboardModifierKey.Kind] = [],
kind: Kind = .text,
text: String,
label: String? = nil
) {
self.modifiers = modifiers
self.kind = kind
self.text = text
self.label = label
}
}

extension EventLegend {
/// The three ways a legend can be drawn, matching the branches every
/// keycap renderer already has.
enum Kind: Equatable {
/// Draw `text`.
case text

/// Draw the named SF Symbol.
case symbol(String)

/// Draw the icon for this mouse action.
case mouseIcon(MouseEvent.Kind)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,13 @@ extension KeyboardSettingsPane.PreviewGroup {
[
.key(
keyCode: KeyboardKeyCode.a.rawValue,
displayString: "A"
legend: Self.previewLegend(for: .a)
)
],
[
.key(
keyCode: KeyboardKeyCode.space.rawValue,
displayString: "Space"
legend: Self.previewLegend(for: .space)
)
],
]
Expand All @@ -129,7 +129,7 @@ extension KeyboardSettingsPane.PreviewGroup {
),
.key(
keyCode: 0x28,
displayString: "K"
legend: Self.previewLegend(for: .k)
),
],
[
Expand All @@ -138,7 +138,7 @@ extension KeyboardSettingsPane.PreviewGroup {
),
.key(
keyCode: KeyboardKeyCode.escape.rawValue,
displayString: KeyboardGlyphCatalog.symbol(for: KeyboardSpecialKey.escape)
legend: Self.previewLegend(for: .escape)
),
],
]
Expand All @@ -152,13 +152,13 @@ extension KeyboardSettingsPane.PreviewGroup {
),
.key(
keyCode: KeyboardKeyCode.tab.rawValue,
displayString: KeyboardGlyphCatalog.tab
legend: Self.previewLegend(for: .tab)
)
],
[
.key(
keyCode: KeyboardKeyCode.returnKey.rawValue,
displayString: KeyboardGlyphCatalog.symbol(for: KeyboardSpecialKey.returnKey)
legend: Self.previewLegend(for: .returnKey)
)
],
]
Expand Down Expand Up @@ -217,3 +217,11 @@ extension KeyboardSettingsPane {
var id: String { "\(self.category.rawValue)-\(self.variant)" }
}
}

// MARK: - Preview Legends
private extension KeyboardSettingsPane.PreviewGroup {
static func previewLegend(for keyCode: KeyboardKeyCode) -> EventLegend {
EventLegendResolver(keyboardLayout: KeyboardInputSourceManager.shared.currentInputSource)
.legend(forKeyCode: keyCode)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import AppKit
import Carbon
import Combine

final class KeyboardVisualizer {
Expand All @@ -29,6 +30,9 @@ final class KeyboardVisualizer {
private let visualizerSettings: KeyboardVisualizerSettings
private let visualizerWindow: KeyboardVisualizerWindow
private let eventCoordinator = KeycapEventCoordinator<KeyboardVisualizerGroupView, KeycapItem>()

// Rebuilt only when the selected keyboard layout changes.
private var cachedLegendResolver: (source: TISInputSource, resolver: EventLegendResolver)?
private var cancellables = Set<AnyCancellable>()
private var currentModifierFlags: NSEvent.ModifierFlags = []
private var lastModifierFlags: NSEvent.ModifierFlags = []
Expand Down Expand Up @@ -141,7 +145,11 @@ final class KeyboardVisualizer {
return
}

let items = KeycapItemFactory.keycapItems(for: keystroke, palette: self.visualizerSettings.palette)
let items = KeycapItemFactory.keycapItems(
for: keystroke,
legend: self.legendResolver.legend(for: .keystroke(keystroke)),
palette: self.visualizerSettings.palette
)
guard !items.isEmpty else { return }

self.prepareForNextContentEvent()
Expand Down Expand Up @@ -239,3 +247,17 @@ final class KeyboardVisualizer {
self.currentModifierFlags.intersection(Self.trackedModifierFlags)
}
}

// MARK: - Legend Resolution
private extension KeyboardVisualizer {
var legendResolver: EventLegendResolver {
let source = KeyboardInputSourceManager.shared.currentInputSource
if let cached = self.cachedLegendResolver, cached.source === source {
return cached.resolver
}

let resolver = EventLegendResolver(keyboardLayout: source)
self.cachedLegendResolver = (source, resolver)
return resolver
}
}
Loading
Loading