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
29 changes: 28 additions & 1 deletion Sources/KeyPathAppKit/Services/Packs/PackRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public enum PackRegistry {
autoShiftSymbols,
numpadLayer,
symbolLayer,
funLayer
funLayer,
launcher
]

/// Look up a pack by id. Returns nil if unknown.
Expand Down Expand Up @@ -387,4 +388,30 @@ public enum PackRegistry {
bindings: [],
associatedCollectionID: RuleCollectionIdentifier.funLayer
)

// MARK: - Pack 13: Quick Launcher

/// Collection-backed pack over `launcher`. Hold the Hyper key to enter
/// the launcher layer, then press a single key to launch an app or
/// open a URL. Pack Detail embeds the same `LauncherCollectionView`
/// the Rules tab uses so you can pick keys, drop apps, and switch
/// between Hold-Hyper and Leader→L activation right from the pack.
///
/// Unlike the nav-layer packs (Numpad, Symbol, Fun, Mission Control,
/// etc.), Launcher activates directly from the base layer via Hyper —
/// so it works standalone without a Leader pack on.
public static let launcher = Pack(
id: "com.keypath.pack.quick-launcher",
version: "1.0.0",
name: "Quick Launcher",
tagline: "Hold Hyper, press a key to launch an app or website",
shortDescription:
"Map any key to launch an app (Slack, Cursor, Figma) or open a URL (gmail.com, calendar.google.com). Tap Hyper + the key. Add and edit mappings inline — drag an app onto a key, or pick from your browser history.",
longDescription: "",
category: "Productivity",
iconSymbol: "arrow.up.forward.app",
quickSettings: [],
bindings: [],
associatedCollectionID: RuleCollectionIdentifier.launcher
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align Quick Launcher install state with enabled collection

com.keypath.pack.quick-launcher is now tied to RuleCollectionIdentifier.launcher, but launcher is a system-default collection that starts enabled, while Gallery/Pack Detail install state is read from InstalledPackTracker records only. For existing users (and fresh installs before this pack is explicitly toggled), this makes Quick Launcher appear OFF even though the launcher layer is already active, and the first edit/toggle performs an unnecessary re-install/reload. Please derive pack state from collection enablement (or seed tracker state) for this collection-backed pack.

Useful? React with 👍 / 👎.

)
}
44 changes: 44 additions & 0 deletions Sources/KeyPathAppKit/UI/Gallery/PackDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ struct PackDetailView: View {
/// Currently-selected layer-preset id (for the Symbol/Fun packs).
@State private var selectedLayerPresetId: String?

/// Local state for the embedded Quick Launcher editor.
@State private var launcherConfig: LauncherGridConfig = .defaultConfig

/// Help sheet presentation (Home Row Mods only — mirrors the Rules tab's
/// `?` button that opens the HRM markdown help).
@State private var showingHomeRowModsHelp = false
Expand Down Expand Up @@ -488,6 +491,18 @@ struct PackDetailView: View {
)
.id(selectedLayerPresetId ?? "")
}
} else if let launcherCollection = associatedLauncherCollection {
// Quick Launcher — keyboard visualization + drawer of mappings,
// activation-mode picker. Same view Rules tab uses; edits flow
// through the same `updateLauncherConfig` VM hook.
embeddedEditor(horizontalPadding: 12) {
LauncherCollectionView(
config: $launcherConfig,
onConfigChanged: { newConfig in
Task { await applyLauncherEdit(newConfig, collectionID: launcherCollection.id) }
}
)
}
} else if let collection = liveAssociatedCollection,
collection.id == RuleCollectionIdentifier.windowSnapping {
// Window Snapping has a custom visual editor in Rules — convention
Expand Down Expand Up @@ -587,6 +602,15 @@ struct PackDetailView: View {
return collection
}

/// Collection backing a launcher pack (Quick Launcher). Matches the
/// `.launcherGrid` configuration case.
private var associatedLauncherCollection: RuleCollection? {
guard let collection = liveAssociatedCollection,
case .launcherGrid = collection.configuration
else { return nil }
return collection
}

/// True if this pack's bindings line up with the Home Row Mods
/// collection's input set — in which case we render that collection's
/// interactive keyboard + modifier controls instead of the generic
Expand Down Expand Up @@ -777,6 +801,9 @@ struct PackDetailView: View {
if let livePreset = liveLayerPresetId() {
selectedLayerPresetId = livePreset
}
if let liveLauncher = liveLauncherConfig() {
launcherConfig = liveLauncher
}
}
}

Expand All @@ -794,6 +821,13 @@ struct PackDetailView: View {
return cfg.selectedPresetId
}

private func liveLauncherConfig() -> LauncherGridConfig? {
guard let collection = associatedLauncherCollection,
case let .launcherGrid(cfg) = collection.configuration
else { return nil }
return cfg
}

private func liveSingleKeySelection() async -> String? {
guard let collectionID = pack.associatedCollectionID else { return nil }
let collections = await kanataManager.underlyingManager
Expand Down Expand Up @@ -856,6 +890,16 @@ struct PackDetailView: View {
await kanataManager.updateCollectionLayerPreset(collectionID, presetId: presetId)
}

/// Mirror of `applyHomeRowEdit` for the Quick Launcher pack. Persists
/// activation-mode + key mappings via the same VM hook Rules uses.
private func applyLauncherEdit(_ newConfig: LauncherGridConfig, collectionID: UUID) async {
launcherConfig = newConfig
if !isInstalled {
await install(skipFinalReload: true)
}
await kanataManager.updateLauncherConfig(collectionID, config: newConfig)
}

/// Supplies the same layer list Rules uses when rendering the Home Row
/// Mods editor, so hold-to-layer bindings resolve correctly inside
/// Pack Detail.
Expand Down
1 change: 1 addition & 0 deletions Tests/KeyPathTests/PackRegistryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class PackRegistryTests: XCTestCase {
XCTAssertTrue(ids.contains("com.keypath.pack.numpad-layer"))
XCTAssertTrue(ids.contains("com.keypath.pack.symbol-layer"))
XCTAssertTrue(ids.contains("com.keypath.pack.fun-layer"))
XCTAssertTrue(ids.contains("com.keypath.pack.quick-launcher"))
}

func testCollectionBackedPacksPointAtRealCollections() {
Expand Down
Loading