Skip to content
This repository was archived by the owner on Oct 17, 2026. It is now read-only.
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ All notable changes to this project are documented here. The format follows [Kee
- System Settings topics and keywords from System Settings' own search terms, so "dark mode", "low power", or "firewall" find the right pane, shown with the pane's icon.
- A result chosen for a query also leans shorter and longer starts of that query its way, so you can type less next time.
- Unit conversions such as "10 km to mi", "70 f in c", or "5 GB in MB", offline, with Return to copy.
- Moving the pointer over a result selects it, and apps, files, and folders can be dragged out of the panel.

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Spotlight owns ⌘Space. To give it to Common Command, turn off **Show Spotlight
| ⌘1 … ⌘9 | Open a result directly; hold ⌘ to see the numbers |
| ⎋ | Close |

The pointer works too: click a result to open it, move over one to select it, or drag an app, file, or folder out into the Dock, Finder, or a document. Rows that shift under a still pointer while you type never take the selection.

What a query finds, in order:

1. **Links.** `github.com/apple`, `https://…`, and `localhost:3000` open in your browser. Only well-known endings count, so `notes.md` and `Info.plist` stay file searches.
Expand Down
11 changes: 10 additions & 1 deletion Sources/CommonCommand/PanelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SwiftUI
final class CommandPanel: NSPanel {
var onKeyEquivalent: ((NSEvent) -> Bool)?
var onModifiersChange: ((NSEvent.ModifierFlags) -> Void)?
var onPointerMove: (() -> Void)?

init() {
super.init(
Expand All @@ -20,6 +21,7 @@ final class CommandPanel: NSPanel {
backgroundColor = .clear
hasShadow = true
animationBehavior = .none
acceptsMouseMovedEvents = true
isReleasedWhenClosed = false
// On the current Space, including over full-screen apps, and never in ⌘` or Mission Control.
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient, .ignoresCycle]
Expand All @@ -33,7 +35,12 @@ final class CommandPanel: NSPanel {
}

override func sendEvent(_ event: NSEvent) {
if event.type == .flagsChanged { onModifiersChange?(event.modifierFlags) }
switch event.type {
case .flagsChanged: onModifiersChange?(event.modifierFlags)
// Before the event reaches the rows' hover tracking, so they know the pointer really moved.
case .mouseMoved: onPointerMove?()
default: break
}
super.sendEvent(event)
}
}
Expand Down Expand Up @@ -61,6 +68,7 @@ final class PanelController: NSObject, NSWindowDelegate {
panel.onModifiersChange = { [weak self] flags in
self?.model.showsShortcuts = flags.intersection(.deviceIndependentFlagsMask) == .command
}
panel.onPointerMove = { [weak self] in self?.model.pointerMoved = true }
model.onResultsChange = { [weak self] in self?.resize() }
library.onChange = { [weak self] in
guard let self, panel.isVisible else { return }
Expand All @@ -80,6 +88,7 @@ final class PanelController: NSObject, NSWindowDelegate {
buildContentIfNeeded()
panel.appearance = preferences.theme.appearance
model.showsShortcuts = false
model.pointerMoved = false
model.update()
position()
panel.makeKeyAndOrderFront(nil)
Expand Down
15 changes: 15 additions & 0 deletions Sources/CommonCommand/PanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ private struct ResultList: View {
)
.id(index)
.onTapGesture { choose(index) }
.onContinuousHover { phase in
if case .active = phase { model.hover(index) }
}
.dragsFile(ranked.item.fileURL)
}
}
.padding(size.listPadding)
Expand All @@ -72,6 +76,17 @@ private struct ResultList: View {
}
}

extension View {
/// Apps, files, and folders drag out like they do from Finder: into the Dock, a folder, or a document.
@ViewBuilder func dragsFile(_ url: URL?) -> some View {
if let url {
onDrag { NSItemProvider(object: url as NSURL) }
} else {
self
}
}
}

private struct ResultRow: View {
let ranked: Ranked
let size: PanelSize
Expand Down
11 changes: 11 additions & 0 deletions Sources/CommonCommand/SearchModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ final class SearchModel {
var query = "" {
didSet {
guard query != oldValue else { return }
pointerMoved = false
update()
}
}
private(set) var results: [Ranked] = []
var selection = 0
/// True while ⌘ is held, so each row shows its ⌘1–⌘9 shortcut.
var showsShortcuts = false
/// Whether the pointer has really moved since the last key. Rows change under a still pointer while
/// you type, and that mustn't take the selection, or Return would open whatever happened to land there.
@ObservationIgnored var pointerMoved = false

@ObservationIgnored var onResultsChange: (() -> Void)?
@ObservationIgnored private let library: Library
Expand All @@ -32,10 +36,17 @@ final class SearchModel {
var selected: Item? { results.indices.contains(selection) ? results[selection].item : nil }

func move(_ delta: Int) {
pointerMoved = false
guard !results.isEmpty else { return }
selection = (selection + delta + results.count) % results.count
}

/// The pointer is over row `index`; it takes the selection only if the pointer got there by moving.
func hover(_ index: Int) {
guard pointerMoved, results.indices.contains(index), selection != index else { return }
selection = index
}

/// Recomputes from what's already known, then asks Spotlight for files. A new query selects the
/// top result. Results that arrive later, such as files or a refreshed app list, keep a selection
/// you moved, but otherwise the top result stays selected even if it changed.
Expand Down
Loading