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
5 changes: 5 additions & 0 deletions .changeset/26563231.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: patch
---

Refresh tile previews on every overlay show so live updates aren't masked by the previous capture
5 changes: 5 additions & 0 deletions .changeset/6e57fbf8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: minor
---

Search mode (cmd+F): filter tiles by substring match on app name + window title; return commits, esc / Cancel clears.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Requires macOS 14+.
| ⌘W | Close selected window |
| ⌘`delete` | Ignore / un-ignore selected window |
| ⌘Y | Toggle "show hidden" view |
| ⌘F | Search / filter visible windows (substring match on app + title) |
| ⌥`g`/`b`/`r`/`y`/`o`/`p` | Tag selected tile (green/blue/red/yellow/orange/purple) |
| ⌥`0` | Clear tag on selected tile |
| `esc` | Dismiss overlay |
Expand Down Expand Up @@ -55,7 +56,7 @@ Right-click the `⌘ ⌘` Dock icon and pick **Open Config…** — that opens `

Binding spec — modifier tokens: `cmd`, `shift`, `opt` (or `option`/`alt`), `ctrl`. Special keys: `esc`, `space`, `return`, `delete`, `left`, `right`, `up`, `down`. Anything else is a single character.

Actions: `pick`, `dismiss`, `move-left|right|up|down`, `swap-left|right|up|down`, `pick-1` … `pick-9`, `ignore`, `toggle-hidden`, `close`, `tag-green|blue|red|yellow|orange|purple|clear`.
Actions: `pick`, `dismiss`, `move-left|right|up|down`, `swap-left|right|up|down`, `pick-1` … `pick-9`, `ignore`, `toggle-hidden`, `close`, `search`, `tag-green|blue|red|yellow|orange|purple|clear`.

## Build

Expand Down
1 change: 1 addition & 0 deletions Sources/cmdcmd/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ struct Config: Codable {
("cmd+w", .close),
("cmd+delete", .ignore),
("cmd+y", .toggleHidden),
("cmd+f", .search),
("opt+g", .tagGreen),
("opt+b", .tagBlue),
("opt+r", .tagRed),
Expand Down
3 changes: 3 additions & 0 deletions Sources/cmdcmd/Keymap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum Action: String, Codable, Hashable {
case tagOrange = "tag-orange"
case tagPurple = "tag-purple"
case tagClear = "tag-clear"
case search
case pick1 = "pick-1"
case pick2 = "pick-2"
case pick3 = "pick-3"
Expand Down Expand Up @@ -53,6 +54,7 @@ enum Action: String, Codable, Hashable {
case .tagOrange: return "Tag orange"
case .tagPurple: return "Tag purple"
case .tagClear: return "Clear tag"
case .search: return "Search / filter visible windows"
case .pick1: return "Pick tile 1"
case .pick2: return "Pick tile 2"
case .pick3: return "Pick tile 3"
Expand Down Expand Up @@ -135,6 +137,7 @@ final class Keymap {
"cmd+delete": .ignore,
"cmd+y": .toggleHidden,
"cmd+w": .close,
"cmd+f": .search,
"opt+g": .tagGreen, "opt+b": .tagBlue, "opt+r": .tagRed, "opt+y": .tagYellow,
"opt+o": .tagOrange, "opt+p": .tagPurple, "opt+0": .tagClear,
"1": .pick1, "2": .pick2, "3": .pick3, "4": .pick4, "5": .pick5,
Expand Down
2 changes: 1 addition & 1 deletion Sources/cmdcmd/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ enum Log {
FileManager.default.createFile(atPath: path, contents: nil)
}
let h = try? FileHandle(forWritingTo: url)
try? h?.seekToEnd()
_ = try? h?.seekToEnd()
return h
}()

Expand Down
100 changes: 88 additions & 12 deletions Sources/cmdcmd/Overlay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ final class Overlay {
private var appActivationObserver: NSObjectProtocol?
private var activityTimer: Timer?
private let hint = HintPill()
private let search = SearchField()
private var searchQuery: String = ""
private var searching: Bool = false

private var cachedShareable: SCShareableContent?
private var cachedShareableAt: CFAbsoluteTime = 0
Expand Down Expand Up @@ -492,15 +495,12 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B
updateSelection()
}
let live = config.livePreviewsEnabled
let delay: TimeInterval = config.animations ? Self.pickDuration + 0.05 : 0
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [ordered] in
Task {
await withTaskGroup(of: Void.self) { group in
for t in ordered {
group.addTask {
await t.snapshot()
if live { await t.start() }
}
Task {
await withTaskGroup(of: Void.self) { group in
for t in ordered {
group.addTask {
await t.snapshot()
if live { await t.start() }
}
}
}
Expand All @@ -509,10 +509,14 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B

private func rebuildDisplayed() {
let ignored = ignoredKeys
let displayed = allTiles.filter { showIgnored ? true : !ignored.contains($0.ignoreKey) }
let baseDisplayed = allTiles.filter { showIgnored ? true : !ignored.contains($0.ignoreKey) }
let displayed = baseDisplayed.filter { Self.matches(tile: $0, query: searchQuery) }
let visibleSet = Set(displayed.map { ObjectIdentifier($0) })
for t in allTiles {
let isIgnored = ignored.contains(t.ignoreKey)
t.layer.isHidden = showIgnored ? false : isIgnored
let inSearch = visibleSet.contains(ObjectIdentifier(t))
let hiddenByIgnore = showIgnored ? false : isIgnored
t.layer.isHidden = hiddenByIgnore || !inSearch
t.layer.opacity = (showIgnored && isIgnored) ? 0.3 : 1.0
t.setNumber(nil)
t.tintColorName = paneColors[CGWindowID(t.scWindow.windowID)]
Expand All @@ -529,6 +533,64 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B
updateSelection()
}

private static func matches(tile: Tile, query: String) -> Bool {
let q = query.trimmingCharacters(in: .whitespaces)
if q.isEmpty { return true }
let app = tile.scWindow.owningApplication?.applicationName ?? ""
let title = tile.scWindow.title ?? ""
let haystack = app + " " + title
return haystack.localizedCaseInsensitiveContains(q)
}

private func enterSearch() {
guard let win = window, let host = win.contentView else { return }
searching = true
search.onChange = { [weak self] q in self?.searchQueryChanged(q) }
search.onCommit = { [weak self] in self?.dispatch(.pick) }
search.onCancel = { [weak self] in self?.commitSearch() }
search.onArrow = { [weak self] d in self?.dispatchSearchArrow(d) }
search.show(in: host, query: searchQuery)
findSearchTextField(in: host)?.onCmdF = { [weak self] in self?.commitSearch() }
}

private func dispatchSearchArrow(_ d: SearchField.ArrowDirection) {
switch d {
case .left: dispatch(.moveLeft)
case .right: dispatch(.moveRight)
case .up: dispatch(.moveUp)
case .down: dispatch(.moveDown)
}
}

private func findSearchTextField(in view: NSView) -> SearchTextField? {
for sub in view.subviews {
if let f = sub as? SearchTextField { return f }
if let nested = findSearchTextField(in: sub) { return nested }
}
return nil
}

private func commitSearch() {
searching = false
search.hide()
if let v = view { window?.makeFirstResponder(v) }
}

private func cancelSearch() {
searching = false
searchQuery = ""
search.hide()
rebuildDisplayed()
layoutTilesAnimated()
if let v = view { window?.makeFirstResponder(v) }
}

private func searchQueryChanged(_ q: String) {
searchQuery = q
rebuildDisplayed()
layoutTilesAnimated()
}

private func tagSelectedColor(_ name: String?) {
guard tiles.indices.contains(selectedIndex) else { return }
let id = CGWindowID(tiles[selectedIndex].scWindow.windowID)
Expand Down Expand Up @@ -575,7 +637,10 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B
switch action {
case .pick: pick()
case .dismiss:
if showIgnored { toggleShowIgnored() } else { dismiss() }
if showIgnored { toggleShowIgnored() }
else if !searchQuery.isEmpty { cancelSearch() }
else { dismiss() }
case .search: enterSearch()
case .moveLeft: move(dx: -1, dy: 0)
case .moveRight: move(dx: 1, dy: 0)
case .moveUp: move(dx: 0, dy: -1)
Expand Down Expand Up @@ -683,6 +748,9 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B
selectedIndex = 0
showIgnored = false
lastLetterJump = nil
searching = false
searchQuery = ""
search.hide()
view?.resetMomentaryPeek()
hint.hide()
Task(priority: .utility) {
Expand Down Expand Up @@ -966,6 +1034,10 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B
if i == selectedIndex {
t.layer.zPosition = 1
t.setFrame(target)
// Fade the accent border + blue glow during the zoom: at full
// size they dominate the screen and read as a flash of color.
t.layer.borderWidth = 0
t.layer.shadowOpacity = 0
} else {
t.layer.opacity = 0
}
Expand All @@ -985,6 +1057,10 @@ private static func windowMostlyOn(displayBounds: CGRect, window: SCWindow) -> B
if i < savedFrames.count { t.setFrame(savedFrames[i]) }
t.layer.zPosition = 0
t.layer.opacity = 1
if i == selectedIndex, t.highlight == .subtle {
t.layer.borderWidth = 3
t.layer.shadowOpacity = 0.6
}
}
CATransaction.commit()
resumeFrames(after: 0.12)
Expand Down
Loading
Loading