Skip to content

Commit 6f86b06

Browse files
committed
feat: add screen freezing support
1 parent 844ec5c commit 6f86b06

File tree

9 files changed

+580
-18
lines changed

9 files changed

+580
-18
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ v . # build
2727
- Include or exclude cursor
2828
- PNG, JXL, QOI and PPM support
2929
- HDR support
30+
- Screen freezing support
3031

3132
### Usage
3233
```sh
@@ -39,6 +40,8 @@ mrpenishot -g "100,200 300x400" # geometry
3940
mrpenishot -g "$(slurp)" # geometry from slurp
4041
mrpenishot -t "$(awmsg t f | jq -j '.foreign')" # capture toplevel in awm
4142
mrpenishot - | wl-copy # copy image to clipboard
43+
mrpenishot -F 'sleep 2' # freeze for 2 seconds and screenshot
44+
mrpenishot -F '' -g '$(slurp)' # screenshot and freeze until slurp exits, geometry from slurp
4245
```
4346

4447
### References

build.vsh

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,23 @@ fn program_installed(name string) {
1818
}
1919
}
2020

21-
fn pkg_installed(name string, version ?f64) {
22-
ins := sh('pkg-config --modversion ${name}').f64()
23-
if ins == 0 {
21+
fn pkg_installed(name string, version ?string) {
22+
ins_str := sh('pkg-config --modversion ${name}')
23+
ins := ins_str.trim_space().split('.').map(|x| x.int())
24+
if ins.len == 0 {
2425
err('package `${name}` not found')
2526
} else {
2627
if ver := version {
27-
if ins < ver {
28-
err('package `${name}` version must be >= ${ver}')
28+
exp := ver.split('.').map(|x| x.int())
29+
if exp.len != ins.len {
30+
err('package version lengths `${ver}` and `${ins_str}` differ')
2931
}
32+
for i in 0..ins.len {
33+
if ins[i] > exp[i] {
34+
return
35+
}
36+
}
37+
err('package `${name}` version must be >= ${ver}')
3038
}
3139
}
3240
}
@@ -45,11 +53,11 @@ if arguments().contains('clean') {
4553
program_installed('pkg-config')
4654
program_installed('wayland-scanner')
4755
48-
pkg_installed('wayland-protocols', 1.41)
56+
pkg_installed('wayland-protocols', '1.41')
4957
pkg_installed('wayland-client', none)
5058
pkg_installed('pixman-1', none)
5159
pkg_installed('libjxl', none)
52-
pkg_installed('libpng16', none)
60+
pkg_installed('libpng16', '2.5.0')
5361
5462
// build vscanner
5563
sh('v ${vscanner_dir}')
@@ -59,11 +67,14 @@ wl_proto_dir := sh('pkg-config --variable=pkgdatadir wayland-protocols').trim_sp
5967
6068
protocols := [
6169
wl_dir + '/wayland.xml',
70+
wl_proto_dir + '/stable/xdg-shell/xdg-shell.xml',
71+
wl_proto_dir + '/stable/viewporter/viewporter.xml',
6272
wl_proto_dir + '/staging/color-management/color-management-v1.xml',
6373
wl_proto_dir + '/staging/ext-image-capture-source/ext-image-capture-source-v1.xml',
6474
wl_proto_dir + '/staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml',
6575
wl_proto_dir + '/staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml',
6676
wl_proto_dir + '/unstable/xdg-output/xdg-output-unstable-v1.xml',
77+
'protocols/wlr-layer-shell-unstable-v1.xml'
6778
]
6879
6980
sh('mkdir -p ${protocols_dir}')

main.v

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ fn main() {
7171
passed_geometry := fp.string('geometry', `g`, '', 'geometry in the format "400,500 200x300"')
7272
output_name := fp.string('output', `o`, '', 'name of output to screenshot')
7373
toplevel_identifier := fp.string('toplevel', `t`, '', 'use a toplevel as the screenshot source by its identifier')
74+
freeze_screen_cmd := fp.string('freeze', `F`, '', 'freeze the screen until passed command finishes or until the -g command finishes')
7475
additional_args := fp.finalize() or {
7576
eprintln(err)
7677
println(fp.usage())
@@ -95,12 +96,16 @@ fn main() {
9596
name
9697
}
9798

98-
// get geometry if passed
99-
mut geometry := if passed_geometry == '' {
100-
Geometry{}
101-
} else {
102-
Geometry.new(passed_geometry) or { panic('invalid geometry') }
99+
// check if geometry needs command execution (prefix with $)
100+
geometry_is_cmd := passed_geometry.starts_with('$') && !passed_geometry.starts_with('$-')
101+
mut geometry_cmd := ''
102+
if geometry_is_cmd {
103+
geometry_cmd = passed_geometry.trim_left('$')
104+
if freeze_screen_cmd != '' {
105+
panic('ERROR: cannot use both --freeze and command prefix in -g')
106+
}
103107
}
108+
mut geometry := Geometry{}
104109

105110
// mutual exclusion
106111
if output_name != '' && geometry != Geometry{} {
@@ -153,6 +158,18 @@ fn main() {
153158
if state.outputs.len == 0 {
154159
panic('no outputs found')
155160
}
161+
needs_freeze := freeze_screen_cmd != '' || geometry_is_cmd
162+
if needs_freeze {
163+
if state.compositor == none {
164+
panic('wl_compositor not supported by compositor')
165+
}
166+
if state.wp_viewporter == none {
167+
panic('wp_viewporter not supported by compositor')
168+
}
169+
if state.wlr_layer_shell_v1 == none {
170+
panic('zwlr_layer_shell_v1 not supported by compositor')
171+
}
172+
}
156173

157174
// init output manager
158175
if mut manager := state.zxdg_output_manager_v1 {
@@ -180,10 +197,10 @@ fn main() {
180197
mut cm_output := color_manager.get_output(output.wl_output.proxy)
181198
cm_output.add_listener(&cm_output_listener, state)
182199
mut description := cm_output.get_image_description()
183-
description.add_listener(&cm_image_description_listener, output.state)
184-
if C.wl_display_roundtrip(display_proxy) < 0 {
185-
panic('wl_display_roundtrip failed')
186-
}
200+
description.add_listener(&cm_image_description_listener, output.state)
201+
if C.wl_display_roundtrip(display_proxy) < 0 {
202+
panic('wl_display_roundtrip failed')
203+
}
187204
}
188205
}
189206

@@ -211,7 +228,7 @@ fn main() {
211228
} else {
212229
// capture output
213230
for output in state.outputs {
214-
if geometry != Geometry{} && !geometry.intersect(output.logical_geometry) {
231+
if !geometry_is_cmd && geometry != Geometry{} && !geometry.intersect(output.logical_geometry) {
215232
continue
216233
}
217234
if output.logical_scale > scale {
@@ -224,12 +241,98 @@ fn main() {
224241
panic('no captures found')
225242
}
226243

227-
// dispatch captures
244+
// dispatch initial captures to get buffer data for overlays
228245
mut done := false
229246
expected_cm := if state.wp_color_manager_v1 != none { state.outputs.len } else { 0 }
230247
for !done && C.wl_display_dispatch(display_proxy) != -1{
231248
done = state.n_done == state.captures.len && state.n_cm_done >= expected_cm
232249
}
250+
251+
// run geometry command with freeze
252+
if geometry_is_cmd {
253+
mut geometry_cmd_overlays := []&Overlay{}
254+
for capture in state.captures {
255+
overlay := Overlay.new(capture)
256+
geometry_cmd_overlays << overlay
257+
}
258+
259+
// run command in background thread
260+
result_ch := chan string{cap: 1}
261+
spawn fn (cmd string, ch chan string) {
262+
result := os.execute(cmd)
263+
ch <- result.output
264+
}(geometry_cmd, result_ch)
265+
266+
// process Wayland events while waiting for command
267+
mut cmd_output := ''
268+
for {
269+
C.wl_display_dispatch(display_proxy)
270+
C.wl_display_flush(display_proxy)
271+
select {
272+
output := <-result_ch {
273+
cmd_output = output
274+
break
275+
}
276+
}
277+
}
278+
for mut overlay in geometry_cmd_overlays {
279+
overlay.destroy()
280+
}
281+
geometry = Geometry.new(cmd_output.trim('\n')) or { panic('invalid geometry from command') }
282+
state.captures = []
283+
state.n_done = 0
284+
state.n_cm_done = 0
285+
for output in state.outputs {
286+
if geometry != Geometry{} && !geometry.intersect(output.logical_geometry) {
287+
continue
288+
}
289+
if output.logical_scale > scale {
290+
scale = output.logical_scale
291+
}
292+
state.capture_output(output, include_cursor)
293+
}
294+
if state.captures.len == 0 {
295+
panic('no captures found after geometry command')
296+
}
297+
// re-dispatch for new captures
298+
done = false
299+
for !done && C.wl_display_dispatch(display_proxy) != -1{
300+
done = state.n_done == state.captures.len && state.n_cm_done >= expected_cm
301+
}
302+
}
303+
304+
// freeze screen overlay
305+
mut overlays := []&Overlay{}
306+
if freeze_screen_cmd != '' {
307+
for capture in state.captures {
308+
overlay := Overlay.new(capture)
309+
overlays << overlay
310+
}
311+
312+
// run command in background thread
313+
ch := chan int{cap: 1}
314+
spawn fn (cmd string, result_ch chan int) {
315+
result := os.execute(cmd)
316+
result_ch <- result.exit_code
317+
}(freeze_screen_cmd, ch)
318+
319+
// process events while waiting for command
320+
for {
321+
C.wl_display_dispatch(display_proxy)
322+
C.wl_display_flush(display_proxy)
323+
select {
324+
exit_code := <-ch {
325+
for mut overlay in overlays {
326+
overlay.destroy()
327+
}
328+
if exit_code != 0 {
329+
eprintln('freeze command exited with code ${exit_code}')
330+
}
331+
break
332+
}
333+
}
334+
}
335+
}
233336
if geometry == Geometry{0, 0, 0, 0} {
234337
geometry = state.get_extents()
235338
}
@@ -307,5 +410,8 @@ fn main() {
307410
if mut manager := state.zxdg_output_manager_v1 {
308411
manager.destroy()
309412
}
413+
if mut manager := state.wp_viewporter {
414+
manager.destroy()
415+
}
310416
C.wl_display_disconnect(display_proxy)
311417
}

overlay.v

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
module main
2+
3+
import wl
4+
import protocols.wlr_layer_shell_unstable_v1 as ls
5+
6+
// needed for xdg_popup interface
7+
import protocols.xdg_shell as _
8+
9+
fn layer_surface_v1_configure(mut overlay Overlay, _ voidptr, serial u32, _ u32, _ u32) {
10+
if mut layer_surface := overlay.layer_surface_v1 {
11+
layer_surface.ack_configure(serial)
12+
}
13+
if mut surface := overlay.surface {
14+
surface.commit()
15+
}
16+
}
17+
18+
fn layer_surface_v1_closed(mut overlay Overlay, _ voidptr) {
19+
panic('Layer surface died unexpectedly')
20+
}
21+
22+
const layer_surface_listener = C.zwlr_layer_surface_v1_listener{
23+
configure: layer_surface_v1_configure
24+
closed: layer_surface_v1_closed
25+
}
26+
27+
const surface_listener = C.wl_surface_listener{
28+
enter: fn (_ voidptr, _ voidptr, _ voidptr) {}
29+
leave: fn (_ voidptr, _ voidptr, _ voidptr) {}
30+
preferred_buffer_scale: fn (_ voidptr, _ voidptr, _ int) {}
31+
preferred_buffer_transform: fn (_ voidptr, _ voidptr, _ u32) {}
32+
}
33+
34+
fn Overlay.new(capture &Capture) &Overlay {
35+
mut overlay := &Overlay{
36+
capture: capture
37+
layer_surface_v1: none
38+
surface: none
39+
}
40+
mut compositor := capture.state.compositor or { panic('Failed to get compositor') }
41+
42+
mut surface := compositor.create_surface()
43+
if surface.proxy == unsafe { nil } {
44+
panic('Failed to create surface')
45+
}
46+
overlay.surface = surface
47+
surface.add_listener(&surface_listener, overlay)
48+
49+
mut wp_viewporter := capture.state.wp_viewporter or { panic('No viewporter init') }
50+
51+
mut wp_viewport := wp_viewporter.get_viewport(surface.proxy)
52+
overlay.wp_viewport = wp_viewport
53+
if wp_viewport == unsafe { nil } {
54+
panic('Failed to create viewport')
55+
}
56+
wp_viewport.set_destination(capture.logical_geometry.width, capture.logical_geometry.height)
57+
wp_viewport.set_source(wl.wl_fixed_from_int(0), wl.wl_fixed_from_int(0), wl.wl_fixed_from_int(int(capture.buffer_width)),
58+
wl.wl_fixed_from_int(int(capture.buffer_height)))
59+
60+
if output := capture.output {
61+
mut wlr_layer_shell := capture.state.wlr_layer_shell_v1 or { panic('No layer shell init') }
62+
63+
mut layer_surface := wlr_layer_shell.get_layer_surface(surface.proxy, output.wl_output.proxy,
64+
u32(ls.ZwlrLayerShellV1_Layer.overlay), 'mrpenishot')
65+
overlay.layer_surface_v1 = layer_surface
66+
if layer_surface == unsafe { nil } {
67+
panic('Failed to get layer surface')
68+
}
69+
layer_surface.add_listener(&layer_surface_listener, overlay)
70+
71+
layer_surface.set_size(u32(output.logical_geometry.width), u32(output.logical_geometry.height))
72+
layer_surface.set_anchor(u32(int(ls.ZwlrLayerSurfaceV1_Anchor.top) | int(ls.ZwlrLayerSurfaceV1_Anchor.bottom) | int(ls.ZwlrLayerSurfaceV1_Anchor.left) | int(ls.ZwlrLayerSurfaceV1_Anchor.right)))
73+
layer_surface.set_exclusive_zone(-1)
74+
75+
surface.commit()
76+
C.wl_display_roundtrip(overlay.capture.state.display.proxy)
77+
78+
if buffer := capture.buffer {
79+
mut shm := capture.state.shm or { panic('No shm init') }
80+
stride := int(get_min_stride(buffer.shm_format, u32(buffer.width)))
81+
mut overlay_buffer := Buffer.new(mut shm, buffer.shm_format, buffer.width, buffer.height, stride)
82+
unsafe {
83+
C.memcpy(overlay_buffer.data, buffer.data, buffer.size)
84+
}
85+
overlay.buffer = overlay_buffer
86+
surface.attach(overlay_buffer.wl_buffer.proxy, 0, 0)
87+
surface.commit()
88+
}
89+
}
90+
91+
return overlay
92+
}
93+
94+
fn (mut overlay Overlay) destroy() {
95+
if mut layer_surface := overlay.layer_surface_v1 {
96+
layer_surface.destroy()
97+
}
98+
if mut surface := overlay.surface {
99+
surface.destroy()
100+
}
101+
if mut wp_viewport := overlay.wp_viewport {
102+
wp_viewport.destroy()
103+
}
104+
if mut buffer := overlay.buffer {
105+
buffer.destroy()
106+
}
107+
}

protocols/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*
2+
!*.xml
3+
!.gitignore

0 commit comments

Comments
 (0)