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.

Ir para a instalação

Informações da origem

Repositório
reason-machines/devtools-skills
Última atividade na origem
28 de maio de 2026 às 17:22
Idioma detectado do SKILL.md
inglês
Estrelas
4
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub