Skip to content

Commit 9a8eebb

Browse files
committed
Add Slack workspace and sidebar switching
1 parent 797d6ba commit 9a8eebb

4 files changed

Lines changed: 725 additions & 31 deletions

File tree

Sources/FFWF/ContentView.swift

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ struct ContentView: View {
6161
tabPrefix = "tab"
6262
}
6363
announcement = "\(tabPrefix) \(title), \(item.ownerName)"
64+
} else if item.isSlackItem {
65+
let prefix: String
66+
switch item.kind {
67+
case .slackWorkspace:
68+
prefix = "workspace"
69+
case .slackChannel:
70+
prefix = "channel"
71+
case .slackDM:
72+
prefix = "dm"
73+
case .window, .terminalTab, .chromeTab:
74+
prefix = "item"
75+
}
76+
announcement = "\(prefix) \(title), Slack"
6477
} else {
6578
let app = item.title.isEmpty ? "" : ", \(item.ownerName)"
6679
announcement = "\(title)\(app)"
@@ -78,14 +91,14 @@ struct ContentView: View {
7891
var body: some View {
7992
VStack(spacing: 0) {
8093
// Search field
81-
TextField("Search windows and tabs...", text: $searchQuery)
94+
TextField("Search windows, tabs, and Slack...", text: $searchQuery)
8295
.textFieldStyle(.plain)
8396
.font(.system(size: 18))
8497
.padding(12)
8598
.background(Color(NSColor.controlBackgroundColor))
8699
.focused($isSearchFocused)
87-
.accessibilityLabel("Search for windows and tabs")
88-
.accessibilityHint("Type to filter windows, tabs, and application names")
100+
.accessibilityLabel("Search for windows, tabs, and Slack items")
101+
.accessibilityHint("Type to filter windows, tabs, Slack workspaces, channels, direct messages, and application names")
89102
.accessibilityValue(searchQuery.isEmpty ? "Empty" : searchQuery)
90103
.onSubmit {
91104
// If in history mode, select the highlighted history item
@@ -309,7 +322,19 @@ struct WindowRow: View {
309322
var accessibilityDescription: String {
310323
let title = window.title.isEmpty ? window.ownerName : window.title
311324
let app = window.title.isEmpty ? "" : ", \(window.ownerName)"
312-
let role = window.isTab ? "Tab" : "Window"
325+
let role: String
326+
switch window.kind {
327+
case .terminalTab, .chromeTab:
328+
role = "Tab"
329+
case .slackWorkspace:
330+
role = "Workspace"
331+
case .slackChannel:
332+
role = "Channel"
333+
case .slackDM:
334+
role = "Direct message"
335+
case .window:
336+
role = "Window"
337+
}
313338
let position = "\(role) \(index) of \(total)"
314339
let state = isSelected ? ", selected" : ""
315340
if window.isTab {
@@ -322,6 +347,21 @@ struct WindowRow: View {
322347
let subtitle = window.subtitle.map { ", \($0)" } ?? ""
323348
return "\(tabPrefix) \(title)\(subtitle)\(app). \(position)\(state)"
324349
}
350+
if window.isSlackItem {
351+
let prefix: String
352+
switch window.kind {
353+
case .slackWorkspace:
354+
prefix = "workspace"
355+
case .slackChannel:
356+
prefix = "channel"
357+
case .slackDM:
358+
prefix = "dm"
359+
case .window, .terminalTab, .chromeTab:
360+
prefix = "item"
361+
}
362+
let subtitle = window.subtitle.map { ", \($0)" } ?? ""
363+
return "\(prefix) \(title)\(subtitle), Slack. \(position)\(state)"
364+
}
325365
return "\(title)\(app). \(position)\(state)"
326366
}
327367

@@ -337,8 +377,8 @@ struct WindowRow: View {
337377

338378
VStack(alignment: .leading, spacing: 2) {
339379
HStack(spacing: 8) {
340-
if window.isTab {
341-
Text("TAB")
380+
if let badgeText = rowBadgeText {
381+
Text(badgeText)
342382
.font(.system(size: 10, weight: .semibold))
343383
.foregroundColor(.accentColor)
344384
.padding(.horizontal, 6)
@@ -371,7 +411,30 @@ struct WindowRow: View {
371411
.cornerRadius(4)
372412
.accessibilityElement(children: .combine)
373413
.accessibilityLabel(accessibilityDescription)
374-
.accessibilityHint(window.isTab ? "Press Enter to switch to this tab" : "Press Enter to switch to this window")
414+
.accessibilityHint(accessibilityHintText)
375415
.accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : [.isButton])
376416
}
417+
418+
private var rowBadgeText: String? {
419+
if window.isTab {
420+
return "TAB"
421+
}
422+
423+
return window.slackBadgeText
424+
}
425+
426+
private var accessibilityHintText: String {
427+
switch window.kind {
428+
case .terminalTab, .chromeTab:
429+
return "Press Enter to switch to this tab"
430+
case .slackWorkspace:
431+
return "Press Enter to switch to this Slack workspace"
432+
case .slackChannel:
433+
return "Press Enter to open this Slack channel"
434+
case .slackDM:
435+
return "Press Enter to open this Slack direct message"
436+
case .window:
437+
return "Press Enter to switch to this window"
438+
}
439+
}
377440
}

Sources/FFWF/FuzzyMatcher.swift

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,12 @@ struct FuzzyMatcher {
3131
if windows.count > 50 {
3232
// Parallel processing for large lists
3333
matches = windows.concurrentCompactMap { window in
34-
let titleScore = matchPreLowered(query: query, againstLower: window.titleLower, original: window.title) ?? 0
35-
let ownerScore = matchPreLowered(query: query, againstLower: window.ownerNameLower, original: window.ownerName) ?? 0
36-
let subtitleScore = matchPreLowered(query: query, againstLower: window.subtitleLower, original: window.subtitle ?? "") ?? 0
37-
let detailScore = matchPreLowered(query: query, againstLower: window.detailTextLower, original: window.detailText) ?? 0
38-
39-
// Prefer title matches over app name matches (1.5x multiplier)
40-
let weightedTitleScore = Int(Double(titleScore) * 1.5)
41-
let bestScore = max(weightedTitleScore, ownerScore, subtitleScore, detailScore)
42-
43-
guard bestScore > 0 else { return nil }
44-
return ScoredWindow(window: window, score: bestScore)
34+
scoreWindow(window, query: query)
4535
}
4636
} else {
4737
// Sequential for small lists (less overhead)
4838
matches = windows.compactMap { window in
49-
let titleScore = matchPreLowered(query: query, againstLower: window.titleLower, original: window.title) ?? 0
50-
let ownerScore = matchPreLowered(query: query, againstLower: window.ownerNameLower, original: window.ownerName) ?? 0
51-
let subtitleScore = matchPreLowered(query: query, againstLower: window.subtitleLower, original: window.subtitle ?? "") ?? 0
52-
let detailScore = matchPreLowered(query: query, againstLower: window.detailTextLower, original: window.detailText) ?? 0
53-
54-
// Prefer title matches over app name matches (1.5x multiplier)
55-
let weightedTitleScore = Int(Double(titleScore) * 1.5)
56-
let bestScore = max(weightedTitleScore, ownerScore, subtitleScore, detailScore)
57-
58-
guard bestScore > 0 else { return nil }
59-
return ScoredWindow(window: window, score: bestScore)
39+
scoreWindow(window, query: query)
6040
}
6141
}
6242

@@ -183,4 +163,30 @@ struct FuzzyMatcher {
183163
// Ensure valid matches always have positive score
184164
return max(score, 1)
185165
}
166+
167+
private static func combinedSearchText(for window: WindowInfo) -> String {
168+
[window.title, window.ownerName, window.subtitle ?? "", window.detailText]
169+
.filter { !$0.isEmpty }
170+
.joined(separator: " ")
171+
}
172+
173+
private static func scoreWindow(_ window: WindowInfo, query: String) -> ScoredWindow? {
174+
let titleScore = matchPreLowered(query: query, againstLower: window.titleLower, original: window.title) ?? 0
175+
let ownerScore = matchPreLowered(query: query, againstLower: window.ownerNameLower, original: window.ownerName) ?? 0
176+
let subtitleScore = matchPreLowered(query: query, againstLower: window.subtitleLower, original: window.subtitle ?? "") ?? 0
177+
let detailScore = matchPreLowered(query: query, againstLower: window.detailTextLower, original: window.detailText) ?? 0
178+
let combinedText = combinedSearchText(for: window)
179+
let combinedScore = matchPreLowered(query: query, againstLower: combinedText.lowercased(), original: combinedText) ?? 0
180+
181+
let weightedTitleScore = Int(Double(titleScore) * 2.2)
182+
let weightedCombinedScore = Int(Double(combinedScore) * 1.2)
183+
var bestScore = max(weightedTitleScore, ownerScore, subtitleScore, detailScore, weightedCombinedScore)
184+
185+
if case .slackWorkspace = window.kind, titleScore > 0 {
186+
bestScore += 1200
187+
}
188+
189+
guard bestScore > 0 else { return nil }
190+
return ScoredWindow(window: window, score: bestScore)
191+
}
186192
}

Sources/FFWF/WindowInfo.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ enum WindowKind: Hashable {
55
case window
66
case terminalTab(windowTitle: String, tabTitle: String, tabIndex: Int)
77
case chromeTab(windowTitle: String, tabTitle: String, tabIndex: Int)
8+
case slackWorkspace(name: String)
9+
case slackChannel(workspace: String, name: String)
10+
case slackDM(workspace: String, name: String)
811
}
912

1013
struct WindowInfo: Identifiable, Hashable {
@@ -84,11 +87,33 @@ struct WindowInfo: Identifiable, Hashable {
8487
switch kind {
8588
case .terminalTab, .chromeTab:
8689
return true
87-
case .window:
90+
case .window, .slackWorkspace, .slackChannel, .slackDM:
8891
return false
8992
}
9093
}
9194

95+
var isSlackItem: Bool {
96+
switch kind {
97+
case .slackWorkspace, .slackChannel, .slackDM:
98+
return true
99+
case .window, .terminalTab, .chromeTab:
100+
return false
101+
}
102+
}
103+
104+
var slackBadgeText: String? {
105+
switch kind {
106+
case .slackWorkspace:
107+
return "WORKSPACE"
108+
case .slackChannel:
109+
return "CHAN"
110+
case .slackDM:
111+
return "DM"
112+
case .window, .terminalTab, .chromeTab:
113+
return nil
114+
}
115+
}
116+
92117
var isTerminalTab: Bool {
93118
if case .terminalTab = kind {
94119
return true
@@ -114,7 +139,7 @@ struct WindowInfo: Identifiable, Hashable {
114139
switch kind {
115140
case .terminalTab(_, _, let tabIndex), .chromeTab(_, _, let tabIndex):
116141
return tabIndex
117-
case .window:
142+
case .window, .slackWorkspace, .slackChannel, .slackDM:
118143
return nil
119144
}
120145
}

0 commit comments

Comments
 (0)