1-- Copyright: None claimed, Public Domain
2--
3-- Description: Cookbook- style functions for the normal tedium
4-- (string and table manipulation, mostly plucked from the AWB project)
5-- These should either expand the various basic tables (string, math)
6-- or namespace prefix with suppl_...
7
8function string.split(instr, delim)
9	if (not instr) then
10		return {};
11	end
12
13	local res = {};
14	local strt = 1;
15	local delim_pos, delim_stp = string.find(instr, delim, strt);
16
17	while delim_pos do
18		table.insert(res, string.sub(instr, strt, delim_pos-1));
19		strt = delim_stp + 1;
20		delim_pos, delim_stp = string.find(instr, delim, strt);
21	end
22
23	table.insert(res, string.sub(instr, strt));
24	return res;
25end
26
27function string.starts_with(instr, prefix)
28	return string.sub(instr, 1, #prefix) == prefix;
29end
30
31--
32--  Similar to split but only returns 'first' and 'rest'.
33--
34--  The edge cases of the delim being at first or last part of the
35--  string, empty strings will be returned instead of nil.
36--
37function string.split_first(instr, delim)
38	if (not instr) then
39		return;
40	end
41	local delim_pos, delim_stp = string.find(instr, delim, 1);
42	if (delim_pos) then
43		local first = string.sub(instr, 1, delim_pos - 1);
44		local rest = string.sub(instr, delim_stp + 1);
45		first = first and first or "";
46		rest = rest and rest or "";
47		return first, rest;
48	else
49		return "", instr;
50	end
51end
52
53-- can shorten further by dropping vowels and characters
54-- in beginning and end as we match more on those
55function string.shorten(s, len)
56	if (s == nil or string.len(s) == 0) then
57		return "";
58	end
59
60	local r = string.gsub(
61		string.gsub(s, " ", ""), "\n", ""
62	);
63	return string.sub(r and r or "", 1, len);
64end
65
66function string.utf8back(src, ofs)
67	if (ofs > 1 and string.len(src)+1 >= ofs) then
68		ofs = ofs - 1;
69		while (ofs > 1 and utf8kind(string.byte(src,ofs) ) == 2) do
70			ofs = ofs - 1;
71		end
72	end
73
74	return ofs;
75end
76
77function math.sign(val)
78	return (val < 0 and -1) or 1;
79end
80
81function math.clamp(val, low, high)
82	if (low and val < low) then
83		return low;
84	end
85	if (high and val > high) then
86		return high;
87	end
88	return val;
89end
90
91function string.to_u8(instr)
92-- drop spaces and make sure we have %2
93	instr = string.gsub(instr, " ", "");
94	local len = string.len(instr);
95	if (len % 2 ~= 0 or len > 8) then
96		return;
97	end
98
99	local s = "";
100	for i=1,len,2 do
101		local num = tonumber(string.sub(instr, i, i+1), 16);
102		if (not num) then
103			return nil;
104		end
105		s = s .. string.char(num);
106	end
107
108	return s;
109end
110
111function string.utf8forward(src, ofs)
112	if (ofs <= string.len(src)) then
113		repeat
114			ofs = ofs + 1;
115		until (ofs > string.len(src) or
116			utf8kind( string.byte(src, ofs) ) < 2);
117	end
118
119	return ofs;
120end
121
122function string.utf8lalign(src, ofs)
123	while (ofs > 1 and utf8kind(string.byte(src, ofs)) == 2) do
124		ofs = ofs - 1;
125	end
126	return ofs;
127end
128
129function string.utf8ralign(src, ofs)
130	while (ofs <= string.len(src) and string.byte(src, ofs)
131		and utf8kind(string.byte(src, ofs)) == 2) do
132		ofs = ofs + 1;
133	end
134	return ofs;
135end
136
137function string.translateofs(src, ofs, beg)
138	local i = beg;
139	local eos = string.len(src);
140
141	-- scan for corresponding UTF-8 position
142	while ofs > 1 and i <= eos do
143		local kind = utf8kind( string.byte(src, i) );
144		if (kind < 2) then
145			ofs = ofs - 1;
146		end
147
148		i = i + 1;
149	end
150
151	return i;
152end
153
154function string.utf8len(src, ofs)
155	local i = 0;
156	local rawlen = string.len(src);
157	ofs = ofs < 1 and 1 or ofs;
158
159	while (ofs <= rawlen) do
160		local kind = utf8kind( string.byte(src, ofs) );
161		if (kind < 2) then
162			i = i + 1;
163		end
164
165		ofs = ofs + 1;
166	end
167
168	return i;
169end
170
171function string.insert(src, msg, ofs, limit)
172	if (limit == nil) then
173		limit = string.len(msg) + ofs;
174	end
175
176	if ofs + string.len(msg) > limit then
177		msg = string.sub(msg, 1, limit - ofs);
178
179-- align to the last possible UTF8 char..
180
181		while (string.len(msg) > 0 and
182			utf8kind( string.byte(msg, string.len(msg))) == 2) do
183			msg = string.sub(msg, 1, string.len(msg) - 1);
184		end
185	end
186
187	return string.sub(src, 1, ofs - 1) .. msg ..
188		string.sub(src, ofs, string.len(src)), string.len(msg);
189end
190
191function string.delete_at(src, ofs)
192	local fwd = string.utf8forward(src, ofs);
193	if (fwd ~= ofs) then
194		return string.sub(src, 1, ofs - 1) .. string.sub(src, fwd, string.len(src));
195	end
196
197	return src;
198end
199
200local function hb(ch)
201	local th = {"0", "1", "2", "3", "4", "5",
202		"6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
203
204	local fd = math.floor(ch/16);
205	local sd = ch - fd * 16;
206	return th[fd+1] .. th[sd+1];
207end
208
209function string.hexenc(instr)
210	return string.gsub(instr, "(.)", function(ch)
211		return hb(ch:byte(1));
212	end);
213end
214
215function string.trim(s)
216  return (s:gsub("^%s*(.-)%s*$", "%1"));
217end
218
219-- to ensure "special" names e.g. connection paths for target alloc,
220-- or for connection pipes where we want to make sure the user can input
221-- no matter keylayout etc.
222strict_fname_valid = function(val)
223	for i in string.gmatch(val, "%W") do
224		if (i ~= '_') then
225			return false;
226		end
227	end
228	return true;
229end
230
231function string.utf8back(src, ofs)
232	if (ofs > 1 and string.len(src)+1 >= ofs) then
233		ofs = ofs - 1;
234		while (ofs > 1 and utf8kind(string.byte(src,ofs) ) == 2) do
235			ofs = ofs - 1;
236		end
237	end
238
239	return ofs;
240end
241
242function table.set_unless_exists(tbl, key, val)
243	tbl[key] = tbl[key] and tbl[key] or val;
244end
245
246function table.intersect(tbl, tbl2)
247	local res = {}
248	for _, v in ipairs(tbl) do
249		if table.find_i(tbl2) then
250			table.insert(res, v)
251		end
252	end
253	return res
254end
255
256-- take the entries in ref and apply to src if they match type
257-- with ref, otherwise use the value in ref
258function table.merge(dst, src, ref, on_error)
259	for k,v in pairs(ref) do
260		if src[k] and type(src[k]) == type(v) then
261			v = src[k];
262		elseif src[k] then
263-- type mismatch is recoverable error
264			on_error(k);
265		end
266
267		if type(k) == "table" then
268			dst[k] = table.copy(v);
269		else
270			dst[k] = v;
271		end
272	end
273end
274
275function table.copy(tbl)
276	if not tbl or not type(tbl) == "table" then
277		return {};
278	end
279
280	local res = {};
281	for k,v in pairs(tbl) do
282		if type(v) == "table" then
283			res[k] = table.copy(v);
284		else
285			res[k] = v;
286		end
287	end
288
289	return res;
290end
291
292function table.remove_match(tbl, match)
293	if (tbl == nil) then
294		return;
295	end
296
297	for k,v in ipairs(tbl) do
298		if (v == match) then
299			table.remove(tbl, k);
300			return v, k;
301		end
302	end
303
304	return nil;
305end
306
307function string.dump(msg)
308	local bt ={};
309	for i=1,string.len(msg) do
310		local ch = string.byte(msg, i);
311		bt[i] = ch;
312	end
313end
314
315function table.remove_vmatch(tbl, match)
316	if (tbl == nil) then
317		return;
318	end
319
320	for k,v in pairs(tbl) do
321		if (v == match) then
322			tbl[k] = nil;
323			return v;
324		end
325	end
326
327	return nil;
328end
329
330function suppl_delete_image_if(vid)
331	if valid_vid(vid) then
332		delete_image(vid);
333	end
334end
335
336function table.find_i(table, r)
337	for k,v in ipairs(table) do
338		if (v == r) then return k; end
339	end
340end
341
342function table.find_key_i(table, field, r)
343	for k,v in ipairs(table) do
344		if (v[field] == r) then
345			return k;
346		end
347	end
348end
349
350function table.insert_unique_i(tbl, i, v)
351	local ind = table.find_i(tbl, v);
352	if (not ind) then
353		table.insert(tbl, i, v);
354	else
355		local cpy = tbl[i];
356		tbl[i] = tbl[ind];
357		tbl[ind] = cpy;
358	end
359end
360
361--
362-- Extract subset of array-like table using
363-- given filter function
364--
365-- Accepts table and filter function.
366-- Input table is not modified in process.
367-- Rest of arguments are passed to filter
368-- function.
369--
370function table.filter(tbl, filter_fn, ...)
371	local res = {};
372
373	for _,v in ipairs(tbl) do
374		if (filter_fn(v, ...) == true) then
375			table.insert(res, v);
376		end
377	end
378
379	return res;
380end
381
382function suppl_strcol_fmt(str, sel)
383	local hv = util.hash(str);
384	return HC_PALETTE[(hv % #HC_PALETTE) + 1];
385end
386
387function suppl_region_stop(trig)
388-- restore repeat- rate state etc.
389	iostatem_restore();
390
391-- then return the input processing pipeline
392	durden_input_sethandler()
393
394-- and allow external input triggers to re-appear
395	dispatch_symbol_unlock(true);
396
397-- and trigger the on-end callback
398	mouse_select_end(trig);
399end
400
401-- Attach a shadow to ctx.
402--
403-- This is a simple/naive version that repeats the fragment shader for every
404-- updated friend. A decent optimization (depending on memory) is to RTT it
405-- and maintain a cache for non/dynamic updates (animations and drag-resize).
406--
407-- Shadows can use a single color or a weighted mix between a base color and
408-- a reference texture map. For this case, the opts.reference is set to the
409-- textured source and the global 'shadow_style' config is set to textured.
410--
411function suppl_region_shadow(ctx, w, h, opts)
412	opts = opts and opts or {};
413	opts.method = opts.method and opts.method or gconfig_get("shadow_style");
414	if (opts.method == "none") then
415		if (valid_vid(ctx.shadow)) then
416			delete_image(ctx.shadow);
417			ctx.shadow = nil;
418		end
419		return;
420	end
421
422-- assume 'soft' for now
423	local shname = opts.shader and opts.shader or "dropshadow";
424
425	local time = opts.time and opts.time or 0;
426	local t = opts.t and opts.t or gconfig_get("shadow_t");
427	local l = opts.l and opts.l or gconfig_get("shadow_l");
428	local d = opts.d and opts.d or gconfig_get("shadow_d");
429	local r = opts.r and opts.r or gconfig_get("shadow_r");
430	local interp = opts.interp and opts.interp or INTERP_SMOOTHSTEP;
431	local cr, cg, cb;
432
433	if (opts.color) then
434		cr, cg, cb = unpack(opts.color);
435	else
436		cr, cg, cb = unpack(gconfig_get("shadow_color"));
437	end
438
439-- allocate on first call
440	if not valid_vid(ctx.shadow) then
441		ctx.shadow = color_surface(w + l + r, h + t + d, cr, cg, cb);
442
443-- and handle OOM
444		if (not valid_vid(ctx.shadow)) then
445			return;
446		end
447
448		if opts.reference and opts.method == "textured" then
449			image_sharestorage(opts.reference, ctx.shadow);
450		end
451
452-- assume we can patch ctx and that it has an anchor
453		blend_image(ctx.shadow, 1.0, time);
454		link_image(ctx.shadow, ctx.anchor);
455		image_inherit_order(ctx.shadow, true);
456		order_image(ctx.shadow, -1);
457
458-- This is slightly problematic as the uniforms are shared, thus
459-- the option of colour vs texture source etc. will be shared.
460--
461-- Though this does not apply here, multi-pass effect composition
462-- etc. that requires indirect blits would not work this way either.
463		local shid = shader_ui_lookup(ctx.shadow, "ui", shname, "active");
464		if shid then
465			shader_uniform(shid, "color", "fff", cr, cg, cb);
466		end
467	else
468		reset_image_transform(ctx.shadow);
469		show_image(ctx.shadow, time, interp);
470	end
471
472	image_color(ctx.shadow, cr, cg, cb);
473	resize_image(ctx.shadow, w + l + r, h + t + d, time, interp);
474	move_image(ctx.shadow, -l, -t);
475end
476
477function suppl_region_select(r, g, b, handler)
478	local col = fill_surface(1, 1, r, g, b);
479	blend_image(col, 0.2);
480	iostatem_save();
481	mouse_select_begin(col);
482	dispatch_meta_reset();
483	shader_setup(col, "ui", "regsel", "active");
484	dispatch_symbol_lock();
485	durden_input_sethandler(durden_regionsel_input, "region-select");
486	DURDEN_REGIONSEL_TRIGGER = handler;
487end
488
489local ffmts =
490{
491	jpg = "image", jpeg = "image", png = "image", bmp = "image",
492	ogg = "audio", m4a = "audio", flac = "audio", mp3 = "audio",
493	mp4 = "video", wmv = "video", mkv = "video", avi = "video",
494	flv = "video", mpg = "video", mpeg = "video", mov = "video",
495	webm = "video", ["*"] = "file",
496};
497
498local function match_ext(v, tbl)
499	if (tbl == nil) then
500		return true;
501	end
502
503	local ext = string.match(v, "^.+(%..+)$");
504	ext = ext ~= nil and string.sub(ext, 2) or ext;
505	if (ext == nil or string.len(ext) == 0) then
506		return false;
507	end
508
509	local ent = tbl[string.lower(ext)];
510	if ent then
511		return ent;
512	else
513		return tbl["*"];
514	end
515end
516
517-- filename to classifier [media, image, audio]
518function suppl_ext_type(fn)
519	return match_ext(fn, ffmts);
520end
521
522local function defer_spawn(wnd, new, t, l, d, r, w, h, closure)
523-- window died before timer?
524	if (not wnd.add_handler) then
525		delete_image(new);
526		return;
527	end
528
529-- don't make the source visible until we can spawn new
530	show_image(new);
531	local cwin = active_display():add_window(new, {scalemode = "stretch"});
532	if (not cwin) then
533		delete_image(new);
534		return;
535	end
536
537-- closure to update the crop if the source changes (shaders etc.)
538	local function recrop()
539		local sprops = image_storage_properties(wnd.canvas);
540		cwin.origo_ll = wnd.origo_ll;
541		cwin:set_crop(
542			t * sprops.height, l * sprops.width,
543			d * sprops.height, r * sprops.width, false, true
544		);
545	end
546
547-- deregister UNLESS the source window is already dead
548	cwin:add_handler("destroy",
549		function()
550			if (wnd.drop_handler) then
551				wnd:drop_handler("resize", recrop);
552			end
553		end
554	);
555
556-- add event handlers so that we update the scaling every time the source changes
557	recrop();
558	cwin:set_title("Slice");
559	cwin.source_name = wnd.name;
560	cwin.name = cwin.name .. "_crop";
561
562-- finally send to a possible source that wants to do additional modifications
563	if closure then
564		closure(cwin, t, l, d, r, w, h);
565	end
566end
567
568local function slice_handler(wnd, x1, y1, x2, y2, closure)
569-- grab the current values
570	local props = image_surface_resolve(wnd.canvas);
571	local px2 = props.x + props.width;
572	local py2 = props.y + props.height;
573
574-- and actually clamp
575	x1 = x1 < props.x and props.x or x1;
576	y1 = y1 < props.y and props.y or y1;
577	x2 = x2 > px2 and px2 or x2;
578	y2 = y2 > py2 and py2 or y2;
579
580-- safeguard against range problems
581	if (x2 - x1 <= 0 or y2 - y1 <= 0) then
582		return;
583	end
584
585-- create clone with proper texture coordinates, this has problems with
586-- source windows that do other coordinate transforms as well and switch
587-- back and forth.
588	local new = null_surface(x2-x1, y2-y1);
589	image_sharestorage(wnd.canvas, new);
590
591-- calculate crop in source surface relative coordinates
592	local t = (y1 - props.y) / props.height;
593	local l = (x1 - props.x) / props.width;
594	local d = (py2 - y2) / props.height;
595	local r = (px2 - x2) / props.width;
596	local w = (x2 - x1);
597	local h = (y2 - y1);
598
599-- work-around the chaining-region-select problem with a timer
600	timer_add_periodic("wndspawn", 1, true, function()
601		defer_spawn(wnd, new, t, l, d, r, w, h, closure);
602	end);
603end
604
605function suppl_wnd_slice(wnd, closure)
606-- like with all suppl_region_select calls, this is race:y as the
607-- selection state can go on indefinitely and things might've changed
608-- due to some event (thing wnd being destroyed while select state is
609-- active)
610	local wnd = active_display().selected;
611	local props = image_surface_resolve(wnd.canvas);
612
613	suppl_region_select(255, 0, 255,
614		function(x1, y1, x2, y2)
615			if (valid_vid(wnd.canvas)) then
616				slice_handler(wnd, x1, y1, x2, y2, closure);
617			end
618		end
619	);
620end
621
622local function build_rt_reg(drt, x1, y1, w, h, srate)
623	if (w <= 0 or h <= 0) then
624		return;
625	end
626
627-- grab in worldspace, translate
628	local props = image_surface_resolve_properties(drt);
629	x1 = x1 - props.x;
630	y1 = y1 - props.y;
631
632	local dst = alloc_surface(w, h);
633	if (not valid_vid(dst)) then
634		warning("build_rt: failed to create intermediate");
635		return;
636	end
637	local cont = null_surface(w, h);
638	if (not valid_vid(cont)) then
639		delete_image(dst);
640		return;
641	end
642
643	image_sharestorage(drt, cont);
644
645-- convert to surface coordinates
646	local s1 = x1 / props.width;
647	local t1 = y1 / props.height;
648	local s2 = (x1+w) / props.width;
649	local t2 = (y1+h) / props.height;
650
651	local txcos = {s1, t1, s2, t1, s2, t2, s1, t2};
652	image_set_txcos(cont, txcos);
653	show_image({cont, dst});
654
655	local shid = image_shader(drt);
656	if (shid) then
657		image_shader(cont, shid);
658	end
659	return dst, {cont};
660end
661
662-- lifted from the label definitions and order in shmif, the values are
663-- offset by 2 as shown in arcan_tuisym.h
664local color_labels =
665{
666	{"primary", "Dominant foreground"},
667	{"secondary", "Dominant alternative foreground"},
668	{"background", "Default background"},
669	{"text", "Default text"},
670	{"cursor", "Default caret or mouse cursor"},
671	{"altcursor", "Default alternative-state caret or mouse cursor"},
672	{"highlight", "Default marked / selection state"},
673	{"label", "Text labels and content annotations"},
674	{"warning", "Labels and text that require additional consideration"},
675	{"error", "Indicate wrong input or other erroneous state"},
676	{"alert", "Areas that require immediate attention"},
677	{"inactive", "Labels where the related content is currently inaccessible"},
678	{"reference", "Actions that reference external contents or trigger navigation"}
679};
680
681-- Generate menu entries for defining colors, where the output will be
682-- sent to cb. This is here in order to reuse the same tables and code
683-- path for both per-window overrides and some global option
684function suppl_color_menu(cb, lookup)
685	local res = {};
686	for k,v in ipairs(color_labels) do
687		table.insert(res, {
688			name = v[1],
689			label =
690				string.upper(string.sub(v[1], 1, 1)) .. string.upper(string.sub(v[1], 2)),
691			kind = "value",
692			hint = "(r g b)(0..255)",
693			widget = "special:colorpick_r8g8b8",
694			validator = suppl_valid_typestr("fff", 0, 255, 0),
695			initial = function()
696				local r, g, b = lookup(v[1]);
697				return string.format("%.0f %.0f %.0f", r, g, b);
698			end,
699			handler = function(ctx, val)
700				local col = suppl_unpack_typestr("fff", val, 0, 255);
701				cb(val, col[1], col[2], col[3]);
702			end
703		});
704	end
705	return res;
706end
707
708-- all the boiler plate needed to figure out the types a uniform has,
709-- generate the corresponding menu entry and with validators for type
710-- and range, taking locale and separators into accoutn.
711local bdelim = (tonumber("1,01") == nil) and "." or ",";
712local rdelim = (bdelim == ".") and "," or ".";
713
714function suppl_unpack_typestr(typestr, val, lowv, highv)
715	string.gsub(val, rdelim, bdelim);
716	local rtbl = string.split(val, ' ');
717	for i=1,#rtbl do
718		rtbl[i] = tonumber(rtbl[i]);
719		if (not rtbl[i]) then
720			return;
721		end
722		if (lowv and rtbl[i] < lowv) then
723			return;
724		end
725		if (highv and rtbl[i] > highv) then
726			return;
727		end
728	end
729	return rtbl;
730end
731
732-- allows empty string in order to 'unset'
733function suppl_valid_name(val)
734	if not string or #val == 0 or string.match(val, "%W") then
735		return false;
736	end
737
738	return true;
739end
740
741-- icon symbol reference or valid utf-8 codepoint
742function suppl_valid_vsymbol(val, base)
743	if (not val) then
744		return false;
745	end
746
747	if (string.len(val) == 0) then
748		return false;
749	end
750
751	if (string.sub(val, 1, 3) == "0x_") then
752		if (not val or not string.to_u8(string.sub(val, 4))) then
753			return false;
754		end
755		val = string.to_u8(string.sub(val, 4));
756	end
757
758-- do note that the icon_ setup actually returns a factory function,
759-- this may be called repeatedly to generate different sizes of the
760-- same icon reference
761	if (string.sub(val, 1, 5) == "icon_") then
762		val = string.sub(val, 6);
763		if icon_known(val) then
764			return true, function(w)
765				local vid = icon_lookup(val, w);
766				local props = image_surface_properties(vid);
767				local new = null_surface(props.width, props.height);
768				image_sharestorage(vid, new);
769				return new;
770			end
771		end
772		return false;
773	end
774
775	if (string.find(val, ":")) then
776		return false;
777	end
778
779	return true, val;
780end
781
782local function append_color_menu(r, g, b, tbl, update_fun)
783	tbl.kind = "value";
784	tbl.widget = "special:colorpick_r8g8b8";
785	tbl.hint = "(r g b)(0..255)";
786	tbl.initial = string.format("%.0f %.0f %.0f", r, g, b);
787	tbl.validator = suppl_valid_typestr("fff", 0, 255, 0);
788	tbl.handler = function(ctx, val)
789		local tbl = suppl_unpack_typestr("fff", val, 0, 255);
790		if (not tbl) then
791			return;
792		end
793		update_fun(
794			string.format("\\#%02x%02x%02x", tbl[1], tbl[2], tbl[3]),
795			tbl[1], tbl[2], tbl[3]);
796	end
797end
798
799function suppl_hexstr_to_rgb(str)
800	local base;
801
802-- safeguard 1.
803	if not type(str) == "string" then
804		str = ""
805	end
806
807-- check for the normal #  and \\#
808	if (string.sub(str, 1,1) == "#") then
809		base = 2;
810	elseif (string.sub(str, 2,2) == "#") then
811		base = 3;
812	else
813		base = 1;
814	end
815
816-- convert based on our assumed starting pos
817	local r = tonumber(string.sub(str, base+0, base+1), 16);
818	local g = tonumber(string.sub(str, base+2, base+3), 16);
819	local b = tonumber(string.sub(str, base+4, base+5), 16);
820
821-- safe so we always return a value
822	r = r and r or 255;
823	g = g and g or 255;
824	b = b and b or 255;
825
826	return r, g, b;
827end
828
829function suppl_append_color_menu(v, tbl, update_fun)
830	if (type(v) == "table") then
831		append_color_menu(v[1], v[2], v[3], tbl, update_fun);
832	else
833		local r, g, b = suppl_hexstr_to_rgb(v);
834		append_color_menu(r, g, b, tbl, update_fun);
835	end
836end
837
838function suppl_button_default_mh(wnd, cmd, altcmd)
839	local res =
840{
841	click = function(btn)
842		dispatch_symbol_wnd(wnd, cmd);
843	end,
844	over = function(btn)
845		btn:switch_state("alert");
846	end,
847	out = function(btn)
848		btn:switch_state(wnd.wm.selected == wnd and "active" or "inactive");
849	end
850};
851	if (altcmd) then
852		res.rclick = function()
853			dispatch_symbol_wnd(altcmd);
854		end
855	end
856	return res;
857end
858
859function suppl_valid_typestr(utype, lowv, highv, defaultv)
860	return function(val)
861		local tbl = suppl_unpack_typestr(utype, val, lowv, highv);
862		return tbl ~= nil and #tbl == string.len(utype);
863	end
864end
865
866function suppl_region_setup(x1, y1, x2, y2, nodef, static, title)
867	local w = x2 - x1;
868	local h = y2 - y1;
869
870-- check sample points if we match a single vid or we need to
871-- use the aggregate surface and restrict to the behaviors of rt
872	local drt = active_display(true);
873	local tiler = active_display();
874
875	local i1 = pick_items(x1, y1, 1, true, drt);
876	local i2 = pick_items(x2, y1, 1, true, drt);
877	local i3 = pick_items(x1, y2, 1, true, drt);
878	local i4 = pick_items(x2, y2, 1, true, drt);
879	local img = drt;
880	local in_float = (tiler.spaces[tiler.space_ind].mode == "float");
881
882-- a possibly better option would be to generate subslices of each
883-- window in the set and dynamically manage the rendertarget, but that
884-- is for later
885	if (
886		in_float or
887		#i1 == 0 or #i2 == 0 or #i3 == 0 or #i4 == 0 or
888		i1[1] ~= i2[1] or i1[1] ~= i3[1] or i1[1] ~= i4[1]) then
889		rendertarget_forceupdate(drt);
890	else
891		img = i1[1];
892	end
893
894	local dvid, grp = build_rt_reg(img, x1, y1, w, h);
895	if (not valid_vid(dvid)) then
896		return;
897	end
898
899	if (nodef) then
900		return dvid, grp;
901	end
902
903	define_rendertarget(dvid, grp,
904		RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, static and 0 or -1);
905
906-- just render once, store and drop the rendertarget as they are costly
907	if (static) then
908		rendertarget_forceupdate(dvid);
909		local dsrf = null_surface(w, h);
910		image_sharestorage(dvid, dsrf);
911		delete_image(dvid);
912		show_image(dsrf);
913		dvid = dsrf;
914	end
915
916	return dvid, grp, {};
917end
918
919local ptn_lut = {
920	p = "prefix",
921	t = "title",
922	i = "ident",
923	a = "atype"
924};
925
926local function get_ptn_str(cb, wnd)
927	if (string.len(cb) == 0) then
928		return;
929	end
930
931	local field = ptn_lut[string.sub(cb, 1, 1)];
932	if (not field or not wnd[field] or not (string.len(wnd[field]) > 0)) then
933		return;
934	end
935
936	local len = tonumber(string.sub(cb, 2));
937	return string.sub(wnd[field], 1, tonumber(string.sub(cb, 2)));
938end
939
940function suppl_ptn_expand(tbl, ptn, wnd)
941	local i = 1;
942	local cb = "";
943	local inch = false;
944
945	local flush_cb = function()
946		local msg = cb;
947
948		if (inch) then
949			msg = get_ptn_str(cb, wnd);
950			msg = msg and msg or "";
951			msg = string.trim(msg);
952		end
953		if (string.len(msg) > 0) then
954			table.insert(tbl, msg);
955			table.insert(tbl, ""); -- need to maintain %2
956		end
957		cb = "";
958	end
959
960	while (i <= string.len(ptn)) do
961		local ch = string.sub(ptn, i, i);
962		if (ch == " " and inch) then
963			flush_cb();
964			inch = false;
965		elseif (ch == "%") then
966			flush_cb();
967			inch = true;
968		else
969			cb = cb .. ch;
970		end
971		i = i + 1;
972	end
973	flush_cb();
974end
975
976function suppl_setup_rec(wnd, val, noaudio)
977	local svid = wnd;
978	local aarr = {};
979
980	if (type(wnd) == "table") then
981		svid = wnd.external;
982		if (not noaudio and wnd.source_audio) then
983			table.insert(aarr, wnd.source_audio);
984		end
985	end
986
987	if (not valid_vid(svid)) then
988		return;
989	end
990
991-- work around link_image constraint
992	local props = image_storage_properties(svid);
993	local pw = props.width + props.width % 2;
994	local ph = props.height + props.height % 2;
995
996	local nsurf = null_surface(pw, ph);
997	image_sharestorage(svid, nsurf);
998	show_image(nsurf);
999	local varr = {nsurf};
1000
1001	local db = alloc_surface(pw, ph);
1002	if (not valid_vid(db)) then
1003		delete_image(nsurf);
1004		warning("setup_rec, couldn't allocate output buffer");
1005		return;
1006	end
1007
1008	local argstr, srate, fn = suppl_build_recargs(varr, aarr, false, val);
1009	define_recordtarget(db, fn, argstr, varr, aarr,
1010		RENDERTARGET_DETACH, RENDERTARGET_NOSCALE, srate,
1011		function(source, stat)
1012			if (stat.kind == "terminated") then
1013				delete_image(source);
1014			end
1015		end
1016	);
1017
1018	if (not valid_vid(db)) then
1019		delete_image(db);
1020		delete_image(nsurf);
1021		warning("setup_rec, failed to spawn recordtarget");
1022		return;
1023	end
1024
1025-- useful for debugging, spawn a new window that shares
1026-- the contents of the allocated surface
1027--	local ns = null_surface(pw, ph);
1028--	image_sharestorage(db, ns);
1029--	show_image(ms);
1030--  local wnd =	active_display():add_window(ns);
1031--  wnd:set_tile("record-test");
1032--
1033-- link the recordtarget with the source for automatic deletion
1034	link_image(db, svid);
1035	return db;
1036end
1037
1038function drop_keys(matchstr)
1039	local rst = {};
1040	for i,v in ipairs(match_keys(matchstr)) do
1041		local pos, stop = string.find(v, "=", 1);
1042		local key = string.sub(v, 1, pos-1);
1043		rst[key] = "";
1044	end
1045	store_key(rst);
1046end
1047
1048-- reformated PD snippet
1049function string.utf8valid(str)
1050  local i, len = 1, #str
1051	local find = string.find;
1052  while i <= len do
1053		if (i == find(str, "[%z\1-\127]", i)) then
1054			i = i + 1;
1055		elseif (i == find(str, "[\194-\223][\123-\191]", i)) then
1056			i = i + 2;
1057		elseif (i == find(str, "\224[\160-\191][\128-\191]", i)
1058			or (i == find(str, "[\225-\236][\128-\191][\128-\191]", i))
1059 			or (i == find(str, "\237[\128-\159][\128-\191]", i))
1060			or (i == find(str, "[\238-\239][\128-\191][\128-\191]", i))) then
1061			i = i + 3;
1062		elseif (i == find(str, "\240[\144-\191][\128-\191][\128-\191]", i)
1063			or (i == find(str, "[\241-\243][\128-\191][\128-\191][\128-\191]", i))
1064			or (i == find(str, "\244[\128-\143][\128-\191][\128-\191]", i))) then
1065			i = i + 4;
1066    else
1067      return false, i;
1068    end
1069  end
1070
1071  return true;
1072end
1073
1074function suppl_bind_u8(hook)
1075	local bwt = gconfig_get("bind_waittime");
1076	local tbhook = function(sym, done, sym2, iotbl)
1077		if (not done) then
1078			return;
1079		end
1080
1081		local bar = active_display():lbar(
1082		function(ctx, instr, done, lastv)
1083			if (not done) then
1084				return instr and string.len(instr) > 0 and string.to_u8(instr) ~= nil;
1085			end
1086
1087			instr = string.to_u8(instr);
1088			if (instr and string.utf8valid(instr)) then
1089					hook(sym, instr, sym2, iotbl);
1090			else
1091				active_display():message("invalid utf-8 sequence specified");
1092			end
1093		end, ctx, {label = "specify byte-sequence (like f0 9f 92 a9):"});
1094		suppl_widget_path(bar, bar.text_anchor, "special:u8");
1095	end;
1096
1097	tiler_bbar(active_display(),
1098		string.format(LBL_BIND_COMBINATION, SYSTEM_KEYS["cancel"]),
1099		"keyorcombo", bwt, nil, SYSTEM_KEYS["cancel"], tbhook);
1100end
1101
1102function suppl_binding_helper(prefix, suffix, bind_fn)
1103	local bwt = gconfig_get("bind_waittime");
1104
1105	local on_input = function(sym, done)
1106		if (not done) then
1107			return;
1108		end
1109
1110		dispatch_symbol_bind(function(path)
1111			if (not path) then
1112				return;
1113			end
1114			bind_fn(prefix .. sym .. suffix, path);
1115		end);
1116	end
1117
1118	local bind_msg = string.format(
1119		LBL_BIND_COMBINATION_REP, SYSTEM_KEYS["cancel"]);
1120
1121	local ctx = tiler_bbar(active_display(), bind_msg,
1122		false, gconfig_get("bind_waittime"), nil,
1123		SYSTEM_KEYS["cancel"],
1124		on_input, gconfig_get("bind_repeat")
1125	);
1126
1127	local lbsz = 2 * active_display().scalef * gconfig_get("lbar_sz");
1128
1129-- tell the widget system that we are in a special context
1130	suppl_widget_path(ctx, ctx.bar, "special:custom", lbsz);
1131	return ctx;
1132end
1133
1134--
1135-- used for the ugly case with the meta-guard where we want to chain multiple
1136-- binding query paths if one binding in the chain succeeds
1137--
1138local binding_queue = {};
1139function suppl_binding_queue(arg)
1140	if (type(arg) == "function") then
1141		table.insert(binding_queue, arg);
1142	elseif (arg) then
1143		binding_queue = {};
1144	else
1145		local ent = table.remove(binding_queue, 1);
1146		if (ent) then
1147			ent();
1148		end
1149	end
1150end
1151
1152local function text_input_sym(ctx, sym)
1153
1154end
1155
1156local function text_input_table(ctx, io, sym)
1157-- first check if modifier is held, and apply normal 'readline' translation
1158	if not io.active then
1159		return;
1160	end
1161
1162-- then check if the symbol matches our default overrides
1163	if sym and ctx.bindings[sym] then
1164		ctx.bindings[sym](ctx);
1165		return;
1166	end
1167
1168-- last normal text input
1169	local keych = io.utf8;
1170	if (keych == nil or keych == '') then
1171		return ctx;
1172	end
1173
1174	ctx.oldmsg = ctx.msg;
1175	ctx.oldpos = ctx.caretpos;
1176	ctx.msg, nch = string.insert(ctx.msg, keych, ctx.caretpos, ctx.nchars);
1177
1178	ctx.caretpos = ctx.caretpos + nch;
1179	ctx:update_caret();
1180end
1181
1182local function text_input_view(ctx)
1183	local rofs = string.utf8ralign(ctx.msg, ctx.chofs + ctx.ulim);
1184	local str = string.sub(ctx.msg, string.utf8ralign(ctx.msg, ctx.chofs), rofs-1);
1185	return str;
1186end
1187
1188local function text_input_caret_str(ctx)
1189	return string.sub(ctx.msg, ctx.chofs, ctx.caretpos - 1);
1190end
1191
1192-- should really be more sophisticated, i.e. a push- function that deletes
1193-- everything after the current undo index, a back function that moves the
1194-- index upwards, a forward function that moves it down, and possible hist
1195-- get / set.
1196local function text_input_undo(ctx)
1197	if (ctx.oldmsg) then
1198		ctx.msg = ctx.oldmsg;
1199		ctx.caretpos = ctx.oldpos;
1200--				redraw(ctx);
1201	end
1202end
1203
1204local function text_input_set(ctx, str)
1205	ctx.msg = (str and #str > 0) and str or "";
1206	ctx.caretpos = string.len( ctx.msg ) + 1;
1207	ctx.chofs = ctx.caretpos - ctx.ulim;
1208	ctx.chofs = ctx.chofs < 1 and 1 or ctx.chofs;
1209	ctx.chofs = string.utf8lalign(ctx.msg, ctx.chofs);
1210	ctx:update_caret();
1211end
1212
1213-- caret index has changed to some arbitrary position,
1214-- make sure the visible window etc. is updated to match
1215local function text_input_caretalign(ctx)
1216	if (ctx.caretpos - ctx.chofs + 1 > ctx.ulim) then
1217		ctx.chofs = string.utf8lalign(ctx.msg, ctx.caretpos - ctx.ulim);
1218	end
1219	ctx:draw();
1220end
1221
1222local function text_input_chome(ctx)
1223	ctx.caretpos = 1;
1224	ctx.chofs    = 1;
1225	ctx:update_caret();
1226end
1227
1228local function text_input_cend(ctx)
1229	ctx.caretpos = string.len( ctx.msg ) + 1;
1230	ctx.chofs = ctx.caretpos - ctx.ulim;
1231	ctx.chofs = ctx.chofs < 1 and 1 or ctx.chofs;
1232	ctx.chofs = string.utf8lalign(ctx.msg, ctx.chofs);
1233	ctx:update_caret();
1234end
1235
1236local function text_input_cleft(ctx)
1237	ctx.caretpos = string.utf8back(ctx.msg, ctx.caretpos);
1238
1239	if (ctx.caretpos < ctx.chofs) then
1240		ctx.chofs = ctx.chofs - ctx.ulim;
1241		ctx.chofs = ctx.chofs < 1 and 1 or ctx.chofs;
1242		ctx.chofs = string.utf8lalign(ctx.msg, ctx.chofs);
1243	end
1244
1245	ctx:update_caret();
1246end
1247
1248local function text_input_cright(ctx)
1249	ctx.caretpos = string.utf8forward(ctx.msg, ctx.caretpos);
1250
1251	if (ctx.chofs + ctx.ulim <= ctx.caretpos) then
1252		ctx.chofs = ctx.chofs + 1;
1253	end
1254
1255	ctx:update_caret();
1256end
1257
1258local function text_input_cdel(ctx)
1259	ctx.msg = string.delete_at(ctx.msg, ctx.caretpos);
1260	ctx:update_caret();
1261end
1262
1263local function text_input_cerase(ctx)
1264	if (ctx.caretpos < 1) then
1265		return;
1266	end
1267
1268	ctx.caretpos = string.utf8back(ctx.msg, ctx.caretpos);
1269	if (ctx.caretpos <= ctx.chofs) then
1270		ctx.chofs = ctx.caretpos - ctx.ulim;
1271		ctx.chofs = ctx.chofs < 0 and 1 or ctx.chofs;
1272	end
1273
1274	ctx.msg = string.delete_at(ctx.msg, ctx.caretpos);
1275	ctx:update_caret();
1276end
1277
1278local function text_input_clear(ctx)
1279	ctx.caretpos = 1;
1280	ctx.msg = "";
1281	ctx:update_caret();
1282end
1283
1284--
1285-- Setup an input field for readline- like text input.
1286-- This function can be used both to feed input and to initialize the context
1287-- for the first time. It does not perform any rendering or allocation itself,
1288-- that is deferred to the caller via the draw(ctx) callback.
1289--
1290-- All relevant offsets [chofs : start drawing @] [caret : current cursor @]
1291-- are aligned positions and the string being built is represented in utf8.
1292--
1293-- example use:
1294-- ctx = suppl_text_input(NULL, input_table, resolved_symbol, draw_function, opts)
1295-- ctx:input(tbl, sym) OR suppl_text_input(ctx, ...)
1296--
1297function suppl_text_input(ctx, iotbl, sym, redraw, opts)
1298	ctx = ctx == nil and {
1299		caretpos = 1,
1300		limit = -1,
1301		chofs = 1,
1302		ulim = VRESW / gconfig_get("font_sz"),
1303		msg = "",
1304
1305-- mainly internal use or for complementing render hooks via the redraw
1306		draw = redraw and redraw or function() end,
1307		view_str = text_input_view,
1308		caret_str = text_input_caret_str,
1309		set_str = text_input_set,
1310		update_caret = text_input_caretalign,
1311		caret_home = text_input_chome,
1312		caret_end = text_input_cend,
1313		caret_left = text_input_cleft,
1314		caret_right = text_input_cright,
1315		erase = text_input_cerase,
1316		delete = text_input_cdel,
1317		clear = text_input_clear,
1318
1319		undo = text_input_undo,
1320		input = text_input_table,
1321	} or ctx;
1322
1323	local bindings = {
1324		k_left = "LEFT",
1325		k_right = "RIGHT",
1326		k_home = "HOME",
1327		k_end = "END",
1328		k_delete = "DELETE",
1329		k_erase = "ERASE"
1330	};
1331
1332	local flut = {
1333		k_left = text_input_cleft,
1334		k_right = text_input_cright,
1335		k_home = text_input_chome,
1336		k_end = text_input_cend,
1337		k_delete = text_input_cdelete,
1338		k_erase = text_input_cerase
1339	};
1340
1341-- overlay any provided keybindings
1342	if (opts.bindings) then
1343		for k,v in pairs(opts.bindings) do
1344			if bindings[k] then
1345				bindings[k] = v;
1346			end
1347		end
1348	end
1349
1350-- and build the real lut
1351	ctx.bindings = {};
1352	for k,v in pairs(bindings) do
1353		ctx.bindings[v] = flut[k];
1354	end
1355
1356	ctx:input(iotbl, sym);
1357	return ctx;
1358end
1359
1360function gen_valid_float(lb, ub)
1361	return gen_valid_num(lb, ub);
1362end
1363
1364function merge_dispatch(m1, m2)
1365	local kt = {};
1366	local res = {};
1367	if (m1 == nil) then
1368		return m2;
1369	end
1370	if (m2 == nil) then
1371		return m1;
1372	end
1373	for k,v in pairs(m1) do
1374		res[k] = v;
1375	end
1376	for k,v in pairs(m2) do
1377		res[k] = v;
1378	end
1379	return res;
1380end
1381
1382function shared_valid_str(inv)
1383	return type(inv) == "string" and #inv > 0;
1384end
1385
1386function shared_valid01_float(inv)
1387	if (string.len(inv) == 0) then
1388		return true;
1389	end
1390
1391	local val = tonumber(inv);
1392	return val and (val >= 0.0 and val <= 1.0) or false;
1393end
1394
1395-- validator returns a function that checks if [val] is tolerated or not,
1396-- but also ranging values to allow other components to provide a selection UI
1397function gen_valid_num(lb, ub, step)
1398	local range = ub - lb;
1399	local step_sz = step ~= nil and step or range * 0.01;
1400
1401	return function(val)
1402		if (not val) then
1403			warning("validator activated with missing val");
1404			return false, lb, ub, step_sz;
1405		end
1406
1407		if (string.len(val) == 0) then
1408			return false, lb, ub, step_sz;
1409		end
1410		local num = tonumber(val);
1411		if (num == nil) then
1412			return false, lb, ub, step_sz;
1413		end
1414		return not(num < lb or num > ub), lb, ub, step_sz;
1415	end
1416end
1417
1418local widgets = {};
1419
1420function suppl_flip_handler(key)
1421	return function(ctx, val)
1422		if (val == LBL_FLIP) then
1423			gconfig_set(key, not gconfig_get(key));
1424		else
1425			gconfig_set(key, val == LBL_YES);
1426		end
1427	end
1428end
1429
1430function suppl_scan_tools()
1431	local list = glob_resource("tools/*.lua", APPL_RESOURCE);
1432	for k,v in ipairs(list) do
1433		local res, msg = system_load("tools/" .. v, false);
1434		if (not res) then
1435			warning(string.format("couldn't parse tool: %s", v));
1436		else
1437			local okstate, msg = pcall(res);
1438			if (not okstate) then
1439				warning(string.format("runtime error loading tool: %s - %s", v, msg));
1440			end
1441		end
1442	end
1443end
1444
1445function suppl_chain_callback(tbl, field, new)
1446	local old = tbl[field];
1447	tbl[field] = function(...)
1448		if (new) then
1449			new(...);
1450		end
1451		if (old) then
1452			tbl[field] = old;
1453			old(...);
1454		end
1455	end
1456end
1457
1458function suppl_scan_widgets()
1459	local res = glob_resource("widgets/*.lua", APPL_RESOURCE);
1460	for k,v in ipairs(res) do
1461		local res = system_load("widgets/" .. v, false);
1462		if (res) then
1463			local ok, wtbl = pcall(res);
1464-- would be a much needed feature to have a compact and elegant
1465-- way of specifying a stronger contract on fields and types in
1466-- place like this.
1467			if (ok and wtbl and wtbl.name and type(wtbl.name) == "string" and
1468				string.len(wtbl.name) > 0 and wtbl.paths and
1469				type(wtbl.paths) == "table") then
1470				widgets[wtbl.name] = wtbl;
1471			else
1472				warning("widget " .. v .. " failed to load");
1473			end
1474		else
1475			warning("widget " .. v .. "f failed to parse");
1476		end
1477	end
1478end
1479
1480--
1481-- used to find and activate support widgets and tie to the set [ctx]:tbl,
1482-- [anchor]:vid will be used for deletion (and should be 0,0 world-space)
1483-- [ident]:str matches path/special:function to match against widget
1484-- paths and [reserved]:num is the number of center- used pixels to avoid.
1485--
1486local widget_destr = {};
1487function suppl_widget_path(ctx, anchor, ident, barh)
1488	local match = {};
1489	local fi = 0;
1490
1491	for k,v in pairs(widget_destr) do
1492		k:destroy();
1493	end
1494	widget_destr = {};
1495
1496	local props = image_surface_resolve_properties(anchor);
1497	local y1 = props.y;
1498	local y2 = props.y + props.height;
1499	local ad = active_display();
1500	local th = math.ceil(gconfig_get("lbar_sz") * active_display().scalef);
1501	local rh = y1 - th;
1502
1503-- sweep all widgets and check their 'paths' table for a path
1504-- or dynamic eval function and compare to the supplied ident
1505	for k,v in pairs(widgets) do
1506		for i,j in ipairs(v.paths) do
1507			local ident_tag;
1508			if (type(j) == "function") then
1509				ident_tag = j(v, ident);
1510			end
1511
1512-- if we actually find a match, probe the widget for how many
1513-- groups of the maximum slot- height that is needed to present
1514			if ((type(j) == "string" and j == ident) or ident_tag) then
1515				local nc = v.probe and v:probe(rh, ident_tag) or 1;
1516
1517-- and if there is a number of groups returned, mark those in the
1518-- tracking table (for later deallocation) and add to the set of
1519-- groups to layout
1520				if (nc > 0) then
1521					widget_destr[v] = true;
1522					for n=1,nc do
1523						table.insert(match, {v, n});
1524					end
1525				end
1526			end
1527		end
1528	end
1529
1530-- abort if there were no widgets that wanted to present a group,
1531-- otherwise start allocating visual resources for the groups and
1532-- proceed to layout
1533	local nm = #match;
1534	if (nm == 0) then
1535		return;
1536	end
1537
1538	local pad = 00;
1539
1540-- create anchors linked to background for automatic deletion, as they
1541-- are used for clipping, distribute in a fair way between top and bottom
1542-- but with special treatment for floating widgets
1543	local start = fi+1;
1544	local ctr = 0;
1545
1546-- the layouting algorithm here is a bit clunky. The algorithms evolved
1547-- from the advfloat autolayouter should really be generalized into a
1548-- helper script and simply be used here as well.
1549	if (nm - fi > 0) then
1550		local ndiv = (#match - fi) / 2;
1551		local cellw = ndiv > 1 and (ad.width - pad - pad) / ndiv or ad.width;
1552		local cx = pad;
1553		while start <= nm do
1554			ctr = ctr + 1;
1555			local anch = null_surface(cellw, rh);
1556			link_image(anch, anchor);
1557			local dy = 0;
1558
1559-- only account for the helper unless the caller explicitly set a height
1560			if (gconfig_get("menu_helper") and not barh and ctr % 2 == 1) then
1561				dy = th;
1562			end
1563
1564			blend_image(anch, 1.0, gconfig_get("animation") * 0.5, INTERP_SINE);
1565			image_inherit_order(anch, true);
1566			image_mask_set(anch, MASK_UNPICKABLE);
1567			local w, h = match[start][1]:show(anch, match[start][2], rh);
1568			start = start + 1;
1569
1570-- position and slide only if we get a hint on dimensions consumed
1571			if (w and h) then
1572				if (ctr % 2 == 1) then
1573					move_image(anch, cx, -h - dy);
1574				else
1575					move_image(anch, cx, props.height + dy + th);
1576					cx = cx + cellw;
1577				end
1578			else
1579				delete_image(anch);
1580			end
1581
1582		end
1583	end
1584end
1585
1586-- register a prefix_debug_listener function to attach/define a
1587-- new debug listener, and return a local queue function to append
1588-- to the log without exposing the table in the global namespace
1589local prefixes = {
1590};
1591function suppl_add_logfn(prefix)
1592	if (prefixes[prefix]) then
1593		return prefixes[prefix][1], prefixes[prefix][2];
1594	end
1595
1596-- nest one level so we can pull the scope down with us
1597	local logscope =
1598	function()
1599		local queue = {};
1600		local handler = nil;
1601
1602		prefixes[prefix] =
1603		{
1604			function(msg)
1605				local exp_msg = CLOCK .. ":" .. msg .. "\n";
1606				if (handler) then
1607					handler(exp_msg);
1608				else
1609					table.insert(queue, exp_msg);
1610					if (#queue > 200) then
1611						table.remove(queue, 1);
1612					end
1613				end
1614			end,
1615-- return a formatter as well so we can nop-out logging when not needed
1616			string.format,
1617		};
1618
1619-- and register a global function that can be used to set the singleton
1620-- that the queue flush to or messages gets immediately forwarded to
1621		_G[prefix .. "_debug_listener"] =
1622		function(newh)
1623			if (newh and type(newh) == "function") then
1624				handler = newh;
1625				for i,v in ipairs(queue) do
1626					newh(v);
1627				end
1628			else
1629				handler = nil;
1630			end
1631			queue = {};
1632		end
1633	end
1634
1635	logscope();
1636	return prefixes[prefix][1], prefixes[prefix][2];
1637end
1638