mirror of
https://github.com/Keyitdev/dotfiles.git
synced 2026-11-08 04:30:16 +00:00
476 lines
14 KiB
Bash
Executable File
476 lines
14 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
# sudo pacman -S --needed sway grim slurp wf-recorder wl-clipboard jq ffmpeg libnotify libpulse procps-ng python-gobject gtk3 gtk-layer-shell
|
|
|
|
[ -z "$WAYLAND_DISPLAY" ] && echo "ERROR: Wayland session required" && exit 1
|
|
|
|
# screenshot directory
|
|
screenshot_directory="$HOME/Pictures/screenshots"
|
|
videos_directory="$HOME/Videos"
|
|
audio_directory="$HOME/Music"
|
|
|
|
mkdir -p "$screenshot_directory"
|
|
mkdir -p "$videos_directory"
|
|
mkdir -p "$audio_directory"
|
|
|
|
filename=$(date '+%Y-%m-%d_%H-%M-%S_pc') #2025-08-10_18-53-58
|
|
|
|
# h264_nvenc libx264rgb
|
|
VIDEO_CODEC="libx264rgb"
|
|
FRAMERATE=60
|
|
BORDER_WIDTH=2
|
|
BORDER_COLOR="#7F8C8D"
|
|
|
|
desktop="$(pactl get-default-sink).monitor"
|
|
output="$(swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name')"
|
|
|
|
window_boxes(){
|
|
swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | "\(.rect.x + .window_rect.x),\(.rect.y + .window_rect.y) \(.window_rect.width)x\(.window_rect.height)"'
|
|
}
|
|
|
|
slurp(){
|
|
window_boxes | command slurp -c "#FFFFFFFF" -d "$@"
|
|
}
|
|
|
|
select_area(){
|
|
python3 - "$BORDER_WIDTH" "$BORDER_COLOR" "$(window_boxes)" << 'EOF'
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import gi
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version("GtkLayerShell", "0.1")
|
|
gi.require_foreign("cairo")
|
|
import cairo
|
|
from gi.repository import Gtk, Gdk, GLib, GtkLayerShell
|
|
|
|
width = int(sys.argv[1])
|
|
color = Gdk.RGBA()
|
|
color.parse(sys.argv[2])
|
|
|
|
boxes = []
|
|
for line in sys.argv[3].splitlines():
|
|
try:
|
|
position, size = line.split(" ")
|
|
box_x, box_y = map(int, position.split(","))
|
|
box_w, box_h = map(int, size.split("x"))
|
|
boxes.append((box_x, box_y, box_w, box_h))
|
|
except ValueError:
|
|
pass
|
|
|
|
state = {"start": None, "dragged": False, "selection": None, "size": None, "pending": None, "result": None}
|
|
condition = threading.Condition()
|
|
windows = []
|
|
|
|
def redraw():
|
|
for window in windows:
|
|
window.queue_draw()
|
|
|
|
def box_at(px, py):
|
|
found = [b for b in boxes if b[0] <= px < b[0] + b[2] and b[1] <= py < b[1] + b[3]]
|
|
return min(found, key=lambda b: b[2] * b[3]) if found else None
|
|
|
|
def set_selection(selection):
|
|
if selection == state["selection"]:
|
|
return
|
|
state["selection"] = selection
|
|
state["size"] = None
|
|
if selection:
|
|
with condition:
|
|
state["pending"] = selection
|
|
condition.notify()
|
|
redraw()
|
|
|
|
def apply_size(selection, size):
|
|
if selection == state["selection"]:
|
|
state["size"] = size
|
|
redraw()
|
|
return False
|
|
|
|
def measure():
|
|
while True:
|
|
with condition:
|
|
while state["pending"] is None:
|
|
condition.wait()
|
|
selection = state["pending"]
|
|
state["pending"] = None
|
|
x, y, w, h = selection
|
|
size = 0
|
|
if w > 0 and h > 0:
|
|
try:
|
|
size = len(subprocess.run(["grim", "-g", f"{x},{y} {w}x{h}", "-"], capture_output=True, check=True).stdout)
|
|
except (subprocess.CalledProcessError, OSError):
|
|
size = None
|
|
GLib.idle_add(apply_size, selection, size)
|
|
|
|
def format_size(size):
|
|
if size is None:
|
|
return "..."
|
|
if size >= 1000000000:
|
|
decimal = f"{size / 1000000000:.2f} GB"
|
|
elif size >= 1000000:
|
|
decimal = f"{size / 1000000:.1f} MB"
|
|
else:
|
|
decimal = f"{size / 1000:.1f} KB"
|
|
if size >= 1073741824:
|
|
binary = f"{size / 1073741824:.2f} GiB"
|
|
elif size >= 1048576:
|
|
binary = f"{size / 1048576:.1f} MiB"
|
|
else:
|
|
binary = f"{size / 1024:.1f} KiB"
|
|
return f"{decimal} ({binary})"
|
|
|
|
def draw(widget, cr, geometry):
|
|
cr.set_operator(cairo.OPERATOR_SOURCE)
|
|
cr.set_source_rgba(0, 0, 0, 0)
|
|
cr.paint()
|
|
cr.set_operator(cairo.OPERATOR_OVER)
|
|
|
|
selection = state["selection"]
|
|
cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
|
|
cr.set_source_rgba(0, 0, 0, 0.25)
|
|
cr.rectangle(0, 0, geometry.width, geometry.height)
|
|
if selection:
|
|
cr.rectangle(selection[0] - geometry.x, selection[1] - geometry.y, selection[2], selection[3])
|
|
cr.fill()
|
|
|
|
if not selection:
|
|
return True
|
|
|
|
x, y, w, h = selection
|
|
x -= geometry.x
|
|
y -= geometry.y
|
|
|
|
Gdk.cairo_set_source_rgba(cr, color)
|
|
cr.set_line_width(width)
|
|
cr.rectangle(x - width / 2, y - width / 2, w + width, h + width)
|
|
cr.stroke()
|
|
|
|
if not (0 <= x + w - 1 < geometry.width and 0 <= y + h - 1 < geometry.height):
|
|
return True
|
|
|
|
text = f"{w}x{h} {format_size(state['size'])}"
|
|
cr.select_font_face("monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
|
cr.set_font_size(14)
|
|
ascent, descent = cr.font_extents()[:2]
|
|
padding = 6
|
|
box_width = cr.text_extents(text).x_advance + padding * 2
|
|
box_height = ascent + descent + padding * 2
|
|
|
|
box_x = min(max(x + w + width - box_width, 0), geometry.width - box_width)
|
|
box_y = y + h + width + 4
|
|
if box_y + box_height > geometry.height:
|
|
box_y = max(y - width - 4 - box_height, 0)
|
|
|
|
cr.set_source_rgba(0, 0, 0, 0.6)
|
|
cr.rectangle(box_x, box_y, box_width, box_height)
|
|
cr.fill()
|
|
cr.set_source_rgba(1, 1, 1, 1)
|
|
cr.move_to(box_x + padding, box_y + padding + ascent)
|
|
cr.show_text(text)
|
|
return True
|
|
|
|
def finish():
|
|
selection = state["selection"]
|
|
state["start"] = None
|
|
if not selection or selection[2] <= 0 or selection[3] <= 0:
|
|
return
|
|
state["result"] = selection
|
|
Gtk.main_quit()
|
|
|
|
def press(widget, event, geometry):
|
|
if event.button == 3:
|
|
Gtk.main_quit()
|
|
return True
|
|
if event.button != 1:
|
|
return True
|
|
state["start"] = (int(event.x) + geometry.x, int(event.y) + geometry.y)
|
|
state["dragged"] = False
|
|
return True
|
|
|
|
def motion(widget, event, geometry):
|
|
px = int(event.x) + geometry.x
|
|
py = int(event.y) + geometry.y
|
|
if state["start"]:
|
|
sx, sy = state["start"]
|
|
if state["dragged"] or abs(px - sx) > 2 or abs(py - sy) > 2:
|
|
state["dragged"] = True
|
|
set_selection((min(sx, px), min(sy, py), abs(px - sx), abs(py - sy)))
|
|
else:
|
|
set_selection(box_at(px, py))
|
|
return True
|
|
|
|
def release(widget, event, geometry):
|
|
if event.button != 1 or not state["start"]:
|
|
return True
|
|
if not state["dragged"]:
|
|
set_selection(box_at(int(event.x) + geometry.x, int(event.y) + geometry.y))
|
|
finish()
|
|
return True
|
|
|
|
def key(widget, event):
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
Gtk.main_quit()
|
|
return True
|
|
|
|
threading.Thread(target=measure, daemon=True).start()
|
|
|
|
display = Gdk.Display.get_default()
|
|
for index in range(display.get_n_monitors()):
|
|
monitor = display.get_monitor(index)
|
|
geometry = monitor.get_geometry()
|
|
|
|
window = Gtk.Window()
|
|
GtkLayerShell.init_for_window(window)
|
|
GtkLayerShell.set_namespace(window, "region-select")
|
|
GtkLayerShell.set_layer(window, GtkLayerShell.Layer.OVERLAY)
|
|
GtkLayerShell.set_monitor(window, monitor)
|
|
GtkLayerShell.set_exclusive_zone(window, -1)
|
|
for edge in (GtkLayerShell.Edge.TOP, GtkLayerShell.Edge.BOTTOM, GtkLayerShell.Edge.LEFT, GtkLayerShell.Edge.RIGHT):
|
|
GtkLayerShell.set_anchor(window, edge, True)
|
|
try:
|
|
GtkLayerShell.set_keyboard_mode(window, GtkLayerShell.KeyboardMode.EXCLUSIVE)
|
|
except AttributeError:
|
|
GtkLayerShell.set_keyboard_interactivity(window, True)
|
|
|
|
window.set_app_paintable(True)
|
|
window.set_visual(window.get_screen().get_rgba_visual())
|
|
window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.KEY_PRESS_MASK)
|
|
|
|
window.connect("draw", draw, geometry)
|
|
window.connect("button-press-event", press, geometry)
|
|
window.connect("motion-notify-event", motion, geometry)
|
|
window.connect("button-release-event", release, geometry)
|
|
window.connect("key-press-event", key)
|
|
window.connect("destroy", Gtk.main_quit)
|
|
window.show_all()
|
|
window.get_window().set_cursor(Gdk.Cursor.new_from_name(display, "crosshair"))
|
|
windows.append(window)
|
|
|
|
Gtk.main()
|
|
|
|
for window in windows:
|
|
window.destroy()
|
|
display.flush()
|
|
|
|
if not state["result"]:
|
|
sys.exit(1)
|
|
|
|
x, y, w, h = state["result"]
|
|
print(f"{x},{y} {w}x{h}")
|
|
EOF
|
|
}
|
|
|
|
show_border(){
|
|
python3 - "$1" "$BORDER_WIDTH" "$BORDER_COLOR" "$2" << 'EOF' &
|
|
import os
|
|
import sys
|
|
import time
|
|
import gi
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version("GtkLayerShell", "0.1")
|
|
gi.require_foreign("cairo")
|
|
import cairo
|
|
from gi.repository import Gtk, Gdk, GLib, GtkLayerShell
|
|
|
|
position, size = sys.argv[1].split(" ")
|
|
x, y = map(int, position.split(","))
|
|
w, h = map(int, size.split("x"))
|
|
width = int(sys.argv[2])
|
|
color = Gdk.RGBA()
|
|
color.parse(sys.argv[3])
|
|
path = sys.argv[4]
|
|
start = time.monotonic()
|
|
|
|
monitor = Gdk.Display.get_default().get_monitor_at_point(x, y)
|
|
geometry = monitor.get_geometry()
|
|
x -= geometry.x
|
|
y -= geometry.y
|
|
|
|
window = Gtk.Window()
|
|
GtkLayerShell.init_for_window(window)
|
|
GtkLayerShell.set_namespace(window, "region-border")
|
|
GtkLayerShell.set_layer(window, GtkLayerShell.Layer.OVERLAY)
|
|
GtkLayerShell.set_monitor(window, monitor)
|
|
GtkLayerShell.set_exclusive_zone(window, -1)
|
|
for edge in (GtkLayerShell.Edge.TOP, GtkLayerShell.Edge.BOTTOM, GtkLayerShell.Edge.LEFT, GtkLayerShell.Edge.RIGHT):
|
|
GtkLayerShell.set_anchor(window, edge, True)
|
|
|
|
window.set_app_paintable(True)
|
|
window.set_visual(window.get_screen().get_rgba_visual())
|
|
|
|
def draw(widget, cr):
|
|
cr.set_operator(cairo.OPERATOR_SOURCE)
|
|
cr.set_source_rgba(0, 0, 0, 0)
|
|
cr.paint()
|
|
cr.set_operator(cairo.OPERATOR_OVER)
|
|
Gdk.cairo_set_source_rgba(cr, color)
|
|
cr.set_line_width(width)
|
|
cr.rectangle(x - width / 2, y - width / 2, w + width, h + width)
|
|
cr.stroke()
|
|
|
|
try:
|
|
size_bytes = os.path.getsize(path)
|
|
except OSError:
|
|
size_bytes = 0
|
|
|
|
if size_bytes >= 1000000000:
|
|
decimal = f"{size_bytes / 1000000000:.2f} GB"
|
|
elif size_bytes >= 1000000:
|
|
decimal = f"{size_bytes / 1000000:.1f} MB"
|
|
else:
|
|
decimal = f"{size_bytes / 1000:.1f} KB"
|
|
|
|
if size_bytes >= 1073741824:
|
|
binary = f"{size_bytes / 1073741824:.2f} GiB"
|
|
elif size_bytes >= 1048576:
|
|
binary = f"{size_bytes / 1048576:.1f} MiB"
|
|
else:
|
|
binary = f"{size_bytes / 1024:.1f} KiB"
|
|
|
|
elapsed = int(time.monotonic() - start)
|
|
hours, rest = divmod(elapsed, 3600)
|
|
minutes, seconds = divmod(rest, 60)
|
|
timer = f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes:02}:{seconds:02}"
|
|
|
|
text = f"{w}x{h} {decimal} ({binary}) {timer}"
|
|
|
|
cr.select_font_face("monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
|
|
cr.set_font_size(14)
|
|
ascent, descent = cr.font_extents()[:2]
|
|
padding = 6
|
|
box_width = cr.text_extents(text).x_advance + padding * 2
|
|
box_height = ascent + descent + padding * 2
|
|
|
|
box_x = min(max(x + w + width - box_width, 0), geometry.width - box_width)
|
|
box_y = y + h + width + 4
|
|
if box_y + box_height > geometry.height:
|
|
box_y = max(y - width - 4 - box_height, 0)
|
|
|
|
cr.set_source_rgba(0, 0, 0, 0.6)
|
|
cr.rectangle(box_x, box_y, box_width, box_height)
|
|
cr.fill()
|
|
cr.set_source_rgba(1, 1, 1, 1)
|
|
cr.move_to(box_x + padding, box_y + padding + ascent)
|
|
cr.show_text(text)
|
|
return True
|
|
|
|
def tick():
|
|
window.queue_draw()
|
|
return True
|
|
|
|
window.connect("draw", draw)
|
|
window.connect("destroy", Gtk.main_quit)
|
|
window.show_all()
|
|
window.input_shape_combine_region(cairo.Region())
|
|
GLib.timeout_add(500, tick)
|
|
Gtk.main()
|
|
EOF
|
|
border_pid=$!
|
|
}
|
|
|
|
screenshot_selected_area(){
|
|
geometry="$(select_area)" || exit 1
|
|
grim -g "$geometry" "$screenshot_directory/$filename.png"
|
|
wl-copy < "$screenshot_directory/$filename.png"
|
|
notify-send -i "$screenshot_directory/$filename.png" "Screenshot" "Area screenshot taken"
|
|
}
|
|
|
|
screenshot_full_screen(){
|
|
grim "$screenshot_directory/$filename.png"
|
|
wl-copy < "$screenshot_directory/$filename.png"
|
|
notify-send -i "$screenshot_directory/$filename.png" "Screenshot" "Full screen screenshot taken"
|
|
}
|
|
|
|
video_selected_area(){
|
|
geometry="$(slurp)" || exit 1
|
|
show_border "$geometry" "$videos_directory/$filename.mp4"
|
|
wf-recorder -g "$geometry" -c "$VIDEO_CODEC" -r "$FRAMERATE" -f "$videos_directory/$filename.mp4"
|
|
kill "$border_pid"
|
|
grim -g "$geometry" -t png "/tmp/$filename.png"
|
|
notify-send -i "/tmp/$filename.png" "Video" "Area video taken"
|
|
}
|
|
|
|
video_full_screen(){
|
|
wf-recorder -o "$output" -c "$VIDEO_CODEC" -r "$FRAMERATE" -f "$videos_directory/$filename.mp4"
|
|
ffmpeg -i "$videos_directory/$filename.mp4" -ss 00:00:01.000 -vframes 1 "/tmp/$filename.png" 2>/dev/null
|
|
notify-send -i "/tmp/$filename.png" "Video" "Full screen video taken"
|
|
}
|
|
|
|
video_selected_area_audio(){
|
|
geometry="$(slurp)" || exit 1
|
|
show_border "$geometry" "$videos_directory/$filename.mp4"
|
|
wf-recorder -g "$geometry" -c "$VIDEO_CODEC" -r "$FRAMERATE" --audio="$desktop" -f "$videos_directory/$filename.mp4"
|
|
kill "$border_pid"
|
|
grim -g "$geometry" -t png "/tmp/$filename.png"
|
|
notify-send -i "/tmp/$filename.png" "Video" "Area video with desktop audio taken"
|
|
}
|
|
|
|
video_full_screen_audio(){
|
|
wf-recorder -o "$output" -c "$VIDEO_CODEC" -r "$FRAMERATE" --audio="$desktop" -f "$videos_directory/$filename.mp4"
|
|
ffmpeg -i "$videos_directory/$filename.mp4" -ss 00:00:01.000 -vframes 1 "/tmp/$filename.png" 2>/dev/null
|
|
notify-send -i "/tmp/$filename.png" "Video" "Full screen video with desktop audio taken"
|
|
}
|
|
|
|
stop(){
|
|
pkill -fxn '(/\S+)*ffmpeg\s.*\spulse\s.*'
|
|
pkill -fxn '(/\S+)*wf-recorder\s.*'
|
|
pkill -fxn '(/\S+)*pw-record\s.*'
|
|
exit 1
|
|
}
|
|
|
|
help(){
|
|
cat << EOF
|
|
-h | -help | --help
|
|
-s | -stop
|
|
-sa | -screenshot_selected_area
|
|
-sf | -screenshot_full_screen
|
|
-va | -video_selected_area
|
|
-vf | -video_full_screen
|
|
-vaad | -video_selected_area_audio
|
|
-vfad | -video_full_screen_audio
|
|
EOF
|
|
}
|
|
|
|
main(){
|
|
while [ "$1" != "" ]; do
|
|
case "$1" in
|
|
-h|-help|--help)
|
|
help
|
|
exit 0
|
|
;;
|
|
-s|-stop)
|
|
stop
|
|
;;
|
|
-sa|-screenshot_selected_area)
|
|
screenshot_selected_area
|
|
;;
|
|
-sf|-screenshot_full_screen)
|
|
screenshot_full_screen
|
|
;;
|
|
-va|-video_selected_area)
|
|
video_selected_area
|
|
;;
|
|
-vf|-video_full_screen)
|
|
video_full_screen
|
|
;;
|
|
-vaad|-video_selected_area_audio)
|
|
video_selected_area_audio
|
|
;;
|
|
-vfad|-video_full_screen_audio)
|
|
video_full_screen_audio
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown parameter $1"
|
|
help
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
main "$@"
|
|
|
|
|