Skip to content

Instantly share code, notes, and snippets.

@lukakostic
Last active May 17, 2024 06:30
Show Gist options
  • Save lukakostic/2f8008f61f2c4c80a43d9465ad1d9c47 to your computer and use it in GitHub Desktop.
Save lukakostic/2f8008f61f2c4c80a43d9465ad1d9c47 to your computer and use it in GitHub Desktop.
Python screencap and screenshot gui tool
#!/usr/bin/env python3
"""
uses following cli tools:
slop - let user area select on screen
maim - cli screenshot tool , uses slop
recordMyDesktop - cli tool to screencap videos
zenity - cli tool for dialogs (save as dialog)
xclip - push things to clipboard from stdin
xdotool - find x window id by PID
###########################
If clipboard ("Copy") was used then image data gets put into clipboard
If tmp or save-as then file path gets put into clipboard (and file saved)
also prints file path to stdout
if clipboard then prints nothing
"""
import subprocess
import time
import tkinter as tk
from tkinter import ttk
RECORDING = False # are we recording a video
RECORDING_PID = None #recordMyDesktop pid
RECORDING_PROC = None #subprocess.Popen instance of r.m.d.
OUT_FILE = None #output file
app = None #tk app
root = None #tk root
def exec(cmd,readOutp=True):
cmd = cmd.strip()
if(readOutp):
result = subprocess.run(cmd, shell=True, text=True, stdout=subprocess.PIPE)
return result.stdout.strip()
else:
result = subprocess.run(cmd, shell=True)
return None
# exec in background, with optional killLine at which we return out
# if killLine empty, we return on first print.
# if program doesnt print anything we are stuck.
def execAsync(cmd,killLine):
proc = subprocess.Popen(cmd,shell=True, text=True,stdout=subprocess.PIPE)
outp = ''
while True:
line = proc.stdout.readline()
if not line:
break
#the real code does filtering here
outp = outp + line
if(line.strip() == killLine or killLine==""): return (outp,proc)
return (outp,proc)
# slop - cli tool to ask user to draw area on screen and return it in stdout
def slop():
out = exec('slop -q -f "%w %h %x %y"')
if(out==''): return None
raw = out
out = tuple(map(lambda x:int(x),out.split(' ')))
return { "width": out[0], "height": out[1], "x":out[2], "y":out[3], "raw":raw }
## exec cmd and push it to background, get its pid and find its window, make it topmost.
def pushTopCmd(cmd):
return f"""{cmd} & pid=$! && sleep 0.2 && wmctrl -i -a `xdotool search --pid "$pid" | tail -1` -b add,above && wait "$pid" """
# zenity - dialogs cli tool
def saveDialog(defName='img'):
return exec(pushTopCmd(f'zenity --file-selection --save --confirm-overwrite --title="Save as" --filename="{defName}"'))
class CaptureApp:
def __init__(self, root):
self.root = root
self.root.title("MyScreenshot")
self.root.geometry("400x300")
self.root.resizable(False, False)
self.center_window()
self.create_widgets()
def center_window(self):
window_width = 400
window_height = 300
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
position_top = int(screen_height / 2 - window_height / 2)
position_right = int(screen_width / 2 - window_width / 2)
self.root.geometry(f'{window_width}x{window_height}+{position_right}+{position_top}')
def create_widgets(self):
self.mode_var = tk.StringVar(value="Screenshot")
self.area_var = tk.StringVar(value="Area")
self.action_var = tk.StringVar(value="Copy")
self.framerate_var = tk.IntVar(value=20)
# Common button style
button_style = {
"font": ("Arial", 12),
"relief": "solid",
"bd": 2,
"bg": "#f0f0f0",
"activebackground": "#d9d9d9",
"width": 10,
"height": 2,
}
# Screenshot / Video Buttons
self.mode_frame = tk.Frame(self.root)
self.mode_frame.pack(fill=tk.X, pady=4)
self.screenshot_button = tk.Radiobutton(self.mode_frame, text="Screenshot", variable=self.mode_var, value="Screenshot", command=self.toggle_video_options, **button_style)
self.screenshot_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.video_button = tk.Radiobutton(self.mode_frame, text="Video", variable=self.mode_var, value="Video", command=self.toggle_video_options, **button_style)
self.video_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Area / Window / Fullscreen Buttons
self.area_frame = tk.Frame(self.root)
self.area_frame.pack(fill=tk.X, pady=4)
self.area_button = tk.Radiobutton(self.area_frame, text="Area", variable=self.area_var, value="Area", **button_style)
self.area_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
#self.window_button = tk.Radiobutton(self.area_frame, text="Window", variable=self.area_var, value="Window", **button_style)
#self.window_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.fullscreen_button = tk.Radiobutton(self.area_frame, text="Fullscreen", variable=self.area_var, value="Fullscreen", **button_style)
self.fullscreen_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Copy / File / Save as
self.action_frame = tk.Frame(self.root)
self.action_frame.pack(fill=tk.X, pady=4)
self.copy_button = tk.Radiobutton(self.action_frame, text="Clipboard", variable=self.action_var, value="Copy", **button_style)
self.copy_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.file_button = tk.Radiobutton(self.action_frame, text="Tmp file", variable=self.action_var, value="Tmp", **button_style)
self.file_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.save_button = tk.Radiobutton(self.action_frame, text="Save as", variable=self.action_var, value="Save", **button_style)
self.save_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Framerate Dropdown
self.framerate_frame = tk.Frame(self.root)
self.framerate_label = tk.Label(self.framerate_frame, text="Framerate:", font=("Arial", 12))
self.framerate_label.pack(side=tk.LEFT, padx=5)
self.framerate_dropdown = ttk.Combobox(self.framerate_frame, textvariable=self.framerate_var, values=[15, 20, 25, 30, 45, 50, 60])
self.framerate_dropdown.pack(side=tk.LEFT, padx=5)
self.framerate_frame.pack(pady=10)
self.framerate_frame.pack_forget() # Hide initially
# Capture Button
self.capture_button = tk.Button(self.root, text="CAPTURE", font=("Arial", 16), bg="green", fg="white", height=2, command=self.capture_action)
self.capture_button.pack(pady=10, fill=tk.X)
def toggle_video_options(self):
if self.mode_var.get() == "Video":
self.framerate_frame.pack(pady=10)
else:
self.framerate_frame.pack_forget()
def GoQuit(self):
if RECORDING:
outp = execAsync(f'kill -2 "{RECORDING_PID}" && sleep 1 && kill -9 "{RECORDING_PID}"',"")
time.sleep(0.2)
#while True:
# line = RECORDING_PROC.stdout.readline()
# if not line:
# break
# print(f"!!!{line}!!!")
exec(f'echo -n "{OUT_FILE}" | xclip -selection c',False)
if(OUT_FILE): print(OUT_FILE)
self.root.destroy()
def capture_action(self):
global RECORDING, RECORDING_PID, OUT_FILE, RECORDING_PROC
if RECORDING:
self.GoQuit()
return
scOrVideo = self.mode_var.get()
sizeMode = self.area_var.get()
fileAction = self.action_var.get()
fps = self.framerate_var.get()
if(scOrVideo == 'Video' and fileAction == 'Copy'):
exec(pushTopCmd('zenity --error --text="Cannot record video to clipboard."'),False)
return
if(fileAction == 'Tmp'):
OUT_FILE = exec('mktemp')
if(OUT_FILE==''):
return
elif(fileAction == 'Save'):
OUT_FILE = saveDialog()
if(OUT_FILE==''):
return
self.root.withdraw()
time.sleep(0.1)
area = None
if(sizeMode == 'Area'):
area = slop()
if(area is None):
self.root.deiconify()
return
if(scOrVideo=='Screenshot'):
cmd = 'maim '
if(sizeMode == 'Area'):
cmd = cmd + f"--geometry={area['width']}x{area['height']}+{area['x']}+{area['y']} "
if(fileAction != 'Copy'):
cmd = cmd + f'| tee "{OUT_FILE}" '
#else:
cmd = cmd + " | xclip -selection clipboard -t image/png "
exec(cmd,False)
elif(scOrVideo=='Video'):
if(OUT_FILE.endswith('.ogv') == False):
OUT_FILE = OUT_FILE + '.ogv'
cmd = f"recordmydesktop --no-frame --overwrite --on-the-fly-encoding --fps {fps} "
if(sizeMode == 'Area'):
cmd = cmd + f"-x {area['x']} -y {area['y']} --width {area['width']} --height {area['height']} "
if(fileAction == 'Copy'):
raise Exception("Cannot copy a video into clipboard")
return
else:
cmd = cmd + ' -o "'+OUT_FILE+'" '
RECORDING = True
cmd = cmd + ' & pid=$! && echo "$pid" '
self.root.deiconify()
outp = execAsync(cmd,"")
RECORDING_PID = outp[0]
RECORDING_PROC = outp[1]
self.capture_button.configure(text="STOP RECORDING")
if(OUT_FILE): print(OUT_FILE)
print(f"PID!!:{RECORDING_PID}")
return
#RECORDING = False
self.GoQuit()
def keyPress(event):
if(event.keysym == 'Return'):
app.capture_action()
if(event.keysym == 'Escape'):
app.GoQuit()
#print(event.keysym)
if __name__ == "__main__":
root = tk.Tk()
app = CaptureApp(root)
root.bind("<Key>", keyPress)
root.mainloop()
@lukakostic
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment