View difference between Paste ID: pT4bd6yL and 7QarNwLL
SHOW: | | - or go back to the newest paste.
1
-- Musiclo music player made by timuzkas
2
-- licensed under Creative Commons CC0
3
-- Simple, sleek player for YT.
4
-- //
5
-- //
6
-- Backend code from @terreng on github, using MIT license
7
-- Frontend code by timuzkas, using MIT license
8
-- Transliterator by timuzkas, using MIT license
9
-- PrimeUI by JackMacWindows, using CC0 license
10
11
local expect = require "cc.expect".expect
12
-- PrimeUI by JackMacWindows
13
-- Public domain/CC0
14
15
-- Initialization code
16
local PrimeUI = {}
17
do
18
    local coros = {}
19
    local restoreCursor
20
21
    --- Adds a task to run in the main loop.
22
    ---@param func function The function to run, usually an `os.pullEvent` loop
23
    function PrimeUI.addTask(func)
24
        expect(1, func, "function")
25
        local t = {coro = coroutine.create(func)}
26
        coros[#coros+1] = t
27
        _, t.filter = coroutine.resume(t.coro)
28
    end
29
30
    --- Sends the provided arguments to the run loop, where they will be returned.
31
    ---@param ... any The parameters to send
32
    function PrimeUI.resolve(...)
33
        coroutine.yield(coros, ...)
34
    end
35
36
    --- Clears the screen and resets all components. Do not use any previously
37
    --- created components after calling this function.
38
    function PrimeUI.clear()
39
        -- Reset the screen.
40
        term.setCursorPos(1, 1)
41
        term.setCursorBlink(false)
42
        term.setBackgroundColor(colors.black)
43
        term.setTextColor(colors.white)
44
        term.clear()
45
        -- Reset the task list and cursor restore function.
46
        coros = {}
47
        restoreCursor = nil
48
    end
49
50
    --- Sets or clears the window that holds where the cursor should be.
51
    ---@param win window|nil The window to set as the active window
52
    function PrimeUI.setCursorWindow(win)
53
        expect(1, win, "table", "nil")
54
        restoreCursor = win and win.restoreCursor
55
    end
56
57
    --- Gets the absolute position of a coordinate relative to a window.
58
    ---@param win window The window to check
59
    ---@param x number The relative X position of the point
60
    ---@param y number The relative Y position of the point
61
    ---@return number x The absolute X position of the window
62
    ---@return number y The absolute Y position of the window
63
    function PrimeUI.getWindowPos(win, x, y)
64
        if win == term then return x, y end
65
        while win ~= term.native() and win ~= term.current() do
66
            if not win.getPosition then return x, y end
67
            local wx, wy = win.getPosition()
68
            x, y = x + wx - 1, y + wy - 1
69
            _, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue
70
        end
71
        return x, y
72
    end
73
74
    --- Runs the main loop, returning information on an action.
75
    ---@return any ... The result of the coroutine that exited
76
    function PrimeUI.run()
77
        while true do
78
            -- Restore the cursor and wait for the next event.
79
            if restoreCursor then restoreCursor() end
80
            local ev = table.pack(os.pullEvent())
81
            -- Run all coroutines.
82
            for _, v in ipairs(coros) do
83
                if v.filter == nil or v.filter == ev[1] then
84
                    -- Resume the coroutine, passing the current event.
85
                    local res = table.pack(coroutine.resume(v.coro, table.unpack(ev, 1, ev.n)))
86
                    -- If the call failed, bail out. Coroutines should never exit.
87
                    if not res[1] then error(res[2], 2) end
88
                    -- If the coroutine resolved, return its values.
89
                    if res[2] == coros then return table.unpack(res, 3, res.n) end
90
                    -- Set the next event filter.
91
                    v.filter = res[2]
92
                end
93
            end
94
        end
95
    end
96
end
97
98
--- Draws a thin border around a screen region.
99
---@param win window The window to draw on
100
---@param x number The X coordinate of the inside of the box
101
---@param y number The Y coordinate of the inside of the box
102
---@param width number The width of the inner box
103
---@param height number The height of the inner box
104
---@param fgColor color|nil The color of the border (defaults to white)
105
---@param bgColor color|nil The color of the background (defaults to black)
106
function PrimeUI.borderBox(win, x, y, width, height, fgColor, bgColor)
107
    expect(1, win, "table")
108
    expect(2, x, "number")
109
    expect(3, y, "number")
110
    expect(4, width, "number")
111
    expect(5, height, "number")
112
    fgColor = expect(6, fgColor, "number", "nil") or colors.white
113
    bgColor = expect(7, bgColor, "number", "nil") or colors.black
114
    -- Draw the top-left corner & top border.
115
    win.setBackgroundColor(bgColor)
116
    win.setTextColor(fgColor)
117
    win.setCursorPos(x - 1, y - 1)
118
    win.write("\x9C" .. ("\x8C"):rep(width))
119
    -- Draw the top-right corner.
120
    win.setBackgroundColor(fgColor)
121
    win.setTextColor(bgColor)
122
    win.write("\x93")
123
    -- Draw the right border.
124
    for i = 1, height do
125
        win.setCursorPos(win.getCursorPos() - 1, y + i - 1)
126
        win.write("\x95")
127
    end
128
    -- Draw the left border.
129
    win.setBackgroundColor(bgColor)
130
    win.setTextColor(fgColor)
131
    for i = 1, height do
132
        win.setCursorPos(x - 1, y + i - 1)
133
        win.write("\x95")
134
    end
135
    -- Draw the bottom border and corners.
136
    win.setCursorPos(x - 1, y + height)
137
    win.write("\x8D" .. ("\x8C"):rep(width) .. "\x8E")
138
end
139
140
--- Creates a clickable button on screen with text.
141
---@param win window The window to draw on
142
---@param x number The X position of the button
143
---@param y number The Y position of the button
144
---@param text string The text to draw on the button
145
---@param action function|string A function to call when clicked, or a string to send with a `run` event
146
---@param fgColor color|nil The color of the button text (defaults to white)
147
---@param bgColor color|nil The color of the button (defaults to light gray)
148
---@param clickedColor color|nil The color of the button when clicked (defaults to gray)
149
---@param periphName string|nil The name of the monitor peripheral, or nil (set if you're using a monitor - events will be filtered to that monitor)
150
function PrimeUI.button(win, x, y, text, action, fgColor, bgColor, clickedColor, periphName)
151
    expect(1, win, "table")
152
    expect(1, win, "table")
153
    expect(2, x, "number")
154
    expect(3, y, "number")
155
    expect(4, text, "string")
156
    expect(5, action, "function", "string")
157
    fgColor = expect(6, fgColor, "number", "nil") or colors.white
158
    bgColor = expect(7, bgColor, "number", "nil") or colors.gray
159
    clickedColor = expect(8, clickedColor, "number", "nil") or colors.lightGray
160
    periphName = expect(9, periphName, "string", "nil")
161
    -- Draw the initial button.
162
    win.setCursorPos(x, y)
163
    win.setBackgroundColor(bgColor)
164
    win.setTextColor(fgColor)
165
    win.write(" " .. text .. " ")
166
    -- Get the screen position and add a click handler.
167
    PrimeUI.addTask(function()
168
        local buttonDown = false
169
        while true do
170
            local event, button, clickX, clickY = os.pullEvent()
171
            local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
172
            if event == "mouse_click" and periphName == nil and button == 1 and clickX >= screenX and clickX < screenX + #text + 2 and clickY == screenY then
173
                -- Initiate a click action (but don't trigger until mouse up).
174
                buttonDown = true
175
                -- Redraw the button with the clicked background color.
176
                win.setCursorPos(x, y)
177
                win.setBackgroundColor(clickedColor)
178
                win.setTextColor(fgColor)
179
                win.write(" " .. text .. " ")
180
            elseif (event == "monitor_touch" and periphName == button and clickX >= screenX and clickX < screenX + #text + 2 and clickY == screenY)
181
                or (event == "mouse_up" and button == 1 and buttonDown) then
182
                -- Finish a click event.
183
                if clickX >= screenX and clickX < screenX + #text + 2 and clickY == screenY then
184
                    -- Trigger the action.
185
                    if type(action) == "string" then
186
                        PrimeUI.resolve("button", action)
187
                    else
188
                        action()
189
                    end
190
                end
191
                -- Redraw the original button state.
192
                win.setCursorPos(x, y)
193
                win.setBackgroundColor(bgColor)
194
                win.setTextColor(fgColor)
195
                win.write(" " .. text .. " ")
196
            end
197
        end
198
    end)
199
end
200
201
--- Draws a line of text, centering it inside a box horizontally.
202
---@param win window The window to draw on
203
---@param x number The X position of the left side of the box
204
---@param y number The Y position of the box
205
---@param width number The width of the box to draw in
206
---@param text string The text to draw
207
---@param fgColor color|nil The color of the text (defaults to white)
208
---@param bgColor color|nil The color of the background (defaults to black)
209
function PrimeUI.centerLabel(win, x, y, width, text, fgColor, bgColor)
210
    expect(1, win, "table")
211
    expect(2, x, "number")
212
    expect(3, y, "number")
213
    expect(4, width, "number")
214
    expect(5, text, "string")
215
    fgColor = expect(6, fgColor, "number", "nil") or colors.white
216
    bgColor = expect(7, bgColor, "number", "nil") or colors.black
217
    assert(#text <= width, "string is too long")
218
    win.setCursorPos(x + math.floor((width - #text) / 2), y)
219
    win.setTextColor(fgColor)
220
    win.setBackgroundColor(bgColor)
221
    win.write(text)
222
end
223
224
--- Creates a list of entries with toggleable check boxes.
225
---@param win window The window to draw on
226
---@param x number The X coordinate of the inside of the box
227
---@param y number The Y coordinate of the inside of the box
228
---@param width number The width of the inner box
229
---@param height number The height of the inner box
230
---@param selections table<string,string|boolean> A list of entries to show, where the value is whether the item is pre-selected (or `"R"` for required/forced selected)
231
---@param action function|string|nil A function or `run` event that's called when a selection is made
232
---@param fgColor color|nil The color of the text (defaults to white)
233
---@param bgColor color|nil The color of the background (defaults to black)
234
function PrimeUI.checkSelectionBox(win, x, y, width, height, selections, action, fgColor, bgColor)
235
    expect(1, win, "table")
236
    expect(2, x, "number")
237
    expect(3, y, "number")
238
    expect(4, width, "number")
239
    expect(5, height, "number")
240
    expect(6, selections, "table")
241
    expect(7, action, "function", "string", "nil")
242
    fgColor = expect(8, fgColor, "number", "nil") or colors.white
243
    bgColor = expect(9, bgColor, "number", "nil") or colors.black
244
    -- Calculate how many selections there are.
245
    local nsel = 0
246
    for _ in pairs(selections) do nsel = nsel + 1 end
247
    -- Create the outer display box.
248
    local outer = window.create(win, x, y, width, height)
249
    outer.setBackgroundColor(bgColor)
250
    outer.clear()
251
    -- Create the inner scroll box.
252
    local inner = window.create(outer, 1, 1, width - 1, nsel)
253
    inner.setBackgroundColor(bgColor)
254
    inner.setTextColor(fgColor)
255
    inner.clear()
256
    -- Draw each line in the window.
257
    local lines = {}
258
    local nl, selected = 1, 1
259
    for k, v in pairs(selections) do
260
        inner.setCursorPos(1, nl)
261
        inner.write((v and (v == "R" and "[-] " or "[\xD7] ") or "[ ] ") .. k)
262
        lines[nl] = {k, not not v}
263
        nl = nl + 1
264
    end
265
    -- Draw a scroll arrow if there is scrolling.
266
    if nsel > height then
267
        outer.setCursorPos(width, height)
268
        outer.setBackgroundColor(bgColor)
269
        outer.setTextColor(fgColor)
270
        outer.write("\31")
271
    end
272
    -- Set cursor blink status.
273
    inner.setCursorPos(2, selected)
274
    inner.setCursorBlink(true)
275
    PrimeUI.setCursorWindow(inner)
276
    -- Get screen coordinates & add run task.
277
    local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
278
    PrimeUI.addTask(function()
279
        local scrollPos = 1
280
        while true do
281
            -- Wait for an event.
282
            local ev = table.pack(os.pullEvent())
283
            -- Look for a scroll event or a selection event.
284
            local dir
285
            if ev[1] == "key" then
286
                if ev[2] == keys.up then dir = -1
287
                elseif ev[2] == keys.down then dir = 1
288
                elseif ev[2] == keys.space and selections[lines[selected][1]] ~= "R" then
289
                    -- (Un)select the item.
290
                    lines[selected][2] = not lines[selected][2]
291
                    inner.setCursorPos(2, selected)
292
                    inner.write(lines[selected][2] and "\xD7" or " ")
293
                    -- Call the action if passed; otherwise, set the original table.
294
                    if type(action) == "string" then PrimeUI.resolve("checkSelectionBox", action, lines[selected][1], lines[selected][2])
295
                    elseif action then action(lines[selected][1], lines[selected][2])
296
                    else selections[lines[selected][1]] = lines[selected][2] end
297
                    -- Redraw all lines in case of changes.
298
                    for i, v in ipairs(lines) do
299
                        local vv = selections[v[1]] == "R" and "R" or v[2]
300
                        inner.setCursorPos(2, i)
301
                        inner.write((vv and (vv == "R" and "-" or "\xD7") or " "))
302
                    end
303
                    inner.setCursorPos(2, selected)
304
                end
305
            elseif ev[1] == "mouse_scroll" and ev[3] >= screenX and ev[3] < screenX + width and ev[4] >= screenY and ev[4] < screenY + height then
306
                dir = ev[2]
307
            end
308
            -- Scroll the screen if required.
309
            if dir and (selected + dir >= 1 and selected + dir <= nsel) then
310
                selected = selected + dir
311
                if selected - scrollPos < 0 or selected - scrollPos >= height then
312
                    scrollPos = scrollPos + dir
313
                    inner.reposition(1, 2 - scrollPos)
314
                end
315
                inner.setCursorPos(2, selected)
316
            end
317
            -- Redraw scroll arrows and reset cursor.
318
            outer.setCursorPos(width, 1)
319
            outer.write(scrollPos > 1 and "\30" or " ")
320
            outer.setCursorPos(width, height)
321
            outer.write(scrollPos < nsel - height + 1 and "\31" or " ")
322
            inner.restoreCursor()
323
        end
324
    end)
325
end
326
327
--- Creates a clickable region on screen without any content.
328
---@param win window The window to draw on
329
---@param x number The X position of the button
330
---@param y number The Y position of the button
331
---@param width number The width of the inner box
332
---@param height number The height of the inner box
333
---@param action function|string A function to call when clicked, or a string to send with a `run` event
334
---@param periphName string|nil The name of the monitor peripheral, or nil (set if you're using a monitor - events will be filtered to that monitor)
335
function PrimeUI.clickRegion(win, x, y, width, height, action, periphName)
336
    expect(1, win, "table")
337
    expect(2, x, "number")
338
    expect(3, y, "number")
339
    expect(4, width, "number")
340
    expect(5, height, "number")
341
    expect(6, action, "function", "string")
342
    expect(7, periphName, "string", "nil")
343
    PrimeUI.addTask(function()
344
        -- Get the screen position and add a click handler.
345
        local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
346
        local buttonDown = false
347
        while true do
348
            local event, button, clickX, clickY = os.pullEvent()
349
            if (event == "monitor_touch" and periphName == button)
350
                or (event == "mouse_click" and button == 1 and periphName == nil) then
351
                -- Finish a click event.
352
                if clickX >= screenX and clickX < screenX + width
353
                    and clickY >= screenY and clickY < screenY + height then
354
                    -- Trigger the action.
355
                    if type(action) == "string" then
356
                        PrimeUI.resolve("clickRegion", action)
357
                    else
358
                        action()
359
                    end
360
                end
361
            end
362
        end
363
    end)
364
end
365
366
--- Draws a NFT-formatted image to the screen.
367
---@param win window The window to draw on
368
---@param x number The X position of the top left corner of the image
369
---@param y number The Y position of the top left corner of the image
370
---@param data string|table The path to the image to load, or the image data itself
371
function PrimeUI.drawNFT(win, x, y, data)
372
    expect(1, win, "table")
373
    expect(2, x, "number")
374
    expect(3, y, "number")
375
    expect(4, data, "string", "table")
376
    -- Load the image file if a string was passed using nft.load.
377
    if type(data) == "string" then
378
        data = assert(nft.load("data/example.nft"), "File is not a valid NFT file")
379
    end
380
    nft.draw(data, x, y , win)
381
end
382
383
--- Draws a block of text inside a window with word wrapping, optionally resizing the window to fit.
384
---@param win window The window to draw in
385
---@param text string The text to draw
386
---@param resizeToFit boolean|nil Whether to resize the window to fit the text (defaults to false). This is useful for scroll boxes.
387
---@param fgColor color|nil The color of the text (defaults to white)
388
---@param bgColor color|nil The color of the background (defaults to black)
389
---@return number lines The total number of lines drawn
390
function PrimeUI.drawText(win, text, resizeToFit, fgColor, bgColor)
391
    expect(1, win, "table")
392
    expect(2, text, "string")
393
    expect(3, resizeToFit, "boolean", "nil")
394
    fgColor = expect(4, fgColor, "number", "nil") or colors.white
395
    bgColor = expect(5, bgColor, "number", "nil") or colors.black
396
    -- Set colors.
397
    win.setBackgroundColor(bgColor)
398
    win.setTextColor(fgColor)
399
    -- Redirect to the window to use print on it.
400
    local old = term.redirect(win)
401
    -- Draw the text using print().
402
    local lines = print(text)
403
    -- Redirect back to the original terminal.
404
    term.redirect(old)
405
    -- Resize the window if desired.
406
    if resizeToFit then
407
        -- Get original parameters.
408
        local x, y = win.getPosition()
409
        local w = win.getSize()
410
        -- Resize the window.
411
        win.reposition(x, y, w, lines)
412
    end
413
    return lines
414
end
415
416
--- Draws a horizontal line at a position with the specified width.
417
---@param win window The window to draw on
418
---@param x number The X position of the left side of the line
419
---@param y number The Y position of the line
420
---@param width number The width/length of the line
421
---@param fgColor color|nil The color of the line (defaults to white)
422
---@param bgColor color|nil The color of the background (defaults to black)
423
function PrimeUI.horizontalLine(win, x, y, width, fgColor, bgColor)
424
    expect(1, win, "table")
425
    expect(2, x, "number")
426
    expect(3, y, "number")
427
    expect(4, width, "number")
428
    fgColor = expect(5, fgColor, "number", "nil") or colors.white
429
    bgColor = expect(6, bgColor, "number", "nil") or colors.black
430
    -- Use drawing characters to draw a thin line.
431
    win.setCursorPos(x, y)
432
    win.setTextColor(fgColor)
433
    win.setBackgroundColor(bgColor)
434
    win.write(("\x8C"):rep(width))
435
end
436
437
--- Creates a text input box.
438
---@param win window The window to draw on
439
---@param x number The X position of the left side of the box
440
---@param y number The Y position of the box
441
---@param width number The width/length of the box
442
---@param action function|string A function or `run` event to call when the enter key is pressed
443
---@param fgColor color|nil The color of the text (defaults to white)
444
---@param bgColor color|nil The color of the background (defaults to black)
445
---@param replacement string|nil A character to replace typed characters with
446
---@param history string[]|nil A list of previous entries to provide
447
---@param completion function|nil A function to call to provide completion
448
---@param default string|nil A string to return if the box is empty
449
function PrimeUI.inputBox(win, x, y, width, action, fgColor, bgColor, replacement, history, completion, default)
450
    expect(1, win, "table")
451
    expect(2, x, "number")
452
    expect(3, y, "number")
453
    expect(4, width, "number")
454
    expect(5, action, "function", "string")
455
    fgColor = expect(6, fgColor, "number", "nil") or colors.white
456
    bgColor = expect(7, bgColor, "number", "nil") or colors.black
457
    expect(8, replacement, "string", "nil")
458
    expect(9, history, "table", "nil")
459
    expect(10, completion, "function", "nil")
460
    expect(11, default, "string", "nil")
461
    -- Create a window to draw the input in.
462
    local box = window.create(win, x, y, width, 1)
463
    box.setTextColor(fgColor)
464
    box.setBackgroundColor(bgColor)
465
    box.clear()
466
    -- Call read() in a new coroutine.
467
    PrimeUI.addTask(function()
468
        -- We need a child coroutine to be able to redirect back to the window.
469
        local coro = coroutine.create(read)
470
        -- Run the function for the first time, redirecting to the window.
471
        local old = term.redirect(box)
472
        local ok, res = coroutine.resume(coro, replacement, history, completion, default)
473
        term.redirect(old)
474
        -- Run the coroutine until it finishes.
475
        while coroutine.status(coro) ~= "dead" do
476
            -- Get the next event.
477
            local ev = table.pack(os.pullEvent())
478
            -- Redirect and resume.
479
            old = term.redirect(box)
480
            ok, res = coroutine.resume(coro, table.unpack(ev, 1, ev.n))
481
            term.redirect(old)
482
            -- Pass any errors along.
483
            if not ok then error(res) end
484
        end
485
        -- Send the result to the receiver.
486
        if type(action) == "string" then PrimeUI.resolve("inputBox", action, res)
487
        else action(res) end
488
        -- Spin forever, because tasks cannot exit.
489
        while true do os.pullEvent() end
490
    end)
491
end
492
493
--- Runs a function or action repeatedly after a specified time period until canceled.
494
--- If a function is passed as an action, it may return a number to change the
495
--- period, or `false` to stop it.
496
---@param time number The amount of time to wait for each time, in seconds
497
---@param action function|string The function to call when the timer completes, or a `run` event to send
498
---@return function cancel A function to cancel the timer
499
function PrimeUI.interval(time, action)
500
    expect(1, time, "number")
501
    expect(2, action, "function", "string")
502
    -- Start the timer.
503
    local timer = os.startTimer(time)
504
    -- Add a task to wait for the timer.
505
    PrimeUI.addTask(function()
506
        while true do
507
            -- Wait for a timer event.
508
            local _, tm = os.pullEvent("timer")
509
            if tm == timer then
510
                -- Fire the timer action.
511
                local res
512
                if type(action) == "string" then PrimeUI.resolve("timeout", action)
513
                else res = action() end
514
                -- Check the return value and adjust time accordingly.
515
                if type(res) == "number" then time = res end
516
                -- Set a new timer if not canceled.
517
                if res ~= false then timer = os.startTimer(time) end
518
            end
519
        end
520
    end)
521
    -- Return a function to cancel the timer.
522
    return function() os.cancelTimer(timer) end
523
end
524
525
--- Adds an action to trigger when a key is pressed.
526
---@param key key The key to trigger on, from `keys.*`
527
---@param action function|string A function to call when clicked, or a string to use as a key for a `run` return event
528
function PrimeUI.keyAction(key, action)
529
    expect(1, key, "number")
530
    expect(2, action, "function", "string")
531
    PrimeUI.addTask(function()
532
        while true do
533
            local _, param1 = os.pullEvent("key") -- wait for key
534
            if param1 == key then
535
                if type(action) == "string" then PrimeUI.resolve("keyAction", action)
536
                else action() end
537
            end
538
        end
539
    end)
540
end
541
542
--- Draws a line of text at a position.
543
---@param win window The window to draw on
544
---@param x number The X position of the left side of the text
545
---@param y number The Y position of the text
546
---@param text string The text to draw
547
---@param fgColor color|nil The color of the text (defaults to white)
548
---@param bgColor color|nil The color of the background (defaults to black)
549
function PrimeUI.label(win, x, y, text, fgColor, bgColor)
550
    expect(1, win, "table")
551
    expect(2, x, "number")
552
    expect(3, y, "number")
553
    expect(4, text, "string")
554
    fgColor = expect(5, fgColor, "number", "nil") or colors.white
555
    bgColor = expect(6, bgColor, "number", "nil") or colors.black
556
    win.setCursorPos(x, y)
557
    win.setTextColor(fgColor)
558
    win.setBackgroundColor(bgColor)
559
    win.write(text)
560
end
561
562
--- Creates a progress bar, which can be updated by calling the returned function.
563
---@param win window The window to draw on
564
---@param x number The X position of the left side of the bar
565
---@param y number The Y position of the bar
566
---@param width number The width of the bar
567
---@param fgColor color|nil The color of the activated part of the bar (defaults to white)
568
---@param bgColor color|nil The color of the inactive part of the bar (defaults to black)
569
---@param useShade boolean|nil Whether to use shaded areas for the inactive part (defaults to false)
570
---@return function redraw A function to call to update the progress of the bar, taking a number from 0.0 to 1.0
571
function PrimeUI.progressBar(win, x, y, width, fgColor, bgColor, useShade)
572
    expect(1, win, "table")
573
    expect(2, x, "number")
574
    expect(3, y, "number")
575
    expect(4, width, "number")
576
    fgColor = expect(5, fgColor, "number", "nil") or colors.white
577
    bgColor = expect(6, bgColor, "number", "nil") or colors.black
578
    expect(7, useShade, "boolean", "nil")
579
    local function redraw(progress)
580
        expect(1, progress, "number")
581
        if progress < 0 or progress > 1 then error("bad argument #1 (value out of range)", 2) end
582
        -- Draw the active part of the bar.
583
        win.setCursorPos(x, y)
584
        win.setBackgroundColor(bgColor)
585
        win.setBackgroundColor(fgColor)
586
        win.write((" "):rep(math.floor(progress * width)))
587
        -- Draw the inactive part of the bar, using shade if desired.
588
        win.setBackgroundColor(bgColor)
589
        win.setTextColor(fgColor)
590
        win.write((useShade and "\x7F" or " "):rep(width - math.floor(progress * width)))
591
    end
592
    redraw(0)
593
    return redraw
594
end
595
596
--- Creates a scrollable window, which allows drawing large content in a small area.
597
---@param win window The parent window of the scroll box
598
---@param x number The X position of the box
599
---@param y number The Y position of the box
600
---@param width number The width of the box
601
---@param height number The height of the outer box
602
---@param innerHeight number The height of the inner scroll area
603
---@param allowArrowKeys boolean|nil Whether to allow arrow keys to scroll the box (defaults to true)
604
---@param showScrollIndicators boolean|nil Whether to show arrow indicators on the right side when scrolling is available, which reduces the inner width by 1 (defaults to false)
605
---@param fgColor number|nil The color of scroll indicators (defaults to white)
606
---@param bgColor color|nil The color of the background (defaults to black)
607
---@return window inner The inner window to draw inside
608
---@return fun(pos:number) scroll A function to manually set the scroll position of the window
609
function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor)
610
    expect(1, win, "table")
611
    expect(2, x, "number")
612
    expect(3, y, "number")
613
    expect(4, width, "number")
614
    expect(5, height, "number")
615
    expect(6, innerHeight, "number")
616
    expect(7, allowArrowKeys, "boolean", "nil")
617
    expect(8, showScrollIndicators, "boolean", "nil")
618
    fgColor = expect(9, fgColor, "number", "nil") or colors.white
619
    bgColor = expect(10, bgColor, "number", "nil") or colors.black
620
    if allowArrowKeys == nil then allowArrowKeys = true end
621
    -- Create the outer container box.
622
    local outer = window.create(win == term and term.current() or win, x, y, width, height)
623
    outer.setBackgroundColor(bgColor)
624
    outer.clear()
625
    -- Create the inner scrolling box.
626
    local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight)
627
    inner.setBackgroundColor(bgColor)
628
    inner.clear()
629
    -- Draw scroll indicators if desired.
630
    if showScrollIndicators then
631
        outer.setBackgroundColor(bgColor)
632
        outer.setTextColor(fgColor)
633
        outer.setCursorPos(width, height)
634
        outer.write(innerHeight > height and "\31" or " ")
635
    end
636
    -- Get the absolute position of the window.
637
    x, y = PrimeUI.getWindowPos(win, x, y)
638
    -- Add the scroll handler.
639
    local scrollPos = 1
640
    
641
    -- Store the original event filter function
642
    local originalEventFilter = PrimeUI.eventFilter
643
    
644
    -- Replace the event filter to adjust mouse coordinates for buttons inside the scroll box
645
    PrimeUI.eventFilter = function(event, ...)
646
        if event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" then
647
            local _, mouseX, mouseY = ...
648
            
649
            -- Check if the click is within the scroll box boundaries
650
            if mouseX >= x and mouseX < x + width and mouseY >= y and mouseY < y + height then
651
                -- Adjust the Y coordinate based on scroll position
652
                local adjustedY = mouseY + scrollPos - 1
653
                
654
                -- Call the original event filter with adjusted coordinates
655
                return originalEventFilter(event, _, mouseX, adjustedY, select(4, ...))
656
            end
657
        end
658
        
659
        -- For other events, use the original filter
660
        return originalEventFilter(event, ...)
661
    end
662
    
663
    PrimeUI.addTask(function()
664
        while true do
665
            -- Wait for next event.
666
            local ev = table.pack(os.pullEvent())
667
            -- Update inner height in case it changed.
668
            innerHeight = select(2, inner.getSize())
669
            -- Check for scroll events and set direction.
670
            local dir
671
            if ev[1] == "key" and allowArrowKeys then
672
                if ev[2] == keys.up then dir = -1
673
                elseif ev[2] == keys.down then dir = 1 end
674
            elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then
675
                dir = ev[2]
676
            end
677
            -- If there's a scroll event, move the window vertically.
678
            if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
679
                scrollPos = scrollPos + dir
680
                inner.reposition(1, 2 - scrollPos)
681
            end
682
            -- Redraw scroll indicators if desired.
683
            if showScrollIndicators then
684
                outer.setBackgroundColor(bgColor)
685
                outer.setTextColor(fgColor)
686
                outer.setCursorPos(width, 1)
687
                outer.write(scrollPos > 1 and "\30" or " ")
688
                outer.setCursorPos(width, height)
689
                outer.write(scrollPos < innerHeight - height and "\31" or " ")
690
            end
691
        end
692
    end)
693
    
694
    -- Make a function to allow external scrolling.
695
    local function scroll(pos)
696
        expect(1, pos, "number")
697
        pos = math.floor(pos)
698
        expect.range(pos, 1, innerHeight - height)
699
        -- Scroll the window.
700
        scrollPos = pos
701
        inner.reposition(1, 2 - scrollPos)
702
        -- Redraw scroll indicators if desired.
703
        if showScrollIndicators then
704
            outer.setBackgroundColor(bgColor)
705
            outer.setTextColor(fgColor)
706
            outer.setCursorPos(width, 1)
707
            outer.write(scrollPos > 1 and "\30" or " ")
708
            outer.setCursorPos(width, height)
709
            outer.write(scrollPos < innerHeight - height and "\31" or " ")
710
        end
711
    end
712
    
713
    -- Add a cleanup task to restore the original event filter when the scroll box is destroyed
714
    PrimeUI.addTask(function()
715
        while true do
716
            local event = os.pullEvent("term_resize")
717
            -- Check if the outer window still exists
718
            if not outer.isColor then
719
                -- Restore the original event filter
720
                PrimeUI.eventFilter = originalEventFilter
721
                return
722
            end
723
        end
724
    end)
725
    
726
    return inner, scroll
727
end
728
729
--- Creates a list of entries that can each be selected.
730
---@param win window The window to draw on
731
---@param x number The X coordinate of the inside of the box
732
---@param y number The Y coordinate of the inside of the box
733
---@param width number The width of the inner box
734
---@param height number The height of the inner box
735
---@param entries string[] A list of entries to show, where the value is whether the item is pre-selected (or `"R"` for required/forced selected)
736
---@param action function|string A function or `run` event that's called when a selection is made
737
---@param selectChangeAction function|string|nil A function or `run` event that's called when the current selection is changed
738
---@param fgColor color|nil The color of the text (defaults to white)
739
---@param bgColor color|nil The color of the background (defaults to black)
740
function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor)
741
    expect(1, win, "table")
742
    expect(2, x, "number")
743
    expect(3, y, "number")
744
    expect(4, width, "number")
745
    expect(5, height, "number")
746
    expect(6, entries, "table")
747
    expect(7, action, "function", "string")
748
    expect(8, selectChangeAction, "function", "string", "nil")
749
    fgColor = expect(9, fgColor, "number", "nil") or colors.white
750
    bgColor = expect(10, bgColor, "number", "nil") or colors.black
751
    -- Check that all entries are strings.
752
    if #entries == 0 then error("bad argument #6 (table must not be empty)", 2) end
753
    for i, v in ipairs(entries) do
754
        if type(v) ~= "string" then error("bad item " .. i .. " in entries table (expected string, got " .. type(v), 2) end
755
    end
756
    -- Create container window.
757
    local entrywin = window.create(win, x, y, width, height)
758
    local selection, scroll = 1, 1
759
    -- Create a function to redraw the entries on screen.
760
    local function drawEntries()
761
        -- Clear and set invisible for performance.
762
        entrywin.setVisible(false)
763
        entrywin.setBackgroundColor(bgColor)
764
        entrywin.clear()
765
        -- Draw each entry in the scrolled region.
766
        for i = scroll, scroll + height - 1 do
767
            -- Get the entry; stop if there's no more.
768
            local e = entries[i]
769
            if not e then break end
770
            -- Set the colors: invert if selected.
771
            entrywin.setCursorPos(2, i - scroll + 1)
772
            if i == selection then
773
                entrywin.setBackgroundColor(fgColor)
774
                entrywin.setTextColor(bgColor)
775
            else
776
                entrywin.setBackgroundColor(bgColor)
777
                entrywin.setTextColor(fgColor)
778
            end
779
            -- Draw the selection.
780
            entrywin.clearLine()
781
            entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
782
        end
783
        -- Draw scroll arrows.
784
        entrywin.setBackgroundColor(bgColor)
785
        entrywin.setTextColor(fgColor)
786
        entrywin.setCursorPos(width, 1)
787
        entrywin.write("\30")
788
        entrywin.setCursorPos(width, height)
789
        entrywin.write("\31")
790
        -- Send updates to the screen.
791
        entrywin.setVisible(true)
792
    end
793
    -- Draw first screen.
794
    drawEntries()
795
    -- Add a task for selection keys.
796
    PrimeUI.addTask(function()
797
        while true do
798
            local event, key, cx, cy = os.pullEvent()
799
            if event == "key" then
800
                if key == keys.down and selection < #entries then
801
                    -- Move selection down.
802
                    selection = selection + 1
803
                    if selection > scroll + height - 1 then scroll = scroll + 1 end
804
                    -- Send action if necessary.
805
                    if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
806
                    elseif selectChangeAction then selectChangeAction(selection) end
807
                    -- Redraw screen.
808
                    drawEntries()
809
                elseif key == keys.up and selection > 1 then
810
                    -- Move selection up.
811
                    selection = selection - 1
812
                    if selection < scroll then scroll = scroll - 1 end
813
                    -- Send action if necessary.
814
                    if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
815
                    elseif selectChangeAction then selectChangeAction(selection) end
816
                    -- Redraw screen.
817
                    drawEntries()
818
                elseif key == keys.enter then
819
                    -- Select the entry: send the action.
820
                    if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries[selection])
821
                    else action(entries[selection]) end
822
                end
823
            elseif event == "mouse_click" and key == 1 then
824
                -- Handle clicking the scroll arrows.
825
                local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
826
                if cx == wx + width - 1 then
827
                    if cy == wy and selection > 1 then
828
                        -- Move selection up.
829
                        selection = selection - 1
830
                        if selection < scroll then scroll = scroll - 1 end
831
                        -- Send action if necessary.
832
                        if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
833
                        elseif selectChangeAction then selectChangeAction(selection) end
834
                        -- Redraw screen.
835
                        drawEntries()
836
                    elseif cy == wy + height - 1 and selection < #entries then
837
                        -- Move selection down.
838
                        selection = selection + 1
839
                        if selection > scroll + height - 1 then scroll = scroll + 1 end
840
                        -- Send action if necessary.
841
                        if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
842
                        elseif selectChangeAction then selectChangeAction(selection) end
843
                        -- Redraw screen.
844
                        drawEntries()
845
                    end
846
                elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
847
                    local sel = scroll + (cy - wy)
848
                    if sel == selection then
849
                        -- Select the entry: send the action.
850
                        if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries[selection])
851
                        else action(entries[selection]) end
852
                    else
853
                        selection = sel
854
                        -- Send action if necessary.
855
                        if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
856
                        elseif selectChangeAction then selectChangeAction(selection) end
857
                        -- Redraw screen.
858
                        drawEntries()
859
                    end
860
                end
861
            elseif event == "mouse_scroll" then
862
                -- Handle mouse scrolling.
863
                local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
864
                if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
865
                    if key < 0 and selection > 1 then
866
                        -- Move selection up.
867
                        selection = selection - 1
868
                        if selection < scroll then scroll = scroll - 1 end
869
                        -- Send action if necessary.
870
                        if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
871
                        elseif selectChangeAction then selectChangeAction(selection) end
872
                        -- Redraw screen.
873
                        drawEntries()
874
                    elseif key > 0 and selection < #entries then
875
                        -- Move selection down.
876
                        selection = selection + 1
877
                        if selection > scroll + height - 1 then scroll = scroll + 1 end
878
                        -- Send action if necessary.
879
                        if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
880
                        elseif selectChangeAction then selectChangeAction(selection) end
881
                        -- Redraw screen.
882
                        drawEntries()
883
                    end
884
                end
885
            end
886
        end
887
    end)
888
end
889
890
--- Creates a text box that wraps text and can have its text modified later.
891
---@param win window The parent window of the text box
892
---@param x number The X position of the box
893
---@param y number The Y position of the box
894
---@param width number The width of the box
895
---@param height number The height of the box
896
---@param text string The initial text to draw
897
---@param fgColor color|nil The color of the text (defaults to white)
898
---@param bgColor color|nil The color of the background (defaults to black)
899
---@return function redraw A function to redraw the window with new contents
900
function PrimeUI.textBox(win, x, y, width, height, text, fgColor, bgColor)
901
    expect(1, win, "table")
902
    expect(2, x, "number")
903
    expect(3, y, "number")
904
    expect(4, width, "number")
905
    expect(5, height, "number")
906
    expect(6, text, "string")
907
    fgColor = expect(7, fgColor, "number", "nil") or colors.white
908
    bgColor = expect(8, bgColor, "number", "nil") or colors.black
909
    -- Create the box window.
910
    local box = window.create(win, x, y, width, height)
911
    -- Override box.getSize to make print not scroll.
912
    function box.getSize()
913
        return width, math.huge
914
    end
915
    -- Define a function to redraw with.
916
    local function redraw(_text)
917
        expect(1, _text, "string")
918
        -- Set window parameters.
919
        box.setBackgroundColor(bgColor)
920
        box.setTextColor(fgColor)
921
        box.clear()
922
        box.setCursorPos(1, 1)
923
        -- Redirect and draw with `print`.
924
        local old = term.redirect(box)
925
        print(_text)
926
        term.redirect(old)
927
    end
928
    redraw(text)
929
    return redraw
930
end
931
932
--- Creates a clickable, toggleable button on screen with text.
933
---@param win window The window to draw on
934
---@param x number The X position of the button
935
---@param y number The Y position of the button
936
---@param textOn string The text to draw on the button when on
937
---@param textOff string The text to draw on the button when off (must be the same length as textOn)
938
---@param action function|string A function to call when clicked, or a string to send with a `run` event
939
---@param fgColor color|nil The color of the button text (defaults to white)
940
---@param bgColor color|nil The color of the button (defaults to light gray)
941
---@param clickedColor color|nil The color of the button when clicked (defaults to gray)
942
---@param periphName string|nil The name of the monitor peripheral, or nil (set if you're using a monitor - events will be filtered to that monitor)
943
function PrimeUI.toggleButton(win, x, y, textOn, textOff, action, fgColor, bgColor, clickedColor, periphName)
944
    expect(1, win, "table")
945
    expect(1, win, "table")
946
    expect(2, x, "number")
947
    expect(3, y, "number")
948
    expect(4, textOn, "string")
949
    expect(5, textOff, "string")
950
    if #textOn ~= #textOff then error("On and off text must be the same length", 2) end
951
    expect(6, action, "function", "string")
952
    fgColor = expect(7, fgColor, "number", "nil") or colors.white
953
    bgColor = expect(8, bgColor, "number", "nil") or colors.gray
954
    clickedColor = expect(9, clickedColor, "number", "nil") or colors.lightGray
955
    periphName = expect(10, periphName, "string", "nil")
956
    -- Draw the initial button.
957
    win.setCursorPos(x, y)
958
    win.setBackgroundColor(bgColor)
959
    win.setTextColor(fgColor)
960
    win.write(" " .. textOff .. " ")
961
    local state = false
962
    -- Get the screen position and add a click handler.
963
    PrimeUI.addTask(function()
964
        local screenX, screenY = PrimeUI.getWindowPos(win, x, y)
965
        local buttonDown = false
966
        while true do
967
            local event, button, clickX, clickY = os.pullEvent()
968
            if event == "mouse_click" and periphName == nil and button == 1 and clickX >= screenX and clickX < screenX + #textOn + 2 and clickY == screenY then
969
                -- Initiate a click action (but don't trigger until mouse up).
970
                buttonDown = true
971
                -- Redraw the button with the clicked background color.
972
                win.setCursorPos(x, y)
973
                win.setBackgroundColor(clickedColor)
974
                win.setTextColor(fgColor)
975
                win.write(" " .. (state and textOn or textOff) .. " ")
976
            elseif (event == "monitor_touch" and periphName == button and clickX >= screenX and clickX < screenX + #textOn + 2 and clickY == screenY)
977
                or (event == "mouse_up" and button == 1 and buttonDown) then
978
                -- Finish a click event.
979
                state = not state
980
                if clickX >= screenX and clickX < screenX + #textOn + 2 and clickY == screenY then
981
                    -- Trigger the action.
982
                    if type(action) == "string" then
983
                        PrimeUI.resolve("toggleButton", action, state)
984
                    else
985
                        action(state)
986
                    end
987
                end
988
                -- Redraw the original button state.
989
                win.setCursorPos(x, y)
990
                win.setBackgroundColor(bgColor)
991
                win.setTextColor(fgColor)
992
                win.write(" " .. (state and textOn or textOff) .. " ")
993
            end
994
        end
995
    end)
996
end
997
998
--- Draws a vertical line at a position with the specified height.
999
---@param win window The window to draw on
1000
---@param x number The X position of the line
1001
---@param y number The Y position of the top of the line
1002
---@param height number The height of the line
1003
---@param right boolean|nil Whether to align the line to the right instead of the left (defaults to false)
1004
---@param fgColor color|nil The color of the line (defaults to white)
1005
---@param bgColor color|nil The color of the background (defaults to black)
1006
function PrimeUI.verticalLine(win, x, y, height, right, fgColor, bgColor)
1007
    expect(1, win, "table")
1008
    expect(2, x, "number")
1009
    expect(3, y, "number")
1010
    expect(4, height, "number")
1011
    right = expect(5, right, "boolean", "nil") or false
1012
    fgColor = expect(6, fgColor, "number", "nil") or colors.white
1013
    bgColor = expect(7, bgColor, "number", "nil") or colors.black
1014
    -- Use drawing characters to draw a thin line.
1015
    win.setTextColor(right and bgColor or fgColor)
1016
    win.setBackgroundColor(right and fgColor or bgColor)
1017
    for j = 1, height do
1018
        win.setCursorPos(x, y + j - 1)
1019
        win.write("\x95")
1020
    end
1021
end
1022
-- local ui = require "primeui"
1023
ui = PrimeUI
1024
1025
-- Transliterator | made by timuzkas
1026
local Transliteration = {}
1027
Transliteration.__index = Transliteration
1028
1029
local cyrillicAlphabet = {
1030
  {"А", "а", "A", "a"}, {"Б", "б", "B", "b"}, {"В", "в", "V", "v"}, {"Г", "г", "G", "g"}, {"Д", "д", "D", "d"},
1031
  {"Е", "е", "E", "e"}, {"Ё", "ё", "YO", "yo"}, {"Ж", "ж", "ZH", "zh"}, {"З", "з", "Z", "z"}, {"И", "и", "I", "i"},
1032
  {"Й", "й", "Y", "y"}, {"К", "к", "K", "k"}, {"Л", "л", "L", "l"}, {"М", "м", "M", "m"}, {"Н", "н", "N", "n"},
1033
  {"О", "о", "O", "o"}, {"П", "п", "P", "p"}, {"Р", "р", "R", "r"}, {"С", "с", "S", "s"}, {"Т", "т", "T", "t"},
1034
  {"У", "у", "U", "u"}, {"Ф", "ф", "F", "f"}, {"Х", "х", "KH", "kh"}, {"Ц", "ц", "TS", "ts"}, {"Ч", "ч", "CH", "ch"},
1035
  {"Ш", "ш", "SH", "sh"}, {"Щ", "щ", "SHCH", "shch"}, {"Ъ", "ъ", "", ""}, {"Ы", "ы", "Y", "y"}, {"Ь", "ь", "", ""},
1036
  {"Э", "э", "E", "e"}, {"Ю", "ю", "YU", "yu"}, {"Я", "я", "YA", "ya"}
1037
}
1038
1039
function Transliteration.new()
1040
  local self = setmetatable({}, Transliteration)
1041
  self.cyrillicToLatin = {}
1042
  self.latinToCyrillic = {}
1043
  self.isSetup = false
1044
  return self
1045
end
1046
1047
function Transliteration:setup()
1048
  if self.isSetup then return end
1049
  for _, pair in ipairs(cyrillicAlphabet) do
1050
    self.cyrillicToLatin[utf8.codepoint(pair[1])] = pair[3]
1051
    self.cyrillicToLatin[utf8.codepoint(pair[2])] = pair[4]
1052
    if pair[3] ~= "" then
1053
      if not self.latinToCyrillic[pair[3]] then
1054
        self.latinToCyrillic[pair[3]] = {}
1055
      end
1056
      table.insert(self.latinToCyrillic[pair[3]], pair[1])
1057
      table.insert(self.latinToCyrillic[pair[3]], pair[2])
1058
    end
1059
    if pair[4] ~= "" then
1060
      if not self.latinToCyrillic[pair[4]] then
1061
        self.latinToCyrillic[pair[4]] = {}
1062
      end
1063
      table.insert(self.latinToCyrillic[pair[4]], pair[1])
1064
      table.insert(self.latinToCyrillic[pair[4]], pair[2])
1065
    end
1066
  end
1067
  self.isSetup = true
1068
end
1069
1070
function Transliteration:translate(str)
1071
  if not self.isSetup then self:setup() end
1072
  local result = ""
1073
  
1074
  local chars = {}
1075
  for char in str:gmatch(utf8.charpattern) do
1076
    table.insert(chars, char)
1077
  end
1078
  
1079
  for _, char in ipairs(chars) do
1080
    local codepoint = utf8.codepoint(char)
1081
    local latin = self.cyrillicToLatin[codepoint]
1082
    if latin then
1083
      result = result .. latin
1084
    else
1085
      result = result .. char
1086
    end
1087
  end
1088
  
1089
  return result
1090
end
1091
1092
-- stripped for size reasons
1093
1094
local function box(terminal, x, y, width, height, color, cornerStyle)
1095
    cornerStyle = cornerStyle or "square"
1096
    terminal.setBackgroundColor(color)
1097
    
1098
    if cornerStyle == "square" then
1099
        for i = y, y + height - 1 do
1100
            terminal.setCursorPos(x, i)
1101
            terminal.write(string.rep(" ", width))
1102
        end
1103
    elseif cornerStyle == "round" then
1104
        terminal.setCursorPos(x + 1, y)
1105
        terminal.write(string.rep(" ", width - 2))
1106
        
1107
        for i = y + 1, y + height - 2 do
1108
            terminal.setCursorPos(x, i)
1109
            terminal.write(string.rep(" ", width))
1110
        end
1111
        
1112
        terminal.setCursorPos(x + 1, y + height - 1)
1113
        terminal.write(string.rep(" ", width - 2))
1114
    end
1115
end
1116
ui.box = box    
1117
1118
local api_base_url = "https://ipod-2to6magyna-uc.a.run.app/"
1119
1120
local width, height = term.getSize()
1121
1122
1123
local last_search_url = nil
1124
local search_results = nil
1125
local playing = false
1126
local queue = {}
1127
local now_playing = nil
1128
local looping = false
1129
1130
local playing_id = nil
1131
local last_download_url = nil
1132
local playing_status = 0
1133
1134
local player_handle = nil
1135
local start = nil
1136
local pcm = nil
1137
local size = nil
1138
local decoder = nil
1139
local needs_next_chunk = 0
1140
local buffer
1141
1142
local speakers = { peripheral.find("speaker") }
1143
1144
if #speakers == 0 then
1145
    error("No speakers attached. You need to connect a speaker to this computer. If this is an Advanced Noisy Pocket Computer, then this is a bug, and you should try restarting your Minecraft game.", 0)
1146
end
1147
1148
local speaker = speakers[1]
1149
1150
os.startTimer(1)
1151
1152
-- ui helper functions
1153
local function playSong(song)
1154
    now_playing = song
1155
    playing = true
1156
    playing_id = nil
1157
end
1158
1159
local function stopPlayback()
1160
    playing = false
1161
    speaker.stop()
1162
    playing_id = nil
1163
end
1164
1165
local function togglePlayPause()
1166
    if playing then
1167
        stopPlayback()
1168
    else
1169
        if now_playing or #queue > 0 then
1170
            playSong(now_playing or queue[1])
1171
        end
1172
    end
1173
end
1174
1175
local function skipSong()
1176
    if #queue > 0 then
1177
        now_playing = queue[1]
1178
        table.remove(queue, 1)
1179
        playing_id = nil
1180
    else
1181
        now_playing = nil
1182
        playing = false
1183
    end
1184
end
1185
1186
local function toggleLoop()
1187
    looping = not looping
1188
end
1189
1190
local function addToQueue(song, position)
1191
    if position then
1192
        table.insert(queue, position, song)
1193
    else
1194
        table.insert(queue, song)
1195
    end
1196
end
1197
1198
local function removeFromQueue(position)
1199
    if position and position <= #queue then
1200
        table.remove(queue, position)
1201
    end
1202
end
1203
1204
local function clearQueue()
1205
    queue = {}
1206
end
1207
1208
local function searchMusic(query)
1209
    last_search = query
1210
    last_search_url = api_base_url .. "?search=" .. textutils.urlEncode(query)
1211
    http.request(last_search_url)
1212
    search_results = nil
1213
    search_error = false
1214
end
1215
1216
-- not used, tho may need later
1217
local function handleAudioStream(response)
1218
    player_handle = response
1219
    start = response.read(4)
1220
    size = 16 * 1024 - 4
1221
    playing_status = 1
1222
    decoder = require "cc.audio.dfpwm".make_decoder()
1223
end
1224
1225
-- custom pallete based on spotify one.
1226
local original_palette = {}
1227
local function initCustomPallete()
1228
    for i=1, 16 do
1229
        original_palette[i] = term.getPaletteColor(i)
1230
    end
1231
1232
    term.setPaletteColor(colors.green, 0x1ED760)
1233
    term.setPaletteColor(colors.lightGray, 0xb3b3b3)
1234
    term.setPaletteColor(colors.gray, 0x212121)
1235
    term.setPaletteColor(colors.purple, 0x457e59)
1236
    term.setPaletteColor(colors.magenta, 0x62d089)
1237
    term.setPaletteColor(colors.brown, 0x2e2e2e)
1238
end
1239
1240
-- truncation and transliteration for text
1241
local function fixString(str, limit)
1242
    if not str then return "" end
1243-
    local transliterator = Transliteration.new()
1243+
    --local transliterator = Transliteration.new()
1244-
    str = transliterator.translate(transliterator, str)
1244+
    --str = transliterator.translate(transliterator, str)
1245
    
1246
    if #str <= limit then
1247
        return str
1248
    end
1249
    
1250
    return string.sub(str, 1, limit - 3) .. "..."
1251
end
1252
1253
1254
-- UI LOOP
1255
1256
ui.page = 1 
1257
1258
local function redrawScreen()
1259
    -- init custom palette
1260
    initCustomPallete()
1261
1262
1263
    while true do
1264
        ui.clear()
1265
        ui.borderBox(term.current(), 3, 2, width-4, 1, colors.gray)
1266
1267
        local isSmallScreen = width <= 30
1268
        
1269
1270
        if now_playing then
1271
            if playing then
1272
                if isSmallScreen then
1273
                    ui.button(term.current(), 4, 2, "S", "stop", colors.white, colors.red, colors.orange)
1274
                else
1275
                    ui.button(term.current(), 4, 2, "Stop", "stop", colors.white, colors.red, colors.orange)
1276
                end
1277
            else
1278
                ui.button(term.current(), 4, 2, "\16", "pause", colors.white, colors.green, colors.lightGray)
1279
                ui.button(term.current(), 8, 2, "R", "clear", colors.white, colors.red, colors.orange)
1280
            end
1281
            if not isSmallScreen then
1282
                ui.label(term.current(), 12, 2, fixString(now_playing.name, 20), colors.white)
1283
                ui.label(term.current(), 12+string.len(fixString(now_playing.name, 20))+1, 2, "| "..fixString(now_playing.artist, 14), colors.lightGray)
1284
            else
1285
                ui.label(term.current(), 8, 2, fixString(now_playing.name, 16), colors.white)
1286
            end
1287
        else
1288
            ui.label(term.current(), 4, 2, "Musiclo", colors.green)
1289
            if not isSmallScreen then
1290
                ui.label(term.current(), 4+string.len("Musiclo")+2, 2, "| CC:T music player made easy", colors.lightGray)
1291
            else 
1292
                ui.label(term.current(), 4+string.len("Musiclo")+1, 2, "| CC:T player", colors.lightGray)
1293
            end
1294
        end
1295
1296
1297
        local titleTruncateLimit = 41
1298
        local artistTruncateLimit = 26
1299
1300
        if isSmallScreen then
1301
            titleTruncateLimit = 19
1302
            artistTruncateLimit = 15
1303
        end
1304
1305
        
1306
1307
        if ui.page == 1 then
1308
            ui.borderBox(term.current(), 3, 5, width-4, height-6, colors.gray)
1309
1310
            ui.button(term.current(), width-9, 4, "Search", "page.2", colors.white, colors.magenta, colors.purple)
1311
            ui.keyAction(keys.enter, "page.2")
1312
            
1313
            ui.label(term.current(), 4, 4, "Queue", colors.white)
1314
            
1315
            ui.keyAction(keys.space, "pause")
1316
1317
            if looping then
1318
                ui.button(term.current(), 4, height-1, "Loop", "loop", colors.white, colors.magenta, colors.purple)
1319
            else
1320
                ui.button(term.current(), 4, height-1, "Loop", "loop", colors.white, colors.gray, colors.lightGray)
1321
            end
1322
1323
            if #queue > 0 then
1324
                ui.button(term.current(), 11, height-1, "Skip", "skip", colors.white, colors.gray, colors.lightGray)
1325
                if isSmallScreen then 
1326
                    ui.button(term.current(), 18, height-1, "Clr", "clear.q", colors.white, colors.red, colors.orange)
1327
                else
1328
                ui.button(term.current(), 18, height-1, "Clear queue", "clear.q", colors.white, colors.red, colors.orange)
1329
                end
1330
            end
1331
            ui.label(term.current(), 4, 6, "Now playing", colors.white)
1332
1333
            local scroller = ui.scrollBox(term.current(), 3, 5, width-4, height-6, 9000, true, true)
1334
1335
            y = 2
1336
            if #queue > 0 then
1337
                for i, song in ipairs(queue) do
1338
                    ui.box(scroller, 1, y, width-5, 5, colors.brown)
1339
                    ui.label(scroller, 2, y+1, fixString(song.name, titleTruncateLimit), colors.white, colors.brown)
1340
                    ui.label(scroller, 2, y+2, fixString(song.artist, artistTruncateLimit), colors.lightGray, colors.brown)
1341
                    if isSmallScreen then y = y + 1 end
1342
                    ui.button(scroller, width-20, y+2, "Play", "play."..i, colors.white, colors.magenta, colors.purple)
1343
                    local songInQueue = false
1344
                    for _, queuedSong in ipairs(queue) do
1345
                        if queuedSong.id == song.id then
1346
                            songInQueue = true
1347
                            break
1348
                        end
1349
                    end
1350
                    if songInQueue then
1351
                        ui.button(scroller, width-13, y+2, "Remove", "rem."..i, colors.white, colors.red, colors.orange)
1352
                    else
1353
                        ui.button(scroller, width-13, y+2, "Add", "add."..i, colors.white, colors.gray, colors.lightGray)
1354
                    end
1355
                    y = y + 6
1356
                end
1357
            else 
1358
                ui.centerLabel(scroller, 1, 5,width-4, "No songs in queue",  colors.lightGray)
1359
                ui.button(scroller, ((width-4-3)/2-(string.len("Add song")/2))+1, 7 ,"Add song", "page.2", colors.white, colors.gray, colors.lightGray)
1360
            end
1361
        elseif ui.page == 2 then
1362
            ui.borderBox(term.current(), 3, 5, width-4, height-6, colors.gray)
1363
1364
            ui.button(term.current(), width-10, 4, "Go back","page.1", colors.white, colors.gray, colors.lightGray)
1365
            ui.label(term.current(), 4, 4, "Search", colors.white)
1366
1367
            ui.label(term.current(), 4, 6, "Search on Youtube...", colors.lightGray)
1368
1369
            ui.horizontalLine(term.current(), 3, 8, width-4, colors.gray)
1370
            
1371
            local scroller = ui.scrollBox(term.current(), 3, 9, width-4, height-10, 9000, true, true)
1372
1373
            y = 2
1374
            if search_results then
1375
                for i, song in ipairs(search_results) do 
1376
                    ui.box(scroller, 1, y, width-6, 5, colors.brown)
1377
                    ui.label(scroller, 2, y+1, fixString(song.name, titleTruncateLimit), colors.white, colors.brown)
1378
                    ui.label(scroller, 2, y+2, fixString(song.artist, artistTruncateLimit), colors.lightGray, colors.brown)
1379
                    if isSmallScreen then y = y + 1 end
1380
                    ui.button(scroller, width-21, y+2, "Play", "play."..i, colors.white, colors.magenta, colors.purple)
1381
                    local songInQueue = false
1382
                    for _, queuedSong in ipairs(queue) do
1383
                        if queuedSong.id == song.id then
1384
                            songInQueue = true
1385
                            break
1386
                        end
1387
                    end
1388
                    if songInQueue then
1389
                        ui.button(scroller, width-14, y+2, "Remove", "rem."..i, colors.white, colors.red, colors.orange)
1390
                    else
1391
                        ui.button(scroller, width-14, y+2, "Add", "add."..i, colors.white, colors.gray, colors.lightGray)
1392
                    end
1393
                    y = y + 6
1394
                end
1395
            end
1396
1397
            ui.inputBox(term.current(), 4, 7, width-7, "search", colors.white, colors.gray)
1398
        end
1399
1400
        
1401
        local object, callback, text = ui.run()
1402
        term.clear()
1403
        term.setCursorPos(1, 1)
1404
1405
        -- callbacks
1406
1407
        if object == "button" then
1408
            if callback == "page.2" then
1409
                ui.page = 2
1410
            elseif callback == "page.1" then
1411
                ui.page = 1
1412
            elseif callback:sub(1, 4) == "play" then
1413
                local index = tonumber(callback:sub(6))
1414
                if index and search_results[index] then
1415
                    playSong(search_results[index])
1416
                    ui.page = 1
1417
                end
1418
            elseif callback:sub(1, 3) == "add" then
1419
                local index = tonumber(callback:sub(5))
1420
                if index and search_results[index] then
1421
                    addToQueue(search_results[index])
1422
                end
1423
            elseif callback:sub(1, 4) == "rem" then
1424
                local index = tonumber(callback:sub(6))
1425
                if index and search_results[index] then
1426
                    removeFromQueue(index)
1427
                end
1428
            elseif callback == "stop" then
1429
                stopPlayback()
1430
            elseif callback == "pause" then
1431
                togglePlayPause()
1432
            elseif callback == "loop" then
1433
                toggleLoop()
1434
            elseif callback == "skip" then
1435
                skipSong()
1436
            elseif callback == "clear.q" then
1437
                clearQueue()
1438
            elseif callback == "clear" then
1439
                playing = false
1440
                now_playing = nil
1441
                playing_id = nil
1442
            end
1443
        elseif object == "keyAction" then
1444
            if callback == "page.2" then
1445
                ui.page = 2
1446
            elseif callback == "page.1" then
1447
                ui.page = 1
1448
            end
1449
        elseif object == "inputBox" and callback == "search" then
1450
            if text ~= "" then
1451
                searchMusic(text)
1452
                term.clear()
1453
                local sx, sy = term.getSize()
1454
                term.setTextColor(colors.lightGray)
1455
                term.setCursorPos(sx/2 - #"Fetching..."/2, sy/2)
1456
                term.write("Fetching...")
1457
                ui.searchDone = false
1458
                repeat
1459
                    sleep(0.1)
1460
                until ui.searchDone == true
1461
                ui.searchDone = false
1462
            end
1463
        elseif object == "rerender" then
1464
            print("rerender")
1465
        else
1466
            term.clear()
1467
            term.setCursorPos(1, 1)
1468
            error("["..(object or "No object").."] "..(callback or "No callback").." "..(text or "No text").." not handled! Exiting",0)
1469
        end
1470
    end
1471
end
1472
1473
local function audioLoop()
1474
    while true do
1475
        -- AUDIO
1476
        sleep(0.1)
1477
        if playing and now_playing then
1478
            if playing_id ~= now_playing.id then
1479
                playing_id = now_playing.id
1480
                last_download_url = api_base_url .. "?v=2&id=" .. textutils.urlEncode(playing_id)
1481
                playing_status = 0
1482
                needs_next_chunk = 1
1483
1484
                http.request({url = last_download_url, binary = true})
1485
				is_loading = true
1486
1487
            end
1488
            if playing_status == 1 and needs_next_chunk == 3 then
1489
                needs_next_chunk = 1
1490
                for _, speaker in ipairs(speakers) do
1491
                    while not speaker.playAudio(buffer) do
1492
                        needs_next_chunk = 2
1493
                        break
1494
                    end
1495
                end
1496
            end
1497
            if playing_status == 1 and needs_next_chunk == 1 then
1498
1499
                while true do
1500
                    local chunk = player_handle.read(size)
1501
                    if not chunk then
1502
                        if looping then
1503
                            playing_id = nil
1504
                        else
1505
                            if #queue > 0 then
1506
                                now_playing = queue[1]
1507
                                table.remove(queue, 1)
1508
                                playing_id = nil
1509
                            else
1510
                                now_playing = nil
1511
                                playing = false
1512
                                playing_id = nil
1513
                                is_loading = false
1514
                                is_error = false
1515
                            end
1516
                        end
1517
1518
1519
                        player_handle.close()
1520
                        needs_next_chunk = 0
1521
                        break
1522
                    else
1523
                        if start then
1524
                            chunk, start = start .. chunk, nil
1525
                            size = size + 4
1526
                        end
1527
                
1528
                        buffer = decoder(chunk)
1529
                        for _, speaker in ipairs(speakers) do
1530
                            while not speaker.playAudio(buffer) do
1531
                                needs_next_chunk = 2
1532
                                break
1533
                            end
1534
                        end
1535
                        if needs_next_chunk == 2 then
1536
                            break
1537
                        end
1538
                    end
1539
                end
1540
1541
            end
1542
        end
1543
    end
1544
end
1545
1546
-- Events
1547
local function eventLoop()
1548
    while true do 
1549
        local event, param1, param2 = os.pullEvent()
1550
1551
        if event == "timer" then
1552
            os.startTimer(1)
1553
        end
1554
1555
        if event == "speaker_audio_empty" then
1556
            if needs_next_chunk == 2 then
1557
                needs_next_chunk = 3
1558
            end
1559
        end
1560
1561
        if event == "http_success" then
1562
            local url = param1
1563
            local handle = param2
1564
            if url == last_search_url then
1565
                search_results = textutils.unserialiseJSON(handle.readAll())
1566
                table.remove(search_results, 1)
1567
                ui.searchDone = true
1568
            end
1569
            if url == last_download_url then
1570
                player_handle = handle
1571
                start = handle.read(4)
1572
                size = 16 * 1024 - 4
1573
                if start == "RIFF" then
1574
                    error("WAV not supported!")
1575
                end
1576
                playing_status = 1
1577
                decoder = require "cc.audio.dfpwm".make_decoder()
1578
            end
1579
        end
1580
1581
        if event == "http_failure" then
1582
            local url = param1
1583
1584
            if url == last_search_url then
1585
                search_error = true
1586
            end
1587
            if url == last_download_url then
1588
                if #queue > 0 then
1589
                    now_playing = queue[1]
1590
                    table.remove(queue, 1)
1591
                    playing_id = nil
1592
                else
1593
                    now_playing = nil
1594
                    playing = false
1595
                    playing_id = nil
1596
                end
1597
            end
1598
        end
1599
    end 
1600
end
1601
1602
parallel.waitForAny(audioLoop, eventLoop, redrawScreen)
1603
1604
-- cleanup 
1605
for  i=1, 16 do
1606
    term.setPaletteColor(i, original_palette[i])
1607
end
1608
term.setCursorBlink(false)
1609
term.clear()
1610
term.setCursorPos(1, 1)