| name | clicklight-macos-click-highlighter |
| description | Master ClickLight, the macOS menu bar app that highlights clicks for demos, recordings, and UX reviews with real-time visual feedback. |
| triggers | ["how do I use ClickLight for click visualization","configure ClickLight click highlighting","customize ClickLight appearance and settings","build ClickLight from source","troubleshoot ClickLight accessibility permissions","integrate ClickLight into my workflow","modify ClickLight click effects","debug ClickLight overlay issues"] |
ClickLight macOS Click Highlighter Skill
Skill by ara.so — Devtools Skills collection.
Overview
ClickLight is a native macOS menu bar application written in Swift that provides real-time click highlighting for live demos, screen sharing, UX reviews, and presentations. Unlike screen recorders that add click effects in post-production, ClickLight shows visual feedback during the live moment itself.
Key features:
- Real-time click highlights with separate visuals for press, release, right-click, and drag
- Optional laser pointer mode with fading freehand strokes
- Customizable size, duration, intensity, and color
- Native Swift/AppKit implementation
- Menu bar integration with quick presets
Installation
Homebrew (Recommended)
brew tap aurorascharff/clicklight https://github.com/aurorascharff/ClickLight
brew install --cask aurorascharff/clicklight/clicklight
brew upgrade --cask clicklight
brew uninstall --cask clicklight
brew uninstall --cask --zap clicklight
Manual Installation
Download ClickLight.zip from GitHub Releases, extract, and move ClickLight.app to /Applications.
Building from Source
git clone https://github.com/aurorascharff/ClickLight.git
cd ClickLight
swift build -c release
make build
make install
make run
Required Permissions
ClickLight requires Accessibility permission to detect clicks system-wide.
Setup:
- Launch ClickLight (you'll be prompted for permission)
- Go to System Settings → Privacy & Security → Accessibility
- Enable ClickLight in the list
- Quit and relaunch ClickLight from the menu bar
If clicks aren't being detected, verify the permission is enabled and restart the app.
Project Structure
ClickLight/
├── Sources/
│ └── ClickLight/
│ ├── main.swift # Entry point
│ ├── AppDelegate.swift # Menu bar controller
│ ├── ClickMonitor.swift # Global click detection
│ ├── OverlayWindow.swift # Click effect rendering
│ ├── SettingsWindow.swift # Preferences UI
│ └── Preferences.swift # UserDefaults wrapper
├── Package.swift # Swift package manifest
├── Makefile # Build automation
└── docs/ # Documentation
Core Architecture
1. Click Monitoring (ClickMonitor.swift)
Listens for global mouse events using CGEvent tap:
import Cocoa
class ClickMonitor {
private var eventTap: CFMachPort?
weak var delegate: ClickMonitorDelegate?
func start() {
let eventMask = (1 << CGEventType.leftMouseDown.rawValue) |
(1 << CGEventType.leftMouseUp.rawValue) |
(1 << CGEventType.rightMouseDown.rawValue) |
(1 << CGEventType.rightMouseUp.rawValue) |
(1 << CGEventType.leftMouseDragged.rawValue)
guard let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(eventMask),
callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
guard let refcon = refcon else { return Unmanaged.passRetained(event) }
let monitor = <>.fromPointer(refcon)
monitor.handleEvent(type: type, event: event)
.passRetained(event)
},
userInfo: .toPointer()
) {
()
}
runLoopSource (kCFAllocatorDefault, eventTap, )
((), runLoopSource, .commonModes)
.tapEnable(tap: eventTap, enable: )
.eventTap eventTap
}
(: , : ) {
location event.location
type {
.leftMouseDown:
delegate.didDetectClick(at: location, type: .press)
.leftMouseUp:
delegate.didDetectClick(at: location, type: .release)
.rightMouseDown:
delegate.didDetectClick(at: location, type: .rightClick)
.leftMouseDragged:
delegate.didDetectDrag(at: location)
:
}
}
() {
eventTap eventTap {
.tapEnable(tap: eventTap, enable: )
(eventTap)
}
}
}
: {
( : , : )
( : )
}
{
press, release, rightClick
}
2. Overlay Rendering (OverlayWindow.swift)
Creates a transparent window overlay for click effects:
import Cocoa
class OverlayWindow: NSWindow {
init() {
let screenFrame = NSScreen.screens.reduce(NSRect.zero) { (result, screen) in
return result.union(screen.frame)
}
super.init(
contentRect: screenFrame,
styleMask: .borderless,
backing: .buffered,
defer: false
)
self.isOpaque = false
self.backgroundColor = .clear
self.level = .floating
self.ignoresMouseEvents = true
self.collectionBehavior = [.canJoinAllSpaces, .stationary]
let overlayView = OverlayView(frame: screenFrame)
self.contentView = overlayView
}
}
class OverlayView: NSView {
private var clickEffects: [ClickEffect] = []
override func draw(_ dirtyRect: NSRect) {
guard let context .current.cgContext { }
effect clickEffects {
effect.draw(in: context)
}
}
( : , : , : ) {
effect (
location: location,
type: type,
size: config.size,
duration: config.duration,
intensity: config.intensity,
color: config.color
)
clickEffects.append(effect)
effect.animate { [ ]
.clickEffects.removeAll { effect }
.needsDisplay
}
.needsDisplay
}
}
{
size:
duration:
intensity:
color:
}
{
location:
type:
currentRadius:
currentAlpha:
maxRadius:
duration:
color:
(: , : , : , : , : , : ) {
.location location
.type type
.maxRadius size
.duration duration
.color color.withAlphaComponent(intensity)
.currentRadius size
}
( : ) {
context.setStrokeColor(color.cgColor)
context.setLineWidth()
context.setAlpha(currentAlpha)
rect (
x: location.x currentRadius,
y: location.y currentRadius,
width: currentRadius ,
height: currentRadius
)
context.strokeEllipse(in: rect)
}
(: () -> ) {
startTime ()
.scheduledTimer(withTimeInterval: , repeats: ) { [ ] timer
{
timer.invalidate()
}
elapsed ().timeIntervalSince(startTime)
progress (elapsed .duration, )
progress {
timer.invalidate()
completion()
}
.currentRadius .maxRadius ( progress )
.currentAlpha progress
}
}
}
3. Preferences Management (Preferences.swift)
import Foundation
import Cocoa
class Preferences {
static let shared = Preferences()
private let defaults = UserDefaults.standard
enum Key: String {
case clickSize = "clickSize"
case clickDuration = "clickDuration"
case clickIntensity = "clickIntensity"
case clickColor = "clickColor"
case laserPointerEnabled = "laserPointerEnabled"
case compactIcon = "compactIcon"
}
var clickSize: CGFloat {
get { CGFloat(defaults.double(forKey: Key.clickSize.rawValue)) }
set { defaults.set(Double(newValue), forKey: Key.clickSize.rawValue) }
}
var clickDuration: TimeInterval {
get { defaults.double(forKey: Key.clickDuration.rawValue) }
set { defaults.set(newValue, forKey: Key.clickDuration.rawValue) }
}
var clickIntensity: {
{ (defaults.double(forKey: .clickIntensity.rawValue)) }
{ defaults.set((newValue), forKey: .clickIntensity.rawValue) }
}
clickColor: {
{
data defaults.data(forKey: .clickColor.rawValue),
color .unarchivedObject(ofClass: ., from: data) {
.systemBlue
}
color
}
{
data .archivedData(withRootObject: newValue, requiringSecureCoding: ) {
defaults.set(data, forKey: .clickColor.rawValue)
}
}
}
laserPointerEnabled: {
{ defaults.bool(forKey: .laserPointerEnabled.rawValue) }
{ defaults.set(newValue, forKey: .laserPointerEnabled.rawValue) }
}
() {
defaults.register(defaults: [
.clickSize.rawValue: ,
.clickDuration.rawValue: ,
.clickIntensity.rawValue: ,
.laserPointerEnabled.rawValue: ,
.compactIcon.rawValue:
])
}
}
4. Menu Bar Integration (AppDelegate.swift)
import Cocoa
@main
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
private var clickMonitor: ClickMonitor!
private var overlayWindow: OverlayWindow!
private var settingsWindow: SettingsWindow?
func applicationDidFinishLaunching(_ notification: Notification) {
Preferences.shared.registerDefaults()
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.image = NSImage(systemSymbolName: "circle.circle", accessibilityDescription: "ClickLight")
statusItem.menu = createMenu()
overlayWindow = OverlayWindow()
overlayWindow.orderFront(nil)
clickMonitor = ClickMonitor()
clickMonitor.delegate = self
clickMonitor.start()
}
private func createMenu() -> {
menu ()
menu.addItem((title: , action: (openSettings), keyEquivalent: ))
menu.addItem(.separator())
sizeMenu ()
sizeMenu.addItem(createPresetItem(title: , size: ))
sizeMenu.addItem(createPresetItem(title: , size: ))
sizeMenu.addItem(createPresetItem(title: , size: ))
sizeItem (title: , action: , keyEquivalent: )
sizeItem.submenu sizeMenu
menu.addItem(sizeItem)
menu.addItem(.separator())
menu.addItem((title: , action: (testPulse), keyEquivalent: ))
menu.addItem(.separator())
menu.addItem((title: , action: (quit), keyEquivalent: ))
menu
}
() {
settingsWindow {
settingsWindow ()
}
settingsWindow.showWindow()
.activate(ignoringOtherApps: )
}
() {
screen .main { }
center (x: screen.frame.midX, y: screen.frame.midY)
config (
size: .shared.clickSize,
duration: .shared.clickDuration,
intensity: .shared.clickIntensity,
color: .shared.clickColor
)
(overlayWindow.contentView ).addClickEffect(at: center, type: .press, config: config)
}
() {
.shared.terminate()
}
(: , : ) -> {
item (title: title, action: (applyPreset(:)), keyEquivalent: )
item.representedObject size
item
}
( : ) {
size sender.representedObject {
.shared.clickSize size
}
}
}
: {
( : , : ) {
config (
size: .shared.clickSize,
duration: .shared.clickDuration,
intensity: .shared.clickIntensity,
color: .shared.clickColor
)
(overlayWindow.contentView ).addClickEffect(at: location, type: type, config: config)
}
( : ) {
.shared.laserPointerEnabled { }
}
}
Configuration
Programmatic Configuration
Modify preferences directly:
Preferences.shared.clickSize = 60.0
Preferences.shared.clickDuration = 0.8
Preferences.shared.clickIntensity = 0.9
Preferences.shared.clickColor = NSColor(red: 1.0, green: 0.3, blue: 0.3, alpha: 1.0)
Preferences.shared.laserPointerEnabled = true
UserDefaults Keys
Access preferences via command line:
defaults read com.yourname.ClickLight
defaults write com.yourname.ClickLight clickSize -float 70
defaults write com.yourname.ClickLight clickDuration -float 0.6
defaults delete com.yourname.ClickLight
Common Patterns
Custom Click Effect Style
class CustomClickEffect: ClickEffect {
override func draw(in context: CGContext) {
let rect = CGRect(
x: location.x - currentRadius,
y: location.y - currentRadius,
width: currentRadius * 2,
height: currentRadius * 2
)
context.setStrokeColor(color.cgColor)
context.setLineWidth(4.0)
context.setAlpha(currentAlpha)
context.stroke(rect)
}
}
Multi-Screen Support
extension OverlayWindow {
func updateForScreenConfiguration() {
let unionFrame = NSScreen.screens.reduce(NSRect.zero) { $0.union($1.frame) }
self.setFrame(unionFrame, display: true)
self.contentView?.frame = unionFrame
}
}
NotificationCenter.default.addObserver(
forName: NSApplication.didChangeScreenParametersNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.overlayWindow.updateForScreenConfiguration()
}
Keyboard Shortcut Integration
import Carbon
class HotkeyManager {
private var eventHandler: EventHandlerRef?
func registerHotkey(key: UInt32, modifiers: UInt32, action: @escaping () -> Void) {
var hotKeyID = EventHotKeyID(signature: 0x4343, id: 1)
var hotKeyRef: EventHotKeyRef?
RegisterEventHotKey(
key,
modifiers,
hotKeyID,
GetApplicationEventTarget(),
0,
&hotKeyRef
)
}
}
let hotkeyManager = HotkeyManager()
hotkeyManager.registerHotkey(key: 8, modifiers: cmdKey | shiftKey) {
}
Development Workflow
Build Commands
swift build
swift build -c release
swift run
swift package clean
swift test
Makefile Shortcuts
make run
make build
make install
make clean
make archive
Debugging
Enable verbose logging:
#if DEBUG
let logger = Logger(subsystem: "com.yourname.ClickLight", category: "debug")
logger.debug("Click detected at \(location)")
#endif
Monitor click events:
log stream --predicate 'subsystem == "com.yourname.ClickLight"' --level debug
Troubleshooting
Clicks Not Detected
-
Check Accessibility permission:
sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
"SELECT client FROM access WHERE service='kTCCServiceAccessibility'"
-
Verify event tap is active:
if eventTap == nil {
print("ERROR: Event tap failed to create")
print("Check Accessibility permission in System Settings")
}
-
Restart after permission grant:
Must quit and relaunch for permission to take effect.
Overlay Not Visible
-
Check window level:
overlayWindow.level = .floating
-
Verify transparency:
print("Overlay opaque: \(overlayWindow.isOpaque)")
print("Background: \(overlayWindow.backgroundColor)")
-
Test with simple shape:
context.setFillColor(NSColor.red.cgColor)
context.fill(CGRect(x: 100, y: 100, width: 100, height: 100))
Performance Issues
-
Limit active effects:
if clickEffects.count > 10 {
clickEffects.removeFirst(clickEffects.count - 10)
}
-
Optimize drawing:
override func draw(_ dirtyRect: NSRect) {
let visibleEffects = clickEffects.filter { effect in
dirtyRect.intersects(effect.boundingRect)
}
for effect in visibleEffects {
effect.draw(in: context)
}
}
-
Reduce animation frequency:
Timer.scheduledTimer(withTimeInterval: 1/30.0, repeats: true) { ... }
Build Errors
swift package tools-version --set-current
swift package resolve
swift package update
rm -rf .build && swift build
Code Signing Issues
codesign --force --deep --sign - ClickLight.app
codesign --verify --verbose ClickLight.app
codesign -d --entitlements - ClickLight.app
Testing
Manual Testing Checklist
Automated Testing
import XCTest
@testable import ClickLight
final class PreferencesTests: XCTestCase {
func testDefaultValues() {
let prefs = Preferences.shared
XCTAssertEqual(prefs.clickSize, 50.0)
XCTAssertEqual(prefs.clickDuration, 0.5)
XCTAssertEqual(prefs.clickIntensity, 0.7)
}
func testColorPersistence() {
let prefs = Preferences.shared
let testColor = NSColor.red
prefs.clickColor = testColor
let retrieved = prefs.clickColor
XCTAssertEqual(retrieved, testColor)
}
}
Run tests:
swift test
Integration Examples
Use with OBS Studio
ClickLight overlays work with OBS window/display capture:
- Launch ClickLight
- In OBS, add Display Capture source
- ClickLight effects will appear in recording
Use with Screen Studio
ClickLight provides live effects; Screen Studio can add post-production effects:
- ClickLight: Real-time visibility during recording
- Screen Studio: Enhanced post-production effects
Both can work together for maximum visibility.
Use with QuickTime Screen Recording
Advanced Customization
Add Custom Preset
struct Preset {
let name: String
let size: CGFloat
let duration: TimeInterval
let intensity: CGFloat
let color: NSColor
}
let presets = [
Preset(name: "Subtle", size: 30, duration: 0.3, intensity: 0.5, color: .systemGray),
Preset(name: "Bold", size: 80, duration: 0.8, intensity: 1.0, color: .systemRed),
Preset(name: "Presentation", size: 60, duration: 0.6, intensity: 0.8, color: .systemBlue)
]
func applyPreset(_ preset: Preset) {
Preferences.shared.clickSize = preset.size
Preferences.shared.clickDuration = preset.duration
Preferences.shared.clickIntensity = preset.intensity
Preferences.shared.clickColor = preset.color
}
Add Sound Effects
import AVFoundation
class SoundPlayer {
private var audioPlayer: AVAudioPlayer?
func playClickSound() {
guard let soundURL = Bundle.main.url(forResource: "click", withExtension: "wav") else { return }
do {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer?.volume = 0.3
audioPlayer?.play()
} catch {
print("Failed to play sound: \(error)")
}
}
}
extension AppDelegate {
func didDetectClick(at location: CGPoint, type: ClickType) {
soundPlayer.playClickSound()
}
}
Export Settings
extension Preferences {
func exportSettings() -> String? {
let settings: [String: Any] = [
"clickSize": clickSize,
"clickDuration": clickDuration,
"clickIntensity": clickIntensity,
"laserPointerEnabled": laserPointerEnabled
]
guard let data = try? JSONSerialization.data(withJSONObject: settings, options: .prettyPrinted),
let json = String(data: data, encoding: .utf8) else {
return nil
}
return json
}
func importSettings(json: String) {
guard let data = json.data(using: .utf8),
let settings = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return
}
if let size = settings["clickSize"] as? Double {
clickSize (size)
}
duration settings[] {
clickDuration duration
}
}
}
Resources