Skip to main content

clicklight-macos-click-highlighter

Master ClickLight, the macOS menu bar app that highlights clicks for demos, recordings, and UX reviews with real-time visual feedback.

Zur Installation springen

Quellinformationen

Repository
reason-machines/devtools-skills
Letzte Quellaktivität
28. Mai 2026 um 17:22
Erkannte Sprache von SKILL.md
Englisch
Sterne
4
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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](https://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) ```bash # Add tap and install brew tap aurorascharff/clicklight https://github.com/aurorascharff/ClickLight brew install --cask aurorascharff/clicklight/clicklight # Update later brew upgrade --cask clicklight # Uninstall brew uninstall --cask clicklight # Uninstall with preferences brew uninstall --cask --zap clicklight ``` ### Manual Installation Download `ClickLight.zip` from [GitHub Releases](https://github.com/aurorascharff/ClickLight/releases), extract, and move `ClickLight.app` to `/Applications`. ### Building from Source ```bash # Clone repository git clone https://github.com/aurorascharff/ClickLight.git cd ClickLight # Build with Swift swift build -c release # Or use the Makefile make build # Install to Applications make install # Run directly make run ``` ## Required Permissions ClickLight requires **Accessibility** permission to detect clicks system-wide. **Setup:** 1. Launch ClickLight (you'll be prompted for permission) 2. Go to **System Settings → Privacy & Security → Accessibility** 3. Enable ClickLight in the list 4. 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: ```swift 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 = Unsafely<ClickMonitor>.fromPointer(refcon) monitor.handleEvent(type: type, event: event) return Unmanaged.passRetained(event) }, userInfo: Unafely.toPointer(self) ) else { print("Failed to create event tap") return } let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: eventTap, enable: true) self.eventTap = eventTap } private func handleEvent(type: CGEventType, event: CGEvent) { let location = event.location switch type { case .leftMouseDown: delegate?.didDetectClick(at: location, type: .press) case .leftMouseUp: delegate?.didDetectClick(at: location, type: .release) case .rightMouseDown: delegate?.didDetectClick(at: location, type: .rightClick) case .leftMouseDragged: delegate?.didDetectDrag(at: location) default: break } } func stop() { if let eventTap = eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) CFMachPortInvalidate(eventTap) } } } protocol ClickMonitorDelegate: AnyObject { func didDetectClick(at location: CGPoint, type: ClickType) func didDetectDrag(at location: CGPoint) } enum ClickType { case press, release, rightClick } ``` ### 2. Overlay Rendering (`OverlayWindow.swift`) Creates a transparent window overlay for click effects: ```swift import Cocoa class OverlayWindow: NSWindow { init() { // Cover all screens 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 = NSGraphicsContext.current?.cgContext else { return } for effect in clickEffects { effect.draw(in: context) } } func addClickEffect(at location: CGPoint, type: ClickType, config: EffectConfig) { let effect = ClickEffect( location: location, type: type, size: config.size, duration: config.duration, intensity: config.intensity, color: config.color ) clickEffects.append(effect) // Animate effect effect.animate { [weak self] in self?.clickEffects.removeAll { $0 === effect } self?.needsDisplay = true } self.needsDisplay = true } } struct EffectConfig { let size: CGFloat let duration: TimeInterval let intensity: CGFloat let color: NSColor } class ClickEffect { let location: CGPoint let type: ClickType var currentRadius: CGFloat var currentAlpha: CGFloat = 1.0 private let maxRadius: CGFloat private let duration: TimeInterval private let color: NSColor init(location: CGPoint, type: ClickType, size: CGFloat, duration: TimeInterval, intensity: CGFloat, color: NSColor) { self.location = location self.type = type self.maxRadius = size self.duration = duration self.color = color.withAlphaComponent(intensity) self.currentRadius = size * 0.5 } func draw(in context: CGContext) { context.setStrokeColor(color.cgColor) context.setLineWidth(3.0) context.setAlpha(currentAlpha) let rect = CGRect( x: location.x - currentRadius, y: location.y - currentRadius, width: currentRadius * 2, height: currentRadius * 2 ) context.strokeEllipse(in: rect) } func animate(completion: @escaping () -> Void) { let startTime = Date() Timer.scheduledTimer(withTimeInterval: 1/60.0, repeats: true) { [weak self] timer in guard let self = self else { timer.invalidate() return } let elapsed = Date().timeIntervalSince(startTime) let progress = min(elapsed / self.duration, 1.0) if progress >= 1.0 { timer.invalidate() completion() return } // Expand and fade self.currentRadius = self.maxRadius * (0.5 + progress * 0.5) self.currentAlpha = 1.0 - progress } } } ``` ### 3. Preferences Management (`Preferences.swift`) ```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: CGFloat { get { CGFloat(defaults.double(forKey: Key.clickIntensity.rawValue)) } set { defaults.set(Double(newValue), forKey: Key.clickIntensity.rawValue) } } var clickColor: NSColor { get { guard let data = defaults.data(forKey: Key.clickColor.rawValue), let color = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: data) else { return .systemBlue } return color } set { if let data = try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false) { defaults.set(data, forKey: Key.clickColor.rawValue) } } } var laserPointerEnabled: Bool { get { defaults.bool(forKey: Key.laserPointerEnabled.rawValue) } set { defaults.set(newValue, forKey: Key.laserPointerEnabled.rawValue) } } func registerDefaults() { defaults.register(defaults: [ Key.clickSize.rawValue: 50.0, Key.clickDuration.rawValue: 0.5, Key.clickIntensity.rawValue: 0.7, Key.laserPointerEnabled.rawValue: false, Key.compactIcon.rawValue: false ]) } } ``` ### 4. Menu Bar Integration (`AppDelegate.swift`) ```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() // Create menu bar item statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem.button?.image = NSImage(systemSymbolName: "circle.circle", accessibilityDescription: "ClickLight") statusItem.menu = createMenu() // Initialize overlay overlayWindow = OverlayWindow() overlayWindow.orderFront(nil) // Start monitoring clickMonitor = ClickMonitor() clickMonitor.delegate = self clickMonitor.start() } private func createMenu() -> NSMenu { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ",")) menu.addItem(NSMenuItem.separator()) // Quick presets let sizeMenu = NSMenu() sizeMenu.addItem(createPresetItem(title: "Small", size: 30)) sizeMenu.addItem(createPresetItem(title: "Medium", size: 50)) sizeMenu.addItem(createPresetItem(title: "Large", size: 80)) let sizeItem = NSMenuItem(title: "Size", action: nil, keyEquivalent: "") sizeItem.submenu = sizeMenu menu.addItem(sizeItem) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Test Pulse", action: #selector(testPulse), keyEquivalent: "t")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit ClickLight", action: #selector(quit), keyEquivalent: "q")) return menu } @objc private func openSettings() { if settingsWindow == nil { settingsWindow = SettingsWindow() } settingsWindow?.showWindow(nil) NSApp.activate(ignoringOtherApps: true) } @objc private func testPulse() { guard let screen = NSScreen.main else { return } let center = CGPoint(x: screen.frame.midX, y: screen.frame.midY) let config = EffectConfig( size: Preferences.shared.clickSize, duration: Preferences.shared.clickDuration, intensity: Preferences.shared.clickIntensity, color: Preferences.shared.clickColor ) (overlayWindow.contentView as? OverlayView)?.addClickEffect(at: center, type: .press, config: config) } @objc private func quit() { NSApplication.shared.terminate(nil) } private func createPresetItem(title: String, size: CGFloat) -> NSMenuItem { let item = NSMenuItem(title: title, action: #selector(applyPreset(_:)), keyEquivalent: "") item.representedObject = size return item } @objc private func applyPreset(_ sender: NSMenuItem) { if let size = sender.representedObject as? CGFloat { Preferences.shared.clickSize = size } } } extension AppDelegate: ClickMonitorDelegate { func didDetectClick(at location: CGPoint, type: ClickType) { let config = EffectConfig( size: Preferences.shared.clickSize, duration: Preferences.shared.clickDuration, intensity: Preferences.shared.clickIntensity, color: Preferences.shared.clickColor ) (overlayWindow.contentView as? OverlayView)?.addClickEffect(at: location, type: type, config: config) } func didDetectDrag(at location: CGPoint) { guard Preferences.shared.laserPointerEnabled else { return } // Implement laser pointer trail logic } } ``` ## Configuration ### Programmatic Configuration Modify preferences directly: ```swift // Adjust click size Preferences.shared.clickSize = 60.0 // Change duration (seconds) Preferences.shared.clickDuration = 0.8 // Set intensity (0.0 to 1.0) Preferences.shared.clickIntensity = 0.9 // Custom color Preferences.shared.clickColor = NSColor(red: 1.0, green: 0.3, blue: 0.3, alpha: 1.0) // Enable laser pointer mode Preferences.shared.laserPointerEnabled = true ``` ### UserDefaults Keys Access preferences via command line: ```bash # View all ClickLight preferences defaults read com.yourname.ClickLight # Set click size defaults write com.yourname.ClickLight clickSize -float 70 # Set duration defaults write com.yourname.ClickLight clickDuration -float 0.6 # Reset to defaults defaults delete com.yourname.ClickLight ``` ## Common Patterns ### Custom Click Effect Style ```swift // Create custom effect renderer class CustomClickEffect: ClickEffect { override func draw(in context: CGContext) { // Draw square instead of circle 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
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen