Implementing a File Watcher and Screen Flasher, and Connecting Them to the Cursor AI Agent
Quick Access: Download Scripts
-
📖 View file_watcher.py with syntax highlighting
#!/usr/bin/env python3 """ File Watcher - A generic file monitoring and command execution script This script monitors a specific file and executes a given command when it appears. It's designed to be a simple, dependency-free tool for automated workflows. WHAT IT DOES: - Polls for the existence of a specified file every second. - When the file is found, it executes a user-defined command and waits for it to complete. - Checks the exit code of the command to determine success or failure. - Deletes the file after the command is run to prepare for the next event. DEPENDENCIES: - Python 3.6+ - No external libraries required. USAGE: 1. Run the script with a command to execute: ./scripts/file-watcher.py --file FILE [--delay SECONDS] [--verbose] -- [COMMAND] [ARGS...] The '--' is recommended to separate script arguments from the command. 2. The script will start polling. 3. Press Ctrl+C to stop. """ import time import subprocess import argparse import logging from pathlib import Path from datetime import datetime # Default configuration DEFAULT_DELAY_SECONDS = 0.5 def execute_command(command): try: logging.debug(f"Executing command: {' '.join(command)}") result = subprocess.run( command, capture_output=True, text=True ) if result.returncode != 0: logging.error(f"Command failed with exit code {result.returncode}") if result.stderr: logging.error(f"Stderr: {result.stderr.strip()}") else: logging.info("Command executed successfully.") if result.stdout: logging.debug(f"Stdout: {result.stdout.strip()}") if result.stderr: # Log non-fatal stderr output too for diagnostics logging.debug(f"Stderr (non-fatal): {result.stderr.strip()}") except FileNotFoundError: logging.error(f"Command not found: {command[0]}") except Exception as e: logging.error(f"Error executing command: {e}") def setup_logging(verbose=False): """Configure logging based on verbosity level.""" level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( level=level, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) def parse_arguments(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="Poll for a file and execute a command when it appears.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Flash screen when file appears, checking every 0.2 seconds %(prog)s --file .cursor_response_complete --delay 0.2 -- ./scripts/FlashScreen # Watch a different file with default delay %(prog)s --file state.log --verbose -- ls -l """ ) parser.add_argument( '-f', '--file', required=True, help='File to watch for.' ) parser.add_argument( '-d', '--delay', type=float, default=DEFAULT_DELAY_SECONDS, help=f'Delay in seconds between polling checks (default: {DEFAULT_DELAY_SECONDS})' ) parser.add_argument( '-v', '--verbose', action='store_true', help='Enable verbose logging' ) parser.add_argument( 'command', nargs=argparse.REMAINDER, help='Command to execute when file is found. Use -- to separate from script args.' ) args = parser.parse_args() if args.command and args.command[0] == '--': args.command = args.command[1:] if not args.command: parser.error( "No command supplied. You must provide a command to execute after the script options.\n" "Example: %(prog)s --file ... -- your_command_here" ) return args def delete_file(filespec): try: filespec.unlink() logging.debug(f"Deleted file: {filespec}") except OSError as e: logging.error(f"Error deleting file {filespec}: {e}") def process_file_presence(file_to_watch, command): timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') print(f"{timestamp} - File FOUND: {file_to_watch}") execute_command(command) def main(): args = parse_arguments() setup_logging(args.verbose) file_to_watch = Path(args.file).resolve() print(f"Polling for file: {file_to_watch}") print(f"Command to run: {' '.join(args.command)}") print("Press Ctrl+C to stop...") try: while True: if file_to_watch.exists(): process_file_presence(file_to_watch, args.command) delete_file(file_to_watch) time.sleep(args.delay) except KeyboardInterrupt: print("\nStopping file watcher.") except Exception as e: logging.error(f"An unexpected error occurred: {e}") if __name__ == "__main__": main()
-
📖 View FlashScreen with syntax highlighting
#!/usr/bin/env swift import Cocoa // Flash duration constants let IMAGE_FLASH_DURATION: TimeInterval = 1.0 let SOLID_COLOR_FLASH_DURATION: TimeInterval = 0.3 // Default image path constants let DEFAULT_IMAGE_PATH_DISPLAY = "~/system-flash-image.jpg" let DEFAULT_IMAGE_PATH = NSString(string: DEFAULT_IMAGE_PATH_DISPLAY).expandingTildeInPath // Color parsing function func parseColor(_ colorString: String) -> NSColor { let lowercased = colorString.lowercased() // Handle named colors switch lowercased { case "white": return NSColor.white case "black": return NSColor.black case "red": return NSColor.red case "green": return NSColor.green case "blue": return NSColor.blue case "yellow": return NSColor.yellow case "orange": return NSColor.orange case "purple": return NSColor.purple case "cyan": return NSColor.cyan case "magenta": return NSColor.magenta case "gray", "grey": return NSColor.gray default: // Try to parse as hex color if colorString.hasPrefix("#") { let hex = String(colorString.dropFirst()) if hex.count == 6, let rgbValue = UInt32(hex, radix: 16) { let red = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0 let green = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0 let blue = CGFloat(rgbValue & 0x0000FF) / 255.0 return NSColor(red: red, green: green, blue: blue, alpha: 1.0) } } // Default to white if parsing fails return NSColor.white } } // Help function func showHelp() { print(""" Flash Screen - Screen Flash Utility USAGE: ./FlashScreen [OPTIONS] [IMAGE_PATH] swift FlashScreen [OPTIONS] [IMAGE_PATH] ARGUMENTS: IMAGE_PATH Optional path to image file to display during flash If not provided, defaults to \(DEFAULT_IMAGE_PATH_DISPLAY) If default image doesn't exist, displays a solid color flash DESCRIPTION: Creates a fullscreen flash effect for visual notifications. - With image: Shows the image for \(IMAGE_FLASH_DURATION) seconds - Without image: Shows solid color flash for \(SOLID_COLOR_FLASH_DURATION) seconds - Default: Checks for \(DEFAULT_IMAGE_PATH_DISPLAY) if no path specified The flash window appears at maximum window level and ignores mouse events. EXAMPLES: ./FlashScreen # Default image or solid color flash ./FlashScreen -n # Force solid color flash (ignore default image) ./FlashScreen -c red # Force red color flash ./FlashScreen --color "#FF5500" # Force orange color flash using hex ./FlashScreen ~/Pictures/flash.jpg # Image flash swift FlashScreen /path/to/image.png # Image flash with swift command OPTIONS: -n, --no-image Force solid color flash, ignoring default image -c, --color COLOR Specify flash color (named color or hex like #FF0000) Named colors: white, black, red, green, blue, yellow, orange, purple, cyan, magenta, gray -h, --help Show this help message and exit """) } // Parse command line arguments let args = CommandLine.arguments var forceNoImage = false var imagePath: String? = nil var flashColor: NSColor = NSColor.white // Process arguments var i = 1 while i < args.count { let arg = args[i] if arg == "-h" || arg == "--help" { showHelp() exit(0) } else if arg == "-n" || arg == "--no-image" { forceNoImage = true i += 1 } else if arg == "-c" || arg == "--color" { if i + 1 < args.count { flashColor = parseColor(args[i + 1]) forceNoImage = true // Color option implies no image i += 2 } else { print("Error: -c/--color requires a color value") exit(1) } } else if !arg.hasPrefix("-") { // This is an image path imagePath = arg break } else { print("Error: Unknown option \(arg)") exit(1) } } let app = NSApplication.shared app.setActivationPolicy(.regular) let window = NSWindow( contentRect: NSScreen.main!.frame, styleMask: [.borderless], backing: .buffered, defer: false ) window.backgroundColor = flashColor window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow))) window.isOpaque = true window.ignoresMouseEvents = true // Determine final image path based on arguments and flags if forceNoImage { // Force solid color flash, ignore any image path or default imagePath = nil } else if imagePath == nil { // No explicit image path provided, check for default imagePath = FileManager.default.fileExists(atPath: DEFAULT_IMAGE_PATH) ? DEFAULT_IMAGE_PATH : nil } // Try to load and display image if let path = imagePath, let image = NSImage(contentsOfFile: path) { let imageView = NSImageView(frame: window.contentView!.bounds) imageView.image = image imageView.imageScaling = .scaleProportionallyUpOrDown window.contentView?.addSubview(imageView) // Show image for longer duration window.makeKeyAndOrderFront(nil) app.activate(ignoringOtherApps: true) DispatchQueue.main.asyncAfter(deadline: .now() + IMAGE_FLASH_DURATION) { window.close() app.terminate(nil) } } else { // Show solid color flash for shorter duration window.makeKeyAndOrderFront(nil) app.activate(ignoringOtherApps: true) DispatchQueue.main.asyncAfter(deadline: .now() + SOLID_COLOR_FLASH_DURATION) { window.close() app.terminate(nil) } } app.run()
-
📖 View sample_notifier.py with syntax highlighting
#!/usr/bin/env python3 """ This script provides a sample notification by flashing the screen, speaking a confirmation message, and sending a native macOS notification. """ import subprocess from pathlib import Path # --- Configuration --- # Get the directory where this script is located SCRIPT_DIR = Path(__file__).parent.resolve() # Infer the project's absolute path from the script's location (one dir up). PROJECT_DIR_ABSOLUTE = SCRIPT_DIR.parent # Abbreviate the home directory with ~ for a cleaner path if applicable try: PROJECT_DIR_DISPLAY = f"~/{PROJECT_DIR_ABSOLUTE.relative_to(Path.home())}" except ValueError: PROJECT_DIR_DISPLAY = str(PROJECT_DIR_ABSOLUTE) # --- Notification Functions --- def flash(): """ Flashes the screen. The FlashScreen script handles default image logic internally. """ flash_script_path = SCRIPT_DIR / "FlashScreen" if not flash_script_path.is_file(): print(f"Error: FlashScreen script not found at {flash_script_path}") return # Use Popen to run in the background, so other notifications can proceed. subprocess.Popen([str(flash_script_path)]) def speak(): """Speaks a confirmation message.""" try: subprocess.run(["say", "AI response complete"], check=True) except subprocess.CalledProcessError as e: print(f"Error with text-to-speech: {e}") except FileNotFoundError: print("Error: 'say' command not found (macOS only)") def terminal_notifier_installed(): """Check if terminal-notifier is available (only once).""" if not hasattr(terminal_notifier_installed, '_checked'): terminal_notifier_installed._checked = True try: subprocess.run(["which", "terminal-notifier"], capture_output=True, text=True, check=True) return True except subprocess.CalledProcessError: print("Error: terminal-notifier not found. Install with: brew install terminal-notifier") return False return True def os_notify(): """Sends a native macOS notification using terminal-notifier.""" if not terminal_notifier_installed(): return message = f"AI response complete in project: {PROJECT_DIR_DISPLAY}" title = "AI Response Complete" # Use terminal-notifier for reliable notifications on macOS 15+; Claude Opus reports that # "macOS 15+ requires explicit notification permissions for command-line tools, # and osascript's basic display notification doesn't always trigger the permission request properly." process = subprocess.run( ["terminal-notifier", "-message", message, "-title", title], capture_output=True, text=True ) if process.returncode != 0: print(f"Error sending notification: {process.stderr.strip()}") # --- Main Execution --- if __name__ == "__main__": flash() os_notify() speak()
-
📖 View watch with syntax highlighting
scripts/file_watcher.py -f .cursor_response_complete -v scripts/sample_notifier.py
Introduction
I have been exploring AI coding assistance, most recently using Cursor AI with Claude 4.0.
Although it often makes mistakes and produces substandard code, it can also be brilliant, correctly completing in seconds what would otherwise take minutes, hours, or even days.
So I continue to use it…but not without some frustrations. For various reasons, my requests sometimes perform quite slowly, even taking several minutes sometimes. While I’m waiting I multitask, processing my email inbox for example, but then I need to return to Cursor periodically to see if the response has completed. This frequent context switching kills my productivity.
💡 Quick Start: If you want to jump straight to implementation, see the Complete Setup Instructions at the end of this article.
High Level Solution
Since my multitasking will likely bring me to an application other than Cursor, a visual notification inside Cursor would not work for me. And since I am often working in coworking places where silence is required, an audio notification is not always suitable either. So I needed to find a way to show a visual notification when the response completed, regardless of which application had focus. I decided to look into flashing the desktop with an image or solid color, and this met my need well.
The solution makes it easy to plug in multiple notification types, so for your convenience I provided
a sample-notifier.py
script which demonstrates how to combine the following notification types:
- flash the desktop with an image or solid color
- show a notification in the Notification Center (requires
terminal-notifier
) - speak text (using macOS
say
command)
But how would Cursor AI communicate to my system that the response was ready? Cursor operates in a sandbox with very limited ability to interact with the host system. There’s one thing that the Cursor AI Agent can do though, and it does it all the time – write to the filesystem – specifically, the project directory tree on the filesystem.
Solution Architecture
So the solution consists of two parts:
- the Cursor AI signaling via the filesystem that the response was completed
- a script that watches the filesystem and performs the notification(s) when signaled
(See Solution Architecture in the appendix for a visual overview, and Detailed Process Flow for a combination of the system workflow shown above with the user’s interactions.)
Let’s discuss these parts one at a time…
Configuring Cursor to Signal Response Completion By Creating a Signal File
If you navigate the menus Cursor -> Settings -> Cursor Settings -> Rules, you can add “User Rules” that will apply to every request of every project you edit in Cursor. I added:
Before starting any response, delete .cursor_response_complete
in the project root if it exists.
After completing that response, recreate (touch) it.
This rule requires the use of Agent mode, because in other modes the AI will not write to the filesystem.
While I was originally unwilling to give it this freedom, I’ve come around to allowing it, since I git commit
frequently and can always revert to the most recent commit if I want to undo the AI’s changes.
Flashing the Image On Signal File Creation
Now that we can detect response completion by the presence of the .cursor_response_complete
file, we need something to watch for that file and flash the image.
I decided to let the AI do the heavy lifting, especially since I am not an expert in Swift or even Bash scripting. After it created a Bash script, I realized that a nontrivial script like this is easier to read and maintain in a scripting language like Python than in shell script, so I asked it to translate the file to Python.
The script was named file_watcher.py
to reflect its generic nature – it can watch for any file and execute any command. I decided to put it in the scripts/
directory under my
project root so that it was version controlled and part of my project.
Another approach would be to put it in a single place on my system where all projects
can access it, and modifications can be made in one place.
Technical Implementation Details
The Python Script Architecture diagram in the appendix visualizes the script structure.
Separate Scripts for File Watching and Notifications
The implementation uses a clean separation of concerns with two main components:
- File monitoring:
- A Python script (
file_watcher.py
) for file monitoring
- A Python script (
- Notifications:
- A macOS-specific Swift script (
FlashScreen
) for displaying the visual flash effect, with a shebang line for direct execution - Use of macOS provided
say
command for text-to-speech notifications - Use of the open source macOS-specific
terminal-notifier
for native macOS notifications - A convenience script (
sample_notifier.py
) that calls the above
- A macOS-specific Swift script (
This architecture allows different platforms to provide their own flash implementation without modifying the Python code. The current macOS implementation uses Swift with a shebang line for direct execution.
A huge benefit of this separation is that you now have scripts you can use for file watching and desktop flashing in any other use case.
Adapting for Other Platforms
The file_watcher.py
script is fully cross-platform since it’s written in Python.
However, the notification mechanisms listed above are Mac-specific and would need to be
implemented for other platforms.
To implement this solution on Windows or Linux, you would keep file_watcher.py
(since it is cross-platform) and use platform-specific mechanisms, writing your own if necessary.
Example approaches for other platforms:
- Windows: A PowerShell script (
.ps1
) could use .NET libraries to create a fullscreen window and theBurntToast
module to send native notifications. - Linux: A shell script could use
notify-send
for system notifications and a simple Python script with a GUI library (like Tkinter) to create a fullscreen flash effect.
You would then pass your custom script to the file watcher:
# Example for a custom Linux script
./scripts/file_watcher.py --file .cursor_response_complete -- ./scripts/my-linux-notifier.sh
Help Output
Both scripts include comprehensive help:
Python script: ./scripts/file_watcher.py --help
(or -h
)
usage: file_watcher.py [-h] -f FILE [-d DELAY] [-v] ...
Poll for a file and execute a command when it appears.
positional arguments:
command Command to execute when file is found. Use -- to separate
from script args.
options:
-h, --help show this help message and exit
-f, --file FILE File to watch for.
-d, --delay DELAY Delay in seconds between polling checks (default: 0.5)
-v, --verbose Enable verbose logging
Examples:
# Flash screen when file appears, checking every 0.2 seconds
file_watcher.py --file .cursor_response_complete --delay 0.2 -- ./scripts/flash_screen
# Watch a different file with default delay
file_watcher.py --file state.log --verbose -- ls -l
Flash script: ./scripts/FlashScreen --help
(or -h
)
Flash Screen - Screen Flash Utility
USAGE:
./FlashScreen [OPTIONS] [IMAGE_PATH]
swift FlashScreen [OPTIONS] [IMAGE_PATH]
ARGUMENTS:
IMAGE_PATH Optional path to image file to display during flash
If not provided, defaults to ~/system-flash-image.jpg
If default image doesn't exist, displays a solid color flash
DESCRIPTION:
Creates a fullscreen flash effect for visual notifications.
- With image: Shows the image for 1.0 seconds
- Without image: Shows solid color flash for 0.3 seconds
- Default: Checks for ~/system-flash-image.jpg if no path specified
The flash window appears at maximum window level and ignores mouse events.
EXAMPLES:
./FlashScreen # Default image or solid color flash
./FlashScreen -n # Force solid color flash (ignore default image)
./FlashScreen -c red # Force red color flash
./FlashScreen --color "#FF5500" # Force orange color flash using hex
./FlashScreen ~/Pictures/flash.jpg # Image flash
swift FlashScreen /path/to/image.png # Image flash with swift command
OPTIONS:
-n, --no-image Force solid color flash, ignoring default image
-c, --color COLOR Specify flash color (named color or hex like #FF0000)
Named colors: white, black, red, green, blue, yellow, orange, purple, cyan, magenta, gray
-h, --help Show this help message and exit
Other Notes
Using a Fixed Image Name for Easy Changes
I tend to get tired of the same image after a while, so it’s helpful to have a scheme that lets me change the image without changing the script configuration. The simplest approach is to always copy your desired image to the same filename:
Simple approach:
# Copy your current image to a fixed name
cp ~/Pictures/my-favorite-image.jpg ~/system-flash-image.jpg
# Later, to change the image, just copy a different one:
cp ~/Pictures/different-image.png ~/system-flash-image.jpg
Note: The flash script accepts any image format that macOS can display (JPEG, PNG, etc.), so format conversion is usually unnecessary. Even copying a .png file to the .jpg filename seems to work; the system ignores the extension and examines the file to determine its type.
Advanced: Using symbolic links (optional) If you’re comfortable with symbolic links, they’re even more convenient since you don’t need to copy large files.
Deployment Options
The Deployment Options Comparison diagram in the appendix shows different ways to deploy these scripts.
Implementation Code
The complete implementation consists of two scripts:
Python File Watcher Script
This Python script handles file monitoring using a simple polling approach. Key features:
- Monitors any specified file creation with
--file
argument - Executes any command when the file appears
- Configurable polling delay with
--delay
argument (default: 0.5 seconds) - No external dependencies - uses only the Python standard library
- Cross-platform compatible
Dependencies: None - uses only the Python 3.6+ standard library
📖 View file_watcher.py with syntax highlighting
#!/usr/bin/env python3
"""
File Watcher - A generic file monitoring and command execution script
This script monitors a specific file and executes a given command when it appears.
It's designed to be a simple, dependency-free tool for automated workflows.
WHAT IT DOES:
- Polls for the existence of a specified file every second.
- When the file is found, it executes a user-defined command and waits for it to complete.
- Checks the exit code of the command to determine success or failure.
- Deletes the file after the command is run to prepare for the next event.
DEPENDENCIES:
- Python 3.6+
- No external libraries required.
USAGE:
1. Run the script with a command to execute:
./scripts/file-watcher.py --file FILE [--delay SECONDS] [--verbose] -- [COMMAND] [ARGS...]
The '--' is recommended to separate script arguments from the command.
2. The script will start polling.
3. Press Ctrl+C to stop.
"""
import time
import subprocess
import argparse
import logging
from pathlib import Path
from datetime import datetime
# Default configuration
DEFAULT_DELAY_SECONDS = 0.5
def execute_command(command):
try:
logging.debug(f"Executing command: {' '.join(command)}")
result = subprocess.run(
command,
capture_output=True,
text=True
)
if result.returncode != 0:
logging.error(f"Command failed with exit code {result.returncode}")
if result.stderr:
logging.error(f"Stderr: {result.stderr.strip()}")
else:
logging.info("Command executed successfully.")
if result.stdout:
logging.debug(f"Stdout: {result.stdout.strip()}")
if result.stderr: # Log non-fatal stderr output too for diagnostics
logging.debug(f"Stderr (non-fatal): {result.stderr.strip()}")
except FileNotFoundError:
logging.error(f"Command not found: {command[0]}")
except Exception as e:
logging.error(f"Error executing command: {e}")
def setup_logging(verbose=False):
"""Configure logging based on verbosity level."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
def parse_arguments():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Poll for a file and execute a command when it appears.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Flash screen when file appears, checking every 0.2 seconds
%(prog)s --file .cursor_response_complete --delay 0.2 -- ./scripts/FlashScreen
# Watch a different file with default delay
%(prog)s --file state.log --verbose -- ls -l
"""
)
parser.add_argument(
'-f', '--file',
required=True,
help='File to watch for.'
)
parser.add_argument(
'-d', '--delay',
type=float,
default=DEFAULT_DELAY_SECONDS,
help=f'Delay in seconds between polling checks (default: {DEFAULT_DELAY_SECONDS})'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Enable verbose logging'
)
parser.add_argument(
'command',
nargs=argparse.REMAINDER,
help='Command to execute when file is found. Use -- to separate from script args.'
)
args = parser.parse_args()
if args.command and args.command[0] == '--':
args.command = args.command[1:]
if not args.command:
parser.error(
"No command supplied. You must provide a command to execute after the script options.\n"
"Example: %(prog)s --file ... -- your_command_here"
)
return args
def delete_file(filespec):
try:
filespec.unlink()
logging.debug(f"Deleted file: {filespec}")
except OSError as e:
logging.error(f"Error deleting file {filespec}: {e}")
def process_file_presence(file_to_watch, command):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"{timestamp} - File FOUND: {file_to_watch}")
execute_command(command)
def main():
args = parse_arguments()
setup_logging(args.verbose)
file_to_watch = Path(args.file).resolve()
print(f"Polling for file: {file_to_watch}")
print(f"Command to run: {' '.join(args.command)}")
print("Press Ctrl+C to stop...")
try:
while True:
if file_to_watch.exists():
process_file_presence(file_to_watch, args.command)
delete_file(file_to_watch)
time.sleep(args.delay)
except KeyboardInterrupt:
print("\nStopping file watcher.")
except Exception as e:
logging.error(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
main()
📝 Note: After downloading, rename to file_watcher.py
and ensure it’s executable with chmod +x file_watcher.py
.
Swift Flash Script
This Swift script creates the visual flash effect using macOS NSWindow APIs. Key features:
- Displays fullscreen image for 1.0 second, or solid color flash for 0.3 seconds
- Supports any image format macOS can display (JPEG, PNG, etc.)
- Supports custom colors with
-c/--color
option (named colors or hex values) - Force solid color mode with
-n/--no-image
option - Default image path:
~/system-flash-image.jpg
- Self-contained executable with help documentation
- Uses shebang line for direct execution (
#!/usr/bin/env swift
)
Requirements: macOS with Swift compiler
📖 View FlashScreen with syntax highlighting
#!/usr/bin/env swift
import Cocoa
// Flash duration constants
let IMAGE_FLASH_DURATION: TimeInterval = 1.0
let SOLID_COLOR_FLASH_DURATION: TimeInterval = 0.3
// Default image path constants
let DEFAULT_IMAGE_PATH_DISPLAY = "~/system-flash-image.jpg"
let DEFAULT_IMAGE_PATH = NSString(string: DEFAULT_IMAGE_PATH_DISPLAY).expandingTildeInPath
// Color parsing function
func parseColor(_ colorString: String) -> NSColor {
let lowercased = colorString.lowercased()
// Handle named colors
switch lowercased {
case "white": return NSColor.white
case "black": return NSColor.black
case "red": return NSColor.red
case "green": return NSColor.green
case "blue": return NSColor.blue
case "yellow": return NSColor.yellow
case "orange": return NSColor.orange
case "purple": return NSColor.purple
case "cyan": return NSColor.cyan
case "magenta": return NSColor.magenta
case "gray", "grey": return NSColor.gray
default:
// Try to parse as hex color
if colorString.hasPrefix("#") {
let hex = String(colorString.dropFirst())
if hex.count == 6, let rgbValue = UInt32(hex, radix: 16) {
let red = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
let green = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
let blue = CGFloat(rgbValue & 0x0000FF) / 255.0
return NSColor(red: red, green: green, blue: blue, alpha: 1.0)
}
}
// Default to white if parsing fails
return NSColor.white
}
}
// Help function
func showHelp() {
print("""
Flash Screen - Screen Flash Utility
USAGE:
./FlashScreen [OPTIONS] [IMAGE_PATH]
swift FlashScreen [OPTIONS] [IMAGE_PATH]
ARGUMENTS:
IMAGE_PATH Optional path to image file to display during flash
If not provided, defaults to \(DEFAULT_IMAGE_PATH_DISPLAY)
If default image doesn't exist, displays a solid color flash
DESCRIPTION:
Creates a fullscreen flash effect for visual notifications.
- With image: Shows the image for \(IMAGE_FLASH_DURATION) seconds
- Without image: Shows solid color flash for \(SOLID_COLOR_FLASH_DURATION) seconds
- Default: Checks for \(DEFAULT_IMAGE_PATH_DISPLAY) if no path specified
The flash window appears at maximum window level and ignores mouse events.
EXAMPLES:
./FlashScreen # Default image or solid color flash
./FlashScreen -n # Force solid color flash (ignore default image)
./FlashScreen -c red # Force red color flash
./FlashScreen --color "#FF5500" # Force orange color flash using hex
./FlashScreen ~/Pictures/flash.jpg # Image flash
swift FlashScreen /path/to/image.png # Image flash with swift command
OPTIONS:
-n, --no-image Force solid color flash, ignoring default image
-c, --color COLOR Specify flash color (named color or hex like #FF0000)
Named colors: white, black, red, green, blue, yellow, orange, purple, cyan, magenta, gray
-h, --help Show this help message and exit
""")
}
// Parse command line arguments
let args = CommandLine.arguments
var forceNoImage = false
var imagePath: String? = nil
var flashColor: NSColor = NSColor.white
// Process arguments
var i = 1
while i < args.count {
let arg = args[i]
if arg == "-h" || arg == "--help" {
showHelp()
exit(0)
} else if arg == "-n" || arg == "--no-image" {
forceNoImage = true
i += 1
} else if arg == "-c" || arg == "--color" {
if i + 1 < args.count {
flashColor = parseColor(args[i + 1])
forceNoImage = true // Color option implies no image
i += 2
} else {
print("Error: -c/--color requires a color value")
exit(1)
}
} else if !arg.hasPrefix("-") {
// This is an image path
imagePath = arg
break
} else {
print("Error: Unknown option \(arg)")
exit(1)
}
}
let app = NSApplication.shared
app.setActivationPolicy(.regular)
let window = NSWindow(
contentRect: NSScreen.main!.frame,
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.backgroundColor = flashColor
window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
window.isOpaque = true
window.ignoresMouseEvents = true
// Determine final image path based on arguments and flags
if forceNoImage {
// Force solid color flash, ignore any image path or default
imagePath = nil
} else if imagePath == nil {
// No explicit image path provided, check for default
imagePath = FileManager.default.fileExists(atPath: DEFAULT_IMAGE_PATH) ? DEFAULT_IMAGE_PATH : nil
}
// Try to load and display image
if let path = imagePath,
let image = NSImage(contentsOfFile: path) {
let imageView = NSImageView(frame: window.contentView!.bounds)
imageView.image = image
imageView.imageScaling = .scaleProportionallyUpOrDown
window.contentView?.addSubview(imageView)
// Show image for longer duration
window.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
DispatchQueue.main.asyncAfter(deadline: .now() + IMAGE_FLASH_DURATION) {
window.close()
app.terminate(nil)
}
} else {
// Show solid color flash for shorter duration
window.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
DispatchQueue.main.asyncAfter(deadline: .now() + SOLID_COLOR_FLASH_DURATION) {
window.close()
app.terminate(nil)
}
}
app.run()
📝 Note: After downloading, rename to FlashScreen
and ensure it’s executable with chmod +x FlashScreen
.
Sample Notifier Script
For users who want more comprehensive notifications including screen flash, text-to-speech, and native macOS notifications, we provide a sample notifier script. This Python script demonstrates how to combine multiple notification types:
Key features:
- Flashes the screen using the
FlashScreen
script - Speaks “AI response complete” using macOS
say
command - Sends a native macOS notification using
terminal-notifier
- Shows the project directory in notifications for context
- Kicks off all three notifications in quick succession for a near-parallel effect
Requirements:
- macOS (for
say
command) terminal-notifier
(install with:brew install terminal-notifier
)
📖 View sample_notifier.py with syntax highlighting
#!/usr/bin/env python3
"""
This script provides a sample notification by flashing the screen,
speaking a confirmation message, and sending a native macOS notification.
"""
import subprocess
from pathlib import Path
# --- Configuration ---
# Get the directory where this script is located
SCRIPT_DIR = Path(__file__).parent.resolve()
# Infer the project's absolute path from the script's location (one dir up).
PROJECT_DIR_ABSOLUTE = SCRIPT_DIR.parent
# Abbreviate the home directory with ~ for a cleaner path if applicable
try:
PROJECT_DIR_DISPLAY = f"~/{PROJECT_DIR_ABSOLUTE.relative_to(Path.home())}"
except ValueError:
PROJECT_DIR_DISPLAY = str(PROJECT_DIR_ABSOLUTE)
# --- Notification Functions ---
def flash():
"""
Flashes the screen. The FlashScreen script handles default image logic internally.
"""
flash_script_path = SCRIPT_DIR / "FlashScreen"
if not flash_script_path.is_file():
print(f"Error: FlashScreen script not found at {flash_script_path}")
return
# Use Popen to run in the background, so other notifications can proceed.
subprocess.Popen([str(flash_script_path)])
def speak():
"""Speaks a confirmation message."""
try:
subprocess.run(["say", "AI response complete"], check=True)
except subprocess.CalledProcessError as e:
print(f"Error with text-to-speech: {e}")
except FileNotFoundError:
print("Error: 'say' command not found (macOS only)")
def terminal_notifier_installed():
"""Check if terminal-notifier is available (only once)."""
if not hasattr(terminal_notifier_installed, '_checked'):
terminal_notifier_installed._checked = True
try:
subprocess.run(["which", "terminal-notifier"],
capture_output=True, text=True, check=True)
return True
except subprocess.CalledProcessError:
print("Error: terminal-notifier not found. Install with: brew install terminal-notifier")
return False
return True
def os_notify():
"""Sends a native macOS notification using terminal-notifier."""
if not terminal_notifier_installed():
return
message = f"AI response complete in project: {PROJECT_DIR_DISPLAY}"
title = "AI Response Complete"
# Use terminal-notifier for reliable notifications on macOS 15+; Claude Opus reports that
# "macOS 15+ requires explicit notification permissions for command-line tools,
# and osascript's basic display notification doesn't always trigger the permission request properly."
process = subprocess.run(
["terminal-notifier", "-message", message, "-title", title],
capture_output=True, text=True
)
if process.returncode != 0:
print(f"Error sending notification: {process.stderr.strip()}")
# --- Main Execution ---
if __name__ == "__main__":
flash()
os_notify()
speak()
📝 Note: After downloading, rename to sample_notifier.py
and ensure it’s executable with chmod +x sample_notifier.py
.
To use this script instead of just the FlashScreen:
./scripts/file_watcher.py --file .cursor_response_complete -- ./scripts/sample_notifier.py
Watch Convenience Script
This is a simple shell script to run the file watcher with the notifier, for convenience:
#!/bin/bash
scripts/file_watcher.py -f .cursor_response_complete -v scripts/sample_notifier.py
Complete Setup Instructions
Here’s everything you need to set up the desktop image flashing system:
Prerequisites
- macOS (for the current implementation)
- Python 3 (usually pre-installed on macOS)
- Cursor AI with Agent mode enabled
Step 1: Configure Cursor AI User Rules
- Open Cursor AI
- Navigate to: Cursor → Settings → Cursor Settings → Rules
-
Add this User Rule (applies to all projects):
Before starting any response, delete `.cursor_response_complete` in the project root if it exists. After completing that response, recreate (touch) it.
- Important: You must use Agent mode for this to work (other modes don’t write to filesystem)
- Important: Enable the AI to complete responses without asking for confirmation
Step 2: Download and Setup Scripts
- Download the scripts using the download buttons in the Implementation Code section above
- Create a scripts directory in your project root:
mkdir scripts
- Place the downloaded files in the scripts directory:
- Rename
file_watcher.py.txt
tofile_watcher.py
- Rename
FlashScreen.txt
toFlashScreen
- Rename
- Make them executable:
chmod +x scripts/file_watcher.py chmod +x scripts/FlashScreen # or cd scripts; chmod +x *; cd - # Make all files in the 'scripts' directory executable
Step 3: Setup Your Flash Image
Choose one of these options:
Option A: Use the default image path (recommended)
# Copy your image to the default location where he FlashScreen script will automatically use it
cp ~/Pictures/my-flash-image.jpg ~/system-flash-image.jpg
Option B: Pass a custom image path
# Pass any image file to FlashScreen directly
./scripts/file_watcher.py --file .cursor_response_complete -- ./scripts/FlashScreen ~/Pictures/my-flash-image.jpg
Step 4: Start the Monitoring Script
Once your scripts are in place, you can start monitoring for the signal file. Open a terminal, navigate to your project’s root directory, and choose one of the following methods.
Option A: Basic Usage (Flash Only)
This command watches for the signal file and triggers only the screen flash.
cd /path/to/your/project
./scripts/file_watcher.py --file .cursor_response_complete -- ./scripts/FlashScreen
Option B: Full Notifications (Recommended)
This command uses the sample_notifier.py
script to trigger all three notifications
(flash, sound, and system notification).
The scripts/watch
convenience script does the same thing with verbose logging enabled.
# Using the notifier script directly
cd /path/to/your/project
./scripts/file_watcher.py --file .cursor_response_complete -- ./scripts/sample_notifier.py
# Or using the convenience script
cd /path/to/your/project
./scripts/watch
You can add the --verbose
flag to any file_watcher.py
command to see more detailed logging.
Step 5: Test the Setup
- Test the flash script directly:
# Test with default image or white flash ./scripts/FlashScreen # Test with a specific image ./scripts/FlashScreen ~/Pictures/test-image.jpg # Test with a colored flash ./scripts/FlashScreen -c red
- Test the complete workflow:
- Start the monitoring script
- Ask Cursor AI a question in Agent mode
- Wait for the response to complete
- You should see a flash when the response finishes
Troubleshooting
- Symptom: Nothing happens when a response completes.
- Cause: The signal file (
.cursor_response_complete
) is likely not being created. - Solution: Ensure you are using Agent Mode in Cursor AI and that your file-creation rule is correctly configured and enabled.
- Cause: The signal file (
- Symptom: Terminal shows a “Permission denied” error.
- Cause: A script you’re trying to run lacks execute permissions.
- Solution: Make the script executable. For example:
chmod +x scripts/file_watcher.py
.
- Symptom: Terminal shows a “command not found” error.
- Cause: A program needed by the scripts is either not installed or not in your system’s
PATH
. - Solution: Identify which command is missing (e.g.,
python3
,say
,terminal-notifier
) and ensure it is installed and accessible from your terminal.
- Cause: A program needed by the scripts is either not installed or not in your system’s
- Symptom: The screen flash is a solid color instead of your image.
- Cause: The
FlashScreen
script cannot find the image file. - Solution: If using the default, verify that
~/system-flash-image.jpg
exists. If passing a path directly, ensure the path is correct.
- Cause: The
- Symptom: The
say
command and system notification appear, but the screen does not flash.- Cause: This points to an issue specifically with the
FlashScreen
script. - Solution:
- Confirm that
scripts/FlashScreen
exists and is executable. - Run
./scripts/FlashScreen
directly from your terminal to see if it produces any errors on its own.
- Confirm that
- Cause: This points to an issue specifically with the
Advanced Options
- Run from any directory: Use absolute paths in the script configuration
- Different flash duration: Modify the Swift script’s timer values
- Cross-platform: Create platform-specific flash scripts for Windows/Linux to replace
scripts/FlashScreen
,say
, andterminal-notifier
- Multiple projects: Place scripts in a system-wide location and use absolute paths
- Customize scripts: View and download the source code in the Implementation Code section above
Appendix: System Diagrams
This section contains all the technical diagrams referenced throughout the article. You may click or tap any diagram to open it in a new tab for full-size viewing.
Solution Architecture
This high-level overview shows how Cursor AI signals response completion through the filesystem, which triggers the file watcher to execute the flash script.
Detailed Process Flow
This sequence diagram combines the system workflow with user interactions, showing the complete flow from user question to visual notification.
Python Script Architecture
This diagram shows the internal structure of the Python file watcher script, including its main components and how they interact with the external flash script.
Deployment Options Comparison
This comparison chart shows different approaches for deploying the scripts, from project-specific to system-wide installations.
About the Author
This solution was developed by Keith Bennett of Bennett Business Solutions, Inc. Keith is an experienced software engineer and consultant specializing in automation, development tooling, and creative technical solutions. He is currently open to work and available for employment and consulting engagements.
Contact: Through the website above or connect on professional networks.