1--
2-- pseudo-window manager helper script for working with wayland and
3-- x-wayland clients specifically.
4--
5-- Returns a factory function for taking a wayland-bridge carrying a
6-- single client and constructing a wm/state tracker - as well as an
7-- optional connection manager that takes any primary connection that
8-- identifies as 'bridge-wayland' and deals with both 1:1 and 1:*
9-- cases.
10--
11-- local factory, connection = system_load("builtin/wayland.lua")()
12-- factory(vid, segreq_table, config)
13--
14-- (manual mode from a handler to an established bridge-wayland):
15--     if status.kind == "segment-request" then
16--          wm = factory(vid, status, config)
17--     end
18--
19-- (connection mode):
20-- 	local vid = target_alloc("wayland_only", function() end)
21--
22-- 	connection(vid,
23-- 	function(source, status)
24--  	wm = factory(source, status, config)
25-- 	end)
26--
27--  a full example can be found in tests/interactive/wltest
28--
29-- config contains display options and possible wm event hooks for
30-- latching into an outer wm scheme.
31--
32-- possible methods in config table:
33--
34--  decorate(window, vid, width, height) => t, l, d, r:
35--      attach / set decorations using vid as anchor and width/height.
36--      called whenever decoration state/sizes change. If 'vid' is not
37--      provided, it means existing decorations should be destroyed.
38--
39--      the decorator should return the number of pixels added in each
40--      direction for maximize/fullscreen calculations to be correct.
41--
42--  destroy(window):
43--      called when a window has been destroyed, just before video
44--      resources are deleted.
45--
46--  focus(window) or focus():
47--      called when a window requests focus, to acknowledge, call
48--      wnd:focus() and wnd:unfocus() respectively.
49--
50--  move(window, x, y, dx, dy):
51--      called when a window requests to be moved to x, y, return
52--      x, y if permitted - or constrain coordinates and return new
53--      position
54--
55--  configure(window, [type]):
56--      request for an initial size for a toplevel window (if any)
57--      should return w, h, x, y
58--
59--  state_change(window, state={
60--      "fullscreen", "realized", "composed",
61--      "maximized", "visible", "focused", "toplevel"}
62--      , [popup|parent], [grab])
63--
64--  mapped(window):
65--      called when a new toplevel window is ready to be drawn
66--
67--  log : function(domain, message) (default: print)
68--  fmt : function(string, va_args) (default: string.format)
69--
70--  the 'window' table passed as arguments provides the following methods:
71--
72--     focus(self):
73--         acknowledge a focus request, raise and change pending visuals
74--
75--     unfocus(self):
76--         mark the window has having lost focus
77--
78--     maximize(self):
79--         acknowledge a maximization request
80--
81--     minimize(self):
82--         acknowledge a minimization request
83--
84--     fullscreen(self):
85--         acknowledge a fullscreen request or force fullscreen
86--
87--     destroy(self):
88--         kill window and associated resources
89--
90--  the 'window' table passed as arguments provides the following properties:
91--      mouse_proxy (vid) set if another object should be used to determine ownership
92--
93-- the returned 'wm' table exposes the following methods:
94--
95--  destroy():
96--      drop all resources tied to this client
97--
98--  resize(width, height, density):
99--      change the 'output' for this client, roughly equivalent to outmost
100--      window resizing, changing display or display resolution - density
101--      in ppcm.
102--
103
104-- for drag and drop purposes, we need to track all bridges so that we can
105-- react on 'drag' and then check underlying vid when cursor-tagged in drag state,
106-- and then test and forward to clients beneath it
107local bridges = {}
108
109local x11_lut =
110{
111	["type"] =
112	function(ctx, source, typename)
113		ctx.states.typed = typename
114		if not ctx.states.mapped then
115			ctx:apply_type()
116		end
117	end,
118	["pair"] =
119	function(ctx, source, wl_id, x11_id)
120		local wl_id = wl_id and wl_id or "missing"
121		local x11_id = x11_id and x11_id or "missing"
122
123		ctx.idstr = wl_id .. "-> " .. x11_id
124		ctx.wm.log("wl_x11", ctx.wm.fmt("paired:wl=%s:x11=%s", wl_id, x11_id))
125-- not much to do with the information
126	end,
127	["fullscreen"] =
128	function(ctx, source, on)
129		ctx.wm.state_change(ctx, "fullscreen")
130	end,
131}
132
133-- this table contains hacks around some bits in wayland that does not map to
134-- regular events in shmif and comes packed in 'message' events
135local wl_top_lut =
136{
137	["move"] =
138	function(ctx)
139		ctx.states.moving = true
140	end,
141	["maximize"] =
142	function(ctx)
143		ctx.wm.state_change(ctx, "maximize")
144	end,
145	["demaximize"] =
146	function(ctx)
147		ctx.wm.state_change(ctx, "demaximize")
148	end,
149	["menu"] =
150	function(ctx)
151		ctx.wm.context_menu(ctx)
152	end,
153	["fullscreen"] =
154	function(ctx, source, on)
155		ctx.wm.state_change(ctx, "fullscreen")
156	end,
157	["resize"] =
158	function(ctx, source, dx, dy)
159		if not dx or not dy then
160			return
161		end
162
163		dx = tonumber(dx)
164		dy = tonumber(dy)
165
166		if not dx or not dy then
167			ctx.states.resizing = false
168			return
169		end
170
171-- masks for moving, used on left,top
172		local mx = 0
173		local my = 0
174		if dx < 0 then
175			mx = 1
176		end
177		if dy < 0 then
178			my = 1
179		end
180
181		ctx.states.resizing = {dx, dy, mx, my}
182	end,
183-- practically speaking there is really only xdg now, though if someone
184-- adds more, any wm specific mapping should be added here
185	["shell"] =
186	function(ctx, shell_type)
187	end,
188	["scale"] =
189	function(ctx, sf)
190	end,
191-- new window geometry
192	["geom"] =
193	function(ctx, x, y, w, h)
194		ctx.wm.log("toplevel", ctx.wm.fmt("anchor_geom:x=%f:y=%f:w=%f:h=%f", x, y, w, h))
195		ctx.anchor_offset = {x, y}
196	end
197}
198
199local function wnd_input_table(wnd, iotbl)
200	if not wnd.states.focused then
201		return
202	end
203
204	target_input(wnd.vid, iotbl)
205end
206
207-- these just request the focus state to change, the wm has final say
208local function wnd_mouse_over(wnd)
209	if wnd.wm.cfg.mouse_focus and not wnd.states.focused then
210		wnd.wm.focus(wnd)
211	end
212end
213
214-- this is not sufficient when we have a popup grab surface as 'out'
215-- may mean in on the popup
216local function wnd_mouse_out(wnd)
217	if wnd.wm.cfg.mouse_focus and wnd.states.focused then
218		wnd.wm.focus()
219	end
220end
221
222local function wnd_mouse_btn(wnd, vid, button, active, x, y)
223	if not active then
224		wnd.states.moving = false
225		wnd.states.resizing = false
226
227	elseif not wnd.states.focused then
228		wnd.wm.focus(wnd)
229	end
230
231-- catch any popups, this will cause spurious 'release' events in
232-- clients if the button mask isn't accounted for
233	if wnd.dismiss_chain then
234		if active then
235			wnd.block_release = button
236			wnd:dismiss_chain()
237		end
238		return
239	end
240
241-- block until focus is ack:ed
242	if not wnd.states.focused then
243		return
244	end
245
246	if wnd.block_release == button and not active then
247		wnd.block_release = nil
248		return
249	end
250
251	target_input(wnd.vid, {
252		kind = "digital",
253		mouse = true,
254		devid = 0,
255		subid = button,
256		active = active,
257	})
258end
259
260local function wnd_mouse_drop(wnd)
261	wnd:drag_resize()
262end
263
264local function wnd_mouse_motion(wnd, vid, x, y, rx, ry)
265	local tx = x - wnd.x
266	local ty = y - wnd.y
267
268	target_input(wnd.vid, {
269		kind = "analog", mouse = true,
270		devid = 0, subid = 0,
271		samples = {tx, rx}
272	})
273
274	target_input(wnd.vid, {
275		kind = "analog", mouse = true,
276		devid = 0, subid = 1,
277		samples = {ty, ry}
278	})
279end
280
281local function wnd_mouse_drag(wnd, vid, dx, dy)
282-- also need to cover 'cursor-tagging' hint here for drag and drop
283
284	if wnd.states.moving then
285		local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy)
286		wnd.x = x
287		wnd.y = y
288
289-- for x11 we also need to message the new position
290		if wnd.send_position then
291			local msg = string.format("kind=move:x=%d:y=%d", wnd.x, wnd.y);
292			wnd.wm.log("wl_x11", msg)
293			target_input(vid, msg)
294		end
295
296		move_image(wnd.vid, wnd.x, wnd.y)
297		return
298
299--w when this is set, the resizing[] mask is also set
300	elseif wnd.states.resizing then
301		wnd:drag_resize(dx, dy)
302
303-- need to clamp to something
304		if wnd.in_resize[1] < 32 then
305			wnd.in_resize[1] = 32
306		end
307
308		if wnd.in_resize[2] < 32 then
309			wnd.in_resize[2] = 32
310		end
311
312		target_displayhint(wnd.vid, wnd.in_resize[1], wnd.in_resize[2])
313
314-- re-emit as motion
315	else
316		local mx, my = mouse_xy()
317		wnd_mouse_motion(wnd, vid, mx, my)
318	end
319end
320
321local function wnd_hint_state(wnd)
322	local mask = 0
323	if not wnd.states.focused then
324		mask = bit.bor(mask, TD_HINT_UNFOCUSED)
325	end
326
327	if not wnd.states.visible then
328		mask = bit.bor(mask, TD_HINT_INVISIBLE)
329	end
330
331	if wnd.states.maximized then
332		mask = bit.bor(mask, TD_HINT_MAXIMIZED)
333	end
334
335	if wnd.states.fullscreen then
336		mask = bit.bor(mask, TD_HINT_FULLSCREEN)
337	end
338
339	return mask
340end
341
342local function wnd_unfocus(wnd)
343	wnd.wm.log(wnd.name, wnd.wm.fmt("focus=off:idstr=%s", wnd.idstr and wnd.idstr or wnd.name))
344	wnd.states.focused = false
345	target_displayhint(wnd.vid, 0, 0, wnd_hint_state(wnd))
346	wnd.wm.custom_cursor = false
347	mouse_switch_cursor("default")
348end
349
350local function wnd_focus(wnd)
351	wnd.wm.log(wnd.name, wnd.wm.fmt("focus=on:idstr=%s", wnd.idstr and wnd.idstr or wnd.name))
352	wnd.states.focused = true
353	target_displayhint(wnd.vid, 0, 0, wnd_hint_state(wnd))
354	wnd.wm.custom_cursor = wnd
355	table.remove_match(wnd.wm.window_stack, wnd)
356	table.insert(wnd.wm.window_stack, wnd)
357	wnd.wm:restack()
358end
359
360local function wnd_destroy(wnd)
361	wnd.wm.log(wnd.name, "destroy")
362	mouse_droplistener(wnd)
363	wnd.wm.windows[wnd.cookie] = nil
364	table.remove_match(wnd.wm.window_stack, wnd)
365	wnd.wm:restack()
366
367	if wnd.wm.custom_cursor == wnd then
368		mouse_switch_cursor("default")
369	end
370	if wnd.wm.cfg.destroy then
371		wnd.wm.cfg.destroy(wnd)
372	end
373	if valid_vid(wnd.vid) then
374		wnd.wm.known_surfaces[wnd.vid] = nil
375		delete_image(wnd.vid)
376	end
377end
378
379local function wnd_fullscreen(wnd)
380	if wnd.states.fullscreen then
381		return
382	end
383
384	wnd.states.fullscreen = {wnd.w, wnd.h, wnd.x, wnd.y}
385	wnd.defer_move = {0, 0}
386	target_displayhint(wnd.vid,
387		wnd.wm.disptbl.width, wnd.wm.disptbl.height, wnd_hint_state(wnd))
388end
389
390local function wnd_maximize(wnd)
391	if wnd.states.maximized then
392		return
393	end
394
395-- drop fullscreen if we have it, but block hint
396	if wnd.states.fullscreen then
397		wnd:revert({no_hint = true})
398	end
399
400	wnd.states.maximized = {wnd.w, wnd.h, wnd.x, wnd.y}
401	wnd.defer_move = {0, 0}
402
403	target_displayhint(wnd.vid,
404		wnd.wm.disptbl.width, wnd.wm.disptbl.height, wnd_hint_state(wnd))
405end
406
407local function wnd_revert(wnd, opts)
408	local tbl
409
410-- drop fullscreen to maximized or 'normal' (the attributes stack)
411	if wnd.states.fullscreen then
412		tbl = wnd.states.fullscreen
413		wnd.states.fullscreen = false
414
415	elseif wnd.states.maximized then
416		tbl = wnd.states.maximized
417		wnd.states.maximized = false
418	else
419		return
420	end
421
422-- the better way of doing this is probably to enable detailed frame
423-- reporting for the surface, and have a state hook after this request
424-- an edge case is where the maximized dimensions correspond to the
425-- unmaximized ones which may be possible if there are no server-side
426-- decorations.
427	if not opts or not opts.no_hint then
428		target_displayhint(wnd.vid, tbl[1], tbl[2], wnd_hint_state(wnd))
429		wnd.defer_move = {tbl[3], tbl[4]}
430	end
431
432	wnd_hint_state(wnd)
433end
434
435local function tl_wnd_resized(wnd, source, status)
436	if not wnd.states.mapped then
437		wnd.states.mapped = true
438		show_image(wnd.vid)
439		wnd.wm.mapped(wnd)
440	end
441
442-- special handling for drag-resize where we request a move
443	local rzmask = wnd.states.resizing
444	if rzmask then
445		local dw = (wnd.w - status.width)
446		local dh = (wnd.h - status.height)
447		local dx = dw * rzmask[3]
448		local dy = dh * rzmask[4]
449
450-- the move handler should account for padding
451		local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy)
452		wnd.x = x
453		wnd.y = y
454		move_image(wnd.vid, wnd.x, wnd.y)
455		wnd.defer_move = nil
456	end
457
458-- special case for state changes (maximized / fullscreen)
459	wnd.w = status.width
460	wnd.h = status.height
461	resize_image(wnd.vid, status.width, status.height)
462
463	if wnd.use_decor then
464		wnd.wm.decorate(wnd, wnd.vid, wnd.w, wnd.h)
465	end
466
467	if wnd.defer_move then
468		local x, y = wnd.wm.move(wnd, wnd.defer_move[1], wnd.defer_move[2])
469		move_image(wnd.vid, x, y)
470		wnd.x = x
471		wnd.y = y
472		wnd.defer_move = nil
473	end
474end
475
476local function self_own(self, vid)
477-- if we are matching or a grab exists and we hold the grab or there is some proxy
478	return self.vid == vid or (self.mouse_proxy and vid == self.mouse_proxy)
479end
480
481local function x11_wnd_realize(wnd, popup, grab)
482	if wnd.realized then
483		return
484	end
485
486	if not wnd.states.mapped or not wnd.states.typed then
487		hide_image(wnd.vid)
488		return
489	end
490
491	show_image(wnd.vid)
492	target_displayhint(wnd.vid, wnd.w, wnd.h)
493	mouse_addlistener(wnd, {"motion", "drag", "drop", "button", "over", "out"})
494	table.insert(wnd.wm.window_stack, 1, wnd)
495
496	wnd.wm.state_change(wnd, "realized", popup, grab)
497	wnd.realized = true
498end
499
500local function x11_wnd_type(wnd)
501	local popup_type =
502		wnd.states.typed == "menu"
503		or wnd.states.typed == "popup"
504		or wnd.states.typed == "tooltip"
505		or wnd.states.typed == "dropdown"
506
507-- icccm says about stacking order:
508-- wm_type_desktop < state_below < (no type) < dock | state_above < fullscreen
509-- other options, dnd, splash, utility (persistent_for)
510
511	if popup_type then
512		wnd.use_decor = false
513		wnd.wm.decorate(wnd)
514		image_inherit_order(wnd.vid, false)
515		order_image(wnd.vid, 65531)
516	else
517-- decorate should come from toplevel
518		image_inherit_order(wnd.vid, true)
519		wnd.use_decor = true
520		order_image(wnd.vid, 1)
521	end
522
523	wnd:realize()
524end
525
526local function x11_nudge(wnd, dx, dy)
527	local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy, dx, dy)
528	move_image(wnd.vid, x, y)
529	wnd.x = x
530	wnd.y = y
531	local msg = string.format("kind=move:x=%d:y=%d", x, y);
532
533	wnd.wm.log("wl_x11", msg)
534	target_input(wnd.vid, msg)
535end
536
537local function wnd_nudge(wnd, dx, dy)
538	local x, y = wnd.wm.move(wnd, wnd.x + dx, wnd.y + dy, dx, dy)
539	move_image(wnd.vid, x, y)
540	wnd.x = x
541	wnd.y = y
542	wnd.wm.log("wnd", wnd.wm.fmt("source=%d:x=%d:y=%d", wnd.vid, x, y))
543end
544
545local function wnd_drag_rz(wnd, dx, dy, mx, my)
546	if not dx then
547		wnd.states.resizing = false
548		wnd.in_resize = nil
549		return
550	end
551
552	if not wnd.in_resize then
553		wnd.in_resize = {wnd.w, wnd.h}
554		if (mx and my) then
555			wnd.states.resizing = {1, 1, mx, my}
556		end
557	end
558-- apply direction mask, clamp against lower / upper constraints
559	local tw = wnd.in_resize[1] + (dx * wnd.states.resizing[1])
560	local th = wnd.in_resize[2] + (dy * wnd.states.resizing[2])
561
562	if tw < wnd.min_w then
563		tw = wnd.min_w
564	end
565
566	if th < wnd.min_h then
567		th = wnd.min_h
568	end
569
570	if wnd.max_w > 0 and tw > wnd.max_w then
571		tw = wnd.max_w
572	end
573
574	if wnd.max_h > 0 and tw > wnd.max_h then
575		tw = wnd.max_h
576	end
577
578	wnd.in_resize = {tw, th}
579	target_displayhint(wnd.vid, wnd.in_resize[1], wnd.in_resize[2])
580	wnd.wm.log("wnd", wnd.wm.fmt("source=%d:drag_rz=%d:%d",
581		wnd.vid, wnd.in_resize[1], wnd.in_resize[2]))
582end
583
584-- several special considerations with x11, particularly that some
585-- things are positioned based on a global 'root' anchor, a rather
586-- involved type model and a number of wm messages that we need to
587-- respond to.
588--
589-- another is that we need to decorate window contents ourselves,
590-- with all the jazz that entails.
591local function x11_vtable()
592	return {
593		name = "x11_bridge",
594		own = self_own,
595		x = 0,
596		y = 0,
597		w = 32,
598		h = 32,
599		pad_x = 0,
600		pad_y = 0,
601		min_w = 32,
602		min_h = 32,
603		max_w = 0,
604		max_h = 0,
605		send_position = true,
606		use_decor = true,
607
608		states = {
609			mapped = false,
610			typed = false,
611			fullscreen = false,
612			maximized = false,
613			visible = false,
614			moving = false,
615			resizing = false
616		},
617
618-- assumes a basic 'window' then we patch things around when we have
619-- been assigned a type / mapped - default is similar to wayland toplevel
620-- with added messaging about window coordinates within the space
621		destroy = wnd_destroy,
622		input_table = wnd_input_table,
623
624		over = wnd_mouse_over,
625		out = wnd_mouse_out,
626		button = wnd_mouse_btn,
627		drag = x11_mouse_drag,
628		motion = wnd_mouse_motion,
629		drop = wnd_mouse_drop,
630
631		focus = wnd_focus,
632		unfocus = wnd_unfocus,
633		revert = wnd_revert,
634		fullscreen = wnd_fullscreen,
635		maximize = wnd_maximize,
636		drag_resize = wnd_drag_rz,
637		nudge = x11_nudge,
638		apply_type = x11_wnd_type,
639		realize = x11_wnd_realize
640	}
641end
642
643local function wnd_dnd_source(wnd, x, y, types)
644-- this needs the 'cursor-tag' attribute when we enter, then some
645-- marking function to say if it is desired or not ..
646end
647
648local function wnd_copy_paste(wnd, src, dst)
649	local nc = #src.states.copy_set
650	if not dst.wm or not valid_vid(dst.wm.control) or nc == 0 then
651		return
652	end
653
654	local control = dst.wm.control
655
656-- send src to copy client, mark as 'latest' src, clamp number of offers
657	target_input(control, "offer")
658	local lim = nc < 32 and nc or 32
659	for i=1,lim do
660		target_input(control, v)
661	end
662	target_input(control, "/offer")
663
664-- when the client has picked an offer, that will come back in the wm
665-- handler on dst and initiate the paste operation then
666	dst.wm.offer_src = src
667end
668
669local function tl_vtable(wm)
670	return {
671		name = "wl_toplevel",
672		wm = wm,
673
674-- states that need to be configurable from the WM and forwarded to the
675-- client so that it can update decorations or modify its input response
676		states = {
677			mapped = false,
678			focused = false,
679			fullscreen = false,
680			maximized = false,
681			visible = false,
682			moving = false,
683			resizing = false,
684			copy_set = {}
685		},
686
687-- wm side calls these to acknowledge or initiat estate changes
688		focus = wnd_focus,
689		unfocus = wnd_unfocus,
690		maximize = wnd_maximize,
691		fullscreen = wnd_fullscreen,
692		revert = wnd_revert,
693		nudge = wnd_nudge,
694		drag_resize = wnd_drag_rz,
695		dnd_source = wnd_dnd_source,
696		copy_paste = wnd_paste_opts,
697
698-- properties that needs to be tracked for drag-resize/drag-move
699		x = 0,
700		y = 0,
701		w = 0,
702		h = 0,
703		min_w = 32,
704		min_h = 32,
705		max_w = 0,
706		max_h = 0,
707
708-- keyboard input
709		input_table = wnd_input_table,
710
711-- touch-mouse input goes here
712		over = wnd_mouse_over,
713		out = wnd_mouse_out,
714		drag = wnd_mouse_drag,
715		drop = wnd_mouse_drop,
716		button = wnd_mouse_btn,
717		motion = wnd_mouse_motion,
718		destroy = wnd_destroy,
719		own = self_own
720	}
721end
722
723local function popup_click(popup, vid, x, y)
724	local tbl =
725	target_input(vid, {
726		kind = "digital",
727		mouse = true,
728		devid = 0,
729		subid = 1,
730		active = true,
731	})
732	target_input(vid, {
733		kind = "digital",
734		mouse = true,
735		devid = 0,
736		subid = 1,
737		active = false,
738	})
739end
740
741-- put an invisible surface at the overlay level and add a mouse-handler that
742-- calls a destroy function if clicked.
743local function setup_grab_surface(popup)
744	local vid = null_surface(popup.wm.disptbl.width, popup.wm.disptbl.height)
745	rendertarget_attach(popup.wm.disptbl.rt, vid, RENDERTARGET_DETACH)
746
747	show_image(vid)
748	order_image(vid, 65530)
749	image_tracetag(vid, "popup_grab")
750
751	local done = false
752	local tbl = {
753		name = "popup_grab_mh",
754		own = function(ctx, tgt)
755			return vid == tgt
756		end,
757		button = function()
758			if not done then
759				done = true
760				popup:destroy()
761			end
762		end
763	}
764	mouse_addlistener(tbl, {"button"})
765	popup.wm.log("popup", popup.wm.fmt("grab_on"))
766
767	return function()
768		popup.wm.log("popup", popup.wm.fmt("grab_free"))
769		mouse_droplistener(tbl)
770		delete_image(vid)
771		done = true
772	end
773end
774
775local function popup_destroy(popup)
776	if popup.grab then
777		popup.grab = popup.grab()
778	end
779
780	mouse_switch_cursor("default")
781
782-- might have chain-destroyed through the parent vid or terminated on its own
783	if valid_vid(popup.vid) then
784		delete_image(popup.vid)
785		popup.wm.known_surfaces[popup.vid] = nil
786	end
787
788	mouse_droplistener(popup)
789	popup.wm.windows[popup.cookie] = nil
790end
791
792local function popup_over(popup)
793-- this might have changed with mouse_out
794	if popup.wm.cursor then
795		mouse_custom_cursor(popup.wm.cursor)
796	else
797		mouse_switch_cursor("default")
798	end
799end
800
801local function popup_vtable()
802	return {
803		name = "popup_mh",
804		own = self_own,
805		motion = wnd_mouse_motion,
806		click = popup_click,
807		destroy = popup_destroy,
808		over = popup_over,
809		out = popup_out,
810		states = {
811		},
812		x = 0,
813		y = 0
814	}
815end
816
817local function on_popup(popup, source, status)
818	if status.kind == "create" then
819		local wnd = popup
820
821-- if we have a popup that has not been assigned to anything when we get
822-- the next one already, not entirely 100% whether that is permitted, and
823-- more importantly, actually used/sanctioned behaviour
824		if wnd.pending_popup and valid_vid(wnd.pending_popup.vid) then
825			wnd.pending_popup:destroy()
826		end
827
828		local popup = popup_vtable()
829		local vid, aid, cookie =
830		accept_target(
831			function(...)
832				return on_popup(popup, ...)
833			end
834		)
835		image_tracetag(vid, "wl_popup")
836		rendertarget_attach(wnd.disptbl.rt, vid, RENDERTARGET_DETACH)
837
838		wnd.known_surfaces[vid] = true
839		wnd.pending_popup = popup
840		link_image(vid, wnd.anchor)
841		popup.wm = wnd
842		popup.cookie = cookie
843		popup.vid = vid
844
845-- also not entirely sure if popup-drag-n-drop behavior is a thing, so just
846-- map clicks and motion for the time being
847		mouse_addlistener(popup, {"motion", "click", "over", "out"})
848
849	elseif status.kind == "terminated" then
850		popup:destroy()
851
852	elseif status.kind == "resized" then
853
854-- wait with showing the popup until it is both viewported and mapped
855		if not popup.states.mapped then
856			popup.states.mapped = true
857			if popup.got_parent then
858				show_image(popup.vid)
859			end
860		end
861		resize_image(popup.vid, status.width, status.height)
862
863	elseif status.kind == "viewport" then
864		local pwnd = popup.wm.windows[status.parent]
865		if not pwnd then
866			popup.wm.log("popup", popup.wm.fmt("bad_parent=%d", status.parent))
867			popup.got_parent = false
868			hide_image(popup.vid)
869			return
870		end
871
872		pwnd.popup = popup
873		popup.parent = pwnd
874		popup.got_parent = true
875
876-- more anchoring and positioning considerations here
877		link_image(popup.vid, pwnd.vid)
878		move_image(popup.vid, status.rel_x, status.rel_y)
879
880-- 'popups' can be used for tooltips and so on as well, take that into account
881-- as well as enable a 'grab' layer that lives with the focused popup
882		if status.focus then
883			order_image(popup.vid, 65531)
884			if not popup.grab then
885				popup.grab = setup_grab_surface(popup)
886			end
887			image_mask_clear(popup.vid, MASK_UNPICKABLE)
888		else
889-- release any existing grab
890			if popup.grab then
891				popup.grab = popup.grab()
892			end
893			order_image(popup.vid, 1)
894			image_mask_set(popup.vid, MASK_UNPICKABLE)
895		end
896
897-- this needs to be synched if the window is moved through code/wm
898		local props = image_surface_resolve(popup.vid)
899		popup.x = props.x
900		popup.y = props.y
901
902-- possible animation hook
903		if popup.states.mapped then
904			show_image(popup.vid)
905		end
906
907		if popup.wm.pending_popup == popup then
908			popup.wm.pending_popup = nil
909		end
910	end
911end
912
913local function on_toplevel(wnd, source, status)
914	if status.kind == "create" then
915		local new = tl_vtable()
916		new.wm = wnd
917
918-- request dimensions from wm
919		local w, h, x, y = wnd.configure(new, "toplevel")
920		local vid, aid, cookie =
921		accept_target(w, h,
922			function(...)
923				return on_toplevel(new, ...)
924			end
925		)
926		rendertarget_attach(wnd.disptbl.rt, vid, RENDERTARGET_DETACH)
927		new.vid = vid
928		new.cookie = cookie
929		wnd.known_surfaces[vid] = true
930		new.x = x
931		new.y = y
932		table.insert(wnd.window_stack, new)
933
934-- tie to bridge as visibility / clipping anchor
935		image_tracetag(vid, "wl_toplevel")
936		new.vid = vid
937		image_inherit_order(vid, true)
938		link_image(vid, wnd.anchor)
939
940-- register mouse handler
941		mouse_addlistener(new, {"over", "out", "drag", "button", "motion", "drop"})
942
943		return new, cookie
944
945	elseif status.kind == "terminated" then
946		wnd:destroy()
947
948-- might need to add frame delivery notification so that we can track/clear
949-- parts that have 'double buffered' state
950
951	elseif status.kind == "resized" then
952-- first time showing
953		tl_wnd_resized(wnd, source, status)
954
955-- viewport is used here to define another window as the current toplevel,
956-- i.e. that should cause this window to hide or otherwise be masked, and
957-- the parent set to order above it (until unset)
958	elseif status.kind == "viewport" then
959		local parent = wnd.wm.windows[status.parent]
960		if parent then
961			wnd.wm.log("wl_toplevel", wnd.wm.fmt("reparent=%d",status.parent))
962			wnd.wm.state_change(wnd, "toplevel", parent)
963		else
964			wnd.wm.log("wl_toplevel", wnd.wm.fmt("viewport:unknown_parent:%d", status.parent))
965		end
966
967-- wl specific wm hacks
968	elseif status.kind == "message" then
969		wnd.wm.log("wl_toplevel", wnd.wm.fmt("message=%s", status.message))
970		local opts = string.split(status.message, ':')
971		if not opts or not opts[1] then
972			return
973		end
974
975		if
976			opts[1] == "shell" and
977			opts[2] == "xdg_top" and
978			opts[3] and wl_top_lut[opts[3]] then
979			wl_top_lut[opts[3]](wnd, source, unpack(opts, 4))
980		end
981	end
982end
983
984local function on_cursor(ctx, source, status)
985	if status.kind == "create" then
986		local cursor = accept_target(
987		function(...)
988			return on_cursor(ctx, ...)
989		end)
990
991		ctx.cursor.vid = cursor
992		link_image(ctx.bridge, cursor)
993		ctx.known_surfaces[cursor] = true
994		image_tracetag(cursor, "wl_cursor")
995		rendertarget_attach(ctx.disptbl.rt, cursor, RENDERTARGET_DETACH)
996
997	elseif status.kind == "resized" then
998		ctx.cursor.width = status.width
999		ctx.cursor.height = status.height
1000		resize_image(ctx.cursor.vid, status.width, status.height)
1001
1002		if ctx.custom_cursor then
1003			mouse_custom_cursor(ctx.cursor)
1004		end
1005
1006	elseif status.kind == "message" then
1007-- hot-spot modification?
1008		if ctx.custom_cursor then
1009			mouse_custom_cursor(ctx.cursor)
1010		end
1011
1012	elseif status.kind == "terminated" then
1013		delete_image(source)
1014		ctx.known_surfaces[source] = nil
1015	end
1016end
1017
1018-- fixme: incomplete, input routing, attachments etc. needed
1019local function on_subsurface(ctx, source, status)
1020	if status.kind == "create" then
1021		local subwnd = {
1022			name = "tl_subsurface"
1023		}
1024		local vid, aid, cookie =
1025		accept_target(
1026			function(...)
1027				return on_subsurface(subwnd, ...)
1028			end
1029		)
1030		subwnd.vid = vid
1031		subwnd.wm = ctx
1032		subwnd.cookie = cookie
1033		ctx.wm.known_surfaces[vid] = true
1034		rendertarget_attach(ctx.wm.disptbl.rt, vid, RENDERTARGET_DETACH)
1035		image_tracetag(vid, "wl_subsurface")
1036
1037-- subsurfaces need a parent to attach to and 'extend',
1038-- input should be translated into its coordinate space as well
1039		return subwnd, cookie
1040	elseif status.kind == "resized" then
1041
1042	elseif status.kind == "viewport" then
1043		link_image(source, parent.vid)
1044
1045	elseif status.kind == "terminated" then
1046		delete_image(source)
1047		ctx.wm.windows[ctx.cookie] = nil
1048		ctx.wm.known_surfaces[source] = nil
1049	end
1050end
1051
1052local function x11_viewport(wnd, source, status)
1053	local anchor = wnd.wm.anchor
1054	if status.parent ~= 0 then
1055		local pwnd = wnd.wm.windows[status.parent]
1056		if pwnd then
1057			anchor = pwnd.vid
1058		end
1059	end
1060
1061-- ignore repositioning hints while we are dragging
1062	if wnd.in_resize then
1063		return
1064	end
1065
1066-- depending on type, we need to order around as well
1067	link_image(wnd.vid, anchor)
1068	move_image(wnd.vid, status.rel_x, status.rel_y)
1069
1070	local props = image_surface_resolve(wnd.vid)
1071	local x, y = wnd.wm.move(wnd, props.x, props.y)
1072	wnd.x = x
1073	wnd.y = y
1074
1075	wnd.wm.log("wl_x11", wnd.wm.fmt(
1076		"viewport:parent=%d:hx=%d:hy=%d:x=%d:y=%d",
1077		status.parent, status.rel_x, status.rel_y, x, y)
1078	)
1079
1080-- we need something more here to protect against an infinite move-loop
1081--	target_input(wnd.vid, string.format("kind=move:x=%d:y=%d", x, y))
1082end
1083
1084local function on_x11(wnd, source, status)
1085-- most involved here as the meta-WM forwards a lot of information
1086	if status.kind == "create" then
1087		local x11 = x11_vtable()
1088		x11.wm = wnd
1089
1090		local w, h, x, y = wnd.configure(x11, "x11")
1091
1092		local vid, aid, cookie =
1093		accept_target(w, h,
1094		function(...)
1095			return on_x11(x11, ...)
1096		end)
1097		rendertarget_attach(wnd.disptbl.rt, vid, RENDERTARGET_DETACH)
1098
1099		x11.x = x
1100		x11.y = y
1101		move_image(vid, x, y)
1102
1103		wnd.known_surfaces[vid] = true
1104
1105-- send our preset position, might not matter if it is override-redirect
1106-- this was removed as it caused a race condition, 'create' doesn't mean that
1107-- the window is realized yet, so defer that until it happens
1108--
1109--		local msg = string.format("kind=move:x=%d:y=%d", x, y)
1110--		wnd.log("wl_x11", msg)
1111--		target_input(vid, msg)
1112--		show_image(vid)
1113
1114		x11.vid = vid
1115		x11.cookie = cookie
1116		image_tracetag(vid, "x11_unknown_type")
1117		image_inherit_order(vid, true)
1118		link_image(vid, wnd.anchor)
1119
1120		return x11, cookie
1121
1122	elseif status.kind == "resized" then
1123		tl_wnd_resized(wnd, source, status)
1124		wnd:realize()
1125
1126-- let the caller decide how we deal with decoration
1127		if wnd.realized and wnd.use_decor then
1128			local t, l, d, r = wnd.wm.decorate(wnd, wnd.vid, wnd.w, wnd.h)
1129			wnd.pad_x = t
1130			wnd.pad_y = l
1131		end
1132
1133	elseif status.kind == "message" then
1134		local opts = string.split(status.message, ':')
1135		if not opts or not opts[1] or not x11_lut[opts[1]] then
1136			wnd.wm.log("wl_x11", wnd.wm.fmt("unhandled_message=%s", status.message))
1137			return
1138		end
1139		wnd.wm.log("wl_x11", wnd.wm.fmt("message=%s", status.message))
1140		return x11_lut[opts[1]](wnd, source, unpack(opts, 2))
1141
1142	elseif status.kind == "registered" then
1143		wnd.guid = status.guid
1144
1145	elseif status.kind == "viewport" then
1146		x11_viewport(wnd, source, status)
1147
1148	elseif status.kind == "terminated" then
1149		wnd:destroy()
1150	end
1151end
1152
1153local function bridge_handler(ctx, source, status)
1154	if status.kind == "terminated" then
1155		ctx:destroy()
1156		return
1157
1158-- message on the bridge is also used to pass metadata about the 'data-device'
1159-- properties like selection changes and type
1160	elseif status.kind == "message" then
1161		local cmd, data = string.split_first(status.message, ":")
1162		ctx.log("bridge", ctx.fmt("message:kind=%s", cmd))
1163		if cmd == "offer" then
1164			if table.find_i(ctx.offer, data) then
1165				return
1166			end
1167
1168-- normal 'behavior' here is that the data source provides a stream of types that
1169-- supposedly is mime, in reality is a mix of whatever, on 'paste' or drag-enter
1170-- you forward these to the destination, it says which one it is, then send
1171-- descriptors in both directions. Since the WM might want to do things with the
1172-- clipboard contents - notify here and provide the methods to forward / trigger
1173-- an offer.
1174			table.insert(ctx.offer, data)
1175
1176		elseif cmd == "offer-reset" then
1177			ctx.offer = {}
1178		end
1179
1180		return
1181	elseif status.kind ~= "segment_request" then
1182		return
1183	end
1184
1185	local permitted = {
1186		cursor = on_cursor,
1187		application = on_toplevel,
1188		popup = on_popup,
1189		multimedia = on_subsurface, -- fixme: still missing
1190		["bridge-x11"] = on_x11
1191	}
1192
1193	local handler = permitted[status.segkind]
1194	if not handler then
1195		warning("unhandled segment type: " .. status.segkind)
1196		return
1197	end
1198
1199-- actual allocation is deferred to the specific handler, some might need to
1200-- call back into the outer WM to get suggested default size/position - x11
1201-- clients need world-space coordinates and so on
1202	local wnd, cookie = handler(ctx, source, {kind = "create"})
1203	if wnd then
1204		ctx.windows[cookie] = wnd
1205	end
1206end
1207
1208local function set_rate(ctx, period, delay)
1209	message_target(ctx.bridge,
1210		string.format("seat:rate:%d,%d", period, delay))
1211end
1212
1213-- first wayland node, limited handler that can only absorb meta info,
1214-- act as clipboard and allocation proxy
1215local function set_bridge(ctx, source)
1216	local w = ctx.disptbl.width
1217	local h = ctx.disptbl.height
1218
1219	target_displayhint(source, w, h, 0, ctx.disptbl)
1220
1221-- wl_drm need to be able to authenticate against the GPU, which may
1222-- have security implications for some people - low risk enough for
1223-- opt-out rather than in
1224	if not ctx.cfg.block_gpu then
1225		target_flags(source, TARGET_ALLOWGPU)
1226	end
1227
1228	target_updatehandler(source,
1229		function(...)
1230			return bridge_handler(ctx, ...)
1231		end
1232	)
1233
1234	ctx.bridge = source
1235	ctx.anchor = null_surface(w, h)
1236	image_tracetag(ctx.anchor, "wl_bridge_anchor")
1237	image_mask_set(ctx.anchor, MASK_UNPICKABLE)
1238
1239	show_image(ctx.anchor)
1240	ctx.mh = {
1241		name = "wl_bg",
1242		own = self_own,
1243		vid = ctx.anchor,
1244		click = function()
1245			ctx.focus()
1246		end,
1247	}
1248	mouse_addlistener(ctx.mh, {"click"})
1249
1250--	ctx:repeat_rate(ctx.cfg.repeat, ctx.cfg.delay)
1251end
1252
1253local function resize_output(ctx, neww, newh, density, refresh)
1254	if density then
1255		ctx.disptbl.vppcm = density
1256		ctx.disptbl.hppcm = density
1257	end
1258
1259	if neww then
1260		ctx.disptbl.width = neww
1261	else
1262		neww = ctx.disptbl.width
1263	end
1264
1265	if newh then
1266		ctx.disptbl.height = newh
1267	else
1268		newh = ctx.disptbl.height
1269	end
1270
1271	if refresh then
1272		ctx.disptbl.refresh = refresh
1273	end
1274
1275	if not valid_vid(ctx.bridge) then
1276		return
1277	end
1278
1279	ctx.log("bridge", ctx.fmt("output_resize=%d:%d", ctx.disptbl.width, ctx.disptbl.height))
1280	target_displayhint(ctx.bridge, neww, newh, 0, ctx.disptbl)
1281
1282-- tell all windows that some of their display parameters have changed,
1283-- if the window is in fullscreen/maximized state - the surface should
1284-- be resized as well
1285	for _, v in pairs(ctx.windows) do
1286-- this will fetch the refreshed display table
1287		if v.reconfigure then
1288			if v.states.fullscreen or v.states.maximized then
1289				v:reconfigure(neww, newh)
1290			else
1291				v:reconfigure(v.w, v.h)
1292			end
1293		end
1294	end
1295end
1296
1297local function reparent_rt(ctx, rt)
1298	ctx.disptbl.rt = rt
1299	for k,v in pairs(ctx.known_surfaces) do
1300		rendertarget_attach(ctx.disptbl.rt, k, RENDERTARGET_DETACH)
1301	end
1302	if valid_vid(ctx.anchor) then
1303		rendertarget_attach(ctx.disptbl.rt, ctx.anchor, RENDERTARGET_DETACH)
1304	end
1305end
1306
1307local window_stack = {}
1308local function restack(ctx)
1309	local cnt = 0
1310	for _, v in pairs(ctx.windows) do
1311		cnt = cnt + 1
1312	end
1313-- re-order
1314	for i,v in ipairs(ctx.window_stack) do
1315		order_image(v.vid, i * 10);
1316	end
1317end
1318
1319local function bridge_table(cfg)
1320	local res = {
1321-- vid to the client bridge
1322		control = BADID,
1323
1324-- The window stack is (default) global for all bridges for ordering to work,
1325-- each window is reserved 10 order slots.
1326		window_stack = window_stack,
1327
1328-- key indexed on window identifier cookie
1329		windows = {},
1330
1331-- tracks all externally allocated VIDs
1332		known_surfaces = {},
1333
1334-- currently active cursor on seat
1335		cursor = {
1336			vid = BADID,
1337			hotspot_x = 0,
1338			hotspot_y = 0,
1339			width = 1,
1340			height = 1
1341		},
1342
1343		offer = {
1344		},
1345
1346-- user table of settings
1347		cfg = cfg,
1348
1349-- last known 'output' properties (vppcm, refresh also possible)
1350		disptbl = {
1351			rt = WORLDID,
1352			width = VRESW,
1353			height = VRESH,
1354			ppcm = VPPCM
1355		},
1356
1357-- call when output properties have changed
1358		resize = resize_output,
1359
1360-- call to update keyboard state knowledge
1361		repeat_rate = set_rate,
1362
1363-- called internally whenever the window stack has changed
1364		restack = restack,
1365
1366-- call to switch attachment to a specific rendertarget
1367		set_rt = reparent_rt,
1368
1369-- swap out for logging / tracing function
1370		log = print,
1371		fmt = string.format
1372	}
1373
1374-- let client config override some defaults
1375	if cfg.width then
1376		res.disptbl.width = cfg.width
1377	end
1378
1379	if cfg.height then
1380		res.disptbl.height = cfg.height
1381	end
1382
1383	if cfg.fmt then
1384		res.fmt = cfg.fmt
1385	end
1386
1387	if cfg.log then
1388		res.log = cfg.log
1389	end
1390
1391	if type(cfg.window_stack) == "table" then
1392		res.window_stack = cfg.window_stack
1393	end
1394
1395-- add client defined event handlers, provide default inplementations if missing
1396	if type(cfg.move) == "function" then
1397		res.log("wlwm", "override_handler=move")
1398		res.move = cfg.move
1399	else
1400		res.log("wlwm", "default_handler=move")
1401		res.move =
1402		function(wnd, x, y, dx, dy)
1403			return x, y
1404		end
1405	end
1406
1407	if type(cfg.context_menu) == "function" then
1408		res.log("wlwm", "override_handler=context_menu")
1409		res.context_menu = cfg.context_menu
1410	else
1411		res.context_menu = function()
1412		end
1413	end
1414
1415	res.configure =
1416	function(...)
1417		local w, h, x, y
1418		if cfg.configure then
1419			w, h, x, y = cfg.configure(...)
1420		end
1421		w = w and w or res.disptbl.width * 0.5
1422		h = h and h or res.disptbl.height * 0.3
1423		if not x or not y then
1424			x, y = mouse_xy()
1425		end
1426		return w, h, x, y
1427	end
1428
1429	if type(cfg.focus) == "function" then
1430		res.log("wlwm", "override_handler=focus")
1431		res.focus = cfg.focus
1432	else
1433		res.log("wlwm", "default_handler=focus")
1434		res.focus =
1435		function()
1436			return true
1437		end
1438	end
1439
1440	if type(cfg.decorate) == "function" then
1441		res.log("wlwm", "override_handler=decorate")
1442		res.decorate = cfg.decorate
1443	else
1444		res.log("wlwm", "default_handler=decorate")
1445		res.decorate =
1446		function()
1447		end
1448	end
1449
1450	if type(cfg.mapped) == "function" then
1451		res.log("wlwm", "override_handler=mapped")
1452		res.mapped = cfg.mapped
1453	else
1454		res.log("wlwm", "default_handler=mapped")
1455		res.mapped =
1456		function()
1457		end
1458	end
1459
1460	if type(cfg.state_change) == "function" then
1461		res.log("wlwm", "override_handler=state_change")
1462		res.state_change = cfg.state_change
1463	else
1464		res.state_change =
1465		function(wnd, state)
1466			if not state then
1467				wnd:revert()
1468			end
1469
1470		end
1471	end
1472
1473	if (cfg.resize_request) == "function" then
1474		res.log("wlwm", "override_handler=resize_request")
1475		res.resize_request = cfg.resize_request
1476	else
1477		res.log("wlwm", "default_handler=resize_request")
1478		res.resize_request =
1479		function(wnd, new_w, new_h)
1480			if new_w > ctx.disptbl.width then
1481				new_w = ctx.disptbl.width
1482			end
1483
1484			if new_h > ctx.disptbl.height then
1485				new_h = ctx.disptbl.height
1486			end
1487
1488			return new_w, new_h
1489		end
1490	end
1491
1492-- destroy always has a builtin handler and then cfg is optionally pulled in
1493	res.destroy =
1494	function()
1495		local rmlist = {}
1496
1497-- convert to in-order and destroy all windows first
1498		for k,v in pairs(res.windows) do
1499			table.insert(rmlist, v)
1500		end
1501		for i,v in ipairs(rmlist) do
1502			if v.destroy then
1503				v:destroy()
1504				if cfg.destroy then
1505					cfg.destroy(v)
1506				end
1507			end
1508		end
1509
1510		if cfg.destroy then
1511			cfg.destroy(res)
1512		end
1513		if valid_vid(res.bridge) then
1514			delete_image(res.bridge)
1515		end
1516		if valid_vid(res.anchor) then
1517			delete_image(res.anchor)
1518		end
1519
1520		mouse_droplistener(res)
1521		local keys = {}
1522		for k,v in pairs(res) do
1523			table.insert(keys, v)
1524		end
1525		for _,k in ipairs(keys) do
1526			res[k] = nil
1527		end
1528	end
1529
1530	return res
1531end
1532
1533local function client_handler(nested, trigger, source, status)
1534-- keep track of anyone that goes through here
1535	if not bridges[source] then
1536		bridges[source] = {}
1537	end
1538
1539	if status.kind == "registered" then
1540-- happens if we are forwarded from the wrong type (api error)
1541		if status.segkind ~= "bridge-wayland" then
1542			delete_image(source)
1543			return
1544		end
1545
1546-- we have a wayland bridge, and need to know if it is used to bootstrap other
1547-- clients or not - we see that if it requests a non-bridge-wayland type on its
1548-- next segment request
1549	elseif status.kind == "segment_request" then
1550
1551-- nested, only allow one 'level'
1552		if status.segkind == "bridge-wayland" then
1553			if nested then
1554				return false
1555			end
1556
1557-- next one will be the one to ask for 'real' windows
1558		local vid =
1559		accept_target(32, 32,
1560			function(...)
1561				return client_handler(true, trigger, ...)
1562			end)
1563
1564-- and those we just forward to the wayland factory
1565		else
1566			local bridge = trigger(source, status)
1567			if bridge then
1568				table.insert(bridges[source], bridge)
1569				rendertarget_attach(bridge.disptbl.rt, vid, RENDERTARGET_DETACH)
1570			end
1571		end
1572
1573-- died before getting anywhere meaningful - or is the bridge itself gone?
1574-- if so, kill off every client associated with it
1575	elseif status.kind == "terminated" then
1576		for k,v in ipairs(bridges[source]) do
1577			v:destroy()
1578		end
1579		delete_image(source)
1580		bridges[source] = nil
1581	end
1582end
1583
1584local function connection_mgmt(source, trigger)
1585	target_updatehandler(source,
1586		function(source, status)
1587			client_handler(false, trigger, source, status)
1588		end
1589	)
1590end
1591
1592-- factory function is intended to be used when a bridge-wayland segment
1593-- requests something that is not a bridge-wayland, then the bridge (ref
1594-- by [vid]) will have its handler overridden and treated as a client.
1595return
1596function(vid, segreq, cfg)
1597	local ctx = bridge_table(cfg)
1598	set_bridge(ctx, vid)
1599	bridge_handler(ctx, vid, segreq)
1600	return ctx
1601end, connection_mgmt
1602