-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutil.lua
More file actions
428 lines (362 loc) · 10.8 KB
/
util.lua
File metadata and controls
428 lines (362 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
-- See docs/util.md for the model.
--invariant: stateless module: pure helpers, no module-level mutable state beyond the REMOVE sentinel
--invariant: util.REMOVE is the canonical delete marker honoured by assign and by mm/cm assignment APIs
local util = {}
function util.print(...)
if not ... then
reaper.ShowConsoleMsg('nil value\n')
return
end
reaper.ShowConsoleMsg(table.concat({...}, '\t') .. '\n')
end
local function print(...)
return util.print(...)
end
function util.print_r(root)
local cache = { [root] = '.' }
local function _dump(t,space,name)
local temp = {}
for k,v in pairs(t) do
local key = tostring(k)
if cache[v] then
temp[#temp+1] = '+' .. key .. ' {' .. cache[v]..'}'
elseif type(v) == 'table' then
local new_key = name .. '.' .. key
cache[v] = new_key
temp[#temp+1] = '+' .. key .. _dump(v,space .. (next(t,k) and '|' or ' ' ).. string.rep(' ',#key),new_key)
else
temp[#temp+1] = '+' .. key .. ' [' .. tostring(v)..']'
end
end
return table.concat(temp,'\n'..space)
end
print(_dump(root, '',''))
end
util.REMOVE = { }
--contract: values equal to util.REMOVE clear the key from t1 instead of being assigned
function util.assign(t1,t2)
if t2 then
for k, v in pairs(t2) do
if v == util.REMOVE then
t1[k] = nil
else
t1[k] = v
end
end
end
return t1
end
function util.add(tbl, val)
tbl[#tbl+1] = val
return val
end
-- Wrap fn so each call is one REAPER undo entry. label is the undo
-- description; the wrapper forwards args, propagates errors after
-- closing the block, and is a no-op when reaper.Undo_BeginBlock is
-- absent (test harness).
function util.atomic(label, fn)
return function(...)
if reaper.Undo_BeginBlock then reaper.Undo_BeginBlock() end
local ok, err = xpcall(fn, debug.traceback, ...)
if reaper.Undo_EndBlock then reaper.Undo_EndBlock(label, -1) end
if not ok then error(err) end
end
end
function util.pick(src, keys, adds)
local dst = {}
for k in keys:gmatch("%S+") do
dst[k] = src[k]
end
if adds then
util.assign(dst, adds)
end
return dst
end
function util.bucket(buckets, key, val)
local b = buckets[key]
if not b then b = {}; buckets[key] = b end
b[#b+1] = val
return b
end
-- Sparse → dense; n is the pre-sparse length.
function util.compact(t, n)
local out = {}
for i = 1, n do if t[i] ~= nil then out[#out+1] = t[i] end end
return out
end
--contract: assumes items sorted by keyFn (defaults to .ppq); 'before' modes scan to first miss then stop
function util.seek(items, mode, key, filter, keyFn)
keyFn = keyFn or function(x) return x.ppq end
local before = mode == 'before' or mode == 'at-or-before'
local cmp
if mode == 'before' then cmp = function(k) return k < key end
elseif mode == 'at-or-before' then cmp = function(k) return k <= key end
elseif mode == 'after' then cmp = function(k) return k > key end
elseif mode == 'at-or-after' then cmp = function(k) return k >= key end
end
local hit
for _, item in ipairs(items) do
if cmp(keyFn(item)) then
if not filter or filter(item) then
if not before then return item end
hit = item
end
elseif before then
break
end
end
return hit
end
--contract: exclude applies only to the outermost table; deep recursion drops the exclude set
function util.clone(src, exclude, deep)
if not src then return end
local dst = {}
for k, v in pairs(src) do
if not (exclude and exclude[k]) then
dst[k] = (deep and type(v) == 'table') and util.clone(v, nil, true) or v
end
end
return dst
end
function util.deepClone(src) return util.clone(src, nil, true) end
function util.deepEq(a, b)
if a == b then return true end
if type(a) ~= 'table' or type(b) ~= 'table' then return false end
for k, v in pairs(a) do if not util.deepEq(v, b[k]) then return false end end
for k in pairs(b) do if a[k] == nil then return false end end
return true
end
local function escape_string(s)
local out = (s:gsub('[\\{},=]', function(c)
return '\\' .. c
end))
-- Disambiguate from numbers/booleans: prepend `\e` (empty marker, decodes
-- to nothing) when the encoded form would otherwise round-trip as a
-- non-string scalar. The decoder uses the presence of any escape as a
-- signal to skip number/boolean coercion.
if tonumber(out) or out == 'true' or out == 'false' then
out = '\\e' .. out
end
return out
end
--contract: listeners filter by signal name at registration; forward requires source itself ran installHooks
function util.installHooks(owner)
local listeners = {}
function owner:subscribe(signal, fn)
listeners[signal] = listeners[signal] or {}
listeners[signal][fn] = true
end
function owner:unsubscribe(signal, fn)
if listeners[signal] then listeners[signal][fn] = nil end
end
local function fire(signal, data)
local subs = listeners[signal]
if subs then for fn in pairs(subs) do fn(data) end end
end
function owner:forward(signal, source)
source:subscribe(signal, function(data) fire(signal, data) end)
end
return fire
end
function util.isNote(e) return e and e.endppq end
--contract: iterates events with ppq in half-open [lo, hi); adjacent windows tile without overlap
function util.between(events, lo, hi, filter)
filter = filter or function() return true end
local i = 0
return function()
while true do
i = i + 1
local evt = events[i]
if not evt or evt.ppq >= hi then return end
if evt.ppq >= lo and filter(evt) then return evt end
end
end
end
function util.clamp(val,min,max)
if val < min then
return min
elseif val > max then
return max
else
return val
end
end
function util.setDigit(val, d, pos, base, half)
local place = base ^ pos // 1
local above = val - (val % (place * base))
return above + d * place + (half and place // 2 or 0)
end
--contract: advances at least one full interval; values already on a boundary do not no-op
function util.snapTo(v, dir, interval)
if dir > 0 then return (math.floor(v / interval) + 1) * interval end
return (math.ceil(v / interval) - 1) * interval
end
function util.nudgedScalar(v, lo, hi, dir, interval)
local target = interval and util.snapTo(v, dir, interval) or (v + dir)
return util.clamp(target, lo, hi)
end
function util.oneOf(choices, txt)
for k in choices:gmatch('%S+') do
if txt == k then return true end
end
return false
end
function util.round(n, to)
if to then
return math.floor(n / to + 0.5) * to
else
return math.floor(n + 0.5)
end
end
function util.gcd(a, b)
a, b = math.abs(a), math.abs(b)
while b ~= 0 do a, b = b, a % b end
return a
end
function util.lcm(a, b) return a // util.gcd(a, b) * b end
local BASE36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
function util.toBase36(n)
if n == 0 then return '0' end
local s = ''
while n > 0 do
local r = n % 36
s = string.sub(BASE36, r + 1, r + 1) .. s
n = n // 36
end
return s
end
function util.fromBase36(txt)
return tonumber(txt, 36)
end
--contract: overloaded on type of v: function => call n times for side effect; else build n-array filled with v
function util.dotimes(n, v)
if type(v) == 'function' then
for _ = 1, n do v() end
return
end
local rv = {}
for i = 1, n do rv[i] = v end
return rv
end
--contract: strict: cycles raise; exclude applies only to the outermost table (recursion drops it)
function util.serialise(value, exclude, seen)
exclude = exclude or { }
local t = type(value)
if t == 'number' or t == 'boolean' then
return tostring(value)
elseif t == 'string' then
return escape_string(value)
elseif t == 'table' then
seen = seen or {}
if seen[value] then
error('cycle detected during serialisation')
end
seen[value] = true
local parts = {}
for k, v in pairs(value) do
if not exclude[k] then
local key_str = util.serialise(k, nil, seen)
local val_str = util.serialise(v, nil, seen)
parts[#parts+1] = key_str .. '=' .. val_str
end
end
seen[value] = nil
return '{' .. table.concat(parts, ',') .. '}'
else
error('unsupported type: ' .. t)
end
end
--contract: strict: trailing chars after the root value raise; scalars decode back to number/boolean/string
function util.unserialise(input)
local pos = 1
local len = #input
local function peek()
return input:sub(pos, pos)
end
local function nextChar()
local c = input:sub(pos, pos)
pos = pos + 1
return c
end
local function parseStringToken()
local buf = {}
local hadEscape = false
while pos <= len do
local c = nextChar()
if c == '\\' then
local n = nextChar()
if n == '{' or n == '}' or n == ',' or n == '=' or n == '\\' then
buf[#buf+1] = n
hadEscape = true
elseif n == 'e' then
-- empty marker: forces string interpretation downstream
hadEscape = true
else
error('invalid escape: \\' .. tostring(n))
end
elseif c == '{' or c == '}' or c == ',' or c == '=' then
pos = pos - 1
break
else
buf[#buf+1] = c
end
end
local s = table.concat(buf)
if hadEscape then return s end
local n = tonumber(s)
if n then return n end
if s == 'true' then return true end
if s == 'false' then return false end
return s
end
local parseValue -- forward decl
local function parseTable()
if nextChar() ~= '{' then
error("expected '{'")
end
local t = {}
if peek() == '}' then
nextChar()
return t
end
while true do
local key = parseValue()
if nextChar() ~= '=' then
error("expected '=' after key")
end
local val = parseValue()
t[key] = val
local c = nextChar()
if c == '}' then
break
elseif c == ',' then
-- continue
else
error("expected ',' or '}'")
end
end
return t
end
function parseValue()
if peek() == '{' then
return parseTable()
else
return parseStringToken()
end
end
local result = parseValue()
if pos <= len then
error('trailing characters')
end
return result
end
--contract: executes the named module file as a fresh chunk, passing `deps` as its `...` argument. Used for factory modules whose file body IS the constructor (vs. stateless `require`d tables). Test seam: a function in util._stubs[name] takes precedence and is called with deps — so harnesses can swap a fake without altering the production graph.
util._stubs = {}
function util.instantiate(name, deps)
local stub = util._stubs[name]
if stub then return stub(deps) end
local path = assert(package.searchpath(name, package.path),
'util.instantiate: cannot find module ' .. name)
return assert(loadfile(path))(deps)
end
return util