#!/usr/bin/env python3
"""
Simple screen region selector using tkinter
More reliable than Electron for this use case
"""
import sys
import json
import tkinter as tk
from tkinter import Canvas
class ScreenCapture:
def __init__(self, output_file):
self.output_file = output_file
self.root = tk.Tk()
# Get the virtual screen dimensions (all monitors combined)
self.root.update_idletasks()
# Calculate total screen area covering all monitors
# On Windows, this gets the virtual screen coordinates
try:
# Try to get actual multi-monitor bounds
import ctypes
user32 = ctypes.windll.user32
# Get the virtual screen dimensions
x_min = user32.GetSystemMetrics(76) # SM_XVIRTUALSCREEN
y_min = user32.GetSystemMetrics(77) # SM_YVIRTUALSCREEN
x_max = user32.GetSystemMetrics(78) # SM_CXVIRTUALSCREEN
y_max = user32.GetSystemMetrics(79) # SM_CYVIRTUALSCREEN
# Set window geometry to cover all monitors
self.root.geometry(f"{x_max}x{y_max}+{x_min}+{y_min}")
self.root.overrideredirect(True)
self.virtual_x_offset = x_min
self.virtual_y_offset = y_min
except Exception as e:
# Fallback to fullscreen on primary monitor
print(f"Warning: Could not detect multi-monitor setup: {e}", file=sys.stderr)
self.root.attributes('-fullscreen', True)
self.virtual_x_offset = 0
self.virtual_y_offset = 0
self.root.attributes('-alpha', 0.3)
self.root.attributes('-topmost', True)
self.root.configure(background='black', cursor='crosshair')
self.canvas = Canvas(self.root, cursor='crosshair', bg='black', highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
self.start_x = None
self.start_y = None
self.rect = None
self.region = None
# Dragging state for instruction box
self.drag_data = {'x': 0, 'y': 0, 'item': None}
self.is_dragging_instructions = False
# Instructions with background box for better visibility
# Center on primary monitor (not virtual screen center)
try:
primary_screen_width = user32.GetSystemMetrics(0) # SM_CXSCREEN
center_x = (primary_screen_width // 2) - self.virtual_x_offset
except:
center_x = self.canvas.winfo_screenwidth() // 2
# Create dark background rectangle for instructions
self.instructions_bg = self.canvas.create_rectangle(
center_x - 400,
10,
center_x + 400,
90,
fill='#1a1a1a',
outline='#00a8ff',
width=3,
tags='instructions'
)
self.instructions = self.canvas.create_text(
center_x,
50,
text="Click and drag to select region. Press Enter to capture, ESC to cancel.\n(Drag this box to move it)",
fill='#ffffff',
font=('Arial', 14, 'bold'),
tags='instructions'
)
# Bind selection events to canvas first (lower priority)
self.canvas.bind('<Button-1>', self.on_press)
self.canvas.bind('<B1-Motion>', self.on_drag)
self.canvas.bind('<ButtonRelease-1>', self.on_release)
# Bind drag events to instruction elements (higher priority, will override canvas bindings)
self.canvas.tag_bind('instructions', '<Button-1>', self.on_instructions_press)
self.canvas.tag_bind('instructions', '<B1-Motion>', self.on_instructions_drag)
self.canvas.tag_bind('instructions', '<ButtonRelease-1>', self.on_instructions_release)
# Keyboard shortcuts
self.root.bind('<Return>', self.on_capture)
self.root.bind('<Escape>', self.on_cancel)
def on_instructions_press(self, event):
"""Record starting position for dragging the instruction box"""
self.is_dragging_instructions = True
self.drag_data['x'] = event.x
self.drag_data['y'] = event.y
self.canvas.config(cursor='fleur') # Change cursor to move cursor
return "break" # Stop event propagation to canvas handlers
def on_instructions_drag(self, event):
"""Move the instruction box"""
if self.is_dragging_instructions:
delta_x = event.x - self.drag_data['x']
delta_y = event.y - self.drag_data['y']
self.canvas.move('instructions', delta_x, delta_y)
self.drag_data['x'] = event.x
self.drag_data['y'] = event.y
return "break" # Stop event propagation to canvas handlers
def on_instructions_release(self, event):
"""Stop dragging the instruction box"""
self.is_dragging_instructions = False
self.canvas.config(cursor='crosshair')
return "break" # Stop event propagation to canvas handlers
def on_press(self, event):
# Check if clicking on instructions - if so, don't start selection
items = self.canvas.find_overlapping(event.x, event.y, event.x, event.y)
for item in items:
if 'instructions' in self.canvas.gettags(item):
return # Don't start selection on instruction box
# Start region selection
self.start_x = event.x
self.start_y = event.y
if self.rect:
self.canvas.delete(self.rect)
self.rect = self.canvas.create_rectangle(
self.start_x, self.start_y, self.start_x, self.start_y,
outline='#00a8ff', width=2
)
def on_drag(self, event):
# Don't update if we're dragging instructions
if self.is_dragging_instructions:
return
# Update region selection
if self.rect:
self.canvas.coords(self.rect, self.start_x, self.start_y, event.x, event.y)
def on_release(self, event):
# Don't finalize if we were dragging instructions
if self.is_dragging_instructions:
return
# Finalize region selection
if self.rect:
x1, y1, x2, y2 = self.canvas.coords(self.rect)
canvas_x = int(min(x1, x2))
canvas_y = int(min(y1, y2))
width = int(abs(x2 - x1))
height = int(abs(y2 - y1))
# The window is positioned at (virtual_x_offset, virtual_y_offset)
# and the canvas fills the window, so canvas (0,0) corresponds to
# screen position (virtual_x_offset, virtual_y_offset).
# Therefore, canvas coordinates ARE already absolute screen coordinates!
# But screenshot-desktop expects coordinates relative to the virtual screen origin,
# which is where canvas (0,0) is, so we use canvas coords directly.
self.region = {
'x': canvas_x,
'y': canvas_y,
'width': width,
'height': height
}
print(f"Region selected: {width}x{height} at ({canvas_x}, {canvas_y})", file=sys.stderr)
def on_capture(self, event=None):
if not self.region:
print("No region selected. Please drag to select a region first.", file=sys.stderr)
return
if self.region['width'] <= 5 or self.region['height'] <= 5:
print(f"Region too small ({self.region['width']}x{self.region['height']}). Please select a larger area.", file=sys.stderr)
return
print(f"Capturing region: {self.region['width']}x{self.region['height']} at ({self.region['x']}, {self.region['y']})", file=sys.stderr)
with open(self.output_file, 'w') as f:
json.dump({'region': self.region, 'cancelled': False}, f)
# Close the overlay after successful capture
self.root.destroy()
def on_cancel(self, event=None):
print("Capture cancelled by user", file=sys.stderr)
with open(self.output_file, 'w') as f:
json.dump({'region': None, 'cancelled': True}, f)
self.root.destroy()
def run(self):
self.root.mainloop()
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: overlay.py <output_file>", file=sys.stderr)
sys.exit(1)
output_file = sys.argv[1]
app = ScreenCapture(output_file)
app.run()