How Lightspot works

Lightspot is a small Swift package with no dependencies. This page walks through the source: how the panel is built, what happens on each keystroke, the one rule that keeps typing fast, how only one copy runs and how it updates itself, and how the app is built, released and tested.

Lightspot guide
  1. Overview
  2. Install, update and uninstall
  3. Searching and the app grid
  4. Calculator and conversions
  5. Developer tools
  6. Terminal history and projects
  7. Custom commands, web search and snippets
  8. Browser, clipboard and system
  9. Turning off Apple Spotlight
  10. Menu bar and settings
  11. Privacy
  12. How Lightspot works

The source

Lightspot is written in Swift with AppKit and SwiftUI and built with the Swift Package Manager: no Xcode project, no third-party packages. The code is split into three layers:

mac-lightspot/
├── Package.swift          Swift 6 package, macOS 13+, no dependencies
├── build.sh               release build → build/Lightspot.app
├── install.sh / uninstall.sh
├── Resources/Info.plist   menu bar app (LSUIElement), no Dock icon
├── scripts/
│   ├── test_engine.swift  engine tests
│   └── deep_verify.swift  checks against the live system
└── Sources/Lightspot/
    ├── AppMain.swift      entry point; wires everything with closures
    ├── Core/              search engine and every result provider
    ├── System/            hotkey, menu bar, login item, terminals, Spotlight,
    │                      single-instance guard, updater
    └── UI/                SwiftUI views and the AppKit panel
  • Core holds the search engine and one provider per kind of result. It uses only Foundation and AppKit, never SwiftUI, and every provider is a shared instance or a static engine.
  • System talks to macOS: the global hot key (Carbon RegisterEventHotKey), the menu bar item, the login item (SMAppService), the seven terminal launchers and the Spotlight switches.
  • UI is SwiftUI inside an AppKit panel. AppMain.swift creates the view model, the panel, the hot key and the menu bar controller and connects them with closures; there is no dependency-injection container or notification bus.

The panel

The window is a borderless, non-activating NSPanel at the floating level, 740 by 560 points, so it can take keystrokes without pulling the app you were in to the front. Inside it are a 54-point capsule for the search field and a 450-point results area with 26-point corners, both backed by NSVisualEffectView with the HUD material blurring what is behind the window: that is where the Spotlight look comes from. It opens on the screen with the mouse pointer, 16% below the top of the visible area, fading in while scaling from 97% to 100% in 0.18 seconds.

The search field is an NSTextField with its own field editor, so arrow keys, Tab, ↵ with modifiers and the overlay shortcuts reach Lightspot before the text field handles them. The same editor reports text that an input method is still composing, which AppKit does not announce by itself; that was the change in version 1.1.2.

What happens on a keystroke

Search is synchronous and runs on the main thread, with no debounce. Every change of the text calls the search engine, which asks each provider in turn and merges the answers:

  1. Applications
  2. System Settings
  3. Quick actions
  4. Process killer
  5. Custom commands
  6. Recent projects
  7. zsh history
  8. Developer tools
  9. Hardware readout
  10. Browser
  11. Clipboard
  12. Snippets
  13. Calculator and conversions
  14. Web search

The engine then adds the learned bonus to each score, picks the Top Hit, sorts every section, cuts it to four results (six for history and browser) and hands the sections to the view in a fixed display order. The display order is an explicit list, deliberately not the order in which the result types are declared, so reordering code cannot silently reshuffle the panel; a test keeps the list complete.

The repository measured a full pass at about 2.8 ms with 7 days of browser history (1,499 pages), 337 bookmarks and 1,200 shell commands in memory, and well under 1 ms without the browser data, which dominates the cost.

The rule that keeps typing fast

Because every provider runs on every keystroke, and SwiftUI re-evaluates menus on every redraw, nothing on the keystroke path may block. The project notes that breaking this rule has happened three times, and each time it was felt the same way: typing became slow. These are the kinds of calls that are not allowed there, with what they cost:

CallCost
Asking Chrome for its open tabs over AppleScript300–465 ms
Asking Finder for the front window’s folderabout 260 ms
Running launchctl or mdutil to read Spotlight’s stateabout 66 ms
Running lsof to map listening ports to processesabout 55–110 ms
Parsing 337 Chrome bookmarksabout 12 ms
Asking macOS whether the login item is enabledabout 3 ms

The pattern used everywhere instead: keep a snapshot in memory, return it at once, and refresh it on a background queue when it is stale. A flag marks a refresh in progress, so a burst of keystrokes queues one refresh instead of one per letter. NSAppleScript is not thread-safe, so the browser and Finder queries each get their own serial queue. Where a blocking call is still needed, for example asking Finder for the folder at the moment you press ↵, it is a separate method from the non-blocking one that search uses.

Work that is in memory but grows with the data counts too. Apps, shell commands, browser items and clipboard entries compute their lowercase and tokenised forms once, when they are loaded, not on every search. Substring matching compares UTF-8 bytes directly instead of using String.contains, which goes through Foundation’s collation: about 0.11 µs instead of 3.3 µs against a 90-character title, multiplied by every candidate on every keystroke.

The last exception, the port lookup behind kill :port, which ran lsof inline, became a background snapshot in version 1.2.0, like the process list next to it.

One copy at a time

Before it does anything else, Lightspot takes an exclusive lock on ~/Library/Application Support/Lightspot/com.lightspot.app.lock and writes its process ID and a build identity (the executable’s path, inode and modification time) into it. The kernel drops the lock when the process ends, so a crash never leaves a stale lock behind. When a second copy starts:

  • If the running copy is the same build, the new one sends it a “show the panel” message and exits, so opening Lightspot from Finder simply shows the search bar. A copy that started less than ten seconds ago ignores that message.
  • If it is a different build (reinstalled, rebuilt or updated), the new copy asks the old one to quit, waits up to five seconds, and takes over; if the old copy is still running then, the new one exits instead.

Both messages are macOS distributed notifications named after the bundle identifier. Copies older than 1.2.0 do not listen for them, so they have to be quit by hand once.

How an update is installed

The update check reads the latest release from the GitHub API and compares versions. Installing downloads Lightspot.zip (only from GitHub hosts, also after redirects), unpacks it with ditto and checks that the bundle is com.lightspot.app, has its executable, and carries exactly the version the release names. There is no signature to check: releases are signed ad hoc, because the project has no Apple Developer ID, so the updater trusts the GitHub release the same way the installer does.

An app cannot safely replace itself while it runs, so Lightspot writes a small shell script to a temporary folder, starts it detached, and quits. The script waits for the old process to exit, moves the old app aside, copies the new one in with ditto, removes the quarantine flag, signs it ad hoc and opens it; if the copy fails, it moves the old app back. When the app’s folder is not writable by your account, the script runs with administrator rights, through Touch ID or the password dialog.

At rest

When the panel is closed, Lightspot waits for its hot key. The only recurring work is the clipboard check every 0.6 seconds, which compares the pasteboard’s change counter and reads the text only when it changed, and the daily update check. Hiding the panel, whether with Esc, the hot key or a click outside, clears the result lists and asks the memory allocator to give unused memory back to the system. On my Mac, idle Lightspot processes show 0.0% CPU and between 12 and 30 MB of memory in ps.

Build and release

build.sh runs swift build -c release with size optimisation (-Osize), whole-module and cross-module optimisation and exclusivity checks turned off, strips the binary, wraps it into Lightspot.app with its Info.plist and icon, and signs it locally. The resulting Apple silicon binary is about 3 MB. The app icon is drawn by a script with Core Graphics and packed with iconutil.

Releases are built by GitHub Actions on a macOS runner: a version tag (or a manual run that bumps the version) sets the version in Info.plist, builds for arm64 and x86_64, zips the app as Lightspot.zip and publishes it as a GitHub release, which is what the installer downloads.

Tests

There is no XCTest target. Instead, two plain Swift programs are compiled together with the sources and run from a clone of the repository:

  • scripts/test_engine.swift checks the engines with about 650 assertions in 32 groups: the calculator and conversions, the fuzzy matcher, every provider, ranking and what search history may keep, custom and pinned commands, settings backup, snippets, the hardware readout, the first-run flow, the Spotlight shortcut state, the updater and the single-instance guard. The public IP lookup is replaced by a stub. The first failure stops the run.
  • scripts/deep_verify.swift checks the live system: that the System Settings links and AppleScripts are valid, that the terminals and apps are found, that calculator answers are right, and that the “no file indexing” promise holds. It runs up to 88 checks, depending on whether the Mac has a zsh history and VS Code projects.
swiftc -o /tmp/test_engine scripts/test_engine.swift Sources/Lightspot/Core/*.swift Sources/Lightspot/System/TerminalLauncher.swift Sources/Lightspot/System/AutoStartManager.swift Sources/Lightspot/System/SpotlightManager.swift Sources/Lightspot/System/FirstRunManager.swift Sources/Lightspot/System/UpdateManager.swift Sources/Lightspot/System/SingleInstanceGuard.swift && /tmp/test_engine

The project’s history is public: commits on GitHub, from the first commit on 2 September 2026 to the 1.2.0 release on 16 October 2026.