Skip to content

Commit 853901e

Browse files
make ItemFilter injectable
1 parent 4e1b1b8 commit 853901e

2 files changed

Lines changed: 42 additions & 25 deletions

File tree

Sources/FloatingFilter/FilterInteractor.swift

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ class FilterInteractor {
1212

1313
private var state: State {
1414
didSet {
15-
self.view.showItems(state.filteredItems)
15+
self.view.showItems(state.filteredItems(filter: filter))
1616
}
1717
}
1818

1919
let view: FilteredItemView
20+
let filter: ItemFilter
2021

21-
init(view: FilteredItemView) {
22+
init(
23+
view: FilteredItemView,
24+
filter: @escaping ItemFilter
25+
) {
2226
self.view = view
27+
self.filter = filter
2328
self.state = State(allItems: [], filterPhrase: "")
2429
}
2530
}
@@ -41,28 +46,8 @@ extension FilterInteractor: FilterChangeDelegate {
4146
}
4247

4348
extension FilterInteractor.State {
44-
fileprivate var filteredItems: [Item] {
49+
fileprivate func filteredItems(filter: ItemFilter) -> [Item] {
4550
guard !filterPhrase.isEmpty else { return allItems }
46-
return allItems.sortedByNormalizedFuzzyMatch(pattern: self.filterPhrase)
47-
}
48-
}
49-
50-
extension Sequence where Iterator.Element == Item {
51-
fileprivate func sortedByNormalizedFuzzyMatch(pattern: String) -> [Element] {
52-
// These magic numbers are totally experimental
53-
let fuzziness = 0.3
54-
let threshold = 0.4
55-
56-
return self
57-
.map { (element: $0, score: $0.title.score(word: pattern, fuzziness: fuzziness)) }
58-
.filter { $0.score > threshold }
59-
.sorted(by: { $0.score > $1.score })
60-
.map { $0.element }
61-
}
62-
}
63-
64-
extension Item {
65-
fileprivate var normalizedTitle: String {
66-
return self.title.lowercased()
51+
return filter(filterPhrase, allItems)
6752
}
6853
}

Sources/FloatingFilter/FloatingFilterModule.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,44 @@ import AppKit
66
private var windowHoldingService = WindowHoldingService()
77

88
public typealias SelectionCallback = (_ selectedItems: [Item]) -> Void
9+
public typealias ItemFilter = (_ needle: String, _ haystack: [Item]) -> [Item]
910

1011
public struct FloatingFilterModule {
12+
/// Uses the default filter that scores all ``Item``s by `pattern` fuzzily, discards matches below a certain
13+
/// threshold, and sorts by score.
14+
public static func defaultFuzzyFilter(
15+
fuzziness: Double = 0.3,
16+
threshold: Double = 0.4
17+
) -> ItemFilter {
18+
return { needle, haystack in
19+
let needle = needle.localizedLowercase
20+
21+
return haystack
22+
.map { (element: $0, score: $0.title.localizedLowercase.score(word: needle, fuzziness: fuzziness)) }
23+
.filter { $0.score > threshold }
24+
.sorted(by: { $0.score > $1.score })
25+
.map { $0.element }
26+
}
27+
}
28+
1129
private init() { }
1230

31+
/// - Parameters:
32+
/// - items: Filter-able items, sorted, to show in the floating filter window.
33+
/// - filterPlaceholderText: Placeholder for the FloatingFilter text field
34+
/// - windowLevel: Level to show the filter window on. Default `.floating` is intended for use
35+
/// as a utility panel.
36+
/// - closeWhenLosingFocus: Whether the filter should disappear when users e.g. activate another app
37+
/// before completing the operation. Defaults to `true` to be used as a temporary utility.
38+
/// - filter: Narrows down all filterable `items` to find `needle`. The default uses a fuzzy string
39+
/// matching algorithm.
40+
/// - selectionCallback: Output port for confirmed selections in the filter window.
1341
public static func showFilterWindow(
1442
items: [Item],
1543
filterPlaceholderText: String = NSLocalizedString("Filter Items", comment: "Placeholder for the FloatingFilter text field"),
1644
windowLevel: NSWindow.Level = .floating,
1745
closeWhenLosingFocus: Bool = true,
46+
filter: @escaping ItemFilter = FloatingFilterModule.defaultFuzzyFilter(),
1847
selection selectionCallback: @escaping SelectionCallback
1948
) {
2049
let windowController = FilterWindowController()
@@ -34,7 +63,10 @@ public struct FloatingFilterModule {
3463
window.makeKeyAndOrderFront(nil)
3564
}
3665

37-
let interactor = FilterInteractor(view: windowController)
66+
let interactor = FilterInteractor(
67+
view: windowController,
68+
filter: filter
69+
)
3870
interactor.showItems(items)
3971

4072
windowController.filterChangeDelegate = interactor

0 commit comments

Comments
 (0)