waffles: screen snip

This commit is contained in:
end-4
2025-12-20 11:46:07 +01:00
parent 169b24bea5
commit 8842df6340
34 changed files with 976 additions and 113 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000">
<path d="M17.5 12C20.5376 12 23 14.4624 23 17.5C23 20.5376 20.5376 23 17.5 23C14.4624 23 12 20.5376 12 17.5C12 14.4624 14.4624 12 17.5 12ZM21 8.5L21.0012 12.0226C19.9907 11.3753 18.7892 11 17.5 11C13.9101 11 11 13.9101 11 17.5C11 18.7892 11.3753 19.9907 12.0226 21.0012L6.25 21C4.45507 21 3 19.5449 3 17.75V8.5H21ZM17.5 14L17.4101 14.0081C17.206 14.0451 17.0451 14.206 17.0081 14.4101L17 14.5V17H14.5L14.4101 17.0081C14.206 17.0451 14.0451 17.206 14.0081 17.4101L14 17.5L14.0081 17.5899C14.0451 17.794 14.206 17.9549 14.4101 17.9919L14.5 18H17V20.5L17.0081 20.5899C17.0451 20.794 17.206 20.9549 17.4101 20.9919L17.5 21L17.5899 20.9919C17.794 20.9549 17.9549 20.794 17.9919 20.5899L18 20.5V18H20.5L20.5899 17.9919C20.794 17.9549 20.9549 17.794 20.9919 17.5899L21 17.5L20.9919 17.4101C20.9549 17.206 20.794 17.0451 20.5899 17.0081L20.5 17H18V14.5L17.9919 14.4101C17.9549 14.206 17.794 14.0451 17.5899 14.0081L17.5 14ZM17.75 3C19.5449 3 21 4.45507 21 6.25V7H3V6.25C3 4.45507 4.45507 3 6.25 3H17.75Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000">
<path d="M17.5 12C20.5376 12 23 14.4624 23 17.5C23 20.5376 20.5376 23 17.5 23C14.4624 23 12 20.5376 12 17.5C12 14.4624 14.4624 12 17.5 12ZM17.75 3C19.5449 3 21 4.45507 21 6.25L21.0012 12.0226C20.5378 11.7258 20.0342 11.4861 19.5004 11.3136L19.5 8.5H4.5V17.75C4.5 18.7165 5.2835 19.5 6.25 19.5L11.3136 19.5004C11.4861 20.0342 11.7258 20.5378 12.0226 21.0012L6.25 21C4.45507 21 3 19.5449 3 17.75V6.25C3 4.45507 4.45507 3 6.25 3H17.75ZM17.5 14L17.4101 14.0081C17.206 14.0451 17.0451 14.206 17.0081 14.4101L17 14.5V17H14.5L14.4101 17.0081C14.206 17.0451 14.0451 17.206 14.0081 17.4101L14 17.5L14.0081 17.5899C14.0451 17.794 14.206 17.9549 14.4101 17.9919L14.5 18H17V20.5L17.0081 20.5899C17.0451 20.794 17.206 20.9549 17.4101 20.9919L17.5 21L17.5899 20.9919C17.794 20.9549 17.9549 20.794 17.9919 20.5899L18 20.5V18H20.5L20.5899 17.9919C20.794 17.9549 20.9549 17.794 20.9919 17.5899L21 17.5L20.9919 17.4101C20.9549 17.206 20.794 17.0451 20.5899 17.0081L20.5 17H18V14.5L17.9919 14.4101C17.9549 14.206 17.794 14.0451 17.5899 14.0081L17.5 14ZM17.75 4.5H6.25C5.2835 4.5 4.5 5.2835 4.5 6.25V7H19.5V6.25C19.5 5.2835 18.7165 4.5 17.75 4.5Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.925 2.503a2.25 2.25 0 0 1 1.94 1.11L16.679 5h2.071A3.25 3.25 0 0 1 22 8.25v9.5A3.25 3.25 0 0 1 18.75 21H5.25A3.25 3.25 0 0 1 2 17.75v-9.5A3.25 3.25 0 0 1 5.25 5h2.08l.875-1.424a2.25 2.25 0 0 1 1.917-1.073h3.803ZM12 8a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Zm0 1.5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 420 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.925 2.503a2.25 2.25 0 0 1 1.94 1.11L16.679 5h2.071A3.25 3.25 0 0 1 22 8.25v9.5A3.25 3.25 0 0 1 18.75 21H5.25A3.25 3.25 0 0 1 2 17.75v-9.5A3.25 3.25 0 0 1 5.25 5h2.08l.875-1.424a2.25 2.25 0 0 1 1.917-1.073h3.803Zm0 1.5h-3.803a.75.75 0 0 0-.574.268l-.065.09L8.39 6.141a.75.75 0 0 1-.639.358h-2.5A1.75 1.75 0 0 0 3.5 8.25v9.5c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75v-9.5a1.75 1.75 0 0 0-1.75-1.75h-2.5a.75.75 0 0 1-.647-.37l-1.032-1.757a.75.75 0 0 0-.646-.37ZM12 8a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm0 1.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 682 B

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000">
<path d="M7 15.5C7 16.2797 7.59489 16.9204 8.35554 16.9931L8.5 17H21C21.5523 17 22 17.4477 22 18C22 18.5128 21.614 18.9355 21.1166 18.9933L21 19H19V21C19 21.5523 18.5523 22 18 22C17.4872 22 17.0645 21.614 17.0067 21.1166L17 21V19H8.5C6.63144 19 5.10487 17.5357 5.00518 15.692L5 15.5V7H3C2.44772 7 2 6.55228 2 6C2 5.48716 2.38604 5.06449 2.88338 5.00673L3 5H5V3C5 2.44772 5.44772 2 6 2C6.51284 2 6.93551 2.38604 6.99327 2.88338L7 3V15.5ZM8 5H15.5C17.3686 5 18.8951 6.46428 18.9948 8.30796L19 8.5V16H17V8.5C17 7.7203 16.4051 7.07955 15.6445 7.00687L15.5 7H8V5Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 721 B

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000">
<path d="M21.25 17C21.6642 17 22 17.3358 22 17.75C22 18.1297 21.7178 18.4435 21.3518 18.4932L21.25 18.5H18.5V21.25C18.5 21.6642 18.1642 22 17.75 22C17.3703 22 17.0565 21.7178 17.0068 21.3518L17 21.25V18.5H8.75C7.01697 18.5 5.60075 17.1435 5.50514 15.4344L5.5 15.25L5.499 7H2.75C2.33579 7 2 6.66421 2 6.25C2 5.8703 2.28215 5.55651 2.64823 5.50685L2.75 5.5H5.499L5.5 2.75C5.5 2.33579 5.83579 2 6.25 2C6.6297 2 6.94349 2.28215 6.99315 2.64823L7 2.75L6.999 5.5H7V7H6.999L7 15.25C7 16.1682 7.70711 16.9212 8.60647 16.9942L8.75 17H21.25ZM8 5.5H15.25C16.983 5.5 18.3992 6.85646 18.4949 8.56558L18.5 8.75V16H17V8.75C17 7.83183 16.2929 7.07881 15.3935 7.0058L15.25 7H8V5.5Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 827 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m11.557 13.646-.083.071-6.927 6.8a3.234 3.234 0 0 0 1.703.481h4.914l.356-1.423c.162-.648.497-1.24.97-1.712l2.11-2.11-2.075-2.036-.094-.078a.75.75 0 0 0-.873.007Zm4.946-5.394a.752.752 0 1 0-1.504 0 .752.752 0 0 0 1.504 0Zm-.843 6.441-2.085-2.046-.128-.117a2.25 2.25 0 0 0-2.888-.006l-.136.123-6.938 6.81A3.234 3.234 0 0 1 3 17.75v-11.5A3.25 3.25 0 0 1 6.25 3h11.499a3.25 3.25 0 0 1 3.25 3.25v4.761a3.279 3.279 0 0 0-2.608.95l-2.731 2.732ZM13.5 8.252a2.252 2.252 0 1 0 4.503 0 2.252 2.252 0 0 0-4.504 0Zm5.598 4.417-5.901 5.901a2.685 2.685 0 0 0-.707 1.248l-.457 1.83c-.2.797.522 1.518 1.318 1.319l1.83-.458a2.685 2.685 0 0 0 1.248-.706L22.33 15.9a2.286 2.286 0 0 0-3.233-3.232Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 804 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20.998 6.25A3.25 3.25 0 0 0 17.748 3H6.25A3.25 3.25 0 0 0 3 6.25v11.499a3.25 3.25 0 0 0 3.25 3.25h4.914l.356-1.424.02-.076H6.25c-.204 0-.4-.035-.582-.1l5.807-5.685.083-.07a.75.75 0 0 1 .966.07l2.079 2.036 1.06-1.06-2.09-2.048-.128-.116a2.25 2.25 0 0 0-3.02.116l-5.822 5.7a1.746 1.746 0 0 1-.103-.593v-11.5c0-.966.783-1.75 1.75-1.75h11.499c.966 0 1.75.784 1.75 1.75v4.983c.478-.19.993-.264 1.5-.22V6.25Zm-3.495 2.502a2.252 2.252 0 1 0-4.504 0 2.252 2.252 0 0 0 4.504 0Zm-3.004 0a.752.752 0 1 1 1.504 0 .752.752 0 0 1-1.504 0Zm4.6 3.917-5.902 5.901a2.685 2.685 0 0 0-.707 1.248l-.457 1.83c-.2.797.522 1.518 1.318 1.319l1.83-.458a2.685 2.685 0 0 0 1.248-.706L22.33 15.9a2.286 2.286 0 0 0-3.233-3.232Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 826 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M2.747 15a.75.75 0 0 1 .743.648l.007.102v3.502l.007.128a1.25 1.25 0 0 0 1.115 1.116l.128.006h3.5l.102.007a.75.75 0 0 1 0 1.486l-.102.007h-3.5l-.167-.005a2.75 2.75 0 0 1-2.578-2.57l-.005-.175V15.75l.007-.102A.75.75 0 0 1 2.747 15Zm18.5 0a.75.75 0 0 1 .743.648l.007.102v3.502a2.75 2.75 0 0 1-2.582 2.745l-.168.005h-3.5a.75.75 0 0 1-.102-1.493l.102-.007h3.5a1.25 1.25 0 0 0 1.244-1.122l.006-.128V15.75a.75.75 0 0 1 .75-.75ZM12 15a1 1 0 0 1 .117 1.993L12 17H8a1 1 0 0 1-.116-1.993L8 15h4Zm4-4a1 1 0 0 1 .117 1.993L16 13H8a1 1 0 0 1-.116-1.993L8 11h8ZM8.247 2a.75.75 0 0 1 .102 1.493l-.102.007h-3.5a1.25 1.25 0 0 0-1.243 1.122l-.007.128v3.502a.75.75 0 0 1-1.493.102l-.007-.102V4.75A2.75 2.75 0 0 1 4.58 2.005L4.747 2h3.5Zm11 0 .168.005a2.75 2.75 0 0 1 2.577 2.57l.005.175v3.502l-.007.102a.75.75 0 0 1-1.486 0l-.007-.102V4.75l-.006-.128a1.25 1.25 0 0 0-1.116-1.116l-.128-.006h-3.5l-.102-.007a.75.75 0 0 1 0-1.486L15.747 2h3.5ZM16 7a1 1 0 0 1 .117 1.993L16 9H8a1 1 0 0 1-.116-1.993L8 7h8Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M2.747 15a.75.75 0 0 1 .743.648l.007.102v3.502l.007.128a1.25 1.25 0 0 0 1.115 1.116l.128.006h3.5l.102.007a.75.75 0 0 1 0 1.486l-.102.007h-3.5l-.167-.005a2.75 2.75 0 0 1-2.578-2.57l-.005-.175V15.75l.007-.102A.75.75 0 0 1 2.747 15Zm18.5 0a.75.75 0 0 1 .743.648l.007.102v3.502a2.75 2.75 0 0 1-2.582 2.745l-.168.005h-3.5a.75.75 0 0 1-.102-1.493l.102-.007h3.5a1.25 1.25 0 0 0 1.244-1.122l.006-.128V15.75a.75.75 0 0 1 .75-.75Zm-8.992.5a.75.75 0 0 1 .102 1.493l-.102.007H7.75a.75.75 0 0 1-.102-1.493l.102-.007h4.505Zm3.995-4.25a.75.75 0 0 1 .102 1.493l-.102.007h-8.5a.75.75 0 0 1-.102-1.493l.102-.007h8.5ZM8.247 2a.75.75 0 0 1 .102 1.493l-.102.007h-3.5a1.25 1.25 0 0 0-1.243 1.122l-.007.128v3.502a.75.75 0 0 1-1.493.102l-.007-.102V4.75A2.75 2.75 0 0 1 4.58 2.005L4.747 2h3.5Zm11 0 .168.005a2.75 2.75 0 0 1 2.577 2.57l.005.175v3.502l-.007.102a.75.75 0 0 1-1.486 0l-.007-.102V4.75l-.006-.128a1.25 1.25 0 0 0-1.116-1.116l-.128-.006h-3.5l-.102-.007a.75.75 0 0 1 0-1.486L15.747 2h3.5ZM16.25 7a.75.75 0 0 1 .102 1.493l-.102.007h-8.5a.75.75 0 0 1-.102-1.493L7.75 7h8.5Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000">
<path d="M3 6.5C3 4.567 4.567 3 6.5 3H9C9.55228 3 10 3.44772 10 4C10 4.55228 9.55228 5 9 5H6.5C5.67157 5 5 5.67157 5 6.5V9C5 9.55229 4.55228 10 4 10C3.44772 10 3 9.55229 3 9V6.5ZM21 17.5C21 19.433 19.433 21 17.5 21H15C14.4477 21 14 20.5523 14 20C14 19.4477 14.4477 19 15 19L17.5 19C18.3284 19 19 18.3284 19 17.5V15C19 14.4477 19.4477 14 20 14C20.5523 14 21 14.4477 21 15V17.5ZM21 6.5C21 4.567 19.433 3 17.5 3H15C14.4477 3 14 3.44772 14 4C14 4.55228 14.4477 5 15 5H17.5C18.3284 5 19 5.67157 19 6.5V9C19 9.55228 19.4477 10 20 10C20.5523 10 21 9.55229 21 9V6.5ZM6.5 21C4.567 21 3 19.433 3 17.5V15C3 14.4477 3.44772 14 4 14C4.55229 14 5 14.4477 5 15L5 17.5C5 18.3284 5.67157 19 6.5 19H9C9.55229 19 10 19.4477 10 20C10 20.5523 9.55228 21 9 21H6.5ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15ZM7.5 8.75C8.19036 8.75 8.75 8.19036 8.75 7.5C8.75 6.80964 8.19036 6.25 7.5 6.25C6.80964 6.25 6.25 6.80964 6.25 7.5C6.25 8.19036 6.80964 8.75 7.5 8.75Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000">
<path d="M17.75 3C19.5449 3 21 4.45508 21 6.25V9.25C21 9.66421 20.6642 10 20.25 10C19.8358 10 19.5 9.66421 19.5 9.25V6.25C19.5 5.2835 18.7165 4.5 17.75 4.5L14.75 4.5C14.3358 4.5 14 4.16421 14 3.75C14 3.33579 14.3358 3 14.75 3H17.75ZM6.25 3C4.45507 3 3 4.45507 3 6.25V9.25C3 9.66421 3.33579 10 3.75 10C4.16421 10 4.5 9.66421 4.5 9.25V6.25C4.5 5.2835 5.2835 4.5 6.25 4.5H9.25C9.66421 4.5 10 4.16421 10 3.75C10 3.33579 9.66421 3 9.25 3H6.25ZM17.75 21C19.5449 21 21 19.5449 21 17.75V14.75C21 14.3358 20.6642 14 20.25 14C19.8358 14 19.5 14.3358 19.5 14.75V17.75C19.5 18.7165 18.7165 19.5 17.75 19.5H14.75C14.3358 19.5 14 19.8358 14 20.25C14 20.6642 14.3358 21 14.75 21H17.75ZM3 17.75C3 19.5449 4.45507 21 6.25 21H9.25C9.66421 21 10 20.6642 10 20.25C10 19.8358 9.66421 19.5 9.25 19.5H6.25C5.2835 19.5 4.5 18.7165 4.5 17.75L4.5 14.75C4.5 14.3358 4.16421 14 3.75 14C3.33579 14 3 14.3358 3 14.75L3 17.75ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15ZM12 13.5C11.1716 13.5 10.5 12.8284 10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5ZM7.5 8.5C8.05229 8.5 8.5 8.05229 8.5 7.5C8.5 6.94772 8.05229 6.5 7.5 6.5C6.94772 6.5 6.5 6.94772 6.5 7.5C6.5 8.05229 6.94772 8.5 7.5 8.5Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 16.25a3.25 3.25 0 0 1-3.25 3.25h-7.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h7.5A3.25 3.25 0 0 1 16 7.75v8.5Zm5.762-10.357a1 1 0 0 1 .238.648v10.918a1 1 0 0 1-1.648.762L17 15.37V8.628l3.352-2.849a1 1 0 0 1 1.41.114Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 361 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 502 B

@@ -0,0 +1,81 @@
pragma ComponentBehavior: Bound
pragma Singleton
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import Qt.labs.synchronizer
import Quickshell
Singleton {
id: root
enum Action {
Copy,
Edit,
Search,
CharRecognition,
Record,
RecordWithSound
}
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
function getCommand(x, y, width, height, screenshotPath, action, saveDir = "") {
// Set command for action
const rx = Math.round(x);
const ry = Math.round(y);
const rw = Math.round(width);
const rh = Math.round(height);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} `
+ `-crop ${rw}x${rh}+${rx}+${ry}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
switch (action) {
case ScreenshotAction.Action.Copy:
if (saveDir === "") {
// not saving the screenshot, just copy to clipboard
return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
return [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${saveDir}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break;
case ScreenshotAction.Action.Edit:
return ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
break;
case ScreenshotAction.Action.Search:
return ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(screenshotPath)})" && ${cleanup}`]
break;
case ScreenshotAction.Action.CharRecognition:
return ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case ScreenshotAction.Action.Record:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case ScreenshotAction.Action.RecordWithSound:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
return;
}
}
}
@@ -0,0 +1,14 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.functions
Process {
id: screenshotProc
running: true
property string screenshotDir: Directories.screenshotTemp
required property ShellScreen screen
property string screenshotPath: `${screenshotDir}/image-${screen.name}`
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(screen.name)}' '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`]
}
@@ -0,0 +1,28 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
Canvas {
id: root
property color color: "#ffffff"
property int dashLength: 6
property int gapLength: 4
property int borderWidth: 1
onDashLengthChanged: requestPaint()
onGapLengthChanged: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.strokeStyle = root.color;
ctx.lineWidth = root.borderWidth;
if (root.gapLength > 0) {
ctx.setLineDash([root.dashLength, root.gapLength]); // Set dash pattern
}
ctx.strokeRect(root.borderWidth / 2, root.borderWidth / 2, width - root.borderWidth, height - root.borderWidth); // Draw it
ctx.restore();
}
}
@@ -14,12 +14,16 @@ MouseArea {
property bool automaticallyReset: true
readonly property real dragDiffX: _dragDiffX
readonly property real dragDiffY: _dragDiffY
property real startX: 0
property real startY: 0
property real regionTopLeftX: Math.min(startX, startX + _dragDiffX)
property real regionTopLeftY: Math.min(startY, startY + _dragDiffY)
property real regionWidth: Math.abs(_dragDiffX)
property real regionHeight: Math.abs(_dragDiffY)
signal dragPressed(diffX: real, diffY: real)
signal dragReleased(diffX: real, diffY: real)
property real startX: 0
property real startY: 0
property bool dragging: false
property real _dragDiffX: 0
property real _dragDiffY: 0
@@ -12,19 +12,30 @@ Item {
required property var tabButtonList
function incrementCurrentIndex() {
tabBar.incrementCurrentIndex()
tabBar.incrementCurrentIndex();
}
function decrementCurrentIndex() {
tabBar.decrementCurrentIndex()
tabBar.decrementCurrentIndex();
}
function setCurrentIndex(index) {
tabBar.setCurrentIndex(index)
tabBar.setCurrentIndex(index);
}
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
implicitWidth: contentItem.implicitWidth
implicitHeight: 40
property Component delegate: ToolbarTabButton {
required property int index
required property var modelData
current: index == root.currentIndex
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index);
}
}
Row {
id: contentItem
z: 1
@@ -33,16 +44,7 @@ Item {
Repeater {
model: root.tabButtonList
delegate: ToolbarTabButton {
required property int index
required property var modelData
current: index == root.currentIndex
text: modelData.name
materialSymbol: modelData.icon
onClicked: {
root.setCurrentIndex(index)
}
}
delegate: root.delegate
}
}
@@ -76,23 +78,23 @@ Item {
z: 2
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
onWheel: (event) => {
onWheel: event => {
if (event.angleDelta.y < 0) {
root.incrementCurrentIndex();
}
else {
} else {
root.decrementCurrentIndex();
}
}
}
// TabBar doesn't allow tabs to be of different sizes. Literally unusable.
// TabBar doesn't allow tabs to be of different sizes. That's what I thought...
// We use it only for the logic and draw stuff manually
TabBar {
id: tabBar
z: -1
background: null
Repeater { // This is to fool the TabBar that it has tabs so it does the indices properly
Repeater {
// This is to fool the TabBar that it has tabs so it does the indices properly
model: root.tabButtonList.length
delegate: TabButton {
background: null
@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
@@ -33,13 +34,7 @@ PanelWindow {
property var selectionMode: RegionSelection.SelectionMode.RectCorners
signal dismiss()
property string saveScreenshotDir: Config.options.screenSnip.savePath !== ""
? Config.options.screenSnip.savePath
: ""
property string screenshotDir: Directories.screenshotTemp
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
property color overlayColor: "#88111111"
property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0
property color brightSecondary: Appearance.m3colors.darkmode ? Appearance.colors.colSecondary : Appearance.colors.colOnSecondary
@@ -180,10 +175,12 @@ PanelWindow {
property real regionX: Math.min(dragStartX, draggingX)
property real regionY: Math.min(dragStartY, draggingY)
Process {
TempScreenshotProcess {
id: screenshotProc
running: true
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`]
screen: root.screen
screenshotDir: root.screenshotDir
screenshotPath: root.screenshotPath
onExited: (exitCode, exitStatus) => {
if (root.enableContentRegions) imageDetectionProcess.running = true;
root.preparationDone = !checkRecordingProc.running;
@@ -229,6 +226,27 @@ PanelWindow {
}
}
function getScreenshotAction() {
switch(root.action) {
case RegionSelection.SnipAction.Copy:
return ScreenshotAction.Action.Copy;
case RegionSelection.SnipAction.Edit:
return ScreenshotAction.Action.Edit;
case RegionSelection.SnipAction.Search:
return ScreenshotAction.Action.Search;
case RegionSelection.SnipAction.CharRecognition:
return ScreenshotAction.Action.CharRecognition;
case RegionSelection.SnipAction.Record:
return ScreenshotAction.Action.Record;
case RegionSelection.SnipAction.RecordWithSound:
return ScreenshotAction.Action.RecordWithSound;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
root.dismiss();
return;
}
}
function snip() {
// Validity check
if (root.regionWidth <= 0 || root.regionHeight <= 0) {
@@ -247,61 +265,19 @@ PanelWindow {
root.action = root.mouseButton === Qt.RightButton ? RegionSelection.SnipAction.Edit : RegionSelection.SnipAction.Copy;
}
// Set command for action
const rx = Math.round(root.regionX * root.monitorScale);
const ry = Math.round(root.regionY * root.monitorScale);
const rw = Math.round(root.regionWidth * root.monitorScale);
const rh = Math.round(root.regionHeight * root.monitorScale);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} `
+ `-crop ${rw}x${rh}+${rx}+${ry}`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
switch (root.action) {
case RegionSelection.SnipAction.Copy:
if (saveScreenshotDir === "") {
// not saving the screenshot, just copy to clipboard
snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
const savePathBase = root.saveScreenshotDir
snipProc.command = [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(savePathBase)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${savePathBase}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break;
case RegionSelection.SnipAction.Edit:
snipProc.command = ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
break;
case RegionSelection.SnipAction.Search:
snipProc.command = ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(root.screenshotPath)})" && ${cleanup}`]
break;
case RegionSelection.SnipAction.CharRecognition:
snipProc.command = ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case RegionSelection.SnipAction.Record:
snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case RegionSelection.SnipAction.RecordWithSound:
snipProc.command = ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
root.dismiss();
return;
}
const screenshotDir = Config.options.screenSnip.savePath !== "" ? //
Config.options.screenSnip.savePath : "";
var screenshotAction = root.getScreenshotAction();
const command = ScreenshotAction.getCommand(
root.regionX * root.monitorScale, //
root.regionY * root.monitorScale, //
root.regionWidth * root.monitorScale,//
root.regionHeight * root.monitorScale, //
root.screenshotPath, //
screenshotAction, //
screenshotDir
)
snipProc.command = command;
// Image post-processing
snipProc.startDetached();
@@ -93,8 +93,8 @@ Singleton {
property color bgPanelFooter: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelFooter : root.lightColors.bgPanelFooter, root.panelLayerTransparency)
property color bgPanelBody: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelBody : root.lightColors.bgPanelBody, root.panelLayerTransparency)
property color bgPanelSeparator: ColorUtils.transparentize(root.dark ? root.darkColors.bgPanelSeparator : root.lightColors.bgPanelSeparator, root.backgroundTransparency)
property color bg0Opaque: root.dark ? root.darkColors.bg0 : root.lightColors.bg0
property color bg0: ColorUtils.transparentize(bg0Opaque, root.backgroundTransparency)
property color bg0Base: root.dark ? root.darkColors.bg0 : root.lightColors.bg0
property color bg0: ColorUtils.transparentize(bg0Base, root.backgroundTransparency)
property color bg0Border: ColorUtils.transparentize(root.dark ? root.darkColors.bg0Border : root.lightColors.bg0Border, root.backgroundTransparency)
property color bg1Base: root.dark ? root.darkColors.bg1Base : root.lightColors.bg1Base
property color bg1: ColorUtils.transparentize(root.dark ? root.darkColors.bg1 : root.lightColors.bg1, root.contentTransparency)
@@ -39,9 +39,10 @@ Button {
}
}
property color fgColor: {
if (!root.enabled) return root.colForegroundDisabled
if (root.checked) return root.colForegroundToggled
if (root.enabled) return root.colForeground
return root.colForegroundDisabled
return root.colForeground
}
property alias horizontalAlignment: buttonText.horizontalAlignment
font {
@@ -76,8 +76,9 @@ Menu {
contentItem: Item {
implicitWidth: menuListView.implicitWidth
implicitHeight: menuListView.implicitHeight
ListView {
WListView {
id: menuListView
interactive: contentHeight > height
anchors {
left: parent.left
right: parent.right
@@ -87,6 +88,7 @@ Menu {
topMargin: root.downDirection ? root.sourceEdgeMargin : root.margins
bottomMargin: root.downDirection ? root.margins : root.sourceEdgeMargin
}
clip: true
implicitHeight: contentHeight
implicitWidth: Array.from({
length: count
@@ -6,6 +6,7 @@ import Quickshell
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
MenuItem {
@@ -14,11 +15,11 @@ MenuItem {
property color colBackground: ColorUtils.transparentize(Looks.colors.bg1)
property color colBackgroundHover: Looks.colors.bg2Hover
property color colBackgroundActive: Looks.colors.bg2Active
property color colBackgroundToggled: Looks.colors.accent
property color colBackgroundToggledHover: Looks.colors.accentHover
property color colBackgroundToggledActive: Looks.colors.accentActive
property color colBackgroundToggled: Looks.colors.bg2Hover
property color colBackgroundToggledHover: Looks.colors.bg2Active
property color colBackgroundToggledActive: Looks.colors.bg2Hover
property color colForeground: Looks.colors.fg
property color colForegroundToggled: Looks.colors.accentFg
property color colForegroundToggled: Looks.colors.fg
property color colForegroundDisabled: ColorUtils.transparentize(Looks.colors.subfg, 0.4)
property color color: {
if (!root.enabled)
@@ -70,27 +71,57 @@ MenuItem {
implicitHeight: Math.max(28, contentItem.implicitHeight) + topInset + bottomInset
implicitWidth: contentItem.implicitWidth + leftInset + rightInset + leftPadding + rightPadding
contentItem: RowLayout {
id: contentLayout
spacing: 12
FluentIcon {
id: buttonIcon
monochrome: true
implicitSize: 20
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
color: root.fgColor
visible: root.icon.name !== "";
icon: root.icon.name
contentItem: Item {
implicitWidth: contentLayout.implicitWidth
implicitHeight: contentLayout.implicitHeight
RowLayout {
id: contentLayout
anchors.fill: parent
spacing: 12
FluentIcon {
id: buttonIcon
monochrome: true
implicitSize: 20
Layout.fillWidth: false
Layout.alignment: Qt.AlignVCenter
color: root.fgColor
visible: root.icon.name !== ""
icon: root.icon.name
}
WText {
id: buttonText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
horizontalAlignment: Text.AlignLeft
font.pixelSize: Looks.font.pixelSize.large
color: root.fgColor
}
}
WText {
id: buttonText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: root.text
horizontalAlignment: Text.AlignLeft
font.pixelSize: Looks.font.pixelSize.large
color: root.fgColor
WFadeLoader {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: -root.leftPadding + width
}
shown: root.checked
sourceComponent: Rectangle {
implicitWidth: 3
implicitHeight: 3
radius: width / 2
color: Looks.colors.accent
property bool forceZeroHeight: true
height: forceZeroHeight ? 0 : Math.max(root.down ? 10 : 16, root.background.height - 18 * 2)
Component.onCompleted: {
forceZeroHeight = false;
}
Behavior on height {
animation: Looks.transition.resize.createObject(this)
}
}
}
}
}
@@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Item {
id: root
property real padding: 9
property alias colBackground: background.color
property alias spacing: toolbarLayout.spacing
property alias radius: background.radius
default property alias data: toolbarLayout.data
implicitWidth: background.implicitWidth
implicitHeight: background.implicitHeight
Rectangle {
id: background
anchors.fill: parent
implicitHeight: 50
implicitWidth: toolbarLayout.implicitWidth + root.padding * 2
radius: Looks.radius.large
color: Looks.colors.bg0Base
border.width: 1
border.color: Looks.colors.bg1Border
RowLayout {
id: toolbarLayout
spacing: 4
anchors {
fill: parent
margins: root.padding
}
}
}
}
@@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
WButton {
implicitHeight: 32
radius: Looks.radius.medium
}
@@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
WToolbarButton {
id: root
implicitWidth: height
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: root.icon.name
implicitSize: 18
color: root.fgColor
}
}
}
@@ -0,0 +1,21 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
TabButton {
id: root
implicitWidth: 38
implicitHeight: 32
padding: 0
background: null
contentItem: Item {
FluentIcon {
anchors.centerIn: parent
icon: root.icon.name
color: root.icon.color
implicitSize: 18
}
}
}
@@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
Rectangle {
Layout.leftMargin: 4
Layout.rightMargin: 4
implicitHeight: 24
implicitWidth: 1
color: Looks.colors.bg0Border
}
@@ -0,0 +1,53 @@
import QtQuick
import QtQuick.Controls
import qs.modules.common
import qs.modules.common.functions
TabBar {
id: root
implicitHeight: 32
background: Rectangle {
radius: Looks.radius.medium
color: Looks.colors.bgPanelFooter
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, 0.7)
border.width: 1
// Indicator
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: root.currentIndex * (root.width / root.count)
Behavior on leftMargin {
animation: Looks.transition.resize.createObject(this)
}
}
radius: Looks.radius.medium
color: Looks.colors.bg2Base
border.color: Looks.colors.bg0Border
border.width: 1
width: root.width / root.count
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 1
}
implicitWidth: pressDetector.containsPress ? 16 : 12
implicitHeight: 3
radius: height / 2
color: Looks.colors.accent
}
}
}
MouseArea {
id: pressDetector
z: 9999
anchors.fill: parent
acceptedButtons: Qt.LeftButton
}
}
@@ -0,0 +1,62 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
Item {
id: root
required property int regionX
required property int regionY
required property int regionWidth
required property int regionHeight
property bool dashed: true
property color borderColor: "#ffffff"
property color overlayColor: ColorUtils.transparentize("#000000", 1)
Component.onCompleted: overlayColor = ColorUtils.transparentize("#000000", 0.4)
Behavior on overlayColor {
ColorAnimation {
duration: 250
easing.type: Easing.InOutQuad
}
}
// Overlay to darken screen
// Base dark overlay around region
Rectangle {
id: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: root.regionX - darkenOverlay.border.width
topMargin: root.regionY - darkenOverlay.border.width
}
width: root.regionWidth + darkenOverlay.border.width * 2
height: root.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
border.color: root.overlayColor
border.width: Math.max(root.width, root.height)
}
// Selection border
DashedBorder {
id: border
z: 2
visible: root.regionWidth > 0 && root.regionHeight > 0
anchors {
left: parent.left
top: parent.top
leftMargin: Math.round(root.regionX - borderWidth)
topMargin: Math.round(root.regionY - borderWidth)
}
width: Math.round(root.regionWidth + borderWidth * 2)
height: Math.round(root.regionHeight + borderWidth * 2)
color: root.borderColor
dashLength: 4
gapLength: root.dashed ? 3 : 0
borderWidth: 1
}
}
@@ -0,0 +1,375 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.synchronizer
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.utils
import qs.modules.common.widgets
import qs.modules.waffle.looks
PanelWindow {
id: root
enum MediaType {
Image,
Video
}
enum ImageAction {
Copy,
Menu,
CharRecognition,
Search
}
enum VideoAction {
Record,
RecordWithSound
}
enum SelectionMode {
Rect,
Window
}
signal closed
function close() {
root.closed();
}
property var mediaType: WRegionSelectionPanel.MediaType.Image
property var imageAction: WRegionSelectionPanel.ImageAction.Copy
property var selectionMode: WRegionSelectionPanel.SelectionMode.Rect
visible: false
color: "transparent"
WlrLayershell.namespace: "quickshell:regionSelector"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
// Hyprland stuff
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating)
return 0;
return a.floating ? -1 : 1;
})
property string screenshotDir: Directories.screenshotTemp
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
TempScreenshotProcess {
id: screenshotProc
running: true
screen: root.screen
screenshotDir: root.screenshotDir
screenshotPath: root.screenshotPath
onExited: (exitCode, exitStatus) => {
root.preparationDone = true;
}
}
property bool preparationDone: false
onPreparationDoneChanged: {
if (!preparationDone)
return;
root.visible = true;
}
function getScreenshotAction() {
switch (root.mediaType) {
case WRegionSelectionPanel.MediaType.Image:
switch (root.imageAction) {
case WRegionSelectionPanel.ImageAction.Copy:
return ScreenshotAction.Action.Copy;
case WRegionSelectionPanel.ImageAction.Menu:
return ScreenshotAction.Action.Edit;
case WRegionSelectionPanel.ImageAction.CharRecognition:
return ScreenshotAction.Action.CharRecognition;
case WRegionSelectionPanel.ImageAction.Search:
return ScreenshotAction.Action.Search;
default:
return ScreenshotAction.Action.Copy;
}
break;
case WRegionSelectionPanel.MediaType.Video:
switch (root.videoAction) {
case WRegionSelectionPanel.VideoAction.Record:
return ScreenshotAction.Action.Record;
case WRegionSelectionPanel.VideoAction.RecordWithSound:
return ScreenshotAction.Action.RecordWithSound;
}
}
}
Process {
id: snipProc
}
ScreencopyView {
id: screencopyView
anchors.fill: parent
live: false
captureSource: root.screen
focus: root.visible
Keys.onPressed: event => { // Esc to close
if (event.key === Qt.Key_Escape) {
root.close();
} else if (event.key === Qt.Key_E && event.modifiers & Qt.ControlModifier) {
if (root.imageAction === WRegionSelectionPanel.ImageAction.Menu) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.imageAction = WRegionSelectionPanel.ImageAction.Menu;
}
}
}
DragManager {
id: dragArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.CrossCursor
property bool isWindowSelection: root.selectionMode === WRegionSelectionPanel.SelectionMode.Window
property var hoveredWindow: root.windows.find(w => {
const inCurrentWorkspace = w.workspace.id === HyprlandData.activeWorkspace.id;
const withinXRange = w.at[0] <= dragArea.mouseX && dragArea.mouseX <= w.at[0] + w.size[0];
const withinYRange = w.at[1] <= dragArea.mouseY && dragArea.mouseY <= w.at[1] + w.size[1];
return inCurrentWorkspace && withinXRange && withinYRange;
})
property int winPadding: 1
property int selectionX: isWindowSelection ? ((hoveredWindow?.at[0] ?? 0) - winPadding) : regionTopLeftX
property int selectionY: isWindowSelection ? ((hoveredWindow?.at[1] ?? 0) - winPadding) : regionTopLeftY
property int selectionWidth: isWindowSelection ? ((hoveredWindow?.size[0] ?? 0) + winPadding * 2) : regionWidth
property int selectionHeight: isWindowSelection ? ((hoveredWindow?.size[1] ?? 0) + winPadding * 2) : regionHeight
onDragReleased: (diffX, diffY) => {
if (selectionWidth === 0 || selectionHeight === 0) {
return;
}
const screenshotDir = Config.options.screenSnip.savePath !== "" ? Config.options.screenSnip.savePath : "";
const screenshotAction = root.getScreenshotAction();
const command = ScreenshotAction.getCommand(dragArea.selectionX * root.monitorScale //
, dragArea.selectionY * root.monitorScale //
, dragArea.selectionWidth * root.monitorScale//
, dragArea.selectionHeight * root.monitorScale //
, root.screenshotPath //
, screenshotAction //
, screenshotDir); // yo wtf is this formatting qmlls do be funnie
snipProc.command = command;
// Image post-processing
snipProc.startDetached();
root.close();
}
WRectangularSelection {
id: rectangularSelection
anchors.fill: parent
regionX: dragArea.selectionX
regionY: dragArea.selectionY
regionWidth: dragArea.selectionWidth
regionHeight: dragArea.selectionHeight
dashed: root.selectionMode === WRegionSelectionPanel.SelectionMode.Rect
}
RegionSelectionOptionsToolbar {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 12
}
}
}
}
component RegionSelectionOptionsToolbar: WToolbar {
// Image/video
WToolbarTabBar {
currentIndex: switch (root.mediaType) {
case WRegionSelectionPanel.MediaType.Image:
return 0;
case WRegionSelectionPanel.MediaType.Video:
return 1;
default:
return 0;
}
WToolbarIconTabButton {
icon.name: "camera"
icon.color: Looks.colors.fg
}
WToolbarIconTabButton {
icon.name: "video"
icon.color: Looks.colors.fg
}
onCurrentIndexChanged: {
switch (currentIndex) {
case 0:
root.mediaType = WRegionSelectionPanel.MediaType.Image;
break;
case 1:
root.mediaType = WRegionSelectionPanel.MediaType.Video;
break;
}
}
WToolTip {
text: Translation.tr("Snip")
}
}
// Selection type
WToolbarButton {
id: selectionTypeBtn
implicitWidth: selectionTypeBtnRow.implicitWidth + 11 * 2
leftPadding: 11
rightPadding: 11
onClicked: {
selectionTypeMenu.visible = !selectionTypeMenu.visible;
}
contentItem: Row {
id: selectionTypeBtnRow
spacing: 4
FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: switch (root.selectionMode) {
case WRegionSelectionPanel.SelectionMode.Rect:
return "crop";
case WRegionSelectionPanel.SelectionMode.Window:
return "calendar-add";
default:
return "crop";
}
implicitSize: 18
}
FluentIcon {
anchors {
top: parent.top
topMargin: (parent.height - height) / 2 + (selectionTypeBtn.down ? 2 : 0)
Behavior on topMargin {
animation: Looks.transition.enter.createObject(this)
}
}
icon: "chevron-down"
implicitSize: 12
}
}
WMenu {
id: selectionTypeMenu
onClosed: screencopyView.focus = true
x: -margins
y: -margins - (selectionTypeBtn.parent.height - selectionTypeBtn.height) - 16
topMargin: -6
height: implicitHeight + sourceEdgeMargin
color: Looks.colors.bg1Base
Action {
icon.name: "crop"
text: Translation.tr("Rectangle")
checked: root.selectionMode === WRegionSelectionPanel.SelectionMode.Rect
onTriggered: {
root.selectionMode = WRegionSelectionPanel.SelectionMode.Rect;
}
}
Action {
icon.name: "calendar-add"
text: Translation.tr("Window")
checked: root.selectionMode === WRegionSelectionPanel.SelectionMode.Window
onTriggered: {
root.selectionMode = WRegionSelectionPanel.SelectionMode.Window;
}
}
}
WToolTip {
text: Translation.tr("Snipping area")
}
}
// Markup
WToolbarIconButton {
icon.name: "image-edit"
enabled: root.mediaType === WRegionSelectionPanel.MediaType.Image
checked: root.imageAction === WRegionSelectionPanel.ImageAction.Menu
onClicked: {
if (root.imageAction === WRegionSelectionPanel.ImageAction.Menu) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.imageAction = WRegionSelectionPanel.ImageAction.Menu;
}
}
WToolTip {
text: Translation.tr("Quick markup (Ctrl+E)")
}
}
WToolbarSeparator {}
// Tools
WToolbarIconButton {
icon.name: "search-visual"
checked: root.imageAction === WRegionSelectionPanel.ImageAction.Search
onClicked: {
if (root.imageAction === WRegionSelectionPanel.ImageAction.Search && root.mediaType === WRegionSelectionPanel.MediaType.Image) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.mediaType = WRegionSelectionPanel.MediaType.Image;
root.imageAction = WRegionSelectionPanel.ImageAction.Search;
}
}
WToolTip {
text: Translation.tr("Image search")
}
}
WToolbarIconButton {
icon.name: "eyedropper"
onClicked: {
Quickshell.execDetached(["bash", "-c", "sleep 0.2; hyprpicker -a"]);
root.closed();
}
WToolTip {
text: Translation.tr("Color picker")
}
}
WToolbarIconButton {
icon.name: "scan-text"
checked: root.imageAction === WRegionSelectionPanel.ImageAction.CharRecognition
onClicked: {
if (root.imageAction === WRegionSelectionPanel.ImageAction.CharRecognition && root.mediaType === WRegionSelectionPanel.MediaType.Image) {
root.imageAction = WRegionSelectionPanel.ImageAction.Copy;
} else {
root.mediaType = WRegionSelectionPanel.MediaType.Image;
root.imageAction = WRegionSelectionPanel.ImageAction.CharRecognition;
}
}
WToolTip {
text: Translation.tr("Text extractor")
}
}
WToolbarSeparator {}
WToolbarIconButton {
icon.name: "dismiss"
onClicked: root.close()
WToolTip {
text: Translation.tr("Close (Esc)")
}
}
}
}
@@ -0,0 +1,106 @@
pragma ComponentBehavior: Bound
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Hyprland
Scope {
id: root
function dismiss() {
GlobalStates.regionSelectorOpen = false;
}
Loader {
id: regionSelectorLoader
active: GlobalStates.regionSelectorOpen
sourceComponent: WRegionSelectionPanel {
onClosed: root.dismiss()
}
}
function screenshot() {
GlobalStates.regionSelectorOpen = true;
}
function ocr() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Image;
regionSelectorLoader.item.imageAction = WRegionSelectionPanel.ImageAction.CharRecognition;
}
function record() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Video;
regionSelectorLoader.item.videoAction = WRegionSelectionPanel.VideoAction.Record;
}
function recordWithSound() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Video;
regionSelectorLoader.item.videoAction = WRegionSelectionPanel.VideoAction.RecordWithSound;
}
function search() {
GlobalStates.regionSelectorOpen = true;
regionSelectorLoader.item.mediaType = WRegionSelectionPanel.MediaType.Image;
regionSelectorLoader.item.imageAction = WRegionSelectionPanel.ImageAction.Search;
}
IpcHandler {
target: "region"
function screenshot() {
root.screenshot();
}
function ocr() {
root.ocr();
}
function record() {
root.record();
}
function recordWithSound() {
root.recordWithSound();
}
function search() {
root.search();
}
}
GlobalShortcut {
name: "regionScreenshot"
description: "Takes a screenshot of the selected region"
onPressed: root.screenshot()
}
GlobalShortcut {
name: "regionSearch"
description: "Searches the selected region"
onPressed: root.search()
}
GlobalShortcut {
name: "regionOcr"
description: "Recognizes text in the selected region"
onPressed: root.ocr()
}
GlobalShortcut {
name: "regionRecord"
description: "Records the selected region"
onPressed: root.record()
}
GlobalShortcut {
name: "regionRecordWithSound"
description: "Records the selected region with sound"
onPressed: root.recordWithSound()
}
}
+3 -1
View File
@@ -34,6 +34,7 @@ import qs.modules.waffle.lock
import qs.modules.waffle.notificationCenter
import qs.modules.waffle.onScreenDisplay
import qs.modules.waffle.polkit
import qs.modules.waffle.screenSnip
import qs.modules.waffle.startMenu
import qs.modules.waffle.sessionScreen
import qs.modules.waffle.taskView
@@ -89,6 +90,7 @@ ShellRoot {
PanelLoader { identifier: "wNotificationCenter"; component: WaffleNotificationCenter {} }
PanelLoader { identifier: "wOnScreenDisplay"; component: WaffleOSD {} }
PanelLoader { identifier: "wPolkit"; component: WafflePolkit {} }
PanelLoader { identifier: "wScreenSnip"; component: WScreenSnip {} }
PanelLoader { identifier: "wStartMenu"; component: WaffleStartMenu {} }
PanelLoader { identifier: "wSessionScreen"; component: WaffleSessionScreen {} }
PanelLoader { identifier: "wTaskView"; component: WaffleTaskView {} }
@@ -104,7 +106,7 @@ ShellRoot {
property list<string> families: ["ii", "waffle"]
property var panelFamilies: ({
"ii": ["iiBar", "iiBackground", "iiCheatsheet", "iiDock", "iiLock", "iiMediaControls", "iiNotificationPopup", "iiOnScreenDisplay", "iiOnScreenKeyboard", "iiOverlay", "iiOverview", "iiPolkit", "iiRegionSelector", "iiScreenCorners", "iiSessionScreen", "iiSidebarLeft", "iiSidebarRight", "iiVerticalBar", "iiWallpaperSelector"],
"waffle": ["wActionCenter", "wBar", "wBackground", "wLock", "wNotificationCenter", "wOnScreenDisplay", "wTaskView", "wPolkit", "wSessionScreen", "wStartMenu", "iiCheatsheet", "iiNotificationPopup", "iiOnScreenKeyboard", "iiOverlay", "iiRegionSelector", "iiWallpaperSelector"],
"waffle": ["wActionCenter", "wBar", "wBackground", "wLock", "wNotificationCenter", "wOnScreenDisplay", "wTaskView", "wPolkit", "wScreenSnip", "wSessionScreen", "wStartMenu", "iiCheatsheet", "iiNotificationPopup", "iiOnScreenKeyboard", "iiOverlay", "iiWallpaperSelector"],
})
function cyclePanelFamily() {
const currentIndex = families.indexOf(Config.options.panelFamily)