{"id":15055373,"url":"https://github.com/dagronf/dsfquickactionbar","last_synced_at":"2025-04-12T07:08:59.931Z","repository":{"id":50631642,"uuid":"388371392","full_name":"dagronf/DSFQuickActionBar","owner":"dagronf","description":"A spotlight-inspired quick action bar for macOS. AppKit/SwiftUI","archived":false,"fork":false,"pushed_at":"2024-07-20T07:41:13.000Z","size":800,"stargazers_count":125,"open_issues_count":0,"forks_count":13,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-01-20T10:08:23.502Z","etag":null,"topics":["macos","quickaction","spotlight","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dagronf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-07-22T07:38:42.000Z","updated_at":"2025-01-16T13:58:49.000Z","dependencies_parsed_at":"2024-04-06T07:25:26.195Z","dependency_job_id":"ee4e9921-3381-4aa7-ac84-890a7983e4b8","html_url":"https://github.com/dagronf/DSFQuickActionBar","commit_stats":{"total_commits":83,"total_committers":1,"mean_commits":83.0,"dds":0.0,"last_synced_commit":"7be7b3bd2cebb19ee119ba1f972ecce581de5aeb"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dagronf%2FDSFQuickActionBar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dagronf%2FDSFQuickActionBar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dagronf%2FDSFQuickActionBar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dagronf%2FDSFQuickActionBar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dagronf","download_url":"https://codeload.github.com/dagronf/DSFQuickActionBar/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234772458,"owners_count":18884061,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["macos","quickaction","spotlight","swift","swiftui"],"created_at":"2024-09-24T21:41:39.365Z","updated_at":"2025-01-20T10:08:51.693Z","avatar_url":"https://github.com/dagronf.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DSFQuickActionBar\n\nA spotlight-inspired quick action bar for macOS.\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/v/tag/dagronf/DSFQuickActionBar\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/macOS-10.13+-red\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Swift-5.4-orange.svg\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/SwiftUI-2.0+-green\" /\u003e\n    \u003ca href=\"https://swift.org/package-manager\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat\" alt=\"Swift Package Manager\" /\u003e\u003c/a\u003e\n    \u003cimg src=\"https://img.shields.io/badge/License-MIT-lightgrey\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n   \u003ca href=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/qab_search.png?raw=true\"\u003e\n      \u003cimg src=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/qab_search.png?raw=true\" alt=\"Swift Package Manager\" width=\"250\"/\u003e\u003c/a\u003e\n   \u003c/a\u003e\n   \u003ca href=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/qab_results.png?raw=true\"\u003e\n      \u003cimg src=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/qab_results.png?raw=true\" alt=\"Swift Package Manager\" width=\"250\"/\u003e\u003c/a\u003e\n   \u003c/a\u003e\n   \u003ca href=\"./Art/kbd-shortcuts.png\"\u003e\n      \u003cimg src=\"./Art/kbd-shortcuts.png\" alt=\"Swift Package Manager\" width=\"250\"/\u003e\u003c/a\u003e\n   \u003c/a\u003e\n\u003c/p\u003e\n\n## Why?\n\nI've seen this in other mac applications (particularly Spotlight and [Boop](https://apps.apple.com/us/app/boop/id1518425043?mt=12)) and it's very useful and convenient.\n\n## Features\n\n* macOS AppKit Swift Support\n* macOS AppKit SwiftUI Support\n* Completely keyboard navigable\n* Optional keyboard shortcuts\n* Asynchronous API to avoid beachballing on complex queries.\n\nYou can present a quick action bar in the context of a window (where it will be centered above and within the bounds of the window as is shown in the image above) or centered in the current screen (like Spotlight currently does).\n\n## Demos\n\nYou can find macOS demo apps in the `Demos` subfolder.\n\n* `Simple Demo` - a simple AppKit application demonstrating a synchronous quick action bar using AppKit, SwiftUI and custom cell types\n* `Doco Demo` - AppKit demo used for generating images for the website\n* `Faux Spotlight` - An AppKit demo showing asynchronous searching support using MDItemQuery()\n* `SwiftUI Demo` - A SwiftUI demonstration\n* `StatusBar Item Demo` - Demonstrates displaying a quick action bar from a statusbar item (in the menu). \n\n## Process\n\n1. Present the quick action bar, automatically focussing on the edit field so your hands can stay on the keyboard\n2. User starts typing in the search field\n3. For each change to the search term -\n   1. The contentSource will be asked for the item(s) that 'match' the search term (`itemsForSearchTerm`). The `items` request is asynchronous, and can be completed at any point in the future (as long as it hasn't been cancelled by another search request)\n   2. For each item, the contentSource will be asked to provide a view which will appear in the result table for that item (`viewForItem`)\n   3. When the user either double-clicks on, or presses the return key on a selected item row, the contentSource will be provided with the item (`didActivateItem`)\n4. The quick action bar will automatically dismiss if\n\t1. The user clicks outside the quick action bar (ie. it loses focus)\n\t2. The user presses the escape key\n\t3. The user double-clicks an item in the result table\n\t4. The user selects a row and presses 'return'\n\n## Implementing for AppKit\n\nYou present a quick action bar by :-\n\n1. creating an instance of `DSFQuickActionBar` \n2. set the content source on the instance\n3. call the `present` method.\n\n### Presenting\n\nCall the `present` method on the quick action bar instance.\n\n| Name                  | Type       | Description |\n|-----------------------|------------|-------------|\n| parentWindow          | `NSWindow` | The window to present the quick action bar over, or nil to display for the current screen (ala Finder Spotlight) |\n| placeholderText       | `String`   | The placeholder text to display in the edit field |\n| searchImage           | `NSImage`  | The image to display on the left of the search edit field. If nil, uses the default magnifying glass image |\n| initialSearchText     | `String`   | Provide an initial search string to appear when the bar displays |\n| width                 | `CGFloat`  | Force the width of the action bar |\n| showKeyboardShortcuts | `Bool`     | Display keyboard shortcuts (↩︎, ⌘1 -\u003e ⌘9) for the first 10 selectable items |\n| didClose              | callback   | Called when the quick action bar closes |\n\n### Content Source\n\nThe contentSource (`DSFQuickActionBarContentSource`) provides the content and feedback for the quick action bar. The basic mechanism is similar to `NSTableViewDataSource`/`NSTableViewDelegate` in that the control will :-\n\n1. query the contentSource for items matching a search term (itemsForSearchTerm)\n2. ask the contentSource for a view for each displayed item (viewForItem)\n3. indicate that the user has pressed/clicked a selection in the results.\n4. (optional) indicate to the contentSource that the quick action bar has been dismissed.\n\n## Delegate style content source\n\n#### itemsForSearchTermTask\n\n```swift\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTermTask task: DSFQuickActionBar.SearchTask)\n```\n\nCalled when the control needs a array of items to display within the control that match a search term.\nThe definition of 'match' is entirely up to you - you can perform any check you want. \n\nThe `task` object contains the search term and a completion block to call when the search results become \navailable. If the search text changes during an asynchronous search call the task is marked as invalid and the\nresult will be ignored.\n\n##### Simple synchronous example\n\nIf you have code using the old synchronous API, it's relatively straightforward to convert your existing code\nto the new api. \n\n```swift\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTermTask task: DSFQuickActionBar.SearchTask)\n   let results = countryNames.filter { $0.name.startsWith(task.searchTerm) }\n   task.complete(with: results)\n}\n```\n\n##### Simple asynchronous example\n\n```swift\nvar currentSearch: SomeRemoteSearchMechanism?\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTermTask task: DSFQuickActionBar.SearchTask)\n   currentSearch?.cancel()\n   currentSearch = SomeRemoteSearchMechanism(task.searchTerm) { [weak self] results in\n      task.complete(with: results)\n      self?.currentSearch = nil\n   }\n}\n```\n\n---\n\n#### viewForItem\n\n```swift\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, viewForItem item: AnyHashable, searchTerm: String) -\u003e NSView?\n```\n\nReturn the view to be displayed in the row for the item. The search term is also provided to allow the view to be customized for the search term (eg. highlighting the match in the name)\n\n---\n\n#### canSelectItem\n\n```swift\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, canSelectItem item: AnyHashable) -\u003e Bool\n```\n\nCalled when a item will be selected (eg. by keyboard navigation or clicking). Return false if this row should not be selected (eg. it's a separator)\n\n---\n\n#### didSelectItem\n\n```swift\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, didSelectItem item: AnyHashable)\n```\n\nCalled when an item is selected within the list.\n\n---\n\n#### didActivateItem\n\n```swift\n// Swift\nfunc quickActionBar(_ quickActionBar: DSFQuickActionBar, didActivateItem item: AnyHashable)\n```\n\nIndicates the user activated an item in the result list. The 'item' parameter is the item that was selected by the user\n\n---\n\n#### didCancel\n\n```swift\nfunc quickActionBarDidCancel(_ quickActionBar: DSFQuickActionBar)\n```\n\nCalled if the user cancels the quick action bar (eg. by hitting the `esc` key or clicking outside the bar)\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003eSwift Example\u003c/summary\u003e\n\n### Swift Example\n\nA simple AppKit example using Core Image Filters as the contentSource.\n\n```swift\nclass ViewController: NSViewController {\n   let quickActionBar = DSFQuickActionBar()\n   override func viewDidLoad() {\n      super.viewDidLoad()\n\n      // Set the content source for the quick action bar\n      quickActionBar.contentSource = self\n   }\n\n   @IBAction func selectFilter(_ sender: Any) {\n      // Present the quick action bar\n      quickActionBar.present(placeholderText: \"Search for filters…\")\n   }\n}\n\n// ContentSource delegate calls\nextension ViewController: DSFQuickActionBarContentSource {\n   func quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTerm searchTerm: String) -\u003e [AnyHashable] {\n      return Filter.search(searchTerm)\n   }\n\n   func quickActionBar(_ quickActionBar: DSFQuickActionBar, viewForItem item: AnyHashable, searchTerm: String) -\u003e NSView? {\n      guard let filter = item as? Filter else { fatalError() }\n      // For the demo, just return a simple text field with the filter's name\n      return NSTextField(labelWithString: filter.userPresenting)\n   }\n\n   func quickActionBar(_ quickActionBar: DSFQuickActionBar, didActivateItem item: AnyHashable) {\n      Swift.print(\"Activated item \\(item as? Filter)\")\n   }\n   \n   func quickActionBarDidCancel(_ quickActionBar: DSFQuickActionBar) {\n      Swift.print(\"Cancelled!\")\n   }\n}\n\n// the datasource for the Quick action bar. Each filter represents a CIFilter\nstruct Filter: Hashable, CustomStringConvertible {\n   let name: String // The name is unique within our dataset, thus the default equality will be enough to uniquely identify\n   var userPresenting: String { return CIFilter.localizedName(forFilterName: self.name) ?? self.name }\n   var description: String { name }\n\n   // All of the available filters\n   static var AllFilters: [Filter] = {\n      let filterNames = CIFilter.filterNames(inCategory: nil).sorted()\n      return filterNames.map { name in Filter(name: name) }\n   }()\n\n   // Return filters matching the search term\n   static func search(_ searchTerm: String) -\u003e [Filter] {\n      if searchTerm.isEmpty { return AllFilters }\n      return Filter.AllFilters\n         .filter { $0.userPresenting.localizedCaseInsensitiveContains(searchTerm) }\n         .sorted(by: { a, b in a.userPresenting \u003c b.userPresenting })\n   }\n}\n```\n\n![Screenshot for the sample data](./Art/documentation-demo.jpg)\n\n\u003c/details\u003e\n\n## SwiftUI interface\n\nThe SwiftUI implementation is a View. You 'install' the quick action bar just like you would any other SwiftUI view.\nThe `QuickActionBar` view is zero-sized, and does not display content within the view its installed on.\n\n```swift\nQuickActionBar\u003cIdentifyingObject, IdentifyingObjectView\u003e\n```\n\nThe QuickActionBar template parameters represent \n\n* `IdentifyingObject` is the type of the object (eg. `URL`)\n* `IdentifyingObjectView` is the type of View used to represent `IdentifyingObject` in the results list (eg. `Text`)\n\nYou present the quick action bar by setting the `visible` parameter to true.\n\nFor example :-\n\n```swift\n@State var quickActionBarVisible = false\n@State var selectedItem: URL = URL(...)\n...\nVStack {\n   Button(\"Show Quick Action Bar\") {\n      quickActionBarVisible = true\n   }\n   QuickActionBar\u003cURL, Text\u003e(\n      location: .window,\n      visible: $quickActionBarVisible,\n      selectedItem: $selectedItem,\n      placeholderText: \"Open Quickly\",\n      itemsForSearchTerm: { searchTask in\n         let results = /* array of matching URLs */\n         searchTask.complete(with: results)\n      },\n      viewForItem: { url, searchTerm in\n         Text(url.path)\n      }\n   )\n   .onChange(of: selectedItem) { newValue in\n      Swift.print(\"Selected item \\(newValue)\")\n   }\n}\n...\n```\n\n| Parameter               | Description              |\n|:------------------------|:-------------------------|\n| `location`              | Where to locate the quick action bar (.window, .screen) |\n| `visible`               | If true, presents the quick action bar on the screen |\n| `showKeyboardShortcuts` | Display keyboard shortcuts for the first 10 selectable items |\n| `requiredClickCount`    | If `.single`, only requires the user to single-click a row to activate it (defaults to `.double`) |\n| `barWidth`              | The width of the presented bar |\n| `searchTerm`            | The search term to use, updated when the quick action bar is closed |\n| `selectedItem`          | The item selected by the user |\n| `placeholderText`       | The text to display in the quick action bar when the search term is empty |\n| `itemsForSearchTerm`    | A block which returns the item(s) for the specified search term |\n| `viewForItem`           | A block which returns the View to display for the specified item |\n\n\u003cdetails\u003e\n\u003csummary\u003eSwiftUI Example\u003c/summary\u003e\n\n### SwiftUI Example\n\nA simple macOS SwiftUI example using Core Image Filters as the content.\n\n#### SwiftUI View\n\n```swift\nstruct DocoContentView: View {\n   // Binding to update when the user selects a filter\n   @State var selectedFilter: Filter?\n   // Binding to show/hide the quick action bar\n   @State var quickActionBarVisible = false\n\n   var body: some View {\n      VStack {\n         Button(\"Show Quick Action Bar\") {\n            quickActionBarVisible = true\n         }\n         QuickActionBar\u003cFilter, Text\u003e(\n            location: .screen,\n            visible: $quickActionBarVisible,\n            selectedItem: $selectedFilter,\n            placeholderText: \"Open Quickly...\",\n            itemsForSearchTerm: { searchTask in\n               let results = filters__.search(searchTask.searchTerm)\n               searchTask.complete(with: results)\n            },\n            viewForItem: { filter, searchTerm in\n               Text(filter.userPresenting)\n            }\n         )\n      }\n   }\n}\n```\n\n#### Data\n\n```swift\n/// The unique object used as the quick action bar item\nstruct Filter: Hashable, CustomStringConvertible {\n   let name: String // The name is unique within our dataset, therefore it will be our identifier\n   var userPresenting: String { return CIFilter.localizedName(forFilterName: self.name) ?? self.name }\n   var description: String { name }\n}\n\nclass Filters {\n   // If true, displays all of the filters if the search term is empty\n   var showAllIfEmpty = true\n\n   // All the filters\n   var all: [Filter] = {\n      let filterNames = CIFilter.filterNames(inCategory: nil).sorted()\n      return filterNames.map { name in Filter(name: name) }\n   }()\n\n   // Return filters matching the search term\n   func search(_ searchTerm: String) -\u003e [Filter] {\n      if searchTerm.isEmpty \u0026\u0026 showAllIfEmpty { return all }\n      return all\n         .filter { $0.userPresenting.localizedCaseInsensitiveContains(searchTerm) }\n         .sorted(by: { a, b in a.userPresenting \u003c b.userPresenting })\n   }\n}\n\nlet filters__ = Filters()\n```\n\n\u003c/details\u003e\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n   \u003ca href=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/filters-empty.png?raw=true\"\u003e\n      \u003cimg src=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/filters-empty.png?raw=true\" width=\"400\"/\u003e\u003c/a\u003e\n   \u003c/a\u003e\u003cbr/\u003e\n   \u003ca href=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/filter.png?raw=true\"\u003e\n      \u003cimg src=\"https://github.com/dagronf/dagronf.github.io/blob/master/art/projects/DSFQuickActionBar/filter.png?raw=true\" width=\"400\"/\u003e\u003c/a\u003e\n   \u003c/a\u003e\n\u003c/p\u003e\n\n## License\n\nMIT. Use it and abuse it for anything you want, just attribute my work. Let me know if you do use it somewhere, I'd love to hear about it!\n\n```\nMIT License\n\nCopyright © 2022 Darren Ford\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdagronf%2Fdsfquickactionbar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdagronf%2Fdsfquickactionbar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdagronf%2Fdsfquickactionbar/lists"}