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
6 changes: 3 additions & 3 deletions Fluid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -613,10 +613,10 @@
};
7C3697872ED70F9C005874CE /* XCRemoteSwiftPackageReference "DynamicNotchKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/MrKai77/DynamicNotchKit";
repositoryURL = "https://github.com/altic-dev/DynamicNotchKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
branch = main;
kind = branch;
};
};
7C5AF1492F15041600DE21B0 /* XCRemoteSwiftPackageReference "mediaremote-adapter" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let package = Package(
.package(url: "https://github.com/mxcl/AppUpdater.git", from: "1.0.0"),
.package(url: "https://github.com/altic-dev/FluidAudio.git", branch: "B/cohere-coreml-asr"),
.package(url: "https://github.com/mxcl/PromiseKit", from: "6.0.0"),
.package(url: "https://github.com/MrKai77/DynamicNotchKit", from: "1.0.0"),
.package(url: "https://github.com/altic-dev/DynamicNotchKit.git", branch: "main"),
.package(url: "https://github.com/exPHAT/SwiftWhisper.git", branch: "master"),
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.0.0"),
],
Expand Down
202 changes: 124 additions & 78 deletions Sources/Fluid/ContentView.swift

Large diffs are not rendered by default.

121 changes: 117 additions & 4 deletions Sources/Fluid/Models/HotkeyShortcut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import Foundation
struct HotkeyShortcut: Codable, Equatable {
var keyCode: UInt16
var modifierFlags: NSEvent.ModifierFlags
enum CodingKeys: String, CodingKey { case keyCode, modifierFlagsRawValue }
var modifierKeyCodes: [UInt16]
enum CodingKeys: String, CodingKey { case keyCode, modifierFlagsRawValue, modifierKeyCodes }

var displayString: String {
let modifierKeyCodes = self.normalizedModifierKeyCodes
let modifierParts = modifierKeyCodes.compactMap(Self.keyCodeToString)
if !modifierParts.isEmpty {
return modifierParts.joined(separator: " + ")
}

var parts: [String] = []
if self.modifierFlags.contains(.function) { parts.append("🌐") }
if self.modifierFlags.contains(.command) { parts.append("⌘") }
Expand Down Expand Up @@ -96,29 +103,135 @@ struct HotkeyShortcut: Codable, Equatable {
default: return nil
}
}

init(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags, modifierKeyCodes: [UInt16] = []) {
let normalizedModifierKeyCodes = Self.normalizedModifierKeyCodes(from: modifierKeyCodes)
if !normalizedModifierKeyCodes.isEmpty {
self.modifierKeyCodes = normalizedModifierKeyCodes
self.keyCode = normalizedModifierKeyCodes.first ?? keyCode

let combinedFlags = normalizedModifierKeyCodes.reduce(into: NSEvent.ModifierFlags()) { flags, modifierKeyCode in
if let flag = Self.modifierFlag(forKeyCode: modifierKeyCode) {
flags.insert(flag)
}
}
if let triggerFlag = Self.modifierFlag(forKeyCode: self.keyCode) {
self.modifierFlags = combinedFlags.subtracting(triggerFlag)
} else {
self.modifierFlags = modifierFlags.intersection(Self.relevantModifierMask)
}
} else {
self.keyCode = keyCode
self.modifierFlags = modifierFlags
self.modifierKeyCodes = []
}
}
}

extension HotkeyShortcut {
private static let relevantModifierMask: NSEvent.ModifierFlags = [.function, .command, .option, .control, .shift]
static let relevantModifierMask: NSEvent.ModifierFlags = [.function, .command, .option, .control, .shift]

static func modifierFlag(forKeyCode keyCode: UInt16) -> NSEvent.ModifierFlags? {
switch keyCode {
case 63:
return .function
case 54, 55:
return .command
case 58, 61:
return .option
case 59, 62:
return .control
case 56, 60:
return .shift
default:
return nil
}
}

private static func modifierSortPriority(forKeyCode keyCode: UInt16) -> Int? {
switch keyCode {
case 63: return 0
case 55: return 1
case 54: return 2
case 58: return 3
case 61: return 4
case 59: return 5
case 62: return 6
case 56: return 7
case 60: return 8
default: return nil
}
}

static func normalizedModifierKeyCodes(from modifierKeyCodes: [UInt16]) -> [UInt16] {
let normalized = Array(Set(modifierKeyCodes)).compactMap { keyCode -> (UInt16, Int)? in
guard let priority = Self.modifierSortPriority(forKeyCode: keyCode) else { return nil }
return (keyCode, priority)
}
.sorted { lhs, rhs in
lhs.1 < rhs.1
}
.map(\.0)

return normalized
}

var relevantModifierFlags: NSEvent.ModifierFlags {
self.modifierFlags.intersection(Self.relevantModifierMask)
}

var normalizedModifierKeyCodes: [UInt16] {
let normalized = Self.normalizedModifierKeyCodes(from: self.modifierKeyCodes)
if !normalized.isEmpty { return normalized }

if self.modifierTriggerFlag != nil, self.relevantModifierFlags.isEmpty {
return [self.keyCode]
}

return []
}

var modifierTriggerFlag: NSEvent.ModifierFlags? {
Self.modifierFlag(forKeyCode: self.keyCode)
}

var isModifierOnlyShortcut: Bool {
self.modifierTriggerFlag != nil
}

var expectedModifierFlags: NSEvent.ModifierFlags? {
guard let triggerFlag = self.modifierTriggerFlag else { return nil }
return self.relevantModifierFlags.union(triggerFlag)
}

func matches(keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> Bool {
keyCode == self.keyCode && modifiers.intersection(Self.relevantModifierMask) == self.relevantModifierFlags
}

static func == (lhs: HotkeyShortcut, rhs: HotkeyShortcut) -> Bool {
let lhsModifierKeyCodes = lhs.normalizedModifierKeyCodes
let rhsModifierKeyCodes = rhs.normalizedModifierKeyCodes
if !lhsModifierKeyCodes.isEmpty, !rhsModifierKeyCodes.isEmpty {
return lhsModifierKeyCodes == rhsModifierKeyCodes
}

return lhs.keyCode == rhs.keyCode && lhs.relevantModifierFlags == rhs.relevantModifierFlags
}

init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.keyCode = try c.decode(UInt16.self, forKey: .keyCode)
let keyCode = try c.decode(UInt16.self, forKey: .keyCode)
let raw = try c.decode(UInt.self, forKey: .modifierFlagsRawValue)
self.modifierFlags = NSEvent.ModifierFlags(rawValue: raw)
let modifierKeyCodes = try c.decodeIfPresent([UInt16].self, forKey: .modifierKeyCodes) ?? []
self.init(keyCode: keyCode, modifierFlags: NSEvent.ModifierFlags(rawValue: raw), modifierKeyCodes: modifierKeyCodes)
}

func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(self.keyCode, forKey: .keyCode)
try c.encode(self.modifierFlags.rawValue, forKey: .modifierFlagsRawValue)
if !self.normalizedModifierKeyCodes.isEmpty {
try c.encode(self.normalizedModifierKeyCodes, forKey: .modifierKeyCodes)
}
}
}
6 changes: 3 additions & 3 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3626,16 +3626,16 @@ extension SettingsStore {
var displayName: String {
switch self {
case .standard:
return "Experimental Direct Typing"
return "Clipboard Free Insert"
case .reliablePaste:
return "Reliable Paste"
return "Clipboard Paste"
}
}

var description: String {
switch self {
case .standard:
return "Tries to avoid clipboard changes by typing directly when possible. Usually a bit slower, and may fail or behave inconsistently in some apps."
return "Tries to insert text without changing the clipboard. Usually a bit slower, and may fail or behave inconsistently in some apps."
case .reliablePaste:
return "Usually faster and works best across browsers and desktop apps. Uses a temporary clipboard paste, so clipboard history apps may briefly record dictated text."
}
Expand Down
Loading
Loading